Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d9b510819 | |||
| 3c4b1066df | |||
| 4c59d9ba7f | |||
| a1038490dd | |||
| 14707330a7 | |||
| 77bc92533c | |||
| 4060119b01 |
26
GEMINI.md
26
GEMINI.md
@ -20,9 +20,16 @@ JSPG operates by deeply integrating the JSON Schema Draft 2020-12 specification
|
|||||||
To support high-throughput operations while allowing for runtime updates (e.g., during hot-reloading), JSPG uses an **Atomic Swap** pattern:
|
To support high-throughput operations while allowing for runtime updates (e.g., during hot-reloading), JSPG uses an **Atomic Swap** pattern:
|
||||||
1. **Parser Phase**: Schema JSONs are parsed into ordered `Schema` structs.
|
1. **Parser Phase**: Schema JSONs are parsed into ordered `Schema` structs.
|
||||||
2. **Compiler Phase**: The database iterates all parsed schemas and pre-computes native optimization maps (Descendants Map, Depths Map, Variations Map).
|
2. **Compiler Phase**: The database iterates all parsed schemas and pre-computes native optimization maps (Descendants Map, Depths Map, Variations Map).
|
||||||
3. **Immutable Validator**: The `Validator` struct immutably owns the `Database` registry and all its global maps. Schemas themselves are completely frozen; `$ref` strings are resolved dynamically at runtime using pre-computed O(1) maps.
|
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.
|
||||||
|
|
||||||
|
### Global API Reference
|
||||||
|
These functions operate on the global `GLOBAL_JSPG` engine instance and provide administrative boundaries:
|
||||||
|
|
||||||
|
* `jspg_setup(database jsonb) -> jsonb`: Initializes the engine. Deserializes the full database schema registry (types, enums, puncs, relations) from Postgres and compiles them into memory atomically.
|
||||||
|
* `jspg_teardown() -> jsonb`: Clears the current session's engine instance from `GLOBAL_JSPG`, resetting the cache.
|
||||||
|
* `jspg_schemas() -> jsonb`: Exports the fully compiled AST snapshot (including all inherited dependencies) out of `GLOBAL_JSPG` into standard JSON Schema representations.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Validator
|
## 2. Validator
|
||||||
@ -30,10 +37,7 @@ To support high-throughput operations while allowing for runtime updates (e.g.,
|
|||||||
The Validator provides strict, schema-driven evaluation for the "Punc" architecture.
|
The Validator provides strict, schema-driven evaluation for the "Punc" architecture.
|
||||||
|
|
||||||
### API Reference
|
### API Reference
|
||||||
* `jspg_setup(database jsonb) -> jsonb`: Loads and compiles the entire registry (types, enums, puncs, relations) atomically.
|
* `jspg_validate(schema_id text, instance jsonb) -> jsonb`: Validates the `instance` JSON payload strictly against the constraints of the registered `schema_id`. Returns boolean-like success or structured error codes.
|
||||||
* `mask_json_schema(schema_id text, instance jsonb) -> jsonb`: Validates and prunes unknown properties dynamically, returning masked data.
|
|
||||||
* `jspg_validate(schema_id text, instance jsonb) -> jsonb`: Returns boolean-like success or structured errors.
|
|
||||||
* `jspg_teardown() -> jsonb`: Clears the current session's schema cache.
|
|
||||||
|
|
||||||
### Custom Features & Deviations
|
### Custom Features & Deviations
|
||||||
JSPG implements specific extensions to the Draft 2020-12 standard to support the Punc architecture's object-oriented needs while heavily optimizing for zero-runtime lookups.
|
JSPG implements specific extensions to the Draft 2020-12 standard to support the Punc architecture's object-oriented needs while heavily optimizing for zero-runtime lookups.
|
||||||
@ -69,11 +73,14 @@ To simplify frontend form validation, format validators specifically for `uuid`,
|
|||||||
|
|
||||||
## 3. Merger
|
## 3. Merger
|
||||||
|
|
||||||
The Merger provides an automated, high-performance graph synchronization engine via the `jspg_merge(cue JSONB)` API. It orchestrates the complex mapping of nested JSON objects into normalized Postgres relational tables, honoring all inheritance and graph constraints.
|
The Merger provides an automated, high-performance graph synchronization engine. It orchestrates the complex mapping of nested JSON objects into normalized Postgres relational tables, honoring all inheritance and graph constraints.
|
||||||
|
|
||||||
|
### API Reference
|
||||||
|
* `jspg_merge(schema_id text, data jsonb) -> jsonb`: Traverses the provided JSON payload according to the compiled relational map of `schema_id`. Dynamically builds and executes relational SQL UPSERT paths natively.
|
||||||
|
|
||||||
### Core Features
|
### Core Features
|
||||||
|
|
||||||
* **Caching Strategy**: The Merger leverages the `Validator`'s in-memory `Database` registry to instantly resolve Foreign Key mapping graphs. It additionally utilizes the concurrent `GLOBAL_JSPG` application memory (`DashMap`) to cache statically constructed SQL `SELECT` strings used during deduplication (`lk_`) and difference tracking calculations.
|
* **Caching Strategy**: The Merger leverages the native `compiled_edges` permanently cached onto the Schema AST via `OnceLock` to instantly resolve Foreign Key mapping graphs natively in absolute `O(1)` time. It additionally utilizes the concurrent `GLOBAL_JSPG` application memory (`DashMap`) to cache statically constructed SQL `SELECT` strings used during deduplication (`lk_`) and difference tracking calculations.
|
||||||
* **Deep Graph Merging**: The Merger walks arbitrary levels of deeply nested JSON schemas (e.g. tracking an `order`, its `customer`, and an array of its `lines`). It intelligently discovers the correct parent-to-child or child-to-parent Foreign Keys stored in the registry and automatically maps the UUIDs across the relationships during UPSERT.
|
* **Deep Graph Merging**: The Merger walks arbitrary levels of deeply nested JSON schemas (e.g. tracking an `order`, its `customer`, and an array of its `lines`). It intelligently discovers the correct parent-to-child or child-to-parent Foreign Keys stored in the registry and automatically maps the UUIDs across the relationships during UPSERT.
|
||||||
* **Prefix Foreign Key Matching**: Handles scenario where multiple relations point to the same table by using database Foreign Key constraint prefixes (`fk_`). For example, if a schema has `shipping_address` and `billing_address`, the merger resolves against `fk_shipping_address_entity` vs `fk_billing_address_entity` automatically to correctly route object properties.
|
* **Prefix Foreign Key Matching**: Handles scenario where multiple relations point to the same table by using database Foreign Key constraint prefixes (`fk_`). For example, if a schema has `shipping_address` and `billing_address`, the merger resolves against `fk_shipping_address_entity` vs `fk_billing_address_entity` automatically to correctly route object properties.
|
||||||
* **Dynamic Deduplication & Lookups**: If a nested object is provided without an `id`, the Merger utilizes Postgres `lk_` index constraints defined in the schema registry (e.g. `lk_person` mapped to `first_name` and `last_name`). It dynamically queries these unique matching constraints to discover the correct UUID to perform an UPDATE, preventing data duplication.
|
* **Dynamic Deduplication & Lookups**: If a nested object is provided without an `id`, the Merger utilizes Postgres `lk_` index constraints defined in the schema registry (e.g. `lk_person` mapped to `first_name` and `last_name`). It dynamically queries these unique matching constraints to discover the correct UUID to perform an UPDATE, preventing data duplication.
|
||||||
@ -91,7 +98,10 @@ The Merger provides an automated, high-performance graph synchronization engine
|
|||||||
|
|
||||||
## 4. Queryer
|
## 4. Queryer
|
||||||
|
|
||||||
The Queryer transforms Postgres into a pre-compiled Semantic Query Engine via the `jspg_query(schema_id text, cue jsonb)` API, designed to serve the exact shape of Punc responses directly via SQL.
|
The Queryer transforms Postgres into a pre-compiled Semantic Query Engine, designed to serve the exact shape of Punc responses directly via SQL.
|
||||||
|
|
||||||
|
### API Reference
|
||||||
|
* `jspg_query(schema_id text, filters jsonb) -> jsonb`: Compiles the JSON Schema AST of `schema_id` directly into pre-planned, nested multi-JOIN SQL execution trees. Processes `filters` structurally.
|
||||||
|
|
||||||
### Core Features
|
### Core Features
|
||||||
|
|
||||||
|
|||||||
819
d1.json
819
d1.json
@ -1,819 +0,0 @@
|
|||||||
{
|
|
||||||
"database": {
|
|
||||||
"puncs": [],
|
|
||||||
"enums": [
|
|
||||||
{
|
|
||||||
"id": "11111111-1111-1111-1111-111111111111",
|
|
||||||
"type": "relation_type",
|
|
||||||
"enum": "relation_type",
|
|
||||||
"values": [
|
|
||||||
"foreign_key",
|
|
||||||
"polymorphic",
|
|
||||||
"graph"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"relations": [
|
|
||||||
{
|
|
||||||
"id": "22222222-2222-2222-2222-222222222222",
|
|
||||||
"type": "relation",
|
|
||||||
"constraint": "fk_order_customer",
|
|
||||||
"source_type": "order",
|
|
||||||
"source_columns": [
|
|
||||||
"customer_id"
|
|
||||||
],
|
|
||||||
"destination_type": "person",
|
|
||||||
"destination_columns": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"prefix": "customer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "33333333-3333-3333-3333-333333333333",
|
|
||||||
"type": "relation",
|
|
||||||
"constraint": "fk_order_line_order",
|
|
||||||
"source_type": "order_line",
|
|
||||||
"source_columns": [
|
|
||||||
"order_id"
|
|
||||||
],
|
|
||||||
"destination_type": "order",
|
|
||||||
"destination_columns": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"prefix": "lines"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "44444444-4444-4444-4444-444444444444",
|
|
||||||
"type": "relation",
|
|
||||||
"constraint": "fk_relationship_source_entity",
|
|
||||||
"source_type": "relationship",
|
|
||||||
"source_columns": [
|
|
||||||
"source_id",
|
|
||||||
"source_type"
|
|
||||||
],
|
|
||||||
"destination_type": "entity",
|
|
||||||
"destination_columns": [
|
|
||||||
"id",
|
|
||||||
"type"
|
|
||||||
],
|
|
||||||
"prefix": "source"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "55555555-5555-5555-5555-555555555555",
|
|
||||||
"type": "relation",
|
|
||||||
"constraint": "fk_relationship_target_entity",
|
|
||||||
"source_type": "relationship",
|
|
||||||
"source_columns": [
|
|
||||||
"target_id",
|
|
||||||
"target_type"
|
|
||||||
],
|
|
||||||
"destination_type": "entity",
|
|
||||||
"destination_columns": [
|
|
||||||
"id",
|
|
||||||
"type"
|
|
||||||
],
|
|
||||||
"prefix": "target"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"types": [
|
|
||||||
{
|
|
||||||
"name": "entity",
|
|
||||||
"schemas": [
|
|
||||||
{
|
|
||||||
"$id": "entity",
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"archived": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"created_by": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"modified_by": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "date-time"
|
|
||||||
},
|
|
||||||
"modified_at": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "date-time"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"id",
|
|
||||||
"type",
|
|
||||||
"created_by",
|
|
||||||
"created_at",
|
|
||||||
"modified_by",
|
|
||||||
"modified_at"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"hierarchy": [
|
|
||||||
"entity"
|
|
||||||
],
|
|
||||||
"fields": [
|
|
||||||
"id",
|
|
||||||
"type",
|
|
||||||
"created_at",
|
|
||||||
"created_by",
|
|
||||||
"modified_at",
|
|
||||||
"modified_by",
|
|
||||||
"archived"
|
|
||||||
],
|
|
||||||
"grouped_fields": {
|
|
||||||
"entity": [
|
|
||||||
"id",
|
|
||||||
"type",
|
|
||||||
"created_at",
|
|
||||||
"created_by",
|
|
||||||
"modified_at",
|
|
||||||
"modified_by",
|
|
||||||
"archived"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"lookup_fields": [],
|
|
||||||
"historical": true,
|
|
||||||
"notify": true,
|
|
||||||
"relationship": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "organization",
|
|
||||||
"schemas": [
|
|
||||||
{
|
|
||||||
"$id": "organization",
|
|
||||||
"$ref": "entity",
|
|
||||||
"properties": {
|
|
||||||
"name": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"hierarchy": [
|
|
||||||
"organization",
|
|
||||||
"entity"
|
|
||||||
],
|
|
||||||
"fields": [
|
|
||||||
"id",
|
|
||||||
"type",
|
|
||||||
"name",
|
|
||||||
"created_at",
|
|
||||||
"created_by",
|
|
||||||
"modified_at",
|
|
||||||
"modified_by",
|
|
||||||
"archived"
|
|
||||||
],
|
|
||||||
"grouped_fields": {
|
|
||||||
"organization": [
|
|
||||||
"id",
|
|
||||||
"type",
|
|
||||||
"name"
|
|
||||||
],
|
|
||||||
"entity": [
|
|
||||||
"id",
|
|
||||||
"type",
|
|
||||||
"created_at",
|
|
||||||
"created_by",
|
|
||||||
"modified_at",
|
|
||||||
"modified_by",
|
|
||||||
"archived"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"lookup_fields": [],
|
|
||||||
"historical": true,
|
|
||||||
"notify": true,
|
|
||||||
"relationship": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "user",
|
|
||||||
"schemas": [
|
|
||||||
{
|
|
||||||
"$id": "user",
|
|
||||||
"$ref": "organization",
|
|
||||||
"properties": {}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"hierarchy": [
|
|
||||||
"user",
|
|
||||||
"organization",
|
|
||||||
"entity"
|
|
||||||
],
|
|
||||||
"fields": [
|
|
||||||
"id",
|
|
||||||
"type",
|
|
||||||
"name",
|
|
||||||
"created_at",
|
|
||||||
"created_by",
|
|
||||||
"modified_at",
|
|
||||||
"modified_by",
|
|
||||||
"archived"
|
|
||||||
],
|
|
||||||
"grouped_fields": {
|
|
||||||
"user": [
|
|
||||||
"id",
|
|
||||||
"type"
|
|
||||||
],
|
|
||||||
"organization": [
|
|
||||||
"id",
|
|
||||||
"type",
|
|
||||||
"name"
|
|
||||||
],
|
|
||||||
"entity": [
|
|
||||||
"id",
|
|
||||||
"type",
|
|
||||||
"created_at",
|
|
||||||
"created_by",
|
|
||||||
"modified_at",
|
|
||||||
"modified_by",
|
|
||||||
"archived"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"lookup_fields": [],
|
|
||||||
"historical": true,
|
|
||||||
"notify": true,
|
|
||||||
"relationship": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "person",
|
|
||||||
"schemas": [
|
|
||||||
{
|
|
||||||
"$id": "person",
|
|
||||||
"$ref": "user",
|
|
||||||
"properties": {
|
|
||||||
"first_name": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"last_name": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"date_of_birth": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"pronouns": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"contact_id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"contacts": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "contact",
|
|
||||||
"properties": {
|
|
||||||
"target": {
|
|
||||||
"oneOf": [
|
|
||||||
{
|
|
||||||
"$ref": "phone_number"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$ref": "email_address"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"hierarchy": [
|
|
||||||
"person",
|
|
||||||
"user",
|
|
||||||
"organization",
|
|
||||||
"entity"
|
|
||||||
],
|
|
||||||
"fields": [
|
|
||||||
"id",
|
|
||||||
"type",
|
|
||||||
"first_name",
|
|
||||||
"last_name",
|
|
||||||
"date_of_birth",
|
|
||||||
"pronouns",
|
|
||||||
"contact_id",
|
|
||||||
"name",
|
|
||||||
"created_at",
|
|
||||||
"created_by",
|
|
||||||
"modified_at",
|
|
||||||
"modified_by",
|
|
||||||
"archived"
|
|
||||||
],
|
|
||||||
"grouped_fields": {
|
|
||||||
"person": [
|
|
||||||
"id",
|
|
||||||
"type",
|
|
||||||
"first_name",
|
|
||||||
"last_name",
|
|
||||||
"date_of_birth",
|
|
||||||
"pronouns",
|
|
||||||
"contact_id"
|
|
||||||
],
|
|
||||||
"user": [
|
|
||||||
"id",
|
|
||||||
"type"
|
|
||||||
],
|
|
||||||
"organization": [
|
|
||||||
"id",
|
|
||||||
"type",
|
|
||||||
"name"
|
|
||||||
],
|
|
||||||
"entity": [
|
|
||||||
"id",
|
|
||||||
"type",
|
|
||||||
"created_at",
|
|
||||||
"created_by",
|
|
||||||
"modified_at",
|
|
||||||
"modified_by",
|
|
||||||
"archived"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"lookup_fields": [
|
|
||||||
"first_name",
|
|
||||||
"last_name",
|
|
||||||
"date_of_birth",
|
|
||||||
"pronouns"
|
|
||||||
],
|
|
||||||
"historical": true,
|
|
||||||
"notify": true,
|
|
||||||
"relationship": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "order",
|
|
||||||
"schemas": [
|
|
||||||
{
|
|
||||||
"$id": "order",
|
|
||||||
"$ref": "entity",
|
|
||||||
"properties": {
|
|
||||||
"total": {
|
|
||||||
"type": "number"
|
|
||||||
},
|
|
||||||
"customer_id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"customer": {
|
|
||||||
"$ref": "person"
|
|
||||||
},
|
|
||||||
"lines": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "order_line"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"hierarchy": [
|
|
||||||
"order",
|
|
||||||
"entity"
|
|
||||||
],
|
|
||||||
"fields": [
|
|
||||||
"id",
|
|
||||||
"type",
|
|
||||||
"total",
|
|
||||||
"customer_id",
|
|
||||||
"created_at",
|
|
||||||
"created_by",
|
|
||||||
"modified_at",
|
|
||||||
"modified_by",
|
|
||||||
"archived"
|
|
||||||
],
|
|
||||||
"grouped_fields": {
|
|
||||||
"order": [
|
|
||||||
"id",
|
|
||||||
"type",
|
|
||||||
"total",
|
|
||||||
"customer_id"
|
|
||||||
],
|
|
||||||
"entity": [
|
|
||||||
"id",
|
|
||||||
"type",
|
|
||||||
"created_at",
|
|
||||||
"created_by",
|
|
||||||
"modified_at",
|
|
||||||
"modified_by",
|
|
||||||
"archived"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"lookup_fields": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"historical": true,
|
|
||||||
"notify": true,
|
|
||||||
"relationship": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "order_line",
|
|
||||||
"schemas": [
|
|
||||||
{
|
|
||||||
"$id": "order_line",
|
|
||||||
"$ref": "entity",
|
|
||||||
"properties": {
|
|
||||||
"order_id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"product": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"price": {
|
|
||||||
"type": "number"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"hierarchy": [
|
|
||||||
"order_line",
|
|
||||||
"entity"
|
|
||||||
],
|
|
||||||
"fields": [
|
|
||||||
"id",
|
|
||||||
"type",
|
|
||||||
"order_id",
|
|
||||||
"product",
|
|
||||||
"price",
|
|
||||||
"created_at",
|
|
||||||
"created_by",
|
|
||||||
"modified_at",
|
|
||||||
"modified_by",
|
|
||||||
"archived"
|
|
||||||
],
|
|
||||||
"grouped_fields": {
|
|
||||||
"order_line": [
|
|
||||||
"id",
|
|
||||||
"type",
|
|
||||||
"order_id",
|
|
||||||
"product",
|
|
||||||
"price"
|
|
||||||
],
|
|
||||||
"entity": [
|
|
||||||
"id",
|
|
||||||
"type",
|
|
||||||
"created_at",
|
|
||||||
"created_by",
|
|
||||||
"modified_at",
|
|
||||||
"modified_by",
|
|
||||||
"archived"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"lookup_fields": [],
|
|
||||||
"historical": true,
|
|
||||||
"notify": true,
|
|
||||||
"relationship": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "relationship",
|
|
||||||
"relationship": true,
|
|
||||||
"hierarchy": [
|
|
||||||
"relationship",
|
|
||||||
"entity"
|
|
||||||
],
|
|
||||||
"fields": [
|
|
||||||
"source_id",
|
|
||||||
"source_type",
|
|
||||||
"target_id",
|
|
||||||
"target_type",
|
|
||||||
"id",
|
|
||||||
"type",
|
|
||||||
"name",
|
|
||||||
"archived",
|
|
||||||
"created_at",
|
|
||||||
"created_by",
|
|
||||||
"modified_at",
|
|
||||||
"modified_by"
|
|
||||||
],
|
|
||||||
"grouped_fields": {
|
|
||||||
"entity": [
|
|
||||||
"id",
|
|
||||||
"type",
|
|
||||||
"name",
|
|
||||||
"archived",
|
|
||||||
"created_at",
|
|
||||||
"created_by",
|
|
||||||
"modified_at",
|
|
||||||
"modified_by"
|
|
||||||
],
|
|
||||||
"relationship": [
|
|
||||||
"source_id",
|
|
||||||
"source_type",
|
|
||||||
"target_id",
|
|
||||||
"target_type"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"field_types": {
|
|
||||||
"id": "uuid",
|
|
||||||
"type": "text",
|
|
||||||
"archived": "boolean",
|
|
||||||
"source_id": "uuid",
|
|
||||||
"source_type": "text",
|
|
||||||
"target_id": "uuid",
|
|
||||||
"target_type": "text",
|
|
||||||
"name": "text",
|
|
||||||
"created_at": "timestamptz",
|
|
||||||
"created_by": "uuid",
|
|
||||||
"modified_at": "timestamptz",
|
|
||||||
"modified_by": "uuid"
|
|
||||||
},
|
|
||||||
"schemas": [
|
|
||||||
{
|
|
||||||
"$id": "relationship",
|
|
||||||
"$ref": "entity",
|
|
||||||
"properties": {}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"lookup_fields": [],
|
|
||||||
"historical": true,
|
|
||||||
"notify": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "contact",
|
|
||||||
"relationship": true,
|
|
||||||
"hierarchy": [
|
|
||||||
"contact",
|
|
||||||
"relationship",
|
|
||||||
"entity"
|
|
||||||
],
|
|
||||||
"fields": [
|
|
||||||
"is_primary",
|
|
||||||
"source_id",
|
|
||||||
"source_type",
|
|
||||||
"target_id",
|
|
||||||
"target_type",
|
|
||||||
"id",
|
|
||||||
"type",
|
|
||||||
"name",
|
|
||||||
"archived",
|
|
||||||
"created_at",
|
|
||||||
"created_by",
|
|
||||||
"modified_at",
|
|
||||||
"modified_by"
|
|
||||||
],
|
|
||||||
"grouped_fields": {
|
|
||||||
"entity": [
|
|
||||||
"id",
|
|
||||||
"type",
|
|
||||||
"name",
|
|
||||||
"archived",
|
|
||||||
"created_at",
|
|
||||||
"created_by",
|
|
||||||
"modified_at",
|
|
||||||
"modified_by"
|
|
||||||
],
|
|
||||||
"relationship": [
|
|
||||||
"source_id",
|
|
||||||
"source_type",
|
|
||||||
"target_id",
|
|
||||||
"target_type"
|
|
||||||
],
|
|
||||||
"contact": [
|
|
||||||
"is_primary"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"field_types": {
|
|
||||||
"id": "uuid",
|
|
||||||
"type": "text",
|
|
||||||
"archived": "boolean",
|
|
||||||
"source_id": "uuid",
|
|
||||||
"source_type": "text",
|
|
||||||
"target_id": "uuid",
|
|
||||||
"target_type": "text",
|
|
||||||
"is_primary": "boolean",
|
|
||||||
"name": "text",
|
|
||||||
"created_at": "timestamptz",
|
|
||||||
"created_by": "uuid",
|
|
||||||
"modified_at": "timestamptz",
|
|
||||||
"modified_by": "uuid"
|
|
||||||
},
|
|
||||||
"schemas": [
|
|
||||||
{
|
|
||||||
"$id": "contact",
|
|
||||||
"$ref": "relationship",
|
|
||||||
"properties": {
|
|
||||||
"is_primary": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"lookup_fields": [],
|
|
||||||
"historical": true,
|
|
||||||
"notify": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "phone_number",
|
|
||||||
"hierarchy": [
|
|
||||||
"phone_number",
|
|
||||||
"entity"
|
|
||||||
],
|
|
||||||
"fields": [
|
|
||||||
"number",
|
|
||||||
"id",
|
|
||||||
"type",
|
|
||||||
"name",
|
|
||||||
"archived",
|
|
||||||
"created_at",
|
|
||||||
"created_by",
|
|
||||||
"modified_at",
|
|
||||||
"modified_by"
|
|
||||||
],
|
|
||||||
"grouped_fields": {
|
|
||||||
"entity": [
|
|
||||||
"id",
|
|
||||||
"type",
|
|
||||||
"name",
|
|
||||||
"archived",
|
|
||||||
"created_at",
|
|
||||||
"created_by",
|
|
||||||
"modified_at",
|
|
||||||
"modified_by"
|
|
||||||
],
|
|
||||||
"phone_number": [
|
|
||||||
"number"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"field_types": {
|
|
||||||
"id": "uuid",
|
|
||||||
"type": "text",
|
|
||||||
"archived": "boolean",
|
|
||||||
"number": "text",
|
|
||||||
"name": "text",
|
|
||||||
"created_at": "timestamptz",
|
|
||||||
"created_by": "uuid",
|
|
||||||
"modified_at": "timestamptz",
|
|
||||||
"modified_by": "uuid"
|
|
||||||
},
|
|
||||||
"schemas": [
|
|
||||||
{
|
|
||||||
"$id": "phone_number",
|
|
||||||
"$ref": "entity",
|
|
||||||
"properties": {
|
|
||||||
"number": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"lookup_fields": [],
|
|
||||||
"historical": true,
|
|
||||||
"notify": true,
|
|
||||||
"relationship": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "email_address",
|
|
||||||
"hierarchy": [
|
|
||||||
"email_address",
|
|
||||||
"entity"
|
|
||||||
],
|
|
||||||
"fields": [
|
|
||||||
"address",
|
|
||||||
"id",
|
|
||||||
"type",
|
|
||||||
"name",
|
|
||||||
"archived",
|
|
||||||
"created_at",
|
|
||||||
"created_by",
|
|
||||||
"modified_at",
|
|
||||||
"modified_by"
|
|
||||||
],
|
|
||||||
"grouped_fields": {
|
|
||||||
"entity": [
|
|
||||||
"id",
|
|
||||||
"type",
|
|
||||||
"name",
|
|
||||||
"archived",
|
|
||||||
"created_at",
|
|
||||||
"created_by",
|
|
||||||
"modified_at",
|
|
||||||
"modified_by"
|
|
||||||
],
|
|
||||||
"email_address": [
|
|
||||||
"address"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"field_types": {
|
|
||||||
"id": "uuid",
|
|
||||||
"type": "text",
|
|
||||||
"archived": "boolean",
|
|
||||||
"address": "text",
|
|
||||||
"name": "text",
|
|
||||||
"created_at": "timestamptz",
|
|
||||||
"created_by": "uuid",
|
|
||||||
"modified_at": "timestamptz",
|
|
||||||
"modified_by": "uuid"
|
|
||||||
},
|
|
||||||
"schemas": [
|
|
||||||
{
|
|
||||||
"$id": "email_address",
|
|
||||||
"$ref": "entity",
|
|
||||||
"properties": {
|
|
||||||
"address": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"lookup_fields": [],
|
|
||||||
"historical": true,
|
|
||||||
"notify": true,
|
|
||||||
"relationship": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "attachment",
|
|
||||||
"schemas": [
|
|
||||||
{
|
|
||||||
"$id": "type_metadata",
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"type": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$id": "other_metadata",
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"other": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$id": "attachment",
|
|
||||||
"$ref": "entity",
|
|
||||||
"properties": {
|
|
||||||
"flags": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type_metadata": {
|
|
||||||
"$ref": "type_metadata"
|
|
||||||
},
|
|
||||||
"other_metadata": {
|
|
||||||
"$ref": "other_metadata"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"hierarchy": [
|
|
||||||
"attachment",
|
|
||||||
"entity"
|
|
||||||
],
|
|
||||||
"fields": [
|
|
||||||
"id",
|
|
||||||
"type",
|
|
||||||
"flags",
|
|
||||||
"type_metadata",
|
|
||||||
"other_metadata",
|
|
||||||
"created_at",
|
|
||||||
"created_by",
|
|
||||||
"modified_at",
|
|
||||||
"modified_by",
|
|
||||||
"archived"
|
|
||||||
],
|
|
||||||
"grouped_fields": {
|
|
||||||
"attachment": [
|
|
||||||
"id",
|
|
||||||
"type",
|
|
||||||
"flags",
|
|
||||||
"type_metadata",
|
|
||||||
"other_metadata"
|
|
||||||
],
|
|
||||||
"entity": [
|
|
||||||
"id",
|
|
||||||
"type",
|
|
||||||
"created_at",
|
|
||||||
"created_by",
|
|
||||||
"modified_at",
|
|
||||||
"modified_by",
|
|
||||||
"archived"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"field_types": {
|
|
||||||
"id": "uuid",
|
|
||||||
"type": "text",
|
|
||||||
"flags": "_text",
|
|
||||||
"type_metadata": "jsonb",
|
|
||||||
"other_metadata": "jsonb",
|
|
||||||
"created_at": "timestamptz",
|
|
||||||
"created_by": "uuid",
|
|
||||||
"modified_at": "timestamptz",
|
|
||||||
"modified_by": "uuid",
|
|
||||||
"archived": "boolean"
|
|
||||||
},
|
|
||||||
"lookup_fields": [],
|
|
||||||
"historical": true,
|
|
||||||
"notify": true,
|
|
||||||
"relationship": false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -2401,6 +2401,215 @@
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Anchor order and insert new line (no line id)",
|
||||||
|
"action": "merge",
|
||||||
|
"data": {
|
||||||
|
"id": "abc",
|
||||||
|
"type": "order",
|
||||||
|
"lines": [
|
||||||
|
{
|
||||||
|
"type": "order_line",
|
||||||
|
"product": "Widget",
|
||||||
|
"price": 99.0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"schema_id": "order",
|
||||||
|
"expect": {
|
||||||
|
"success": true,
|
||||||
|
"sql": [
|
||||||
|
[
|
||||||
|
"INSERT INTO agreego.\"entity\" (",
|
||||||
|
" \"created_at\",",
|
||||||
|
" \"created_by\",",
|
||||||
|
" \"id\",",
|
||||||
|
" \"modified_at\",",
|
||||||
|
" \"modified_by\",",
|
||||||
|
" \"type\"",
|
||||||
|
")",
|
||||||
|
"VALUES (",
|
||||||
|
" '{{timestamp}}',",
|
||||||
|
" '00000000-0000-0000-0000-000000000000',",
|
||||||
|
" '{{uuid:line_id}}',",
|
||||||
|
" '{{timestamp}}',",
|
||||||
|
" '00000000-0000-0000-0000-000000000000',",
|
||||||
|
" 'order_line'",
|
||||||
|
")"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"INSERT INTO agreego.\"order_line\" (",
|
||||||
|
" \"id\",",
|
||||||
|
" \"order_id\",",
|
||||||
|
" \"price\",",
|
||||||
|
" \"product\",",
|
||||||
|
" \"type\"",
|
||||||
|
")",
|
||||||
|
"VALUES (",
|
||||||
|
" '{{uuid:line_id}}',",
|
||||||
|
" '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\"",
|
||||||
|
" }',",
|
||||||
|
" '{{uuid:line_id}}',",
|
||||||
|
" '{{uuid}}',",
|
||||||
|
" 'create',",
|
||||||
|
" '{{timestamp}}',",
|
||||||
|
" '00000000-0000-0000-0000-000000000000'",
|
||||||
|
")"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"SELECT pg_notify('entity', '{",
|
||||||
|
" \"complete\":{",
|
||||||
|
" \"created_at\":\"{{timestamp}}\",",
|
||||||
|
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||||
|
" \"id\":\"{{uuid:line_id}}\",",
|
||||||
|
" \"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\"",
|
||||||
|
" }",
|
||||||
|
" }')"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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\"",
|
||||||
|
" }",
|
||||||
|
" }')"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1408,6 +1408,44 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"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)"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Order select with customer and lines",
|
"description": "Order select with customer and lines",
|
||||||
"action": "query",
|
"action": "query",
|
||||||
|
|||||||
@ -79,7 +79,18 @@ impl Database {
|
|||||||
db.relations.insert(def.constraint.clone(), def);
|
db.relations.insert(def.constraint.clone(), def);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => println!("DATABASE RELATION PARSE FAILED: {:?}", e),
|
Err(e) => {
|
||||||
|
return Err(crate::drop::Drop::with_errors(vec![crate::drop::Error {
|
||||||
|
code: "DATABASE_RELATION_PARSE_FAILED".to_string(),
|
||||||
|
message: format!("Failed to parse database relation: {}", e),
|
||||||
|
details: crate::drop::ErrorDetails {
|
||||||
|
path: "".to_string(),
|
||||||
|
cause: None,
|
||||||
|
context: None,
|
||||||
|
schema: None,
|
||||||
|
},
|
||||||
|
}]));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -137,7 +148,30 @@ impl Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn compile(&mut self) -> Result<(), crate::drop::Drop> {
|
pub fn compile(&mut self) -> Result<(), crate::drop::Drop> {
|
||||||
self.collect_schemas();
|
let mut harvested = Vec::new();
|
||||||
|
for schema in self.schemas.values_mut() {
|
||||||
|
if let Err(msg) = schema.collect_schemas(None, &mut harvested) {
|
||||||
|
return Err(crate::drop::Drop::with_errors(vec![crate::drop::Error {
|
||||||
|
code: "SCHEMA_VALIDATION_FAILED".to_string(),
|
||||||
|
message: msg,
|
||||||
|
details: crate::drop::ErrorDetails { path: "".to_string(), cause: None, context: None, schema: None },
|
||||||
|
}]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.schemas.extend(harvested);
|
||||||
|
|
||||||
|
if let Err(msg) = self.collect_schemas() {
|
||||||
|
return Err(crate::drop::Drop::with_errors(vec![crate::drop::Error {
|
||||||
|
code: "SCHEMA_VALIDATION_FAILED".to_string(),
|
||||||
|
message: msg,
|
||||||
|
details: crate::drop::ErrorDetails {
|
||||||
|
path: "".to_string(),
|
||||||
|
cause: None,
|
||||||
|
context: None,
|
||||||
|
schema: None,
|
||||||
|
},
|
||||||
|
}]));
|
||||||
|
}
|
||||||
self.collect_depths();
|
self.collect_depths();
|
||||||
self.collect_descendants();
|
self.collect_descendants();
|
||||||
|
|
||||||
@ -150,29 +184,31 @@ impl Database {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn collect_schemas(&mut self) {
|
fn collect_schemas(&mut self) -> Result<(), String> {
|
||||||
let mut to_insert = Vec::new();
|
let mut to_insert = Vec::new();
|
||||||
|
|
||||||
// Pass 1: Extract all Schemas structurally off top level definitions into the master registry.
|
// Pass 1: Extract all Schemas structurally off top level definitions into the master registry.
|
||||||
|
// Validate every node recursively via string filters natively!
|
||||||
for type_def in self.types.values() {
|
for type_def in self.types.values() {
|
||||||
for mut schema in type_def.schemas.clone() {
|
for mut schema in type_def.schemas.clone() {
|
||||||
schema.harvest(&mut to_insert);
|
schema.collect_schemas(None, &mut to_insert)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for punc_def in self.puncs.values() {
|
for punc_def in self.puncs.values() {
|
||||||
for mut schema in punc_def.schemas.clone() {
|
for mut schema in punc_def.schemas.clone() {
|
||||||
schema.harvest(&mut to_insert);
|
schema.collect_schemas(None, &mut to_insert)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for enum_def in self.enums.values() {
|
for enum_def in self.enums.values() {
|
||||||
for mut schema in enum_def.schemas.clone() {
|
for mut schema in enum_def.schemas.clone() {
|
||||||
schema.harvest(&mut to_insert);
|
schema.collect_schemas(None, &mut to_insert)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (id, schema) in to_insert {
|
for (id, schema) in to_insert {
|
||||||
self.schemas.insert(id, schema);
|
self.schemas.insert(id, schema);
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn collect_depths(&mut self) {
|
fn collect_depths(&mut self) {
|
||||||
@ -228,82 +264,6 @@ impl Database {
|
|||||||
self.descendants = descendants;
|
self.descendants = descendants;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_relation(
|
|
||||||
&self,
|
|
||||||
parent_type: &str,
|
|
||||||
child_type: &str,
|
|
||||||
prop_name: &str,
|
|
||||||
relative_keys: Option<&Vec<String>>,
|
|
||||||
) -> Option<(&Relation, bool)> {
|
|
||||||
if parent_type == "entity" && child_type == "entity" {
|
|
||||||
return None; // Ignore entity <-> entity generic fallbacks, they aren't useful edges
|
|
||||||
}
|
|
||||||
|
|
||||||
let p_def = self.types.get(parent_type)?;
|
|
||||||
let c_def = self.types.get(child_type)?;
|
|
||||||
|
|
||||||
let mut matching_rels = Vec::new();
|
|
||||||
let mut directions = Vec::new();
|
|
||||||
|
|
||||||
for rel in self.relations.values() {
|
|
||||||
let is_forward = p_def.hierarchy.contains(&rel.source_type)
|
|
||||||
&& c_def.hierarchy.contains(&rel.destination_type);
|
|
||||||
let is_reverse = p_def.hierarchy.contains(&rel.destination_type)
|
|
||||||
&& c_def.hierarchy.contains(&rel.source_type);
|
|
||||||
|
|
||||||
if is_forward {
|
|
||||||
matching_rels.push(rel);
|
|
||||||
directions.push(true);
|
|
||||||
} else if is_reverse {
|
|
||||||
matching_rels.push(rel);
|
|
||||||
directions.push(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if matching_rels.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
if matching_rels.len() == 1 {
|
|
||||||
return Some((matching_rels[0], directions[0]));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut chosen_idx = 0;
|
|
||||||
let mut resolved = false;
|
|
||||||
|
|
||||||
// Reduce ambiguity with prefix
|
|
||||||
for (i, rel) in matching_rels.iter().enumerate() {
|
|
||||||
if let Some(prefix) = &rel.prefix {
|
|
||||||
if prop_name.starts_with(prefix)
|
|
||||||
|| prefix.starts_with(prop_name)
|
|
||||||
|| prefix.replace("_", "") == prop_name.replace("_", "")
|
|
||||||
{
|
|
||||||
chosen_idx = i;
|
|
||||||
resolved = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reduce ambiguity by checking if relative payload OMITS the prefix (M:M heuristic)
|
|
||||||
if !resolved && relative_keys.is_some() {
|
|
||||||
let keys = relative_keys.unwrap();
|
|
||||||
let mut missing_prefix_ids = Vec::new();
|
|
||||||
for (i, rel) in matching_rels.iter().enumerate() {
|
|
||||||
if let Some(prefix) = &rel.prefix {
|
|
||||||
if !keys.contains(prefix) {
|
|
||||||
missing_prefix_ids.push(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if missing_prefix_ids.len() == 1 {
|
|
||||||
chosen_idx = missing_prefix_ids[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Some((matching_rels[chosen_idx], directions[chosen_idx]))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn collect_descendants_recursively(
|
fn collect_descendants_recursively(
|
||||||
target: &str,
|
target: &str,
|
||||||
direct_refs: &std::collections::HashMap<String, Vec<String>>,
|
direct_refs: &std::collections::HashMap<String, Vec<String>>,
|
||||||
|
|||||||
@ -393,67 +393,108 @@ impl Schema {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn harvest(&mut self, to_insert: &mut Vec<(String, Schema)>) {
|
#[allow(unused_variables)]
|
||||||
if let Some(id) = &self.obj.id {
|
fn validate_identifier(id: &str, field_name: &str) -> Result<(), String> {
|
||||||
to_insert.push((id.clone(), self.clone()));
|
#[cfg(not(test))]
|
||||||
|
for c in id.chars() {
|
||||||
|
if !c.is_ascii_lowercase() && !c.is_ascii_digit() && c != '_' && c != '.' {
|
||||||
|
return Err(format!("Invalid character '{}' in JSON Schema '{}' property: '{}'. Identifiers must exclusively contain [a-z0-9_.]", c, field_name, id));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.harvest_children(|child| child.harvest(to_insert));
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn harvest_children<F>(&mut self, mut f: F)
|
pub fn collect_schemas(
|
||||||
where
|
&mut self,
|
||||||
F: FnMut(&mut Schema),
|
tracking_path: Option<String>,
|
||||||
{
|
to_insert: &mut Vec<(String, Schema)>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
if let Some(id) = &self.obj.id {
|
||||||
|
Self::validate_identifier(id, "$id")?;
|
||||||
|
to_insert.push((id.clone(), self.clone()));
|
||||||
|
}
|
||||||
|
if let Some(r#ref) = &self.obj.r#ref {
|
||||||
|
Self::validate_identifier(r#ref, "$ref")?;
|
||||||
|
}
|
||||||
|
if let Some(family) = &self.obj.family {
|
||||||
|
Self::validate_identifier(family, "$family")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is this schema an inline ad-hoc composition?
|
||||||
|
// Meaning it has a tracking context, lacks an explicit $id, but extends an Entity ref with explicit properties!
|
||||||
|
if self.obj.id.is_none() && self.obj.r#ref.is_some() && self.obj.properties.is_some() {
|
||||||
|
if let Some(ref path) = tracking_path {
|
||||||
|
to_insert.push((path.clone(), self.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provide the path origin to children natively, prioritizing the explicit `$id` boundary if one exists
|
||||||
|
let origin_path = self.obj.id.clone().or(tracking_path);
|
||||||
|
|
||||||
|
self.collect_child_schemas(origin_path, to_insert)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn collect_child_schemas(
|
||||||
|
&mut self,
|
||||||
|
origin_path: Option<String>,
|
||||||
|
to_insert: &mut Vec<(String, Schema)>,
|
||||||
|
) -> Result<(), String> {
|
||||||
if let Some(props) = &mut self.obj.properties {
|
if let Some(props) = &mut self.obj.properties {
|
||||||
for v in props.values_mut() {
|
for (k, v) in props.iter_mut() {
|
||||||
let mut inner = (**v).clone();
|
let mut inner = (**v).clone();
|
||||||
f(&mut inner);
|
let next_path = origin_path.as_ref().map(|o| format!("{}/{}", o, k));
|
||||||
|
inner.collect_schemas(next_path, to_insert)?;
|
||||||
*v = Arc::new(inner);
|
*v = Arc::new(inner);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(pattern_props) = &mut self.obj.pattern_properties {
|
if let Some(pattern_props) = &mut self.obj.pattern_properties {
|
||||||
for v in pattern_props.values_mut() {
|
for (k, v) in pattern_props.iter_mut() {
|
||||||
let mut inner = (**v).clone();
|
let mut inner = (**v).clone();
|
||||||
f(&mut inner);
|
let next_path = origin_path.as_ref().map(|o| format!("{}/{}", o, k));
|
||||||
|
inner.collect_schemas(next_path, to_insert)?;
|
||||||
*v = Arc::new(inner);
|
*v = Arc::new(inner);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut map_arr = |arr: &mut Vec<Arc<Schema>>| {
|
let mut map_arr = |arr: &mut Vec<Arc<Schema>>| -> Result<(), String> {
|
||||||
for v in arr.iter_mut() {
|
for v in arr.iter_mut() {
|
||||||
let mut inner = (**v).clone();
|
let mut inner = (**v).clone();
|
||||||
f(&mut inner);
|
inner.collect_schemas(origin_path.clone(), to_insert)?;
|
||||||
*v = Arc::new(inner);
|
*v = Arc::new(inner);
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(arr) = &mut self.obj.prefix_items {
|
if let Some(arr) = &mut self.obj.prefix_items { map_arr(arr)?; }
|
||||||
map_arr(arr);
|
if let Some(arr) = &mut self.obj.all_of { map_arr(arr)?; }
|
||||||
}
|
if let Some(arr) = &mut self.obj.one_of { map_arr(arr)?; }
|
||||||
if let Some(arr) = &mut self.obj.all_of {
|
|
||||||
map_arr(arr);
|
|
||||||
}
|
|
||||||
if let Some(arr) = &mut self.obj.one_of {
|
|
||||||
map_arr(arr);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut map_opt = |opt: &mut Option<Arc<Schema>>| {
|
let mut map_opt = |opt: &mut Option<Arc<Schema>>, pass_path: bool| -> Result<(), String> {
|
||||||
if let Some(v) = opt {
|
if let Some(v) = opt {
|
||||||
let mut inner = (**v).clone();
|
let mut inner = (**v).clone();
|
||||||
f(&mut inner);
|
let next = if pass_path { origin_path.clone() } else { None };
|
||||||
|
inner.collect_schemas(next, to_insert)?;
|
||||||
*v = Arc::new(inner);
|
*v = Arc::new(inner);
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
};
|
};
|
||||||
|
|
||||||
map_opt(&mut self.obj.additional_properties);
|
map_opt(&mut self.obj.additional_properties, false)?;
|
||||||
map_opt(&mut self.obj.items);
|
|
||||||
map_opt(&mut self.obj.contains);
|
// `items` absolutely must inherit the EXACT property path assigned to the Array wrapper!
|
||||||
map_opt(&mut self.obj.property_names);
|
// This allows nested Arrays enclosing bare Entity structs to correctly register as the boundary mapping.
|
||||||
map_opt(&mut self.obj.not);
|
map_opt(&mut self.obj.items, true)?;
|
||||||
map_opt(&mut self.obj.if_);
|
|
||||||
map_opt(&mut self.obj.then_);
|
map_opt(&mut self.obj.not, false)?;
|
||||||
map_opt(&mut self.obj.else_);
|
map_opt(&mut self.obj.contains, false)?;
|
||||||
|
map_opt(&mut self.obj.property_names, false)?;
|
||||||
|
map_opt(&mut self.obj.if_, false)?;
|
||||||
|
map_opt(&mut self.obj.then_, false)?;
|
||||||
|
map_opt(&mut self.obj.else_, false)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn compile_edges(
|
pub fn compile_edges(
|
||||||
@ -507,7 +548,7 @@ impl Schema {
|
|||||||
let keys_for_ambiguity: Vec<String> =
|
let keys_for_ambiguity: Vec<String> =
|
||||||
compiled_target_props.keys().cloned().collect();
|
compiled_target_props.keys().cloned().collect();
|
||||||
if let Some((relation, is_forward)) =
|
if let Some((relation, is_forward)) =
|
||||||
db.resolve_relation(&p_type, &c_type, prop_name, Some(&keys_for_ambiguity))
|
resolve_relation(db, &p_type, &c_type, prop_name, Some(&keys_for_ambiguity))
|
||||||
{
|
{
|
||||||
schema_edges.insert(
|
schema_edges.insert(
|
||||||
prop_name.clone(),
|
prop_name.clone(),
|
||||||
@ -527,6 +568,80 @@ impl Schema {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn resolve_relation<'a>(
|
||||||
|
db: &'a crate::database::Database,
|
||||||
|
parent_type: &str,
|
||||||
|
child_type: &str,
|
||||||
|
prop_name: &str,
|
||||||
|
relative_keys: Option<&Vec<String>>,
|
||||||
|
) -> Option<(&'a crate::database::relation::Relation, bool)> {
|
||||||
|
if parent_type == "entity" && child_type == "entity" {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let p_def = db.types.get(parent_type)?;
|
||||||
|
let c_def = db.types.get(child_type)?;
|
||||||
|
|
||||||
|
let mut matching_rels = Vec::new();
|
||||||
|
let mut directions = Vec::new();
|
||||||
|
|
||||||
|
for rel in db.relations.values() {
|
||||||
|
let is_forward = p_def.hierarchy.contains(&rel.source_type)
|
||||||
|
&& c_def.hierarchy.contains(&rel.destination_type);
|
||||||
|
let is_reverse = p_def.hierarchy.contains(&rel.destination_type)
|
||||||
|
&& c_def.hierarchy.contains(&rel.source_type);
|
||||||
|
|
||||||
|
if is_forward {
|
||||||
|
matching_rels.push(rel);
|
||||||
|
directions.push(true);
|
||||||
|
} else if is_reverse {
|
||||||
|
matching_rels.push(rel);
|
||||||
|
directions.push(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if matching_rels.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if matching_rels.len() == 1 {
|
||||||
|
return Some((matching_rels[0], directions[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut chosen_idx = 0;
|
||||||
|
let mut resolved = false;
|
||||||
|
|
||||||
|
for (i, rel) in matching_rels.iter().enumerate() {
|
||||||
|
if let Some(prefix) = &rel.prefix {
|
||||||
|
if prop_name.starts_with(prefix)
|
||||||
|
|| prefix.starts_with(prop_name)
|
||||||
|
|| prefix.replace("_", "") == prop_name.replace("_", "")
|
||||||
|
{
|
||||||
|
chosen_idx = i;
|
||||||
|
resolved = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !resolved && relative_keys.is_some() {
|
||||||
|
let keys = relative_keys.unwrap();
|
||||||
|
let mut missing_prefix_ids = Vec::new();
|
||||||
|
for (i, rel) in matching_rels.iter().enumerate() {
|
||||||
|
if let Some(prefix) = &rel.prefix {
|
||||||
|
if !keys.contains(prefix) {
|
||||||
|
missing_prefix_ids.push(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if missing_prefix_ids.len() == 1 {
|
||||||
|
chosen_idx = missing_prefix_ids[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some((matching_rels[chosen_idx], directions[chosen_idx]))
|
||||||
|
}
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for Schema {
|
impl<'de> Deserialize<'de> for Schema {
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
where
|
where
|
||||||
|
|||||||
@ -403,6 +403,23 @@ impl Merger {
|
|||||||
> {
|
> {
|
||||||
let type_name = type_def.name.as_str();
|
let type_name = type_def.name.as_str();
|
||||||
|
|
||||||
|
// 🚀 Anchor Short-Circuit Optimization
|
||||||
|
// An anchor is STRICTLY a struct containing merely an `id` and `type`.
|
||||||
|
// 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.
|
||||||
|
let is_anchor = entity_fields.len() == 2
|
||||||
|
&& entity_fields.contains_key("id")
|
||||||
|
&& entity_fields.contains_key("type");
|
||||||
|
|
||||||
|
let has_valid_id = entity_fields
|
||||||
|
.get("id")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map_or(false, |s| !s.is_empty());
|
||||||
|
|
||||||
|
if is_anchor && has_valid_id {
|
||||||
|
return Ok((entity_fields, None, None));
|
||||||
|
}
|
||||||
|
|
||||||
let entity_fetched = self.fetch_entity(&entity_fields, type_def)?;
|
let entity_fetched = self.fetch_entity(&entity_fields, type_def)?;
|
||||||
|
|
||||||
let system_keys = vec![
|
let system_keys = vec![
|
||||||
|
|||||||
@ -1451,6 +1451,12 @@ fn test_queryer_0_6() {
|
|||||||
crate::tests::runner::run_test_case(&path, 0, 6).unwrap();
|
crate::tests::runner::run_test_case(&path, 0, 6).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_queryer_0_7() {
|
||||||
|
let path = format!("{}/fixtures/queryer.json", env!("CARGO_MANIFEST_DIR"));
|
||||||
|
crate::tests::runner::run_test_case(&path, 0, 7).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_not_0_0() {
|
fn test_not_0_0() {
|
||||||
let path = format!("{}/fixtures/not.json", env!("CARGO_MANIFEST_DIR"));
|
let path = format!("{}/fixtures/not.json", env!("CARGO_MANIFEST_DIR"));
|
||||||
@ -8542,3 +8548,15 @@ fn test_merger_0_8() {
|
|||||||
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, 8).unwrap();
|
crate::tests::runner::run_test_case(&path, 0, 8).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_merger_0_9() {
|
||||||
|
let path = format!("{}/fixtures/merger.json", env!("CARGO_MANIFEST_DIR"));
|
||||||
|
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