Compare commits

...

10 Commits

Author SHA1 Message Date
9e362f2168 version: 1.0.159 2026-06-15 16:44:04 -04:00
9fffc7707f Merge branch 'main' of gitea-ssh.thoughtpatterns.ai:cellular/jspg 2026-06-15 16:42:13 -04:00
c5d652c6fd fixed merger ordering issue 2026-06-15 16:41:59 -04:00
d2cdd680ed insert fix checkpoint 2026-06-15 16:18:44 -04:00
6f1c0d7ee9 version: 1.0.158 2026-06-12 22:11:14 -04:00
25bbf2b564 merger: generate entity and changelog ids with uuid v7
Switch the two UUID generation sites in the merger from v4 to time-ordered
v7, and swap the uuid crate feature flag from v4 to v7 accordingly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 22:11:09 -04:00
c8cc4cbde8 version: 1.0.157 2026-06-11 20:40:36 -04:00
5af2399e3b fixed issue with filter generation where filters or conditions are used internally 2026-06-11 20:40:27 -04:00
1d56bae9a5 version: 1.0.156 2026-06-11 17:37:43 -04:00
813e9ff3c2 more executor reverts 2026-06-11 17:37:34 -04:00
14 changed files with 1193 additions and 984 deletions

View File

@ -16,7 +16,7 @@ url = "2.5.8"
fluent-uri = "0.3.2" fluent-uri = "0.3.2"
idna = "1.1.0" idna = "1.1.0"
percent-encoding = "2.3.2" percent-encoding = "2.3.2"
uuid = { version = "1.20.0", features = ["v4", "serde"] } uuid = { version = "1.20.0", features = ["v7", "serde"] }
chrono = { version = "0.4.43", features = ["serde"] } chrono = { version = "0.4.43", features = ["serde"] }
json-pointer = "0.3.4" json-pointer = "0.3.4"
indexmap = { version = "2.13.0", features = ["serde"] } indexmap = { version = "2.13.0", features = ["serde"] }

View File

@ -175,6 +175,7 @@ In the Punc architecture, filters are automatically synthesized, strongly-typed
* **Conditions**: A condition schema is the contract defining the mathematical operations allowed on a primitive field. For example, a `string.condition` allows `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$of` (IN), and `$nof` (NOT IN). * **Conditions**: A condition schema is the contract defining the mathematical operations allowed on a primitive field. For example, a `string.condition` allows `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$of` (IN), and `$nof` (NOT IN).
* **Enum Conditions**: When JSPG synthesizes an enum, it dynamically generates an `<enum>.condition` (e.g., `address_kind.condition`). This strongly-typed condition perfectly mirrors the operations of a `string.condition`, but strictly limits the arrays and inputs of `$eq`, `$ne`, `$of`, and `$nof` to the exact variations defined by that Enum. This context ensures that UI generators know exactly when to render `<Select>` dropdowns instead of generic `<Text>` boxes. * **Enum Conditions**: When JSPG synthesizes an enum, it dynamically generates an `<enum>.condition` (e.g., `address_kind.condition`). This strongly-typed condition perfectly mirrors the operations of a `string.condition`, but strictly limits the arrays and inputs of `$eq`, `$ne`, `$of`, and `$nof` to the exact variations defined by that Enum. This context ensures that UI generators know exactly when to render `<Select>` dropdowns instead of generic `<Text>` boxes.
* **Pre-compiled Condition and Filter Mapping**: To prevent redundant double-wrapping of search structures, any schema property whose type is already a `.condition` or `.filter` type (such as `"string.condition"` or `"$kind.filter"`) maps directly to itself during filter synthesis rather than receiving a redundant `.filter` suffix.
* **Filters**: A filter schema (e.g., `person.filter`) is an object containing condition properties used to filter entities. It natively supports structural composition: * **Filters**: A filter schema (e.g., `person.filter`) is an object containing condition properties used to filter entities. It natively supports structural composition:
* **Inherited Properties**: Filters automatically inherit all valid database columns from their base type schema, immediately converting them to their respective `.condition` schemas. * **Inherited Properties**: Filters automatically inherit all valid database columns from their base type schema, immediately converting them to their respective `.condition` schemas.
* **Relational Proxies**: If a table has a foreign key to another table, the filter automatically generates a proxy property pointing to the related entity's filter (e.g., the `person` filter automatically gains an `organization` property that points to `organization.filter`), allowing infinitely deep nested queries natively. * **Relational Proxies**: If a table has a foreign key to another table, the filter automatically generates a proxy property pointing to the related entity's filter (e.g., the `person` filter automatically gains an `organization` property that points to `organization.filter`), allowing infinitely deep nested queries natively.

View File

@ -791,8 +791,8 @@
} }
}, },
"hierarchy": [ "hierarchy": [
"invoice", "entity",
"entity" "invoice"
], ],
"fields": [ "fields": [
"id", "id",
@ -867,8 +867,8 @@
} }
}, },
"hierarchy": [ "hierarchy": [
"invoice_line", "entity",
"entity" "invoice_line"
], ],
"fields": [ "fields": [
"id", "id",

View File

@ -466,7 +466,7 @@
}, },
"filter": { "filter": {
"type": [ "type": [
"$kind.filter.filter", "$kind.filter",
"null" "null"
] ]
}, },

View File

@ -110,8 +110,8 @@
} }
}, },
"hierarchy": [ "hierarchy": [
"activity", "entity",
"entity" "activity"
], ],
"variations": [ "variations": [
"activity", "activity",
@ -166,9 +166,9 @@
} }
}, },
"hierarchy": [ "hierarchy": [
"invoice", "entity",
"activity", "activity",
"entity" "invoice"
], ],
"variations": [ "variations": [
"invoice" "invoice"
@ -237,8 +237,8 @@
} }
}, },
"hierarchy": [ "hierarchy": [
"attachment", "entity",
"entity" "attachment"
], ],
"variations": [ "variations": [
"attachment" "attachment"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -159,7 +159,9 @@ impl Schema {
}, },
"null" => None, "null" => None,
custom => { custom => {
if db.enums.contains_key(custom) { if custom.ends_with(".condition") || custom.ends_with(".filter") {
Some(vec![custom.to_string()])
} else if db.enums.contains_key(custom) {
Some(vec![format!("{}.condition", custom)]) Some(vec![format!("{}.condition", custom)])
} else { } else {
// Assume anything else is a Relational cross-boundary that already has its own .filter dynamically built // Assume anything else is a Relational cross-boundary that already has its own .filter dynamically built

View File

@ -85,7 +85,6 @@ impl DatabaseExecutor for MockExecutor {
Ok("2026-03-10T00:00:00Z".to_string()) Ok("2026-03-10T00:00:00Z".to_string())
} }
#[cfg(test)] #[cfg(test)]
fn get_queries(&self) -> Vec<String> { fn get_queries(&self) -> Vec<String> {
MOCK_STATE.with(|state| state.borrow().captured_queries.clone()) MOCK_STATE.with(|state| state.borrow().captured_queries.clone())

View File

@ -20,7 +20,6 @@ pub trait DatabaseExecutor: Send + Sync {
/// Returns the current transaction timestamp /// Returns the current transaction timestamp
fn timestamp(&self) -> Result<String, String>; fn timestamp(&self) -> Result<String, String>;
#[cfg(test)] #[cfg(test)]
fn get_queries(&self) -> Vec<String>; fn get_queries(&self) -> Vec<String>;

View File

@ -150,5 +150,4 @@ impl DatabaseExecutor for SpiExecutor {
}) })
}) })
} }
} }

View File

@ -138,7 +138,9 @@ impl Merger {
is_child: bool, is_child: bool,
) -> Result<Value, String> { ) -> Result<Value, String> {
match data { match data {
Value::Array(items) => self.merge_array(schema, items, notifications, parent_org_id, is_child), Value::Array(items) => {
self.merge_array(schema, items, notifications, parent_org_id, is_child)
}
Value::Object(map) => { Value::Object(map) => {
if let Some(options) = schema.obj.compiled_options.get() { if let Some(options) = schema.obj.compiled_options.get() {
if let Some(disc) = schema.obj.compiled_discriminator.get() { if let Some(disc) = schema.obj.compiled_discriminator.get() {
@ -210,7 +212,13 @@ impl Merger {
let mut resolved_items = Vec::new(); let mut resolved_items = Vec::new();
for item in items { for item in items {
let resolved = self.merge_internal(item_schema.clone(), item, notifications, parent_org_id.clone(), is_child)?; let resolved = self.merge_internal(
item_schema.clone(),
item,
notifications,
parent_org_id.clone(),
is_child,
)?;
resolved_items.push(resolved); resolved_items.push(resolved);
} }
Ok(Value::Array(resolved_items)) Ok(Value::Array(resolved_items))
@ -340,7 +348,10 @@ impl Merger {
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;
let org_id_to_pass = entity_fields.get("organization_id").and_then(|v| v.as_str()).map(|s| s.to_string()); 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 parent_is_source {
let mut merged_relative = match self.merge_internal( let mut merged_relative = match self.merge_internal(
rel_schema.clone(), rel_schema.clone(),
@ -443,7 +454,10 @@ impl Merger {
} }
} }
let org_id_to_pass = entity_fields.get("organization_id").and_then(|v| v.as_str()).map(|s| s.to_string()); 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(); let mut relative_responses = Vec::new();
for relative_item_val in relative_arr { for relative_item_val in relative_arr {
if let Value::Object(mut relative_item) = relative_item_val { if let Value::Object(mut relative_item) = relative_item_val {
@ -574,7 +588,7 @@ impl Merger {
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.unwrap_or(""); .unwrap_or("");
let id_val = if entity_id.is_empty() { let id_val = if entity_id.is_empty() {
Value::String(uuid::Uuid::new_v4().to_string()) Value::String(uuid::Uuid::now_v7().to_string())
} else { } else {
Value::String(entity_id.to_string()) Value::String(entity_id.to_string())
}; };
@ -777,13 +791,8 @@ impl Merger {
} }
}; };
let mut execute_order: Vec<String> = entity_type.hierarchy.clone(); for table_name in &entity_type.hierarchy {
if change_kind == "create" { let table_fields = match grouped_fields.get(table_name).and_then(|v| v.as_array()) {
execute_order.reverse();
}
for table_name in execute_order {
let table_fields = match grouped_fields.get(&table_name).and_then(|v| v.as_array()) {
Some(arr) => arr Some(arr) => arr
.iter() .iter()
.filter_map(|v| v.as_str().map(|s| s.to_string())) .filter_map(|v| v.as_str().map(|s| s.to_string()))
@ -946,12 +955,10 @@ impl Merger {
Value::Object(old_vals) Value::Object(old_vals)
}; };
let entity_type_name = type_name.as_str().unwrap_or(&type_obj.name);
let mut notification = serde_json::Map::new(); let mut notification = serde_json::Map::new();
notification.insert("kind".to_string(), Value::String(change_kind.to_string()));
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());
notification.insert("kind".to_string(), Value::String(change_kind.to_string()));
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());
@ -964,15 +971,14 @@ impl Merger {
let mut notify_sql = None; let mut notify_sql = None;
if type_obj.historical && change_kind != "replace" { if type_obj.historical && change_kind != "replace" {
let change_sql = format!( let change_sql = format!(
"INSERT INTO agreego.change (\"old\", \"new\", \"entity_id\", \"id\", \"kind\", \"modified_at\", \"modified_by\", \"entity_type\") VALUES ({}, {}, {}, {}, {}, {}, {}, {})", "INSERT INTO agreego.change (\"old\", \"new\", \"entity_id\", \"id\", \"kind\", \"modified_at\", \"modified_by\") VALUES ({}, {}, {}, {}, {}, {}, {})",
Self::quote_literal(&old_val_obj), Self::quote_literal(&old_val_obj),
Self::quote_literal(&new_val_obj), Self::quote_literal(&new_val_obj),
Self::quote_literal(id_str), Self::quote_literal(id_str),
Self::quote_literal(&Value::String(uuid::Uuid::new_v4().to_string())), Self::quote_literal(&Value::String(uuid::Uuid::now_v7().to_string())),
Self::quote_literal(&Value::String(change_kind.to_string())), Self::quote_literal(&Value::String(change_kind.to_string())),
Self::quote_literal(&Value::String(timestamp.to_string())), Self::quote_literal(&Value::String(timestamp.to_string())),
Self::quote_literal(&Value::String(user_id.to_string())), Self::quote_literal(&Value::String(user_id.to_string()))
Self::quote_literal(&Value::String(entity_type_name.to_string()))
); );
self.db.execute(&change_sql, None)?; self.db.execute(&change_sql, None)?;

View File

@ -44,7 +44,7 @@ fn test_library_api() {
{ {
"name": "source_schema", "name": "source_schema",
"variations": ["source_schema"], "variations": ["source_schema"],
"hierarchy": ["source_schema", "entity"], "hierarchy": ["entity", "source_schema"],
"schemas": { "schemas": {
"source_schema": { "source_schema": {
"type": "object", "type": "object",
@ -60,7 +60,7 @@ fn test_library_api() {
{ {
"name": "target_schema", "name": "target_schema",
"variations": ["target_schema"], "variations": ["target_schema"],
"hierarchy": ["target_schema", "entity"], "hierarchy": ["entity", "target_schema"],
"schemas": { "schemas": {
"target_schema": { "target_schema": {
"type": "object", "type": "object",
@ -109,7 +109,7 @@ fn test_library_api() {
"field_types": null, "field_types": null,
"fields": [], "fields": [],
"grouped_fields": null, "grouped_fields": null,
"hierarchy": ["source_schema", "entity"], "hierarchy": ["entity", "source_schema"],
"historical": false, "historical": false,
"id": "", "id": "",
"longevity": null, "longevity": null,
@ -174,7 +174,7 @@ fn test_library_api() {
"field_types": null, "field_types": null,
"fields": [], "fields": [],
"grouped_fields": null, "grouped_fields": null,
"hierarchy": ["target_schema", "entity"], "hierarchy": ["entity", "target_schema"],
"historical": false, "historical": false,
"id": "", "id": "",
"longevity": null, "longevity": null,

View File

@ -1 +1 @@
1.0.155 1.0.159