Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cad651dbd8 | |||
| ea9ac8469c | |||
| ebcdb661fa | |||
| c893e29c59 | |||
| 7523431007 | |||
| dd98bfac9e | |||
| 2f3a1d16b7 | |||
| e86fe5cc4e |
@ -23,6 +23,12 @@ To support high-throughput operations while allowing for runtime updates (e.g.,
|
|||||||
3. **Immutable AST Caching**: The `Validator` struct immutably owns the `Database` registry. Schemas themselves are frozen structurally, but utilize `OnceLock` interior mutability during the Compilation Phase to permanently cache resolved `$ref` inheritances, properties, and `compiled_edges` directly onto their AST nodes. This guarantees strict `O(1)` relationship and property validation execution at runtime without locking or recursive DB polling.
|
3. **Immutable AST Caching**: The `Validator` struct immutably owns the `Database` registry. Schemas themselves are frozen structurally, but utilize `OnceLock` interior mutability during the Compilation Phase to permanently cache resolved `$ref` inheritances, properties, and `compiled_edges` directly onto their AST nodes. This guarantees strict `O(1)` relationship and property validation execution at runtime without locking or recursive DB polling.
|
||||||
4. **Lock-Free Reads**: Incoming operations acquire a read lock just long enough to clone the `Arc` inside an `RwLock<Option<Arc<Validator>>>`, ensuring zero blocking during schema updates.
|
4. **Lock-Free Reads**: Incoming operations acquire a read lock just long enough to clone the `Arc` inside an `RwLock<Option<Arc<Validator>>>`, ensuring zero blocking during schema updates.
|
||||||
|
|
||||||
|
### Relational Edge Resolution
|
||||||
|
When compiling nested object graphs or arrays, the JSPG engine must dynamically infer which Postgres Foreign Key constraint correctly bridges the parent to the nested schema. It utilizes a strict 3-step hierarchical resolution:
|
||||||
|
1. **Direct Prefix Match**: If an explicitly prefixed Foreign Key (e.g. `fk_invoice_counterparty_entity` -> `prefix: "counterparty"`) matches the exact name of the requested schema property (e.g. `{"counterparty": {...}}`), it is instantly selected.
|
||||||
|
2. **Base Edge Fallback (1:M)**: If no explicit prefix directly matches the property name, the compiler filters for explicitly one remaining relation with a `null` prefix (e.g. `fk_invoice_line_invoice` -> `prefix: null`). A `null` prefix mathematically denotes the standard structural parent-child ownership edge (bypassing any M:M ambiguity) and is safely picked over explicit (but unmatched) property edges.
|
||||||
|
3. **Ambiguity Elimination (M:M)**: If multiple explicitly prefixed relations remain (which happens by design in Many-to-Many junction tables like `contact` utilizing `fk_relationship_source` and `fk_relationship_target`), the compiler uses a process of elimination. It checks which of the prefix names the child schema *natively consumes* as an outbound property (e.g. `contact` defines `{ "target": ... }`). It considers that prefix "used up" and mathematically deduces the *remaining* explicitly prefixed relation (`"source"`) must be the inbound link from the parent.
|
||||||
|
|
||||||
### Global API Reference
|
### Global API Reference
|
||||||
These functions operate on the global `GLOBAL_JSPG` engine instance and provide administrative boundaries:
|
These functions operate on the global `GLOBAL_JSPG` engine instance and provide administrative boundaries:
|
||||||
|
|
||||||
|
|||||||
@ -19,7 +19,7 @@
|
|||||||
{
|
{
|
||||||
"id": "22222222-2222-2222-2222-222222222222",
|
"id": "22222222-2222-2222-2222-222222222222",
|
||||||
"type": "relation",
|
"type": "relation",
|
||||||
"constraint": "fk_order_customer",
|
"constraint": "fk_order_customer_person",
|
||||||
"source_type": "order",
|
"source_type": "order",
|
||||||
"source_columns": [
|
"source_columns": [
|
||||||
"customer_id"
|
"customer_id"
|
||||||
@ -41,8 +41,7 @@
|
|||||||
"destination_type": "order",
|
"destination_type": "order",
|
||||||
"destination_columns": [
|
"destination_columns": [
|
||||||
"id"
|
"id"
|
||||||
],
|
]
|
||||||
"prefix": "lines"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "44444444-4444-4444-4444-444444444444",
|
"id": "44444444-4444-4444-4444-444444444444",
|
||||||
@ -75,6 +74,20 @@
|
|||||||
"type"
|
"type"
|
||||||
],
|
],
|
||||||
"prefix": "target"
|
"prefix": "target"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "66666666-6666-6666-6666-666666666666",
|
||||||
|
"type": "relation",
|
||||||
|
"constraint": "fk_entity_organization",
|
||||||
|
"source_type": "entity",
|
||||||
|
"source_columns": [
|
||||||
|
"organization_id"
|
||||||
|
],
|
||||||
|
"destination_type": "organization",
|
||||||
|
"destination_columns": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"prefix": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"types": [
|
"types": [
|
||||||
@ -283,6 +296,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"email_addresses": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "contact",
|
||||||
|
"properties": {
|
||||||
|
"target": {
|
||||||
|
"$ref": "email_address"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1834,16 +1858,18 @@
|
|||||||
"type": "contact",
|
"type": "contact",
|
||||||
"is_primary": false,
|
"is_primary": false,
|
||||||
"target": {
|
"target": {
|
||||||
"type": "phone_number",
|
"type": "email_address",
|
||||||
"number": "555-0002"
|
"address": "test@example.com"
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
],
|
||||||
|
"email_addresses": [
|
||||||
{
|
{
|
||||||
"type": "contact",
|
"type": "contact",
|
||||||
"is_primary": false,
|
"is_primary": false,
|
||||||
"target": {
|
"target": {
|
||||||
"type": "email_address",
|
"type": "email_address",
|
||||||
"address": "test@example.com"
|
"address": "test2@example.com"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -1935,7 +1961,10 @@
|
|||||||
" modified_by",
|
" modified_by",
|
||||||
") VALUES (",
|
") VALUES (",
|
||||||
" NULL,",
|
" NULL,",
|
||||||
" '{\"number\":\"555-0001\",\"type\":\"phone_number\"}',",
|
" '{",
|
||||||
|
" \"number\":\"555-0001\",",
|
||||||
|
" \"type\":\"phone_number\"",
|
||||||
|
" }',",
|
||||||
" '{{uuid:phone1_id}}',",
|
" '{{uuid:phone1_id}}',",
|
||||||
" '{{uuid}}',",
|
" '{{uuid}}',",
|
||||||
" 'create',",
|
" 'create',",
|
||||||
@ -2006,115 +2035,6 @@
|
|||||||
" '00000000-0000-0000-0000-000000000000'",
|
" '00000000-0000-0000-0000-000000000000'",
|
||||||
")"
|
")"
|
||||||
],
|
],
|
||||||
[
|
|
||||||
"INSERT INTO agreego.\"entity\" (",
|
|
||||||
" \"created_at\",",
|
|
||||||
" \"created_by\",",
|
|
||||||
" \"id\",",
|
|
||||||
" \"modified_at\",",
|
|
||||||
" \"modified_by\",",
|
|
||||||
" \"type\"",
|
|
||||||
") VALUES (",
|
|
||||||
" '{{timestamp}}',",
|
|
||||||
" '00000000-0000-0000-0000-000000000000',",
|
|
||||||
" '{{uuid:phone2_id}}',",
|
|
||||||
" '{{timestamp}}',",
|
|
||||||
" '00000000-0000-0000-0000-000000000000',",
|
|
||||||
" 'phone_number'",
|
|
||||||
")"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"INSERT INTO agreego.\"phone_number\" (",
|
|
||||||
" \"number\"",
|
|
||||||
") VALUES (",
|
|
||||||
" '555-0002'",
|
|
||||||
")"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"INSERT INTO agreego.change (",
|
|
||||||
" \"old\",",
|
|
||||||
" \"new\",",
|
|
||||||
" entity_id,",
|
|
||||||
" id,",
|
|
||||||
" kind,",
|
|
||||||
" modified_at,",
|
|
||||||
" modified_by",
|
|
||||||
") VALUES (",
|
|
||||||
" NULL,",
|
|
||||||
" '{",
|
|
||||||
" \"number\":\"555-0002\",",
|
|
||||||
" \"type\":\"phone_number\"",
|
|
||||||
" }',",
|
|
||||||
" '{{uuid:phone2_id}}',",
|
|
||||||
" '{{uuid}}',",
|
|
||||||
" 'create',",
|
|
||||||
" '{{timestamp}}',",
|
|
||||||
" '00000000-0000-0000-0000-000000000000'",
|
|
||||||
")"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"INSERT INTO agreego.\"entity\" (",
|
|
||||||
" \"created_at\",",
|
|
||||||
" \"created_by\",",
|
|
||||||
" \"id\",",
|
|
||||||
" \"modified_at\",",
|
|
||||||
" \"modified_by\",",
|
|
||||||
" \"type\"",
|
|
||||||
") VALUES (",
|
|
||||||
" '{{timestamp}}',",
|
|
||||||
" '00000000-0000-0000-0000-000000000000',",
|
|
||||||
" '{{uuid:contact2_id}}',",
|
|
||||||
" '{{timestamp}}',",
|
|
||||||
" '00000000-0000-0000-0000-000000000000',",
|
|
||||||
" 'contact'",
|
|
||||||
")"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"INSERT INTO agreego.\"relationship\" (",
|
|
||||||
" \"source_id\",",
|
|
||||||
" \"source_type\",",
|
|
||||||
" \"target_id\",",
|
|
||||||
" \"target_type\"",
|
|
||||||
") VALUES (",
|
|
||||||
" '{{uuid:person_id}}',",
|
|
||||||
" 'person',",
|
|
||||||
" '{{uuid:phone2_id}}',",
|
|
||||||
" 'phone_number'",
|
|
||||||
")"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"INSERT INTO agreego.\"contact\" (",
|
|
||||||
" \"is_primary\"",
|
|
||||||
") VALUES (",
|
|
||||||
" false",
|
|
||||||
")"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"INSERT INTO agreego.change (",
|
|
||||||
" \"old\",",
|
|
||||||
" \"new\",",
|
|
||||||
" entity_id,",
|
|
||||||
" id,",
|
|
||||||
" kind,",
|
|
||||||
" modified_at,",
|
|
||||||
" modified_by",
|
|
||||||
") VALUES (",
|
|
||||||
" NULL,",
|
|
||||||
" '{",
|
|
||||||
" \"is_primary\":false,",
|
|
||||||
" \"source_id\":\"{{uuid:person_id}}\",",
|
|
||||||
" \"source_type\":\"person\",",
|
|
||||||
" \"target_id\":\"{{uuid:phone2_id}}\",",
|
|
||||||
" \"target_type\":\"phone_number\",",
|
|
||||||
" \"type\":\"contact\"",
|
|
||||||
" }',",
|
|
||||||
" '{{uuid:contact2_id}}',",
|
|
||||||
" '{{uuid}}',",
|
|
||||||
" 'create',",
|
|
||||||
" '{{timestamp}}',",
|
|
||||||
" '00000000-0000-0000-0000-000000000000'",
|
|
||||||
")"
|
|
||||||
],
|
|
||||||
[
|
[
|
||||||
"INSERT INTO agreego.\"entity\" (",
|
"INSERT INTO agreego.\"entity\" (",
|
||||||
" \"created_at\",",
|
" \"created_at\",",
|
||||||
@ -2172,7 +2092,7 @@
|
|||||||
") VALUES (",
|
") VALUES (",
|
||||||
" '{{timestamp}}',",
|
" '{{timestamp}}',",
|
||||||
" '00000000-0000-0000-0000-000000000000',",
|
" '00000000-0000-0000-0000-000000000000',",
|
||||||
" '{{uuid:contact3_id}}',",
|
" '{{uuid:contact2_id}}',",
|
||||||
" '{{timestamp}}',",
|
" '{{timestamp}}',",
|
||||||
" '00000000-0000-0000-0000-000000000000',",
|
" '00000000-0000-0000-0000-000000000000',",
|
||||||
" 'contact'",
|
" 'contact'",
|
||||||
@ -2217,6 +2137,115 @@
|
|||||||
" \"target_type\":\"email_address\",",
|
" \"target_type\":\"email_address\",",
|
||||||
" \"type\":\"contact\"",
|
" \"type\":\"contact\"",
|
||||||
" }',",
|
" }',",
|
||||||
|
" '{{uuid:contact2_id}}',",
|
||||||
|
" '{{uuid}}',",
|
||||||
|
" 'create',",
|
||||||
|
" '{{timestamp}}',",
|
||||||
|
" '00000000-0000-0000-0000-000000000000'",
|
||||||
|
")"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"INSERT INTO agreego.\"entity\" (",
|
||||||
|
" \"created_at\",",
|
||||||
|
" \"created_by\",",
|
||||||
|
" \"id\",",
|
||||||
|
" \"modified_at\",",
|
||||||
|
" \"modified_by\",",
|
||||||
|
" \"type\"",
|
||||||
|
") VALUES (",
|
||||||
|
" '{{timestamp}}',",
|
||||||
|
" '00000000-0000-0000-0000-000000000000',",
|
||||||
|
" '{{uuid:email2_id}}',",
|
||||||
|
" '{{timestamp}}',",
|
||||||
|
" '00000000-0000-0000-0000-000000000000',",
|
||||||
|
" 'email_address'",
|
||||||
|
")"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"INSERT INTO agreego.\"email_address\" (",
|
||||||
|
" \"address\"",
|
||||||
|
") VALUES (",
|
||||||
|
" 'test2@example.com'",
|
||||||
|
")"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"INSERT INTO agreego.change (",
|
||||||
|
" \"old\",",
|
||||||
|
" \"new\",",
|
||||||
|
" entity_id,",
|
||||||
|
" id,",
|
||||||
|
" kind,",
|
||||||
|
" modified_at,",
|
||||||
|
" modified_by",
|
||||||
|
") VALUES (",
|
||||||
|
" NULL,",
|
||||||
|
" '{",
|
||||||
|
" \"address\":\"test2@example.com\",",
|
||||||
|
" \"type\":\"email_address\"",
|
||||||
|
" }',",
|
||||||
|
" '{{uuid:email2_id}}',",
|
||||||
|
" '{{uuid}}',",
|
||||||
|
" 'create',",
|
||||||
|
" '{{timestamp}}',",
|
||||||
|
" '00000000-0000-0000-0000-000000000000'",
|
||||||
|
")"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"INSERT INTO agreego.\"entity\" (",
|
||||||
|
" \"created_at\",",
|
||||||
|
" \"created_by\",",
|
||||||
|
" \"id\",",
|
||||||
|
" \"modified_at\",",
|
||||||
|
" \"modified_by\",",
|
||||||
|
" \"type\"",
|
||||||
|
") VALUES (",
|
||||||
|
" '{{timestamp}}',",
|
||||||
|
" '00000000-0000-0000-0000-000000000000',",
|
||||||
|
" '{{uuid:contact3_id}}',",
|
||||||
|
" '{{timestamp}}',",
|
||||||
|
" '00000000-0000-0000-0000-000000000000',",
|
||||||
|
" 'contact'",
|
||||||
|
")"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"INSERT INTO agreego.\"relationship\" (",
|
||||||
|
" \"source_id\",",
|
||||||
|
" \"source_type\",",
|
||||||
|
" \"target_id\",",
|
||||||
|
" \"target_type\"",
|
||||||
|
") VALUES (",
|
||||||
|
" '{{uuid:person_id}}',",
|
||||||
|
" 'person',",
|
||||||
|
" '{{uuid:email2_id}}',",
|
||||||
|
" 'email_address'",
|
||||||
|
")"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"INSERT INTO agreego.\"contact\" (",
|
||||||
|
" \"is_primary\"",
|
||||||
|
") VALUES (",
|
||||||
|
" false",
|
||||||
|
")"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"INSERT INTO agreego.change (",
|
||||||
|
" \"old\",",
|
||||||
|
" \"new\",",
|
||||||
|
" entity_id,",
|
||||||
|
" id,",
|
||||||
|
" kind,",
|
||||||
|
" modified_at,",
|
||||||
|
" modified_by",
|
||||||
|
") VALUES (",
|
||||||
|
" NULL,",
|
||||||
|
" '{",
|
||||||
|
" \"is_primary\":false,",
|
||||||
|
" \"source_id\":\"{{uuid:person_id}}\",",
|
||||||
|
" \"source_type\":\"person\",",
|
||||||
|
" \"target_id\":\"{{uuid:email2_id}}\",",
|
||||||
|
" \"target_type\":\"email_address\",",
|
||||||
|
" \"type\":\"contact\"",
|
||||||
|
" }',",
|
||||||
" '{{uuid:contact3_id}}',",
|
" '{{uuid:contact3_id}}',",
|
||||||
" '{{uuid}}',",
|
" '{{uuid}}',",
|
||||||
" 'create',",
|
" 'create',",
|
||||||
@ -2249,16 +2278,16 @@
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
"SELECT pg_notify('entity', '{",
|
"SELECT pg_notify('entity', '{",
|
||||||
" \"complete\":{",
|
" \"complete\":{",
|
||||||
" \"created_at\":\"{{timestamp}}\",",
|
" \"created_at\":\"{{timestamp}}\",",
|
||||||
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
|
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||||
" \"first_name\":\"Relation\",",
|
" \"first_name\":\"Relation\",",
|
||||||
" \"id\":\"{{uuid:person_id}}\",",
|
" \"id\":\"{{uuid:person_id}}\",",
|
||||||
" \"last_name\":\"Test\",",
|
" \"last_name\":\"Test\",",
|
||||||
" \"modified_at\":\"{{timestamp}}\",",
|
" \"modified_at\":\"{{timestamp}}\",",
|
||||||
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
|
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||||
" \"type\":\"person\"",
|
" \"type\":\"person\"",
|
||||||
" },",
|
" },",
|
||||||
" \"new\":{",
|
" \"new\":{",
|
||||||
" \"first_name\":\"Relation\",",
|
" \"first_name\":\"Relation\",",
|
||||||
" \"last_name\":\"Test\",",
|
" \"last_name\":\"Test\",",
|
||||||
@ -2268,19 +2297,19 @@
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
"SELECT pg_notify('entity', '{",
|
"SELECT pg_notify('entity', '{",
|
||||||
" \"complete\":{",
|
" \"complete\":{",
|
||||||
" \"created_at\":\"{{timestamp}}\",",
|
" \"created_at\":\"{{timestamp}}\",",
|
||||||
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
|
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||||
" \"id\":\"{{uuid:contact1_id}}\",",
|
" \"id\":\"{{uuid:contact1_id}}\",",
|
||||||
" \"is_primary\":true,",
|
" \"is_primary\":true,",
|
||||||
" \"modified_at\":\"{{timestamp}}\",",
|
" \"modified_at\":\"{{timestamp}}\",",
|
||||||
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
|
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||||
" \"source_id\":\"{{uuid:person_id}}\",",
|
" \"source_id\":\"{{uuid:person_id}}\",",
|
||||||
" \"source_type\":\"person\",",
|
" \"source_type\":\"person\",",
|
||||||
" \"target_id\":\"{{uuid:phone1_id}}\",",
|
" \"target_id\":\"{{uuid:phone1_id}}\",",
|
||||||
" \"target_type\":\"phone_number\",",
|
" \"target_type\":\"phone_number\",",
|
||||||
" \"type\":\"contact\"",
|
" \"type\":\"contact\"",
|
||||||
" },",
|
" },",
|
||||||
" \"new\":{",
|
" \"new\":{",
|
||||||
" \"is_primary\":true,",
|
" \"is_primary\":true,",
|
||||||
" \"source_id\":\"{{uuid:person_id}}\",",
|
" \"source_id\":\"{{uuid:person_id}}\",",
|
||||||
@ -2293,15 +2322,15 @@
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
"SELECT pg_notify('entity', '{",
|
"SELECT pg_notify('entity', '{",
|
||||||
" \"complete\":{",
|
" \"complete\":{",
|
||||||
" \"created_at\":\"{{timestamp}}\",",
|
" \"created_at\":\"{{timestamp}}\",",
|
||||||
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
|
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||||
" \"id\":\"{{uuid:phone1_id}}\",",
|
" \"id\":\"{{uuid:phone1_id}}\",",
|
||||||
" \"modified_at\":\"{{timestamp}}\",",
|
" \"modified_at\":\"{{timestamp}}\",",
|
||||||
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
|
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||||
" \"number\":\"555-0001\",",
|
" \"number\":\"555-0001\",",
|
||||||
" \"type\":\"phone_number\"",
|
" \"type\":\"phone_number\"",
|
||||||
" },",
|
" },",
|
||||||
" \"new\":{",
|
" \"new\":{",
|
||||||
" \"number\":\"555-0001\",",
|
" \"number\":\"555-0001\",",
|
||||||
" \"type\":\"phone_number\"",
|
" \"type\":\"phone_number\"",
|
||||||
@ -2310,87 +2339,87 @@
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
"SELECT pg_notify('entity', '{",
|
"SELECT pg_notify('entity', '{",
|
||||||
" \"complete\":{",
|
" \"complete\":{",
|
||||||
" \"created_at\":\"{{timestamp}}\",",
|
" \"created_at\":\"{{timestamp}}\",",
|
||||||
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
|
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||||
" \"id\":\"{{uuid:contact2_id}}\",",
|
" \"id\":\"{{uuid:contact2_id}}\",",
|
||||||
" \"is_primary\":false,",
|
" \"is_primary\":false,",
|
||||||
" \"modified_at\":\"{{timestamp}}\",",
|
" \"modified_at\":\"{{timestamp}}\",",
|
||||||
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
|
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||||
" \"source_id\":\"{{uuid:person_id}}\",",
|
" \"source_id\":\"{{uuid:person_id}}\",",
|
||||||
" \"source_type\":\"person\",",
|
" \"source_type\":\"person\",",
|
||||||
" \"target_id\":\"{{uuid:phone2_id}}\",",
|
" \"target_id\":\"{{uuid:email1_id}}\",",
|
||||||
" \"target_type\":\"phone_number\",",
|
" \"target_type\":\"email_address\",",
|
||||||
" \"type\":\"contact\"",
|
" \"type\":\"contact\"",
|
||||||
" },",
|
" },",
|
||||||
" \"new\":{",
|
" \"new\":{",
|
||||||
" \"is_primary\":false,",
|
" \"is_primary\":false,",
|
||||||
" \"source_id\":\"{{uuid:person_id}}\",",
|
" \"source_id\":\"{{uuid:person_id}}\",",
|
||||||
" \"source_type\":\"person\",",
|
" \"source_type\":\"person\",",
|
||||||
" \"target_id\":\"{{uuid:phone2_id}}\",",
|
" \"target_id\":\"{{uuid:email1_id}}\",",
|
||||||
" \"target_type\":\"phone_number\",",
|
" \"target_type\":\"email_address\",",
|
||||||
" \"type\":\"contact\"",
|
" \"type\":\"contact\"",
|
||||||
" }",
|
" }",
|
||||||
" }')"
|
"}')"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"SELECT pg_notify('entity', '{",
|
"SELECT pg_notify('entity', '{",
|
||||||
" \"complete\":{",
|
" \"complete\":{",
|
||||||
" \"created_at\":\"{{timestamp}}\",",
|
" \"address\":\"test@example.com\",",
|
||||||
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
|
" \"created_at\":\"{{timestamp}}\",",
|
||||||
" \"id\":\"{{uuid:phone2_id}}\",",
|
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||||
" \"modified_at\":\"{{timestamp}}\",",
|
" \"id\":\"{{uuid:email1_id}}\",",
|
||||||
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
|
" \"modified_at\":\"{{timestamp}}\",",
|
||||||
" \"number\":\"555-0002\",",
|
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||||
" \"type\":\"phone_number\"",
|
" \"type\":\"email_address\"",
|
||||||
" },",
|
" },",
|
||||||
" \"new\":{",
|
" \"new\":{",
|
||||||
" \"number\":\"555-0002\",",
|
" \"address\":\"test@example.com\",",
|
||||||
" \"type\":\"phone_number\"",
|
" \"type\":\"email_address\"",
|
||||||
" }",
|
" }",
|
||||||
" }')"
|
"}')"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"SELECT pg_notify('entity', '{",
|
"SELECT pg_notify('entity', '{",
|
||||||
" \"complete\":{",
|
" \"complete\":{",
|
||||||
" \"created_at\":\"{{timestamp}}\",",
|
" \"created_at\":\"{{timestamp}}\",",
|
||||||
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
|
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||||
" \"id\":\"{{uuid:contact3_id}}\",",
|
" \"id\":\"{{uuid:contact3_id}}\",",
|
||||||
" \"is_primary\":false,",
|
" \"is_primary\":false,",
|
||||||
" \"modified_at\":\"{{timestamp}}\",",
|
" \"modified_at\":\"{{timestamp}}\",",
|
||||||
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
|
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||||
" \"source_id\":\"{{uuid:person_id}}\",",
|
" \"source_id\":\"{{uuid:person_id}}\",",
|
||||||
" \"source_type\":\"person\",",
|
" \"source_type\":\"person\",",
|
||||||
" \"target_id\":\"{{uuid:email1_id}}\",",
|
" \"target_id\":\"{{uuid:email2_id}}\",",
|
||||||
" \"target_type\":\"email_address\",",
|
" \"target_type\":\"email_address\",",
|
||||||
" \"type\":\"contact\"",
|
" \"type\":\"contact\"",
|
||||||
" },",
|
" },",
|
||||||
" \"new\":{",
|
" \"new\":{",
|
||||||
" \"is_primary\":false,",
|
" \"is_primary\":false,",
|
||||||
" \"source_id\":\"{{uuid:person_id}}\",",
|
" \"source_id\":\"{{uuid:person_id}}\",",
|
||||||
" \"source_type\":\"person\",",
|
" \"source_type\":\"person\",",
|
||||||
" \"target_id\":\"{{uuid:email1_id}}\",",
|
" \"target_id\":\"{{uuid:email2_id}}\",",
|
||||||
" \"target_type\":\"email_address\",",
|
" \"target_type\":\"email_address\",",
|
||||||
" \"type\":\"contact\"",
|
" \"type\":\"contact\"",
|
||||||
" }",
|
" }",
|
||||||
" }')"
|
"}')"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"SELECT pg_notify('entity', '{",
|
"SELECT pg_notify('entity', '{",
|
||||||
" \"complete\":{",
|
" \"complete\":{",
|
||||||
" \"address\":\"test@example.com\",",
|
" \"address\":\"test2@example.com\",",
|
||||||
" \"created_at\":\"{{timestamp}}\",",
|
" \"created_at\":\"{{timestamp}}\",",
|
||||||
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
|
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||||
" \"id\":\"{{uuid:email1_id}}\",",
|
" \"id\":\"{{uuid:email2_id}}\",",
|
||||||
" \"modified_at\":\"{{timestamp}}\",",
|
" \"modified_at\":\"{{timestamp}}\",",
|
||||||
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
|
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||||
" \"type\":\"email_address\"",
|
" \"type\":\"email_address\"",
|
||||||
" },",
|
" },",
|
||||||
" \"new\":{",
|
" \"new\":{",
|
||||||
" \"address\":\"test@example.com\",",
|
" \"address\":\"test2@example.com\",",
|
||||||
" \"type\":\"email_address\"",
|
" \"type\":\"email_address\"",
|
||||||
" }",
|
" }",
|
||||||
" }')"
|
"}')"
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,7 +27,9 @@
|
|||||||
{
|
{
|
||||||
"$id": "get_orders.response",
|
"$id": "get_orders.response",
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": { "$ref": "light.order" }
|
"items": {
|
||||||
|
"$ref": "light.order"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -69,7 +71,7 @@
|
|||||||
{
|
{
|
||||||
"id": "22222222-2222-2222-2222-222222222222",
|
"id": "22222222-2222-2222-2222-222222222222",
|
||||||
"type": "relation",
|
"type": "relation",
|
||||||
"constraint": "fk_order_customer",
|
"constraint": "fk_order_customer_person",
|
||||||
"source_type": "order",
|
"source_type": "order",
|
||||||
"source_columns": [
|
"source_columns": [
|
||||||
"customer_id"
|
"customer_id"
|
||||||
@ -80,6 +82,22 @@
|
|||||||
],
|
],
|
||||||
"prefix": "customer"
|
"prefix": "customer"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "22222222-2222-2222-2222-222222222227",
|
||||||
|
"type": "relation",
|
||||||
|
"constraint": "fk_order_counterparty_entity",
|
||||||
|
"source_type": "order",
|
||||||
|
"source_columns": [
|
||||||
|
"counterparty_id",
|
||||||
|
"counterparty_type"
|
||||||
|
],
|
||||||
|
"destination_type": "entity",
|
||||||
|
"destination_columns": [
|
||||||
|
"id",
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"prefix": "counterparty"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "33333333-3333-3333-3333-333333333333",
|
"id": "33333333-3333-3333-3333-333333333333",
|
||||||
"type": "relation",
|
"type": "relation",
|
||||||
@ -91,8 +109,7 @@
|
|||||||
"destination_type": "order",
|
"destination_type": "order",
|
||||||
"destination_columns": [
|
"destination_columns": [
|
||||||
"id"
|
"id"
|
||||||
],
|
]
|
||||||
"prefix": "lines"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"types": [
|
"types": [
|
||||||
@ -713,14 +730,18 @@
|
|||||||
"created_by",
|
"created_by",
|
||||||
"modified_at",
|
"modified_at",
|
||||||
"modified_by",
|
"modified_by",
|
||||||
"archived"
|
"archived",
|
||||||
|
"counterparty_id",
|
||||||
|
"counterparty_type"
|
||||||
],
|
],
|
||||||
"grouped_fields": {
|
"grouped_fields": {
|
||||||
"order": [
|
"order": [
|
||||||
"id",
|
"id",
|
||||||
"type",
|
"type",
|
||||||
"total",
|
"total",
|
||||||
"customer_id"
|
"customer_id",
|
||||||
|
"counterparty_id",
|
||||||
|
"counterparty_type"
|
||||||
],
|
],
|
||||||
"entity": [
|
"entity": [
|
||||||
"id",
|
"id",
|
||||||
@ -748,7 +769,9 @@
|
|||||||
"created_at": "timestamptz",
|
"created_at": "timestamptz",
|
||||||
"created_by": "uuid",
|
"created_by": "uuid",
|
||||||
"modified_at": "timestamptz",
|
"modified_at": "timestamptz",
|
||||||
"modified_by": "uuid"
|
"modified_by": "uuid",
|
||||||
|
"counterparty_id": "uuid",
|
||||||
|
"counterparty_type": "text"
|
||||||
},
|
},
|
||||||
"variations": [
|
"variations": [
|
||||||
"order"
|
"order"
|
||||||
|
|||||||
@ -79,9 +79,9 @@ impl DatabaseExecutor for SpiExecutor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pgrx::debug1!("JSPG_SQL: {}", sql);
|
||||||
self.transact(|| {
|
self.transact(|| {
|
||||||
Spi::connect(|client| {
|
Spi::connect(|client| {
|
||||||
pgrx::notice!("JSPG_SQL: {}", sql);
|
|
||||||
match client.select(sql, Some(args_with_oid.len() as i64), &args_with_oid) {
|
match client.select(sql, Some(args_with_oid.len() as i64), &args_with_oid) {
|
||||||
Ok(tup_table) => {
|
Ok(tup_table) => {
|
||||||
let mut results = Vec::new();
|
let mut results = Vec::new();
|
||||||
@ -110,9 +110,9 @@ impl DatabaseExecutor for SpiExecutor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pgrx::debug1!("JSPG_SQL: {}", sql);
|
||||||
self.transact(|| {
|
self.transact(|| {
|
||||||
Spi::connect_mut(|client| {
|
Spi::connect_mut(|client| {
|
||||||
pgrx::notice!("JSPG_SQL: {}", sql);
|
|
||||||
match client.update(sql, Some(args_with_oid.len() as i64), &args_with_oid) {
|
match client.update(sql, Some(args_with_oid.len() as i64), &args_with_oid) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(e) => Err(format!("SPI Execution Failure: {}", e)),
|
Err(e) => Err(format!("SPI Execution Failure: {}", e)),
|
||||||
|
|||||||
@ -508,7 +508,7 @@ impl Schema {
|
|||||||
if let Some(family) = &self.obj.family {
|
if let Some(family) = &self.obj.family {
|
||||||
parent_type_name = Some(family.split('.').next_back().unwrap_or(family).to_string());
|
parent_type_name = Some(family.split('.').next_back().unwrap_or(family).to_string());
|
||||||
} else if let Some(identifier) = self.obj.identifier() {
|
} else if let Some(identifier) = self.obj.identifier() {
|
||||||
parent_type_name = Some(identifier);
|
parent_type_name = Some(identifier.split('.').next_back().unwrap_or(&identifier).to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(p_type) = parent_type_name {
|
if let Some(p_type) = parent_type_name {
|
||||||
@ -530,11 +530,11 @@ impl Schema {
|
|||||||
if let Some(family) = &target_schema.obj.family {
|
if let Some(family) = &target_schema.obj.family {
|
||||||
child_type_name = Some(family.split('.').next_back().unwrap_or(family).to_string());
|
child_type_name = Some(family.split('.').next_back().unwrap_or(family).to_string());
|
||||||
} else if let Some(ref_id) = target_schema.obj.identifier() {
|
} else if let Some(ref_id) = target_schema.obj.identifier() {
|
||||||
child_type_name = Some(ref_id);
|
child_type_name = Some(ref_id.split('.').next_back().unwrap_or(&ref_id).to_string());
|
||||||
} else if let Some(arr) = &target_schema.obj.one_of {
|
} else if let Some(arr) = &target_schema.obj.one_of {
|
||||||
if let Some(first) = arr.first() {
|
if let Some(first) = arr.first() {
|
||||||
if let Some(ref_id) = first.obj.identifier() {
|
if let Some(ref_id) = first.obj.identifier() {
|
||||||
child_type_name = Some(ref_id);
|
child_type_name = Some(ref_id.split('.').next_back().unwrap_or(&ref_id).to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -623,17 +623,51 @@ pub(crate) fn resolve_relation<'a>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !resolved && relative_keys.is_some() {
|
if !resolved && relative_keys.is_some() {
|
||||||
|
// 1. M:M Disambiguation: The child schema explicitly defines an outbound property
|
||||||
|
// matching one of the relational prefixes (e.g. "target"). We first identify that consumed relation.
|
||||||
let keys = relative_keys.unwrap();
|
let keys = relative_keys.unwrap();
|
||||||
let mut missing_prefix_ids = Vec::new();
|
let mut consumed_rel_idx = None;
|
||||||
for (i, rel) in matching_rels.iter().enumerate() {
|
for (i, rel) in matching_rels.iter().enumerate() {
|
||||||
if let Some(prefix) = &rel.prefix {
|
if let Some(prefix) = &rel.prefix {
|
||||||
if !keys.contains(prefix) {
|
if keys.contains(prefix) {
|
||||||
missing_prefix_ids.push(i);
|
consumed_rel_idx = Some(i);
|
||||||
|
break; // Found the routing edge explicitly consumed by the schema payload
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if missing_prefix_ids.len() == 1 {
|
|
||||||
chosen_idx = missing_prefix_ids[0];
|
// Then, we find its exact Twin on the same junction boundary that provides the reverse ownership.
|
||||||
|
if let Some(used_idx) = consumed_rel_idx {
|
||||||
|
let used_rel = matching_rels[used_idx];
|
||||||
|
let mut twin_ids = Vec::new();
|
||||||
|
for (i, rel) in matching_rels.iter().enumerate() {
|
||||||
|
if i != used_idx
|
||||||
|
&& rel.source_type == used_rel.source_type
|
||||||
|
&& rel.destination_type == used_rel.destination_type
|
||||||
|
&& rel.prefix.is_some()
|
||||||
|
{
|
||||||
|
twin_ids.push(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if twin_ids.len() == 1 {
|
||||||
|
chosen_idx = twin_ids[0];
|
||||||
|
resolved = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !resolved {
|
||||||
|
// 2. Base 1:M Fallback. If there's EXACTLY ONE relation with a null prefix, it's the base structural edge.
|
||||||
|
let mut null_prefix_ids = Vec::new();
|
||||||
|
for (i, rel) in matching_rels.iter().enumerate() {
|
||||||
|
if rel.prefix.is_none() {
|
||||||
|
null_prefix_ids.push(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if null_prefix_ids.len() == 1 {
|
||||||
|
chosen_idx = null_prefix_ids[0];
|
||||||
|
// resolved = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,8 +3,8 @@
|
|||||||
|
|
||||||
pub mod cache;
|
pub mod cache;
|
||||||
|
|
||||||
use crate::database::r#type::Type;
|
|
||||||
use crate::database::Database;
|
use crate::database::Database;
|
||||||
|
use crate::database::r#type::Type;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
@ -25,19 +25,19 @@ impl Merger {
|
|||||||
let mut notifications_queue = Vec::new();
|
let mut notifications_queue = Vec::new();
|
||||||
|
|
||||||
let target_schema = match self.db.schemas.get(schema_id) {
|
let target_schema = match self.db.schemas.get(schema_id) {
|
||||||
Some(s) => Arc::new(s.clone()),
|
Some(s) => Arc::new(s.clone()),
|
||||||
None => {
|
None => {
|
||||||
return crate::drop::Drop::with_errors(vec![crate::drop::Error {
|
return crate::drop::Drop::with_errors(vec![crate::drop::Error {
|
||||||
code: "MERGE_FAILED".to_string(),
|
code: "MERGE_FAILED".to_string(),
|
||||||
message: format!("Unknown schema_id: {}", schema_id),
|
message: format!("Unknown schema_id: {}", schema_id),
|
||||||
details: crate::drop::ErrorDetails {
|
details: crate::drop::ErrorDetails {
|
||||||
path: "".to_string(),
|
path: "".to_string(),
|
||||||
cause: None,
|
cause: None,
|
||||||
context: Some(data),
|
context: Some(data),
|
||||||
schema: None,
|
schema: None,
|
||||||
},
|
},
|
||||||
}]);
|
}]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = self.merge_internal(target_schema, data.clone(), &mut notifications_queue);
|
let result = self.merge_internal(target_schema, data.clone(), &mut notifications_queue);
|
||||||
@ -50,18 +50,24 @@ impl Merger {
|
|||||||
let mut final_cause = None;
|
let mut final_cause = None;
|
||||||
|
|
||||||
if let Ok(Value::Object(map)) = serde_json::from_str::<Value>(&msg) {
|
if let Ok(Value::Object(map)) = serde_json::from_str::<Value>(&msg) {
|
||||||
if let (Some(Value::String(e_msg)), Some(Value::String(e_code))) = (map.get("error"), map.get("code")) {
|
if let (Some(Value::String(e_msg)), Some(Value::String(e_code))) =
|
||||||
|
(map.get("error"), map.get("code"))
|
||||||
|
{
|
||||||
final_message = e_msg.clone();
|
final_message = e_msg.clone();
|
||||||
final_code = e_code.clone();
|
final_code = e_code.clone();
|
||||||
let mut cause_parts = Vec::new();
|
let mut cause_parts = Vec::new();
|
||||||
if let Some(Value::String(d)) = map.get("detail") {
|
if let Some(Value::String(d)) = map.get("detail") {
|
||||||
if !d.is_empty() { cause_parts.push(d.clone()); }
|
if !d.is_empty() {
|
||||||
|
cause_parts.push(d.clone());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if let Some(Value::String(h)) = map.get("hint") {
|
if let Some(Value::String(h)) = map.get("hint") {
|
||||||
if !h.is_empty() { cause_parts.push(h.clone()); }
|
if !h.is_empty() {
|
||||||
|
cause_parts.push(h.clone());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if !cause_parts.is_empty() {
|
if !cause_parts.is_empty() {
|
||||||
final_cause = Some(cause_parts.join("\n"));
|
final_cause = Some(cause_parts.join("\n"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -144,11 +150,11 @@ impl Merger {
|
|||||||
) -> Result<Value, String> {
|
) -> Result<Value, String> {
|
||||||
let mut item_schema = schema.clone();
|
let mut item_schema = schema.clone();
|
||||||
if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = &schema.obj.type_ {
|
if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = &schema.obj.type_ {
|
||||||
if t == "array" {
|
if t == "array" {
|
||||||
if let Some(items_def) = &schema.obj.items {
|
if let Some(items_def) = &schema.obj.items {
|
||||||
item_schema = items_def.clone();
|
item_schema = items_def.clone();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut resolved_items = Vec::new();
|
let mut resolved_items = Vec::new();
|
||||||
@ -178,8 +184,8 @@ impl Merger {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let compiled_props = match schema.obj.compiled_properties.get() {
|
let compiled_props = match schema.obj.compiled_properties.get() {
|
||||||
Some(props) => props,
|
Some(props) => props,
|
||||||
None => return Err("Schema has no compiled properties for merging".to_string()),
|
None => return Err("Schema has no compiled properties for merging".to_string()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut entity_fields = serde_json::Map::new();
|
let mut entity_fields = serde_json::Map::new();
|
||||||
@ -189,37 +195,37 @@ impl Merger {
|
|||||||
for (k, v) in obj {
|
for (k, v) in obj {
|
||||||
// Always retain system and unmapped core fields natively implicitly mapped to the Postgres tables
|
// Always retain system and unmapped core fields natively implicitly mapped to the Postgres tables
|
||||||
if k == "id" || k == "type" || k == "created" {
|
if k == "id" || k == "type" || k == "created" {
|
||||||
entity_fields.insert(k.clone(), v.clone());
|
entity_fields.insert(k.clone(), v.clone());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(prop_schema) = compiled_props.get(&k) {
|
if let Some(prop_schema) = compiled_props.get(&k) {
|
||||||
let mut is_edge = false;
|
let mut is_edge = false;
|
||||||
if let Some(edges) = schema.obj.compiled_edges.get() {
|
if let Some(edges) = schema.obj.compiled_edges.get() {
|
||||||
if edges.contains_key(&k) {
|
if edges.contains_key(&k) {
|
||||||
is_edge = true;
|
is_edge = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_edge {
|
if is_edge {
|
||||||
let typeof_v = match &v {
|
let typeof_v = match &v {
|
||||||
Value::Object(_) => "object",
|
Value::Object(_) => "object",
|
||||||
Value::Array(_) => "array",
|
Value::Array(_) => "array",
|
||||||
_ => "field", // Malformed edge data?
|
_ => "field", // Malformed edge data?
|
||||||
};
|
};
|
||||||
if typeof_v == "object" {
|
if typeof_v == "object" {
|
||||||
entity_objects.insert(k.clone(), (v.clone(), prop_schema.clone()));
|
entity_objects.insert(k.clone(), (v.clone(), prop_schema.clone()));
|
||||||
} else if typeof_v == "array" {
|
} else if typeof_v == "array" {
|
||||||
entity_arrays.insert(k.clone(), (v.clone(), prop_schema.clone()));
|
entity_arrays.insert(k.clone(), (v.clone(), prop_schema.clone()));
|
||||||
} else {
|
} else {
|
||||||
entity_fields.insert(k.clone(), v.clone());
|
entity_fields.insert(k.clone(), v.clone());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Not an edge! It's a raw Postgres column (e.g., JSONB, text[])
|
// Not an edge! It's a raw Postgres column (e.g., JSONB, text[])
|
||||||
entity_fields.insert(k.clone(), v.clone());
|
|
||||||
}
|
|
||||||
} else if type_def.fields.contains(&k) {
|
|
||||||
entity_fields.insert(k.clone(), v.clone());
|
entity_fields.insert(k.clone(), v.clone());
|
||||||
|
}
|
||||||
|
} else if type_def.fields.contains(&k) {
|
||||||
|
entity_fields.insert(k.clone(), v.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -253,12 +259,16 @@ impl Merger {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if let Some(compiled_edges) = schema.obj.compiled_edges.get() {
|
if let Some(compiled_edges) = schema.obj.compiled_edges.get() {
|
||||||
println!("Compiled Edges keys for relation {}: {:?}", relation_name, compiled_edges.keys().collect::<Vec<_>>());
|
println!(
|
||||||
|
"Compiled Edges keys for relation {}: {:?}",
|
||||||
|
relation_name,
|
||||||
|
compiled_edges.keys().collect::<Vec<_>>()
|
||||||
|
);
|
||||||
if let Some(edge) = compiled_edges.get(&relation_name) {
|
if let Some(edge) = compiled_edges.get(&relation_name) {
|
||||||
println!("FOUND EDGE {} -> {:?}", relation_name, edge.constraint);
|
println!("FOUND EDGE {} -> {:?}", relation_name, edge.constraint);
|
||||||
if let Some(relation) = self.db.relations.get(&edge.constraint) {
|
if let Some(relation) = self.db.relations.get(&edge.constraint) {
|
||||||
let parent_is_source = edge.forward;
|
let parent_is_source = edge.forward;
|
||||||
|
|
||||||
if parent_is_source {
|
if parent_is_source {
|
||||||
if !relative.contains_key("organization_id") {
|
if !relative.contains_key("organization_id") {
|
||||||
if let Some(org_id) = entity_fields.get("organization_id") {
|
if let Some(org_id) = entity_fields.get("organization_id") {
|
||||||
@ -266,15 +276,16 @@ impl Merger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut merged_relative = match self.merge_internal(rel_schema.clone(), Value::Object(relative), notifications)? {
|
let mut merged_relative = match self.merge_internal(
|
||||||
|
rel_schema.clone(),
|
||||||
|
Value::Object(relative),
|
||||||
|
notifications,
|
||||||
|
)? {
|
||||||
Value::Object(m) => m,
|
Value::Object(m) => m,
|
||||||
_ => continue,
|
_ => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
merged_relative.insert(
|
merged_relative.insert("type".to_string(), Value::String(relative_type_name));
|
||||||
"type".to_string(),
|
|
||||||
Value::String(relative_type_name),
|
|
||||||
);
|
|
||||||
|
|
||||||
Self::apply_entity_relation(
|
Self::apply_entity_relation(
|
||||||
&mut entity_fields,
|
&mut entity_fields,
|
||||||
@ -297,7 +308,11 @@ impl Merger {
|
|||||||
&entity_fields,
|
&entity_fields,
|
||||||
);
|
);
|
||||||
|
|
||||||
let merged_relative = match self.merge_internal(rel_schema.clone(), Value::Object(relative), notifications)? {
|
let merged_relative = match self.merge_internal(
|
||||||
|
rel_schema.clone(),
|
||||||
|
Value::Object(relative),
|
||||||
|
notifications,
|
||||||
|
)? {
|
||||||
Value::Object(m) => m,
|
Value::Object(m) => m,
|
||||||
_ => continue,
|
_ => continue,
|
||||||
};
|
};
|
||||||
@ -360,19 +375,24 @@ impl Merger {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let mut item_schema = rel_schema.clone();
|
let mut item_schema = rel_schema.clone();
|
||||||
if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = &rel_schema.obj.type_ {
|
if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) =
|
||||||
if t == "array" {
|
&rel_schema.obj.type_
|
||||||
if let Some(items_def) = &rel_schema.obj.items {
|
{
|
||||||
item_schema = items_def.clone();
|
if t == "array" {
|
||||||
}
|
if let Some(items_def) = &rel_schema.obj.items {
|
||||||
|
item_schema = items_def.clone();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let merged_relative =
|
let merged_relative = match self.merge_internal(
|
||||||
match self.merge_internal(item_schema, Value::Object(relative_item), notifications)? {
|
item_schema,
|
||||||
Value::Object(m) => m,
|
Value::Object(relative_item),
|
||||||
_ => continue,
|
notifications,
|
||||||
};
|
)? {
|
||||||
|
Value::Object(m) => m,
|
||||||
|
_ => continue,
|
||||||
|
};
|
||||||
|
|
||||||
relative_responses.push(Value::Object(merged_relative));
|
relative_responses.push(Value::Object(merged_relative));
|
||||||
}
|
}
|
||||||
@ -433,8 +453,8 @@ impl Merger {
|
|||||||
// An anchor is STRICTLY a struct containing merely an `id` and `type`.
|
// An anchor is STRICTLY a struct containing merely an `id` and `type`.
|
||||||
// We aggressively bypass Database SPI `SELECT` fetches because there are no primitive
|
// We aggressively bypass Database SPI `SELECT` fetches because there are no primitive
|
||||||
// mutations to apply to the row. PostgreSQL inherently protects relationships via Foreign Keys downstream.
|
// mutations to apply to the row. PostgreSQL inherently protects relationships via Foreign Keys downstream.
|
||||||
let is_anchor = entity_fields.len() == 2
|
let is_anchor = entity_fields.len() == 2
|
||||||
&& entity_fields.contains_key("id")
|
&& entity_fields.contains_key("id")
|
||||||
&& entity_fields.contains_key("type");
|
&& entity_fields.contains_key("type");
|
||||||
|
|
||||||
let has_valid_id = entity_fields
|
let has_valid_id = entity_fields
|
||||||
@ -450,13 +470,13 @@ impl Merger {
|
|||||||
|
|
||||||
let mut replaces_id = None;
|
let mut replaces_id = None;
|
||||||
if let Some(ref fetched_row) = entity_fetched {
|
if let Some(ref fetched_row) = entity_fetched {
|
||||||
let provided_id = entity_fields.get("id").and_then(|v| v.as_str());
|
let provided_id = entity_fields.get("id").and_then(|v| v.as_str());
|
||||||
let fetched_id = fetched_row.get("id").and_then(|v| v.as_str());
|
let fetched_id = fetched_row.get("id").and_then(|v| v.as_str());
|
||||||
if let (Some(pid), Some(fid)) = (provided_id, fetched_id) {
|
if let (Some(pid), Some(fid)) = (provided_id, fetched_id) {
|
||||||
if !pid.is_empty() && pid != fid {
|
if !pid.is_empty() && pid != fid {
|
||||||
replaces_id = Some(pid.to_string());
|
replaces_id = Some(pid.to_string());
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let system_keys = vec![
|
let system_keys = vec![
|
||||||
@ -548,7 +568,12 @@ impl Merger {
|
|||||||
entity_fields = new_fields;
|
entity_fields = new_fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok((entity_fields, entity_change_kind, entity_fetched, replaces_id))
|
Ok((
|
||||||
|
entity_fields,
|
||||||
|
entity_change_kind,
|
||||||
|
entity_fetched,
|
||||||
|
replaces_id,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fetch_entity(
|
fn fetch_entity(
|
||||||
@ -735,9 +760,7 @@ impl Merger {
|
|||||||
columns.join(", "),
|
columns.join(", "),
|
||||||
values.join(", ")
|
values.join(", ")
|
||||||
);
|
);
|
||||||
self
|
self.db.execute(&sql, None)?;
|
||||||
.db
|
|
||||||
.execute(&sql, None)?;
|
|
||||||
} else if change_kind == "update" || change_kind == "delete" {
|
} else if change_kind == "update" || change_kind == "delete" {
|
||||||
entity_pairs.remove("id");
|
entity_pairs.remove("id");
|
||||||
entity_pairs.remove("type");
|
entity_pairs.remove("type");
|
||||||
@ -769,9 +792,7 @@ impl Merger {
|
|||||||
set_clauses.join(", "),
|
set_clauses.join(", "),
|
||||||
Self::quote_literal(&Value::String(id_str.to_string()))
|
Self::quote_literal(&Value::String(id_str.to_string()))
|
||||||
);
|
);
|
||||||
self
|
self.db.execute(&sql, None)?;
|
||||||
.db
|
|
||||||
.execute(&sql, None)?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -857,13 +878,13 @@ impl Merger {
|
|||||||
let mut notification = serde_json::Map::new();
|
let mut notification = serde_json::Map::new();
|
||||||
notification.insert("complete".to_string(), Value::Object(complete));
|
notification.insert("complete".to_string(), Value::Object(complete));
|
||||||
notification.insert("new".to_string(), new_val_obj.clone());
|
notification.insert("new".to_string(), new_val_obj.clone());
|
||||||
|
|
||||||
if old_val_obj != Value::Null {
|
if old_val_obj != Value::Null {
|
||||||
notification.insert("old".to_string(), old_val_obj.clone());
|
notification.insert("old".to_string(), old_val_obj.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(rep) = replaces_id {
|
if let Some(rep) = replaces_id {
|
||||||
notification.insert("replaces".to_string(), Value::String(rep.to_string()));
|
notification.insert("replaces".to_string(), Value::String(rep.to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut notify_sql = None;
|
let mut notify_sql = None;
|
||||||
|
|||||||
Reference in New Issue
Block a user