Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ca9017cc4 | |||
| 10c57e59ec | |||
| ef4571767c | |||
| 29bd25eaff | |||
| 4d9b510819 | |||
| 3c4b1066df |
@ -2403,7 +2403,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Anchor order and insert new line",
|
"description": "Anchor order and insert new line (no line id)",
|
||||||
"action": "merge",
|
"action": "merge",
|
||||||
"data": {
|
"data": {
|
||||||
"id": "abc",
|
"id": "abc",
|
||||||
@ -2502,6 +2502,114 @@
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Anchor order and insert new line (with line id)",
|
||||||
|
"action": "merge",
|
||||||
|
"data": {
|
||||||
|
"id": "abc",
|
||||||
|
"type": "order",
|
||||||
|
"lines": [
|
||||||
|
{
|
||||||
|
"id": "11111111-2222-3333-4444-555555555555",
|
||||||
|
"type": "order_line",
|
||||||
|
"product": "Widget",
|
||||||
|
"price": 99.0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"schema_id": "order",
|
||||||
|
"expect": {
|
||||||
|
"success": true,
|
||||||
|
"sql": [
|
||||||
|
[
|
||||||
|
"SELECT to_jsonb(t1.*) || to_jsonb(t2.*)",
|
||||||
|
"FROM agreego.\"order_line\" t1",
|
||||||
|
"LEFT JOIN agreego.\"entity\" t2 ON t2.id = t1.id",
|
||||||
|
"WHERE t1.id = '11111111-2222-3333-4444-555555555555'"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"INSERT INTO agreego.\"entity\" (",
|
||||||
|
" \"created_at\",",
|
||||||
|
" \"created_by\",",
|
||||||
|
" \"id\",",
|
||||||
|
" \"modified_at\",",
|
||||||
|
" \"modified_by\",",
|
||||||
|
" \"type\"",
|
||||||
|
")",
|
||||||
|
"VALUES (",
|
||||||
|
" '{{timestamp}}',",
|
||||||
|
" '00000000-0000-0000-0000-000000000000',",
|
||||||
|
" '11111111-2222-3333-4444-555555555555',",
|
||||||
|
" '{{timestamp}}',",
|
||||||
|
" '00000000-0000-0000-0000-000000000000',",
|
||||||
|
" 'order_line'",
|
||||||
|
")"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"INSERT INTO agreego.\"order_line\" (",
|
||||||
|
" \"id\",",
|
||||||
|
" \"order_id\",",
|
||||||
|
" \"price\",",
|
||||||
|
" \"product\",",
|
||||||
|
" \"type\"",
|
||||||
|
")",
|
||||||
|
"VALUES (",
|
||||||
|
" '11111111-2222-3333-4444-555555555555',",
|
||||||
|
" 'abc',",
|
||||||
|
" 99,",
|
||||||
|
" 'Widget',",
|
||||||
|
" 'order_line'",
|
||||||
|
")"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"INSERT INTO agreego.change (",
|
||||||
|
" \"old\",",
|
||||||
|
" \"new\",",
|
||||||
|
" entity_id,",
|
||||||
|
" id,",
|
||||||
|
" kind,",
|
||||||
|
" modified_at,",
|
||||||
|
" modified_by",
|
||||||
|
")",
|
||||||
|
"VALUES (",
|
||||||
|
" NULL,",
|
||||||
|
" '{",
|
||||||
|
" \"order_id\":\"abc\",",
|
||||||
|
" \"price\":99.0,",
|
||||||
|
" \"product\":\"Widget\",",
|
||||||
|
" \"type\":\"order_line\"",
|
||||||
|
" }',",
|
||||||
|
" '11111111-2222-3333-4444-555555555555',",
|
||||||
|
" '{{uuid}}',",
|
||||||
|
" 'create',",
|
||||||
|
" '{{timestamp}}',",
|
||||||
|
" '00000000-0000-0000-0000-000000000000'",
|
||||||
|
")"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"SELECT pg_notify('entity', '{",
|
||||||
|
" \"complete\":{",
|
||||||
|
" \"created_at\":\"{{timestamp}}\",",
|
||||||
|
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||||
|
" \"id\":\"11111111-2222-3333-4444-555555555555\",",
|
||||||
|
" \"modified_at\":\"{{timestamp}}\",",
|
||||||
|
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||||
|
" \"order_id\":\"abc\",",
|
||||||
|
" \"price\":99.0,",
|
||||||
|
" \"product\":\"Widget\",",
|
||||||
|
" \"type\":\"order_line\"",
|
||||||
|
" },",
|
||||||
|
" \"new\":{",
|
||||||
|
" \"order_id\":\"abc\",",
|
||||||
|
" \"price\":99.0,",
|
||||||
|
" \"product\":\"Widget\",",
|
||||||
|
" \"type\":\"order_line\"",
|
||||||
|
" }",
|
||||||
|
" }')"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1163,7 +1163,7 @@
|
|||||||
"$eq": true,
|
"$eq": true,
|
||||||
"$ne": false
|
"$ne": false
|
||||||
},
|
},
|
||||||
"contacts.#.is_primary": {
|
"contacts/is_primary": {
|
||||||
"$eq": true
|
"$eq": true
|
||||||
},
|
},
|
||||||
"created_at": {
|
"created_at": {
|
||||||
@ -1203,7 +1203,7 @@
|
|||||||
"$eq": "%Doe%",
|
"$eq": "%Doe%",
|
||||||
"$ne": "%Smith%"
|
"$ne": "%Smith%"
|
||||||
},
|
},
|
||||||
"phone_numbers.#.target.number": {
|
"phone_numbers/target/number": {
|
||||||
"$eq": "555-1234"
|
"$eq": "555-1234"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"description": "Person ad-hoc email addresses select",
|
|
||||||
"action": "query",
|
|
||||||
"schema_id": "full.person/email_addresses",
|
|
||||||
"expect": {
|
|
||||||
"success": true,
|
|
||||||
"sql": [
|
|
||||||
[
|
|
||||||
"(SELECT jsonb_build_object(",
|
|
||||||
" 'archived', entity_3.archived,",
|
|
||||||
" 'created_at', entity_3.created_at,",
|
|
||||||
" 'id', entity_3.id,",
|
|
||||||
" 'is_primary', contact_1.is_primary,",
|
|
||||||
" 'name', entity_3.name,",
|
|
||||||
" 'target',",
|
|
||||||
" (SELECT jsonb_build_object(",
|
|
||||||
" 'address', email_address_4.address,",
|
|
||||||
" 'archived', entity_5.archived,",
|
|
||||||
" 'created_at', entity_5.created_at,",
|
|
||||||
" 'id', entity_5.id,",
|
|
||||||
" 'name', entity_5.name,",
|
|
||||||
" 'type', entity_5.type",
|
|
||||||
" )",
|
|
||||||
" FROM agreego.email_address email_address_4",
|
|
||||||
" JOIN agreego.entity entity_5 ON entity_5.id = email_address_4.id",
|
|
||||||
" WHERE",
|
|
||||||
" NOT entity_5.archived",
|
|
||||||
" AND relationship_2.target_id = entity_5.id),",
|
|
||||||
" 'type', entity_3.type",
|
|
||||||
")",
|
|
||||||
"FROM agreego.contact contact_1",
|
|
||||||
"JOIN agreego.relationship relationship_2 ON relationship_2.id = contact_1.id",
|
|
||||||
"JOIN agreego.entity entity_3 ON entity_3.id = relationship_2.id",
|
|
||||||
"WHERE NOT entity_3.archived)"
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -64,11 +64,7 @@ impl<'a> Compiler<'a> {
|
|||||||
|
|
||||||
fn compile_array(&mut self, node: Node<'a>) -> Result<(String, String), String> {
|
fn compile_array(&mut self, node: Node<'a>) -> Result<(String, String), String> {
|
||||||
if let Some(items) = &node.schema.obj.items {
|
if let Some(items) = &node.schema.obj.items {
|
||||||
let next_path = if node.ast_path.is_empty() {
|
let next_path = node.ast_path.clone();
|
||||||
String::from("#")
|
|
||||||
} else {
|
|
||||||
format!("{}.#", node.ast_path)
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(ref_id) = &items.obj.r#ref {
|
if let Some(ref_id) = &items.obj.r#ref {
|
||||||
if let Some(type_def) = self.db.types.get(ref_id) {
|
if let Some(type_def) = self.db.types.get(ref_id) {
|
||||||
@ -448,22 +444,21 @@ impl<'a> Compiler<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut child_node = node.clone();
|
let child_node = Node {
|
||||||
child_node.parent_alias = owner_alias.clone();
|
schema: std::sync::Arc::clone(prop_schema),
|
||||||
let arc_aliases = std::sync::Arc::new(table_aliases.clone());
|
parent_alias: owner_alias.clone(),
|
||||||
child_node.parent_type_aliases = Some(arc_aliases);
|
parent_type_aliases: Some(std::sync::Arc::new(table_aliases.clone())),
|
||||||
child_node.parent_type = Some(r#type);
|
parent_type: Some(r#type),
|
||||||
child_node.parent_schema = Some(std::sync::Arc::clone(&node.schema));
|
parent_schema: Some(std::sync::Arc::clone(&node.schema)),
|
||||||
child_node.property_name = Some(prop_key.clone());
|
property_name: Some(prop_key.clone()),
|
||||||
child_node.depth += 1;
|
depth: node.depth + 1,
|
||||||
let next_path = if node.ast_path.is_empty() {
|
ast_path: if node.ast_path.is_empty() {
|
||||||
prop_key.clone()
|
prop_key.clone()
|
||||||
} else {
|
} else {
|
||||||
format!("{}.{}", node.ast_path, prop_key)
|
format!("{}/{}", node.ast_path, prop_key)
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
child_node.ast_path = next_path;
|
|
||||||
child_node.schema = std::sync::Arc::clone(prop_schema);
|
|
||||||
|
|
||||||
let (val_sql, val_type) = self.compile_node(child_node)?;
|
let (val_sql, val_type) = self.compile_node(child_node)?;
|
||||||
|
|
||||||
@ -491,7 +486,14 @@ impl<'a> Compiler<'a> {
|
|||||||
.unwrap_or_else(|| base_alias.clone());
|
.unwrap_or_else(|| base_alias.clone());
|
||||||
|
|
||||||
let mut where_clauses = Vec::new();
|
let mut where_clauses = Vec::new();
|
||||||
where_clauses.push(format!("NOT {}.archived", entity_alias));
|
|
||||||
|
// Dynamically apply the 'active-only' default ONLY if the client
|
||||||
|
// didn't explicitly request to filter on 'archived' themselves!
|
||||||
|
let has_archived_override = self.filter_keys.iter().any(|k| k == "archived");
|
||||||
|
|
||||||
|
if !has_archived_override {
|
||||||
|
where_clauses.push(format!("NOT {}.archived", entity_alias));
|
||||||
|
}
|
||||||
|
|
||||||
self.compile_filter_conditions(r#type, type_aliases, &node, &base_alias, &mut where_clauses);
|
self.compile_filter_conditions(r#type, type_aliases, &node, &base_alias, &mut where_clauses);
|
||||||
self.compile_relation_conditions(
|
self.compile_relation_conditions(
|
||||||
@ -580,15 +582,15 @@ impl<'a> Compiler<'a> {
|
|||||||
let op = parts.next().unwrap_or("$eq");
|
let op = parts.next().unwrap_or("$eq");
|
||||||
|
|
||||||
let field_name = if node.ast_path.is_empty() {
|
let field_name = if node.ast_path.is_empty() {
|
||||||
if full_field_path.contains('.') || full_field_path.contains('#') {
|
if full_field_path.contains('/') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
full_field_path
|
full_field_path
|
||||||
} else {
|
} else {
|
||||||
let prefix = format!("{}.", node.ast_path);
|
let prefix = format!("{}/", node.ast_path);
|
||||||
if full_field_path.starts_with(&prefix) {
|
if full_field_path.starts_with(&prefix) {
|
||||||
let remainder = &full_field_path[prefix.len()..];
|
let remainder = &full_field_path[prefix.len()..];
|
||||||
if remainder.contains('.') || remainder.contains('#') {
|
if remainder.contains('/') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
remainder
|
remainder
|
||||||
|
|||||||
@ -54,6 +54,45 @@ impl Queryer {
|
|||||||
self.execute_sql(schema_id, &sql, &args)
|
self.execute_sql(schema_id, &sql, &args)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn extract_filters(
|
||||||
|
prefix: String,
|
||||||
|
val: &serde_json::Value,
|
||||||
|
entries: &mut Vec<(String, serde_json::Value)>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
if let Some(obj) = val.as_object() {
|
||||||
|
let mut is_op_obj = false;
|
||||||
|
if let Some(first_key) = obj.keys().next() {
|
||||||
|
if first_key.starts_with('$') {
|
||||||
|
is_op_obj = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_op_obj {
|
||||||
|
for (op, op_val) in obj {
|
||||||
|
if !op.starts_with('$') {
|
||||||
|
return Err(format!("Filter operator must start with '$', got: {}", op));
|
||||||
|
}
|
||||||
|
entries.push((format!("{}:{}", prefix, op), op_val.clone()));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (k, v) in obj {
|
||||||
|
let next_prefix = if prefix.is_empty() {
|
||||||
|
k.clone()
|
||||||
|
} else {
|
||||||
|
format!("{}/{}", prefix, k)
|
||||||
|
};
|
||||||
|
Self::extract_filters(next_prefix, v, entries)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(format!(
|
||||||
|
"Filter for path '{}' must be an operator object like {{$eq: ...}} or a nested map.",
|
||||||
|
prefix
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_filter_entries(
|
fn parse_filter_entries(
|
||||||
&self,
|
&self,
|
||||||
filters_map: Option<&serde_json::Map<String, serde_json::Value>>,
|
filters_map: Option<&serde_json::Map<String, serde_json::Value>>,
|
||||||
@ -61,19 +100,7 @@ impl Queryer {
|
|||||||
let mut filter_entries: Vec<(String, serde_json::Value)> = Vec::new();
|
let mut filter_entries: Vec<(String, serde_json::Value)> = Vec::new();
|
||||||
if let Some(fm) = filters_map {
|
if let Some(fm) = filters_map {
|
||||||
for (key, val) in fm {
|
for (key, val) in fm {
|
||||||
if let Some(obj) = val.as_object() {
|
Self::extract_filters(key.clone(), val, &mut filter_entries)?;
|
||||||
for (op, op_val) in obj {
|
|
||||||
if !op.starts_with('$') {
|
|
||||||
return Err(format!("Filter operator must start with '$', got: {}", op));
|
|
||||||
}
|
|
||||||
filter_entries.push((format!("{}:{}", key, op), op_val.clone()));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return Err(format!(
|
|
||||||
"Filter for field '{}' must be an object with operators like $eq, $in, etc.",
|
|
||||||
key
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
filter_entries.sort_by(|a, b| a.0.cmp(&b.0));
|
filter_entries.sort_by(|a, b| a.0.cmp(&b.0));
|
||||||
|
|||||||
@ -8554,3 +8554,9 @@ fn test_merger_0_9() {
|
|||||||
let path = format!("{}/fixtures/merger.json", env!("CARGO_MANIFEST_DIR"));
|
let path = format!("{}/fixtures/merger.json", env!("CARGO_MANIFEST_DIR"));
|
||||||
crate::tests::runner::run_test_case(&path, 0, 9).unwrap();
|
crate::tests::runner::run_test_case(&path, 0, 9).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_merger_0_10() {
|
||||||
|
let path = format!("{}/fixtures/merger.json", env!("CARGO_MANIFEST_DIR"));
|
||||||
|
crate::tests::runner::run_test_case(&path, 0, 10).unwrap();
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user