Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 99ae2a7e89 | |||
| 53776fc696 | |||
| d43be1def4 | |||
| 37bce3ce61 | |||
| ab4df37fd0 | |||
| 3414a32bd6 | |||
| 06432cf0f5 |
18
GEMINI.md
18
GEMINI.md
@ -184,23 +184,27 @@ It evaluates as an **Independent Declarative Rules Engine**. Every `Case` block
|
||||
* **`array`**: Homogeneous collection of items matching the `items` schema. Validation is homogeneous, so strictness checking does not apply. `"extensible"` is not applicable at the `array` level (tuple-like `prefixItems` are removed).
|
||||
* **Inheritance Boundaries**: Strictness resets when crossing non-primitive `type` boundaries. A schema extending a strict parent remains strict unless it explicitly overrides with `"extensible": true`.
|
||||
|
||||
### Immutable Properties (`"immutable": true`)
|
||||
To distinguish read-only hydrated endpoint references, computed properties, system-managed timestamps (`created_at`, `modified_at`), or audit fields from writable properties, JSPG introduces the universal `"immutable": true` schema keyword.
|
||||
### Immutable Properties (`"immutable": "always" | "external"`)
|
||||
To distinguish read-only hydrated endpoint references, computed properties, system-managed timestamps (`created_at`, `modified_at`), or audit fields from writable properties, JSPG supports the `"immutable"` property schema attribute, which takes string enum values (`"always"`, `"external"`, or omitted).
|
||||
|
||||
* **Developer Perspective**: Annotate properties in database schemas or trait definitions with `"immutable": true` when the property should be visible on reads (`jspg_query` / `.response`), but rejected or ignored on writes (`jspg_merge` / `.request`).
|
||||
* **Enum Values**:
|
||||
* `"always"`: Property is permanently read-only across all boundaries (e.g. system-managed audit timestamps like `created_at`, `modified_at`, or computed fields).
|
||||
* `"external"`: Property is read-only from external client API requests (`.request` payloads), but can be populated or mutated by internal system operations.
|
||||
* Omitted: Property is fully writable.
|
||||
* **Developer Perspective**: Annotate properties in database schemas or trait definitions with `"immutable": "always"` or `"immutable": "external"` when the property should be visible on reads (`jspg_query` / `.response`), but rejected or ignored on writes (`jspg_merge` / `.request`).
|
||||
```json
|
||||
"properties": {
|
||||
"source": {
|
||||
"family": "lite.organization",
|
||||
"immutable": true,
|
||||
"immutable": "always",
|
||||
"description": "Read-only hydrated member entity summary on a membership edge."
|
||||
}
|
||||
}
|
||||
```
|
||||
* **Behavior Across Pillars**:
|
||||
* **Queryer (`jspg_query`)**: Hydrates and includes `immutable` properties in output read responses without restriction.
|
||||
* **Validator (`jspg_validate`)**: Context-aware. Returns `IMMUTABLE_PROPERTY_VIOLATION` if an `immutable` property is supplied in a write/request payload (schema IDs not ending in `.response`). Response schemas (`.response`) permit `immutable` fields.
|
||||
* **Merger (`jspg_merge`)**: Automatically skips `immutable` properties during object graph merging so client payloads can never mutate database columns or relationship edges.
|
||||
* **Queryer (`jspg_query`)**: Hydrates and includes `"immutable"` properties in output read responses without restriction.
|
||||
* **Validator (`jspg_validate`)**: Context-aware. Rejects specified `"immutable"` properties supplied in write/request payloads. Response schemas (`.response`) permit `"immutable"` fields.
|
||||
* **Merger (`jspg_merge`)**: Automatically skips `"immutable"` properties during object graph merging so client payloads can never mutate database columns or relationship edges.
|
||||
|
||||
### Format Leniency for Empty Strings
|
||||
To simplify frontend form validation, format validators specifically for `uuid`, `date-time`, and `email` explicitly allow empty strings (`""`), treating them as "present but unset".
|
||||
|
||||
@ -132,6 +132,15 @@
|
||||
"columns": [
|
||||
"name"
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "relationship",
|
||||
"columns": [
|
||||
"type",
|
||||
"source_id",
|
||||
"target_id",
|
||||
"start_date"
|
||||
]
|
||||
}
|
||||
],
|
||||
"types": [
|
||||
@ -573,6 +582,8 @@
|
||||
"source_type",
|
||||
"target_id",
|
||||
"target_type",
|
||||
"start_date",
|
||||
"end_date",
|
||||
"id",
|
||||
"type",
|
||||
"name",
|
||||
@ -597,7 +608,9 @@
|
||||
"source_id",
|
||||
"source_type",
|
||||
"target_id",
|
||||
"target_type"
|
||||
"target_type",
|
||||
"start_date",
|
||||
"end_date"
|
||||
]
|
||||
},
|
||||
"field_types": {
|
||||
@ -608,6 +621,8 @@
|
||||
"source_type": "text",
|
||||
"target_id": "uuid",
|
||||
"target_type": "text",
|
||||
"start_date": "timestamptz",
|
||||
"end_date": "timestamptz",
|
||||
"name": "text",
|
||||
"created_at": "timestamptz",
|
||||
"created_by": "uuid",
|
||||
@ -620,7 +635,16 @@
|
||||
"properties": {}
|
||||
}
|
||||
},
|
||||
"lookup_fields": [],
|
||||
"lookup_fields": [
|
||||
"type",
|
||||
"source_id",
|
||||
"target_id",
|
||||
"start_date"
|
||||
],
|
||||
"field_defaults": {
|
||||
"start_date": "0001-01-01T00:00:00Z",
|
||||
"end_date": "9999-12-31T23:59:59Z"
|
||||
},
|
||||
"historical": true,
|
||||
"notify": true
|
||||
},
|
||||
@ -2766,6 +2790,17 @@
|
||||
" '00000000-0000-0000-0000-000000000000'",
|
||||
")"
|
||||
],
|
||||
[
|
||||
"(SELECT COALESCE(to_jsonb(t1.*), '{}') || COALESCE(to_jsonb(t2.*), '{}') || COALESCE(to_jsonb(t3.*), '{}')",
|
||||
"FROM agreego.\"entity\" t1",
|
||||
"LEFT JOIN agreego.\"relationship\" t2 ON t2.id = t1.id",
|
||||
"LEFT JOIN agreego.\"contact\" t3 ON t3.id = t1.id",
|
||||
"WHERE",
|
||||
" (t1.\"type\" = 'contact'",
|
||||
" AND \"source_id\" = '{{uuid:generated_0}}'",
|
||||
" AND \"target_id\" = '{{uuid:generated_1}}'",
|
||||
" AND \"start_date\" = '{{timestamp}}'))"
|
||||
],
|
||||
[
|
||||
"INSERT INTO agreego.\"entity\" (",
|
||||
" \"created_at\",",
|
||||
@ -2887,6 +2922,17 @@
|
||||
" '00000000-0000-0000-0000-000000000000'",
|
||||
")"
|
||||
],
|
||||
[
|
||||
"(SELECT COALESCE(to_jsonb(t1.*), '{}') || COALESCE(to_jsonb(t2.*), '{}') || COALESCE(to_jsonb(t3.*), '{}')",
|
||||
"FROM agreego.\"entity\" t1",
|
||||
"LEFT JOIN agreego.\"relationship\" t2 ON t2.id = t1.id",
|
||||
"LEFT JOIN agreego.\"contact\" t3 ON t3.id = t1.id",
|
||||
"WHERE",
|
||||
" (t1.\"type\" = 'contact'",
|
||||
" AND \"source_id\" = '{{uuid:generated_0}}'",
|
||||
" AND \"target_id\" = '{{uuid:generated_5}}'",
|
||||
" AND \"start_date\" = '{{timestamp}}'))"
|
||||
],
|
||||
[
|
||||
"INSERT INTO agreego.\"entity\" (",
|
||||
" \"created_at\",",
|
||||
@ -3008,6 +3054,17 @@
|
||||
" '00000000-0000-0000-0000-000000000000'",
|
||||
")"
|
||||
],
|
||||
[
|
||||
"(SELECT COALESCE(to_jsonb(t1.*), '{}') || COALESCE(to_jsonb(t2.*), '{}') || COALESCE(to_jsonb(t3.*), '{}')",
|
||||
"FROM agreego.\"entity\" t1",
|
||||
"LEFT JOIN agreego.\"relationship\" t2 ON t2.id = t1.id",
|
||||
"LEFT JOIN agreego.\"contact\" t3 ON t3.id = t1.id",
|
||||
"WHERE",
|
||||
" (t1.\"type\" = 'contact'",
|
||||
" AND \"source_id\" = '{{uuid:generated_0}}'",
|
||||
" AND \"target_id\" = '{{uuid:generated_9}}'",
|
||||
" AND \"start_date\" = '{{timestamp}}'))"
|
||||
],
|
||||
[
|
||||
"INSERT INTO agreego.\"entity\" (",
|
||||
" \"created_at\",",
|
||||
|
||||
@ -260,12 +260,9 @@
|
||||
},
|
||||
"lookup_fields": [],
|
||||
"null_fields": [],
|
||||
"default_fields": [
|
||||
"id",
|
||||
"type",
|
||||
"created_at",
|
||||
"archived"
|
||||
],
|
||||
"field_defaults": {
|
||||
"archived": false
|
||||
},
|
||||
"variations": [
|
||||
"bot",
|
||||
"organization",
|
||||
|
||||
@ -77,8 +77,8 @@ impl DatabaseExecutor for SpiExecutor {
|
||||
|
||||
pgrx::debug1!("JSPG_SQL: {}", sql);
|
||||
self.transact(|| {
|
||||
Spi::connect(|client| {
|
||||
match client.select(sql, Some(args_with_oid.len() as i64), &args_with_oid) {
|
||||
Spi::connect_mut(|client| {
|
||||
match client.update(sql, Some(args_with_oid.len() as i64), &args_with_oid) {
|
||||
Ok(tup_table) => {
|
||||
let mut results = Vec::new();
|
||||
for row in tup_table {
|
||||
|
||||
@ -36,7 +36,7 @@ pub struct Type {
|
||||
#[serde(default)]
|
||||
pub null_fields: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub default_fields: Vec<String>,
|
||||
pub field_defaults: IndexMap<String, Value>,
|
||||
pub field_types: Option<Value>,
|
||||
#[serde(default)]
|
||||
pub schemas: IndexMap<String, Arc<Schema>>,
|
||||
|
||||
@ -57,7 +57,7 @@ pub fn jspg_setup(database: Json) -> Json {
|
||||
Json(serde_json::to_value(drop).unwrap())
|
||||
}
|
||||
|
||||
#[cfg_attr(not(test), pg_extern)]
|
||||
#[cfg_attr(not(test), pg_extern(volatile))]
|
||||
pub fn jspg_merge(schema_id: &str, data: JsonB) -> JsonB {
|
||||
// Try to acquire a read lock to get a clone of the Engine Arc
|
||||
let engine_opt = {
|
||||
@ -74,7 +74,7 @@ pub fn jspg_merge(schema_id: &str, data: JsonB) -> JsonB {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(not(test), pg_extern)]
|
||||
#[cfg_attr(not(test), pg_extern(volatile))]
|
||||
pub fn jspg_merge_ordered(schema_id: &str, data: Json) -> Json {
|
||||
let engine_opt = {
|
||||
let lock = GLOBAL_JSPG.read().unwrap();
|
||||
|
||||
@ -335,6 +335,17 @@ impl Merger {
|
||||
}
|
||||
}
|
||||
|
||||
// Hydrate missing fields from field_defaults for entity_type and its hierarchy
|
||||
for parent_type_name in &type_def.hierarchy {
|
||||
if let Some(parent_type) = self.db.types.get(parent_type_name) {
|
||||
for (k, v) in &parent_type.field_defaults {
|
||||
if !entity_fields.contains_key(k) {
|
||||
entity_fields.insert(k.clone(), v.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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") {
|
||||
|
||||
@ -104,7 +104,7 @@ fn test_library_api() {
|
||||
},
|
||||
"types": {
|
||||
"source_schema": {
|
||||
"default_fields": [],
|
||||
"field_defaults": {},
|
||||
"field_types": null,
|
||||
"fields": [],
|
||||
"grouped_fields": null,
|
||||
@ -169,7 +169,7 @@ fn test_library_api() {
|
||||
"variations": ["source_schema"]
|
||||
},
|
||||
"target_schema": {
|
||||
"default_fields": [],
|
||||
"field_defaults": {},
|
||||
"field_types": null,
|
||||
"fields": [],
|
||||
"grouped_fields": null,
|
||||
|
||||
Reference in New Issue
Block a user