diff --git a/fixtures/filter.json b/fixtures/filter.json index 5ce1415..4c468f8 100644 --- a/fixtures/filter.json +++ b/fixtures/filter.json @@ -87,6 +87,34 @@ "type": "string" } } + }, + "schedule": { + "type": [ + "opening_hours", + "null" + ] + } + } + }, + "season": { + "type": "object", + "properties": { + "label": { + "type": "string" + } + } + }, + "opening_hours": { + "type": "object", + "properties": { + "open": { + "type": "string" + }, + "seasons": { + "type": "array", + "items": { + "type": "season" + } } } } @@ -262,6 +290,7 @@ "uuid_field", "tags", "ad_hoc", + "schedule", "$and", "$or" ], @@ -277,6 +306,7 @@ "uuid_field", "tags", "ad_hoc", + "schedule", "$and", "$or" ], @@ -298,6 +328,7 @@ "uuid_field", "tags", "ad_hoc", + "schedule", "$and", "$or" ], @@ -366,6 +397,41 @@ "string.condition", "null" ] + }, + "schedule": { + "type": [ + "filter", + "null" + ], + "compiledPropertyNames": [ + "open", + "seasons" + ], + "properties": { + "open": { + "type": [ + "string.condition", + "null" + ] + }, + "seasons": { + "type": [ + "filter", + "null" + ], + "compiledPropertyNames": [ + "label" + ], + "properties": { + "label": { + "type": [ + "string.condition", + "null" + ] + } + } + } + } } }, "type": "filter" @@ -483,10 +549,12 @@ ] } } - } + }, + "opening_hours": {}, + "season": {} } } } ] } -] \ No newline at end of file +] diff --git a/flows b/flows index 89748a2..0d9bd86 160000 --- a/flows +++ b/flows @@ -1 +1 @@ -Subproject commit 89748a246e36a34c0b6ca2b27b4e6a2ab198cce0 +Subproject commit 0d9bd8644eadf89fe5bcaba37f6833c688822d16 diff --git a/src/database/compile/filter.rs b/src/database/compile/filter.rs index 2474432..783df37 100644 --- a/src/database/compile/filter.rs +++ b/src/database/compile/filter.rs @@ -26,10 +26,14 @@ impl Schema { if let Some(items) = &child.obj.items { if !items.is_proxy() { structural_filter = items.compile_filter(_db, "", _errors); + } else if let Some(target) = Self::value_type_target(items, _db) { + structural_filter = target.compile_filter(_db, "", _errors); } } } else if !child.is_proxy() { structural_filter = child.compile_filter(_db, "", _errors); + } else if let Some(target) = Self::value_type_target(child, _db) { + structural_filter = target.compile_filter(_db, "", _errors); } if let Some(mut inline_schema) = structural_filter { @@ -117,6 +121,37 @@ impl Schema { None } + /// Resolves a pure type-pointer schema to a named non-table value type's own schema — + /// a reusable, schema-only object (e.g. an operating-hours config). Naming a value type + /// is a reuse choice, not a semantics choice: it filters structurally, exactly like an + /// inline object. Table-backed boundaries keep the lazy {type}.filter reference instead. + fn value_type_target<'a>(schema: &Arc, db: &'a Database) -> Option<&'a Arc> { + let t = match &schema.obj.type_ { + Some(SchemaTypeOrArray::Single(t)) => Some(t.as_str()), + Some(SchemaTypeOrArray::Multiple(types)) => { + types.iter().find(|t| *t != "null").map(|s| s.as_str()) + } + None => None, + }?; + if matches!( + t, + "string" | "integer" | "number" | "boolean" | "object" | "array" | "null" + ) { + return None; + } + if t.ends_with(".condition") || t.ends_with(".filter") { + return None; + } + if db.enums.contains_key(t) { + return None; + } + let base = t.split('.').next_back().unwrap_or(t); + if db.types.contains_key(base) { + return None; + } + db.schemas.get(t) + } + fn resolve_filter_type(schema: &Arc, db: &Database) -> Option> { if let Some(type_) = &schema.obj.type_ { match type_ { @@ -165,8 +200,16 @@ impl Schema { } else if db.enums.contains_key(custom) { Some(vec![format!("{}.condition", custom)]) } else { - // Assume anything else is a Relational cross-boundary that already has its own .filter dynamically built - Some(vec![format!("{}.filter", custom)]) + // A Relational cross-boundary gets a reference to the target's dynamically built + // .filter — but only a table-backed boundary has one. A named non-table value type + // compiles structurally upstream (value_type_target); reaching here means it had + // no compilable structure — omit it rather than emit a dangling .filter reference. + let base = custom.split('.').next_back().unwrap_or(custom); + if db.types.contains_key(base) { + Some(vec![format!("{}.filter", custom)]) + } else { + None + } } } }