Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d918a1acc | |||
| 1f9b407074 | |||
| 6ea6007d86 | |||
| c129864c89 | |||
| 777fc8bbf8 | |||
| 803d62b2fb | |||
| 8845dcdef2 | |||
| 40e08cbf09 | |||
| c7372891d8 | |||
| 952c5036be | |||
| 1fb378def2 | |||
| 6cc4f4ad86 |
@ -111,6 +111,10 @@ Polymorphism is how an object boundary can dynamically take on entirely differen
|
||||
* *Setup*: `{ "family": "widget" }` (Where `widget` is a table type but has no external variations).
|
||||
* *Execution*: The engine queries `db.types.get("widget").variations` and finds only `["widget"]`. Since it lacks table inheritance, it is treated as STI. The engine scans the specific, confined `schemas` array directly under `db.types.get("widget")` for any registered key terminating in the base `.widget` (e.g., `stock.widget`). The `family` automatically uses `kind` as the discriminator.
|
||||
* *Options*: `stock` -> `stock.widget`, `tasks` -> `tasks.widget`.
|
||||
* **Scenario D: JSONB Bubble Inheritance (Field-Backed)**
|
||||
* *Setup*: `{ "family": "panel" }` (Where `panel` is NOT a table type, but rather an isolated JSONB boundary defined within another table's `schemas`).
|
||||
* *Execution*: The engine observes `panel` is not in `db.types` (because it has no physical table). It falls back to scanning the global `db.schemas` registry for any registered key terminating in the base `.panel` (e.g., `balance.panel`, `units.panel`). The `family` automatically uses `kind` as the discriminator.
|
||||
* *Options*: `balance` -> `balance.panel`, `units` -> `units.panel`.
|
||||
|
||||
* **`oneOf` (Strict Tagged Unions)**: A hardcoded list of candidate schemas. Unlike `family` which relies on global DB metadata, `oneOf` forces pure mathematical structural evaluation of the provided candidates. It strictly bans typical JSON Schema "Union of Sets" fallback searches. Every candidate MUST possess a mathematically unique discriminator payload to allow $O(1)$ routing.
|
||||
* **Disjoint Types**: `oneOf: [{ "type": "person" }, { "type": "widget" }]`. The engine succeeds because the native `type` acts as a unique discriminator (`"person"` vs `"widget"`).
|
||||
|
||||
@ -146,6 +146,9 @@
|
||||
"modified_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"organization_id": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@ -168,7 +171,8 @@
|
||||
"created_by",
|
||||
"modified_at",
|
||||
"modified_by",
|
||||
"archived"
|
||||
"archived",
|
||||
"organization_id"
|
||||
],
|
||||
"grouped_fields": {
|
||||
"entity": [
|
||||
@ -178,7 +182,8 @@
|
||||
"created_by",
|
||||
"modified_at",
|
||||
"modified_by",
|
||||
"archived"
|
||||
"archived",
|
||||
"organization_id"
|
||||
]
|
||||
},
|
||||
"lookup_fields": [],
|
||||
@ -345,6 +350,10 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"organization_id": {
|
||||
"type": "string",
|
||||
"const": "ffffffff-ffff-ffff-ffff-ffffffffffff"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -368,7 +377,8 @@
|
||||
"created_by",
|
||||
"modified_at",
|
||||
"modified_by",
|
||||
"archived"
|
||||
"archived",
|
||||
"organization_id"
|
||||
],
|
||||
"grouped_fields": {
|
||||
"person": [
|
||||
@ -396,7 +406,8 @@
|
||||
"created_by",
|
||||
"modified_at",
|
||||
"modified_by",
|
||||
"archived"
|
||||
"archived",
|
||||
"organization_id"
|
||||
]
|
||||
},
|
||||
"lookup_fields": [
|
||||
@ -446,7 +457,8 @@
|
||||
"created_by",
|
||||
"modified_at",
|
||||
"modified_by",
|
||||
"archived"
|
||||
"archived",
|
||||
"organization_id"
|
||||
],
|
||||
"grouped_fields": {
|
||||
"order": [
|
||||
@ -462,7 +474,8 @@
|
||||
"created_by",
|
||||
"modified_at",
|
||||
"modified_by",
|
||||
"archived"
|
||||
"archived",
|
||||
"organization_id"
|
||||
]
|
||||
},
|
||||
"lookup_fields": [
|
||||
@ -504,7 +517,8 @@
|
||||
"created_by",
|
||||
"modified_at",
|
||||
"modified_by",
|
||||
"archived"
|
||||
"archived",
|
||||
"organization_id"
|
||||
],
|
||||
"grouped_fields": {
|
||||
"order_line": [
|
||||
@ -521,7 +535,8 @@
|
||||
"created_by",
|
||||
"modified_at",
|
||||
"modified_by",
|
||||
"archived"
|
||||
"archived",
|
||||
"organization_id"
|
||||
]
|
||||
},
|
||||
"lookup_fields": [],
|
||||
@ -1791,6 +1806,7 @@
|
||||
" \"id\",",
|
||||
" \"modified_at\",",
|
||||
" \"modified_by\",",
|
||||
" \"organization_id\",",
|
||||
" \"type\"",
|
||||
")",
|
||||
"VALUES (",
|
||||
@ -1799,6 +1815,7 @@
|
||||
" '{{uuid:customer_id}}',",
|
||||
" '{{timestamp}}',",
|
||||
" '00000000-0000-0000-0000-000000000000',",
|
||||
" 'ffffffff-ffff-ffff-ffff-ffffffffffff',",
|
||||
" 'person'",
|
||||
")"
|
||||
],
|
||||
@ -1854,6 +1871,7 @@
|
||||
" \"date_of_birth\":\"2000-01-01\",",
|
||||
" \"first_name\":\"Bob\",",
|
||||
" \"last_name\":\"Smith\",",
|
||||
" \"organization_id\":\"ffffffff-ffff-ffff-ffff-ffffffffffff\",",
|
||||
" \"type\":\"person\"",
|
||||
" }',",
|
||||
" '{{uuid:customer_id}}',",
|
||||
@ -1949,12 +1967,14 @@
|
||||
" \"last_name\":\"Smith\",",
|
||||
" \"modified_at\":\"{{timestamp}}\",",
|
||||
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||
" \"organization_id\":\"ffffffff-ffff-ffff-ffff-ffffffffffff\",",
|
||||
" \"type\":\"person\"",
|
||||
" },",
|
||||
" \"new\":{",
|
||||
" \"date_of_birth\":\"2000-01-01\",",
|
||||
" \"first_name\":\"Bob\",",
|
||||
" \"last_name\":\"Smith\",",
|
||||
" \"organization_id\":\"ffffffff-ffff-ffff-ffff-ffffffffffff\",",
|
||||
" \"type\":\"person\"",
|
||||
" }",
|
||||
" }')"
|
||||
@ -3126,10 +3146,26 @@
|
||||
"type": "invoice",
|
||||
"number": "INV-1001",
|
||||
"total": 200.0,
|
||||
"metadata_line": {"price": 50},
|
||||
"metadata_lines": [{"price": 25}],
|
||||
"metadata_nested_line": {"line": {"price": 75}},
|
||||
"metadata_nested_lines": {"lines": [{"price": 100}]}
|
||||
"metadata_line": {
|
||||
"price": 50
|
||||
},
|
||||
"metadata_lines": [
|
||||
{
|
||||
"price": 25
|
||||
}
|
||||
],
|
||||
"metadata_nested_line": {
|
||||
"line": {
|
||||
"price": 75
|
||||
}
|
||||
},
|
||||
"metadata_nested_lines": {
|
||||
"lines": [
|
||||
{
|
||||
"price": 100
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"expect": {
|
||||
"success": true,
|
||||
@ -3304,6 +3340,359 @@
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Test organization_id syntactic sugar permutations",
|
||||
"action": "merge",
|
||||
"data": {
|
||||
"type": "order",
|
||||
"organization_id": "parent-org-id",
|
||||
"customer": {
|
||||
"type": "person",
|
||||
"first_name": "Const",
|
||||
"last_name": "Person"
|
||||
},
|
||||
"lines": [
|
||||
{
|
||||
"type": "order_line"
|
||||
},
|
||||
{
|
||||
"type": "order_line",
|
||||
"organization_id": "explicit-org-id"
|
||||
}
|
||||
]
|
||||
},
|
||||
"schema_id": "order",
|
||||
"expect": {
|
||||
"success": true,
|
||||
"sql": [
|
||||
[
|
||||
"INSERT INTO agreego.\"entity\" (",
|
||||
" \"created_at\",",
|
||||
" \"created_by\",",
|
||||
" \"id\",",
|
||||
" \"modified_at\",",
|
||||
" \"modified_by\",",
|
||||
" \"organization_id\",",
|
||||
" \"type\"",
|
||||
")",
|
||||
"VALUES (",
|
||||
" '{{timestamp}}',",
|
||||
" '{{uuid}}',",
|
||||
" '{{uuid:person_id}}',",
|
||||
" '{{timestamp}}',",
|
||||
" '{{uuid}}',",
|
||||
" 'ffffffff-ffff-ffff-ffff-ffffffffffff',",
|
||||
" 'person'",
|
||||
")"
|
||||
],
|
||||
[
|
||||
"INSERT INTO agreego.\"organization\" (",
|
||||
" \"id\",",
|
||||
" \"type\"",
|
||||
")",
|
||||
"VALUES (",
|
||||
" '{{uuid:person_id}}',",
|
||||
" 'person'",
|
||||
")"
|
||||
],
|
||||
[
|
||||
"INSERT INTO agreego.\"user\" (",
|
||||
" \"id\",",
|
||||
" \"type\"",
|
||||
")",
|
||||
"VALUES (",
|
||||
" '{{uuid:person_id}}',",
|
||||
" 'person'",
|
||||
")"
|
||||
],
|
||||
[
|
||||
"INSERT INTO agreego.\"person\" (",
|
||||
" \"first_name\",",
|
||||
" \"id\",",
|
||||
" \"last_name\",",
|
||||
" \"type\"",
|
||||
")",
|
||||
"VALUES (",
|
||||
" 'Const',",
|
||||
" '{{uuid:person_id}}',",
|
||||
" 'Person',",
|
||||
" 'person'",
|
||||
")"
|
||||
],
|
||||
[
|
||||
"INSERT INTO agreego.change (",
|
||||
" \"old\",",
|
||||
" \"new\",",
|
||||
" entity_id,",
|
||||
" id,",
|
||||
" kind,",
|
||||
" modified_at,",
|
||||
" modified_by",
|
||||
")",
|
||||
"VALUES (",
|
||||
" NULL,",
|
||||
" '{",
|
||||
" \"first_name\":\"Const\",",
|
||||
" \"last_name\":\"Person\",",
|
||||
" \"organization_id\":\"ffffffff-ffff-ffff-ffff-ffffffffffff\",",
|
||||
" \"type\":\"person\"",
|
||||
" }',",
|
||||
" '{{uuid:person_id}}',",
|
||||
" '{{uuid}}',",
|
||||
" 'create',",
|
||||
" '{{timestamp}}',",
|
||||
" '{{uuid}}'",
|
||||
")"
|
||||
],
|
||||
[
|
||||
"INSERT INTO agreego.\"entity\" (",
|
||||
" \"created_at\",",
|
||||
" \"created_by\",",
|
||||
" \"id\",",
|
||||
" \"modified_at\",",
|
||||
" \"modified_by\",",
|
||||
" \"organization_id\",",
|
||||
" \"type\"",
|
||||
")",
|
||||
"VALUES (",
|
||||
" '{{timestamp}}',",
|
||||
" '{{uuid}}',",
|
||||
" '{{uuid:order_id}}',",
|
||||
" '{{timestamp}}',",
|
||||
" '{{uuid}}',",
|
||||
" 'parent-org-id',",
|
||||
" 'order'",
|
||||
")"
|
||||
],
|
||||
[
|
||||
"INSERT INTO agreego.\"order\" (",
|
||||
" \"customer_id\",",
|
||||
" \"id\",",
|
||||
" \"type\"",
|
||||
")",
|
||||
"VALUES (",
|
||||
" '{{uuid:person_id}}',",
|
||||
" '{{uuid:order_id}}',",
|
||||
" 'order'",
|
||||
")"
|
||||
],
|
||||
[
|
||||
"INSERT INTO agreego.\"entity\" (",
|
||||
" \"created_at\",",
|
||||
" \"created_by\",",
|
||||
" \"id\",",
|
||||
" \"modified_at\",",
|
||||
" \"modified_by\",",
|
||||
" \"organization_id\",",
|
||||
" \"type\"",
|
||||
")",
|
||||
"VALUES (",
|
||||
" '{{timestamp}}',",
|
||||
" '{{uuid}}',",
|
||||
" '{{uuid:line1_id}}',",
|
||||
" '{{timestamp}}',",
|
||||
" '{{uuid}}',",
|
||||
" 'parent-org-id',",
|
||||
" 'order_line'",
|
||||
")"
|
||||
],
|
||||
[
|
||||
"INSERT INTO agreego.\"order_line\" (",
|
||||
" \"id\",",
|
||||
" \"order_id\",",
|
||||
" \"type\"",
|
||||
")",
|
||||
"VALUES (",
|
||||
" '{{uuid:line1_id}}',",
|
||||
" '{{uuid:order_id}}',",
|
||||
" 'order_line'",
|
||||
")"
|
||||
],
|
||||
[
|
||||
"INSERT INTO agreego.change (",
|
||||
" \"old\",",
|
||||
" \"new\",",
|
||||
" entity_id,",
|
||||
" id,",
|
||||
" kind,",
|
||||
" modified_at,",
|
||||
" modified_by",
|
||||
")",
|
||||
"VALUES (",
|
||||
" NULL,",
|
||||
" '{",
|
||||
" \"order_id\":\"{{uuid:order_id}}\",",
|
||||
" \"organization_id\":\"parent-org-id\",",
|
||||
" \"type\":\"order_line\"",
|
||||
" }',",
|
||||
" '{{uuid:line1_id}}',",
|
||||
" '{{uuid}}',",
|
||||
" 'create',",
|
||||
" '{{timestamp}}',",
|
||||
" '{{uuid}}'",
|
||||
")"
|
||||
],
|
||||
[
|
||||
"INSERT INTO agreego.\"entity\" (",
|
||||
" \"created_at\",",
|
||||
" \"created_by\",",
|
||||
" \"id\",",
|
||||
" \"modified_at\",",
|
||||
" \"modified_by\",",
|
||||
" \"organization_id\",",
|
||||
" \"type\"",
|
||||
")",
|
||||
"VALUES (",
|
||||
" '{{timestamp}}',",
|
||||
" '{{uuid}}',",
|
||||
" '{{uuid:line2_id}}',",
|
||||
" '{{timestamp}}',",
|
||||
" '{{uuid}}',",
|
||||
" 'explicit-org-id',",
|
||||
" 'order_line'",
|
||||
")"
|
||||
],
|
||||
[
|
||||
"INSERT INTO agreego.\"order_line\" (",
|
||||
" \"id\",",
|
||||
" \"order_id\",",
|
||||
" \"type\"",
|
||||
")",
|
||||
"VALUES (",
|
||||
" '{{uuid:line2_id}}',",
|
||||
" '{{uuid:order_id}}',",
|
||||
" 'order_line'",
|
||||
")"
|
||||
],
|
||||
[
|
||||
"INSERT INTO agreego.change (",
|
||||
" \"old\",",
|
||||
" \"new\",",
|
||||
" entity_id,",
|
||||
" id,",
|
||||
" kind,",
|
||||
" modified_at,",
|
||||
" modified_by",
|
||||
")",
|
||||
"VALUES (",
|
||||
" NULL,",
|
||||
" '{",
|
||||
" \"order_id\":\"{{uuid:order_id}}\",",
|
||||
" \"organization_id\":\"explicit-org-id\",",
|
||||
" \"type\":\"order_line\"",
|
||||
" }',",
|
||||
" '{{uuid:line2_id}}',",
|
||||
" '{{uuid}}',",
|
||||
" 'create',",
|
||||
" '{{timestamp}}',",
|
||||
" '{{uuid}}'",
|
||||
")"
|
||||
],
|
||||
[
|
||||
"INSERT INTO agreego.change (",
|
||||
" \"old\",",
|
||||
" \"new\",",
|
||||
" entity_id,",
|
||||
" id,",
|
||||
" kind,",
|
||||
" modified_at,",
|
||||
" modified_by",
|
||||
")",
|
||||
"VALUES (",
|
||||
" NULL,",
|
||||
" '{",
|
||||
" \"customer_id\":\"{{uuid:person_id}}\",",
|
||||
" \"organization_id\":\"parent-org-id\",",
|
||||
" \"type\":\"order\"",
|
||||
" }',",
|
||||
" '{{uuid:order_id}}',",
|
||||
" '{{uuid}}',",
|
||||
" 'create',",
|
||||
" '{{timestamp}}',",
|
||||
" '{{uuid}}'",
|
||||
")"
|
||||
],
|
||||
[
|
||||
"SELECT pg_notify('entity', '{",
|
||||
" \"complete\":{",
|
||||
" \"created_at\":\"{{timestamp}}\",",
|
||||
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||
" \"customer_id\":\"{{uuid:person_id}}\",",
|
||||
" \"id\":\"{{uuid:order_id}}\",",
|
||||
" \"modified_at\":\"{{timestamp}}\",",
|
||||
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||
" \"organization_id\":\"parent-org-id\",",
|
||||
" \"type\":\"order\"",
|
||||
" },",
|
||||
" \"new\":{",
|
||||
" \"customer_id\":\"{{uuid:person_id}}\",",
|
||||
" \"organization_id\":\"parent-org-id\",",
|
||||
" \"type\":\"order\"",
|
||||
" }",
|
||||
" }')"
|
||||
],
|
||||
[
|
||||
"SELECT pg_notify('entity', '{",
|
||||
" \"complete\":{",
|
||||
" \"created_at\":\"{{timestamp}}\",",
|
||||
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||
" \"first_name\":\"Const\",",
|
||||
" \"id\":\"{{uuid:person_id}}\",",
|
||||
" \"last_name\":\"Person\",",
|
||||
" \"modified_at\":\"{{timestamp}}\",",
|
||||
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||
" \"organization_id\":\"ffffffff-ffff-ffff-ffff-ffffffffffff\",",
|
||||
" \"type\":\"person\"",
|
||||
" },",
|
||||
" \"new\":{",
|
||||
" \"first_name\":\"Const\",",
|
||||
" \"last_name\":\"Person\",",
|
||||
" \"organization_id\":\"ffffffff-ffff-ffff-ffff-ffffffffffff\",",
|
||||
" \"type\":\"person\"",
|
||||
" }",
|
||||
" }')"
|
||||
],
|
||||
[
|
||||
"SELECT pg_notify('entity', '{",
|
||||
" \"complete\":{",
|
||||
" \"created_at\":\"{{timestamp}}\",",
|
||||
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||
" \"id\":\"{{uuid:line1_id}}\",",
|
||||
" \"modified_at\":\"{{timestamp}}\",",
|
||||
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||
" \"order_id\":\"{{uuid:order_id}}\",",
|
||||
" \"organization_id\":\"parent-org-id\",",
|
||||
" \"type\":\"order_line\"",
|
||||
" },",
|
||||
" \"new\":{",
|
||||
" \"order_id\":\"{{uuid:order_id}}\",",
|
||||
" \"organization_id\":\"parent-org-id\",",
|
||||
" \"type\":\"order_line\"",
|
||||
" }",
|
||||
" }')"
|
||||
],
|
||||
[
|
||||
"SELECT pg_notify('entity', '{",
|
||||
" \"complete\":{",
|
||||
" \"created_at\":\"{{timestamp}}\",",
|
||||
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||
" \"id\":\"{{uuid:line2_id}}\",",
|
||||
" \"modified_at\":\"{{timestamp}}\",",
|
||||
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||
" \"order_id\":\"{{uuid:order_id}}\",",
|
||||
" \"organization_id\":\"explicit-org-id\",",
|
||||
" \"type\":\"order_line\"",
|
||||
" },",
|
||||
" \"new\":{",
|
||||
" \"order_id\":\"{{uuid:order_id}}\",",
|
||||
" \"organization_id\":\"explicit-org-id\",",
|
||||
" \"type\":\"order_line\"",
|
||||
" }",
|
||||
" }')"
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[
|
||||
{
|
||||
"description": "Vertical family Routing (Across Tables)",
|
||||
"description": "Vertical family Routing (Scenario A)",
|
||||
"database": {
|
||||
"types": [
|
||||
{
|
||||
@ -153,7 +153,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Matrix family Routing (Vertical + Horizontal Intersections)",
|
||||
"description": "Matrix family Routing (Scenario B)",
|
||||
"database": {
|
||||
"types": [
|
||||
{
|
||||
@ -284,7 +284,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Horizontal family Routing (Virtual Variations)",
|
||||
"description": "Horizontal family Routing (Scenario C)",
|
||||
"database": {
|
||||
"types": [
|
||||
{
|
||||
@ -776,5 +776,123 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "JSONB Field Bubble family Routing (Scenario D)",
|
||||
"database": {
|
||||
"types": [
|
||||
{
|
||||
"name": "dashboard",
|
||||
"variations": [
|
||||
"dashboard"
|
||||
],
|
||||
"schemas": {
|
||||
"dashboard": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"panel": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"kind"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"balance.panel": {
|
||||
"type": "panel",
|
||||
"properties": {
|
||||
"amount": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"units.panel": {
|
||||
"type": "panel",
|
||||
"properties": {
|
||||
"count": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "family_panel",
|
||||
"schemas": {
|
||||
"family_panel": {
|
||||
"family": "panel"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "Successfully routes to nested balance panel",
|
||||
"schema_id": "family_panel",
|
||||
"data": {
|
||||
"id": "123",
|
||||
"kind": "balance",
|
||||
"amount": 500
|
||||
},
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Fails validation on routed schema due to invalid property type",
|
||||
"schema_id": "family_panel",
|
||||
"data": {
|
||||
"id": "123",
|
||||
"kind": "balance",
|
||||
"amount": "not_an_int"
|
||||
},
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false,
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_TYPE",
|
||||
"details": {
|
||||
"path": "amount"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Fails when discriminator does not match any bubble schema",
|
||||
"schema_id": "family_panel",
|
||||
"data": {
|
||||
"id": "123",
|
||||
"kind": "unknown_panel"
|
||||
},
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false,
|
||||
"errors": [
|
||||
{
|
||||
"code": "NO_FAMILY_MATCH",
|
||||
"details": {
|
||||
"path": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
2
flows
2
flows
Submodule flows updated: 4d61e13e00...0d9bd8644e
@ -9,7 +9,7 @@ impl Schema {
|
||||
errors: &mut Vec<crate::drop::Error>,
|
||||
) {
|
||||
let mut options = std::collections::BTreeMap::new();
|
||||
let mut strategy = String::new();
|
||||
let strategy: &str;
|
||||
|
||||
if let Some(family) = &self.obj.family {
|
||||
// Formalize the <Variant>.<Base> topology
|
||||
@ -24,7 +24,7 @@ impl Schema {
|
||||
if let Some(type_def) = db.types.get(&family_base) {
|
||||
if type_def.variations.len() > 1 && type_def.variations.iter().any(|v| v != &family_base) {
|
||||
// Scenario A / B: Table Variations
|
||||
strategy = "type".to_string();
|
||||
strategy = "type";
|
||||
for var in &type_def.variations {
|
||||
let target_id = if family_prefix.is_empty() {
|
||||
var.to_string()
|
||||
@ -38,7 +38,7 @@ impl Schema {
|
||||
}
|
||||
} else {
|
||||
// Scenario C: Single Table Inheritance (Horizontal)
|
||||
strategy = "kind".to_string();
|
||||
strategy = "kind";
|
||||
|
||||
let suffix = format!(".{}", family_base);
|
||||
|
||||
@ -50,6 +50,19 @@ impl Schema {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Scenario D: Field-Backed JSONB Bubble STI (No explicit table representation)
|
||||
strategy = "kind";
|
||||
let suffix = format!(".{}", family_base);
|
||||
|
||||
// Scan the entire database schemas registry for matching suffixes
|
||||
for (id, schema) in &db.schemas {
|
||||
if id.ends_with(&suffix) || id == &family_base {
|
||||
if let Some(kind_val) = schema.obj.get_discriminator_value("kind", id) {
|
||||
options.insert(kind_val, (None, Some(id.to_string())));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let Some(one_of) = &self.obj.one_of {
|
||||
let mut type_vals = std::collections::HashSet::new();
|
||||
@ -84,7 +97,7 @@ impl Schema {
|
||||
}
|
||||
|
||||
if disjoint_base && structural_types.len() == one_of.len() {
|
||||
strategy = "".to_string();
|
||||
strategy = "";
|
||||
for (i, c) in one_of.iter().enumerate() {
|
||||
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &c.obj.type_ {
|
||||
if crate::database::object::is_primitive_type(t) {
|
||||
@ -96,11 +109,11 @@ impl Schema {
|
||||
}
|
||||
} else {
|
||||
strategy = if type_vals.len() > 1 && type_vals.len() == one_of.len() {
|
||||
"type".to_string()
|
||||
"type"
|
||||
} else if kind_vals.len() > 1 && kind_vals.len() == one_of.len() {
|
||||
"kind".to_string()
|
||||
"kind"
|
||||
} else {
|
||||
"".to_string()
|
||||
""
|
||||
};
|
||||
|
||||
if strategy.is_empty() {
|
||||
@ -148,7 +161,7 @@ impl Schema {
|
||||
|
||||
if !options.is_empty() {
|
||||
if !strategy.is_empty() {
|
||||
let _ = self.obj.compiled_discriminator.set(strategy);
|
||||
let _ = self.obj.compiled_discriminator.set(strategy.to_string());
|
||||
}
|
||||
let _ = self.obj.compiled_options.set(options);
|
||||
}
|
||||
|
||||
@ -40,7 +40,7 @@ impl Merger {
|
||||
}
|
||||
};
|
||||
|
||||
let result = self.merge_internal(target_schema, data, &mut notifications_queue);
|
||||
let result = self.merge_internal(target_schema, data, &mut notifications_queue, None, false);
|
||||
|
||||
let val_resolved = match result {
|
||||
Ok(val) => val,
|
||||
@ -134,9 +134,11 @@ impl Merger {
|
||||
mut schema: Arc<crate::database::schema::Schema>,
|
||||
data: Value,
|
||||
notifications: &mut Vec<String>,
|
||||
parent_org_id: Option<String>,
|
||||
is_child: bool,
|
||||
) -> Result<Value, String> {
|
||||
match data {
|
||||
Value::Array(items) => self.merge_array(schema, items, notifications),
|
||||
Value::Array(items) => self.merge_array(schema, items, notifications, parent_org_id, is_child),
|
||||
Value::Object(map) => {
|
||||
if let Some(options) = schema.obj.compiled_options.get() {
|
||||
if let Some(disc) = schema.obj.compiled_discriminator.get() {
|
||||
@ -144,9 +146,7 @@ impl Merger {
|
||||
if let Some(v) = val {
|
||||
if let Some((idx_opt, target_id_opt)) = options.get(v) {
|
||||
if let Some(target_id) = target_id_opt {
|
||||
if let Some(target_schema) =
|
||||
self.db.schemas.get(target_id)
|
||||
{
|
||||
if let Some(target_schema) = self.db.schemas.get(target_id) {
|
||||
schema = target_schema.clone();
|
||||
} else {
|
||||
return Err(format!(
|
||||
@ -185,7 +185,7 @@ impl Merger {
|
||||
}
|
||||
}
|
||||
}
|
||||
self.merge_object(schema, map, notifications)
|
||||
self.merge_object(schema, map, notifications, parent_org_id, is_child)
|
||||
}
|
||||
_ => Err("Invalid merge payload: root must be an Object or Array".to_string()),
|
||||
}
|
||||
@ -196,6 +196,8 @@ impl Merger {
|
||||
schema: Arc<crate::database::schema::Schema>,
|
||||
items: Vec<Value>,
|
||||
notifications: &mut Vec<String>,
|
||||
parent_org_id: Option<String>,
|
||||
is_child: bool,
|
||||
) -> Result<Value, String> {
|
||||
let mut item_schema = schema.clone();
|
||||
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &schema.obj.type_ {
|
||||
@ -208,7 +210,7 @@ impl Merger {
|
||||
|
||||
let mut resolved_items = Vec::new();
|
||||
for item in items {
|
||||
let resolved = self.merge_internal(item_schema.clone(), item, notifications)?;
|
||||
let resolved = self.merge_internal(item_schema.clone(), item, notifications, parent_org_id.clone(), is_child)?;
|
||||
resolved_items.push(resolved);
|
||||
}
|
||||
Ok(Value::Array(resolved_items))
|
||||
@ -219,6 +221,8 @@ impl Merger {
|
||||
schema: Arc<crate::database::schema::Schema>,
|
||||
obj: serde_json::Map<String, Value>,
|
||||
notifications: &mut Vec<String>,
|
||||
parent_org_id: Option<String>,
|
||||
is_child: bool,
|
||||
) -> Result<Value, String> {
|
||||
let queue_start = notifications.len();
|
||||
|
||||
@ -278,6 +282,20 @@ impl Merger {
|
||||
}
|
||||
}
|
||||
|
||||
let mut current_org_id = None;
|
||||
if let Some(compiled_props) = schema.obj.compiled_properties.get() {
|
||||
if let Some(org_schema) = compiled_props.get("organization_id") {
|
||||
if let Some(c) = &org_schema.obj.const_ {
|
||||
if let Some(c_str) = c.as_str() {
|
||||
current_org_id = Some(c_str.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if current_org_id.is_none() {
|
||||
current_org_id = parent_org_id.clone();
|
||||
}
|
||||
|
||||
let user_id = self.db.auth_user_id()?;
|
||||
let timestamp = self.db.timestamp()?;
|
||||
|
||||
@ -292,6 +310,16 @@ impl Merger {
|
||||
entity_change_kind = kind;
|
||||
entity_fetched = fetched;
|
||||
entity_replaces = replaces;
|
||||
|
||||
if entity_change_kind.as_deref() == Some("create") {
|
||||
if is_child {
|
||||
if !entity_fields.contains_key("organization_id") {
|
||||
if let Some(ref org_id) = current_org_id {
|
||||
entity_fields.insert("organization_id".to_string(), Value::String(org_id.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut entity_response = serde_json::Map::new();
|
||||
@ -312,17 +340,14 @@ impl Merger {
|
||||
if let Some(relation) = self.db.relations.get(&edge.constraint) {
|
||||
let parent_is_source = edge.forward;
|
||||
|
||||
let org_id_to_pass = entity_fields.get("organization_id").and_then(|v| v.as_str()).map(|s| s.to_string());
|
||||
if parent_is_source {
|
||||
if !relative.contains_key("organization_id") {
|
||||
if let Some(org_id) = entity_fields.get("organization_id") {
|
||||
relative.insert("organization_id".to_string(), org_id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let mut merged_relative = match self.merge_internal(
|
||||
rel_schema.clone(),
|
||||
Value::Object(relative),
|
||||
notifications,
|
||||
org_id_to_pass.clone(),
|
||||
true,
|
||||
)? {
|
||||
Value::Object(m) => m,
|
||||
_ => continue,
|
||||
@ -338,12 +363,6 @@ impl Merger {
|
||||
);
|
||||
entity_response.insert(relation_name, Value::Object(merged_relative));
|
||||
} else {
|
||||
if !relative.contains_key("organization_id") {
|
||||
if let Some(org_id) = entity_fields.get("organization_id") {
|
||||
relative.insert("organization_id".to_string(), org_id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Self::apply_entity_relation(
|
||||
&mut relative,
|
||||
&relation.source_columns,
|
||||
@ -355,6 +374,8 @@ impl Merger {
|
||||
rel_schema.clone(),
|
||||
Value::Object(relative),
|
||||
notifications,
|
||||
org_id_to_pass.clone(),
|
||||
true,
|
||||
)? {
|
||||
Value::Object(m) => m,
|
||||
_ => continue,
|
||||
@ -374,6 +395,16 @@ impl Merger {
|
||||
entity_change_kind = kind;
|
||||
entity_fetched = fetched;
|
||||
entity_replaces = replaces;
|
||||
|
||||
if entity_change_kind.as_deref() == Some("create") {
|
||||
if is_child {
|
||||
if !entity_fields.contains_key("organization_id") {
|
||||
if let Some(ref org_id) = current_org_id {
|
||||
entity_fields.insert("organization_id".to_string(), Value::String(org_id.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.merge_entity_fields(
|
||||
@ -401,22 +432,6 @@ impl Merger {
|
||||
if let Some(compiled_edges) = schema.obj.compiled_edges.get() {
|
||||
if let Some(edge) = compiled_edges.get(&relation_name) {
|
||||
if let Some(relation) = self.db.relations.get(&edge.constraint) {
|
||||
let mut relative_responses = Vec::new();
|
||||
for relative_item_val in relative_arr {
|
||||
if let Value::Object(mut relative_item) = relative_item_val {
|
||||
if !relative_item.contains_key("organization_id") {
|
||||
if let Some(org_id) = entity_fields.get("organization_id") {
|
||||
relative_item.insert("organization_id".to_string(), org_id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Self::apply_entity_relation(
|
||||
&mut relative_item,
|
||||
&relation.source_columns,
|
||||
&relation.destination_columns,
|
||||
&entity_fields,
|
||||
);
|
||||
|
||||
let mut item_schema = rel_schema.clone();
|
||||
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) =
|
||||
&rel_schema.obj.type_
|
||||
@ -428,10 +443,23 @@ impl Merger {
|
||||
}
|
||||
}
|
||||
|
||||
let org_id_to_pass = entity_fields.get("organization_id").and_then(|v| v.as_str()).map(|s| s.to_string());
|
||||
let mut relative_responses = Vec::new();
|
||||
for relative_item_val in relative_arr {
|
||||
if let Value::Object(mut relative_item) = relative_item_val {
|
||||
Self::apply_entity_relation(
|
||||
&mut relative_item,
|
||||
&relation.source_columns,
|
||||
&relation.destination_columns,
|
||||
&entity_fields,
|
||||
);
|
||||
|
||||
let merged_relative = match self.merge_internal(
|
||||
item_schema,
|
||||
item_schema.clone(),
|
||||
Value::Object(relative_item),
|
||||
notifications,
|
||||
org_id_to_pass.clone(),
|
||||
true,
|
||||
)? {
|
||||
Value::Object(m) => m,
|
||||
_ => continue,
|
||||
|
||||
@ -1619,6 +1619,24 @@ fn test_polymorphism_5_2() {
|
||||
crate::tests::runner::run_test_case(&path, 5, 2).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_polymorphism_6_0() {
|
||||
let path = format!("{}/fixtures/polymorphism.json", env!("CARGO_MANIFEST_DIR"));
|
||||
crate::tests::runner::run_test_case(&path, 6, 0).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_polymorphism_6_1() {
|
||||
let path = format!("{}/fixtures/polymorphism.json", env!("CARGO_MANIFEST_DIR"));
|
||||
crate::tests::runner::run_test_case(&path, 6, 1).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_polymorphism_6_2() {
|
||||
let path = format!("{}/fixtures/polymorphism.json", env!("CARGO_MANIFEST_DIR"));
|
||||
crate::tests::runner::run_test_case(&path, 6, 2).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_not_0_0() {
|
||||
let path = format!("{}/fixtures/not.json", env!("CARGO_MANIFEST_DIR"));
|
||||
@ -8170,3 +8188,9 @@ fn test_merger_0_14() {
|
||||
let path = format!("{}/fixtures/merger.json", env!("CARGO_MANIFEST_DIR"));
|
||||
crate::tests::runner::run_test_case(&path, 0, 14).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_merger_0_15() {
|
||||
let path = format!("{}/fixtures/merger.json", env!("CARGO_MANIFEST_DIR"));
|
||||
crate::tests::runner::run_test_case(&path, 0, 15).unwrap();
|
||||
}
|
||||
|
||||
27
test_out.txt
27
test_out.txt
File diff suppressed because one or more lines are too long
109
test_output.txt
109
test_output.txt
@ -1,109 +0,0 @@
|
||||
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.43s
|
||||
Running unittests src/lib.rs (target/debug/deps/jspg-d3f18ff3a7e2b386)
|
||||
|
||||
running 1 test
|
||||
test tests::test_filter_0_0 ... FAILED
|
||||
|
||||
failures:
|
||||
|
||||
---- tests::test_filter_0_0 stdout ----
|
||||
TEST COMPILE ERROR FOR 'Assert filter generation map accurately represents strongly typed conditions natively.': Detailed Schema Match Failure for 'gender.condition'!
|
||||
|
||||
Expected:
|
||||
{
|
||||
"compiledPropertyNames": [
|
||||
"$eq",
|
||||
"$ne",
|
||||
"$nof",
|
||||
"$of"
|
||||
],
|
||||
"properties": {
|
||||
"$eq": {
|
||||
"type": [
|
||||
"gender",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"$ne": {
|
||||
"type": [
|
||||
"gender",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"$nof": {
|
||||
"items": {
|
||||
"type": "gender"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"$of": {
|
||||
"items": {
|
||||
"type": "gender"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
|
||||
Actual:
|
||||
{
|
||||
"compiledPropertyNames": [
|
||||
"$eq",
|
||||
"$ne",
|
||||
"$nof",
|
||||
"$of",
|
||||
"kind"
|
||||
],
|
||||
"properties": {
|
||||
"$eq": {
|
||||
"type": [
|
||||
"gender",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"$ne": {
|
||||
"type": [
|
||||
"gender",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"$nof": {
|
||||
"items": {
|
||||
"type": "gender"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"$of": {
|
||||
"items": {
|
||||
"type": "gender"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "condition"
|
||||
}
|
||||
|
||||
thread 'tests::test_filter_0_0' (118346550) panicked at src/tests/fixtures.rs:539:54:
|
||||
called `Result::unwrap()` on an `Err` value: "[Filter Synthesis Object-Oriented Composition] Compile Test 'Assert filter generation map accurately represents strongly typed conditions natively.' failed. Error: Detailed Schema Match Failure for 'gender.condition'!\n\nExpected:\n{\n \"compiledPropertyNames\": [\n \"$eq\",\n \"$ne\",\n \"$nof\",\n \"$of\"\n ],\n \"properties\": {\n \"$eq\": {\n \"type\": [\n \"gender\",\n \"null\"\n ]\n },\n \"$ne\": {\n \"type\": [\n \"gender\",\n \"null\"\n ]\n },\n \"$nof\": {\n \"items\": {\n \"type\": \"gender\"\n },\n \"type\": [\n \"array\",\n \"null\"\n ]\n },\n \"$of\": {\n \"items\": {\n \"type\": \"gender\"\n },\n \"type\": [\n \"array\",\n \"null\"\n ]\n }\n },\n \"type\": \"object\"\n}\n\nActual:\n{\n \"compiledPropertyNames\": [\n \"$eq\",\n \"$ne\",\n \"$nof\",\n \"$of\",\n \"kind\"\n ],\n \"properties\": {\n \"$eq\": {\n \"type\": [\n \"gender\",\n \"null\"\n ]\n },\n \"$ne\": {\n \"type\": [\n \"gender\",\n \"null\"\n ]\n },\n \"$nof\": {\n \"items\": {\n \"type\": \"gender\"\n },\n \"type\": [\n \"array\",\n \"null\"\n ]\n },\n \"$of\": {\n \"items\": {\n \"type\": \"gender\"\n },\n \"type\": [\n \"array\",\n \"null\"\n ]\n }\n },\n \"type\": \"condition\"\n}"
|
||||
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
|
||||
|
||||
|
||||
failures:
|
||||
tests::test_filter_0_0
|
||||
|
||||
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 1362 filtered out; finished in 0.00s
|
||||
|
||||
error: test failed, to rerun pass `--lib`
|
||||
Reference in New Issue
Block a user