Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eb91b65e65 | |||
| 8bf3649465 | |||
| 9fe5a34163 | |||
| f5bf21eb58 |
178
fixtures/paths.json
Normal file
178
fixtures/paths.json
Normal file
@ -0,0 +1,178 @@
|
||||
[
|
||||
{
|
||||
"description": "Hybrid Array Pathing",
|
||||
"database": {
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "hybrid_pathing",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"primitives": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"ad_hoc_objects": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name"
|
||||
]
|
||||
}
|
||||
},
|
||||
"entities": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "number",
|
||||
"minimum": 10
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"deep_entities": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"nested": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"flag": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "happy path passes structural validation",
|
||||
"data": {
|
||||
"primitives": ["a", "b"],
|
||||
"ad_hoc_objects": [{"name": "obj1"}],
|
||||
"entities": [{"id": "entity-1", "value": 15}],
|
||||
"deep_entities": [
|
||||
{
|
||||
"id": "parent-1",
|
||||
"nested": [{"id": "child-1", "flag": true}]
|
||||
}
|
||||
]
|
||||
},
|
||||
"schema_id": "hybrid_pathing",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "primitive arrays use numeric indexing",
|
||||
"data": {
|
||||
"primitives": ["a", 123]
|
||||
},
|
||||
"schema_id": "hybrid_pathing",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false,
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_TYPE",
|
||||
"path": "/primitives/1"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "ad-hoc objects without ids use numeric indexing",
|
||||
"data": {
|
||||
"ad_hoc_objects": [
|
||||
{"name": "valid"},
|
||||
{"age": 30}
|
||||
]
|
||||
},
|
||||
"schema_id": "hybrid_pathing",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false,
|
||||
"errors": [
|
||||
{
|
||||
"code": "REQUIRED_FIELD_MISSING",
|
||||
"path": "/ad_hoc_objects/1/name"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "arrays of objects with ids use topological uuid indexing",
|
||||
"data": {
|
||||
"entities": [
|
||||
{"id": "entity-alpha", "value": 20},
|
||||
{"id": "entity-beta", "value": 5}
|
||||
]
|
||||
},
|
||||
"schema_id": "hybrid_pathing",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false,
|
||||
"errors": [
|
||||
{
|
||||
"code": "MINIMUM_VIOLATED",
|
||||
"path": "/entities/entity-beta/value"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "deeply nested entity arrays retain full topological paths",
|
||||
"data": {
|
||||
"deep_entities": [
|
||||
{
|
||||
"id": "parent-omega",
|
||||
"nested": [
|
||||
{"id": "child-alpha", "flag": true},
|
||||
{"id": "child-beta", "flag": "invalid-string"}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"schema_id": "hybrid_pathing",
|
||||
"action": "validate",
|
||||
"expect": {
|
||||
"success": false,
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_TYPE",
|
||||
"path": "/deep_entities/parent-omega/nested/child-beta/flag"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@ -20,6 +20,16 @@
|
||||
"$family": "base.person"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "get_orders",
|
||||
"schemas": [
|
||||
{
|
||||
"$id": "get_orders.response",
|
||||
"type": "array",
|
||||
"items": { "$ref": "light.order" }
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"enums": [],
|
||||
@ -664,6 +674,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"$id": "light.order",
|
||||
"$ref": "order",
|
||||
"properties": {
|
||||
"customer": {
|
||||
"$ref": "base.person"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"$id": "full.order",
|
||||
"$ref": "order",
|
||||
@ -1569,6 +1588,47 @@
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Root Array SQL evaluation for Order fetching Light Order",
|
||||
"action": "query",
|
||||
"schema_id": "get_orders.response",
|
||||
"expect": {
|
||||
"success": true,
|
||||
"sql": [
|
||||
[
|
||||
"(SELECT COALESCE(jsonb_agg(jsonb_build_object(",
|
||||
" 'archived', entity_2.archived,",
|
||||
" 'created_at', entity_2.created_at,",
|
||||
" 'customer',",
|
||||
" (SELECT jsonb_build_object(",
|
||||
" 'age', person_3.age,",
|
||||
" 'archived', entity_5.archived,",
|
||||
" 'created_at', entity_5.created_at,",
|
||||
" 'first_name', person_3.first_name,",
|
||||
" 'id', entity_5.id,",
|
||||
" 'last_name', person_3.last_name,",
|
||||
" 'name', entity_5.name,",
|
||||
" 'type', entity_5.type",
|
||||
" )",
|
||||
" FROM agreego.person person_3",
|
||||
" JOIN agreego.organization organization_4 ON organization_4.id = person_3.id",
|
||||
" JOIN agreego.entity entity_5 ON entity_5.id = organization_4.id",
|
||||
" WHERE",
|
||||
" NOT entity_5.archived",
|
||||
" AND order_1.customer_id = person_3.id),",
|
||||
" 'customer_id', order_1.customer_id,",
|
||||
" 'id', entity_2.id,",
|
||||
" 'name', entity_2.name,",
|
||||
" 'total', order_1.total,",
|
||||
" 'type', entity_2.type",
|
||||
")), '[]'::jsonb)",
|
||||
"FROM agreego.order order_1",
|
||||
"JOIN agreego.entity entity_2 ON entity_2.id = order_1.id",
|
||||
"WHERE NOT entity_2.archived)"
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -63,33 +63,33 @@ impl<'a> Compiler<'a> {
|
||||
}
|
||||
|
||||
fn compile_array(&mut self, node: Node<'a>) -> Result<(String, String), String> {
|
||||
// 1. Array of DB Entities (`$ref` or `$family` pointing to a table limit)
|
||||
if let Some(items) = &node.schema.obj.items {
|
||||
let next_path = node.ast_path.clone();
|
||||
|
||||
if let Some(ref_id) = &items.obj.r#ref {
|
||||
if let Some(type_def) = self.db.types.get(ref_id) {
|
||||
let mut entity_node = node.clone();
|
||||
entity_node.ast_path = next_path;
|
||||
entity_node.schema = std::sync::Arc::clone(items);
|
||||
return self.compile_entity(type_def, entity_node, true);
|
||||
}
|
||||
let mut resolved_type = None;
|
||||
if let Some(family_target) = items.obj.family.as_ref() {
|
||||
let base_type_name = family_target.split('.').next_back().unwrap_or(family_target);
|
||||
resolved_type = self.db.types.get(base_type_name);
|
||||
} else if let Some(base_type_name) = items.obj.identifier() {
|
||||
resolved_type = self.db.types.get(&base_type_name);
|
||||
}
|
||||
|
||||
let mut next_node = node.clone();
|
||||
next_node.depth += 1;
|
||||
next_node.ast_path = next_path;
|
||||
next_node.schema = std::sync::Arc::clone(items);
|
||||
let (item_sql, _) = self.compile_node(next_node)?;
|
||||
if let Some(type_def) = resolved_type {
|
||||
let mut entity_node = node.clone();
|
||||
entity_node.schema = std::sync::Arc::clone(items);
|
||||
return self.compile_entity(type_def, entity_node, true);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Arrays of mapped Native Postgres Columns (e.g. `jsonb`, `text[]`)
|
||||
if let Some(prop) = &node.property_name {
|
||||
return Ok((
|
||||
format!("(SELECT jsonb_agg({}) FROM TODO)", item_sql),
|
||||
format!("{}.{}", node.parent_alias, prop),
|
||||
"array".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok((
|
||||
"SELECT jsonb_agg(TODO) FROM TODO".to_string(),
|
||||
"array".to_string(),
|
||||
))
|
||||
// 3. Fallback for root execution of standalone non-entity arrays
|
||||
Err("Cannot compile a root array without a valid entity reference or table mapped via `items`.".to_string())
|
||||
}
|
||||
|
||||
fn compile_reference(&mut self, node: Node<'a>) -> Result<(String, String), String> {
|
||||
|
||||
@ -1457,6 +1457,12 @@ fn test_queryer_0_7() {
|
||||
crate::tests::runner::run_test_case(&path, 0, 7).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_queryer_0_8() {
|
||||
let path = format!("{}/fixtures/queryer.json", env!("CARGO_MANIFEST_DIR"));
|
||||
crate::tests::runner::run_test_case(&path, 0, 8).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_not_0_0() {
|
||||
let path = format!("{}/fixtures/not.json", env!("CARGO_MANIFEST_DIR"));
|
||||
@ -2921,6 +2927,36 @@ fn test_minimum_1_6() {
|
||||
crate::tests::runner::run_test_case(&path, 1, 6).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paths_0_0() {
|
||||
let path = format!("{}/fixtures/paths.json", env!("CARGO_MANIFEST_DIR"));
|
||||
crate::tests::runner::run_test_case(&path, 0, 0).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paths_0_1() {
|
||||
let path = format!("{}/fixtures/paths.json", env!("CARGO_MANIFEST_DIR"));
|
||||
crate::tests::runner::run_test_case(&path, 0, 1).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paths_0_2() {
|
||||
let path = format!("{}/fixtures/paths.json", env!("CARGO_MANIFEST_DIR"));
|
||||
crate::tests::runner::run_test_case(&path, 0, 2).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paths_0_3() {
|
||||
let path = format!("{}/fixtures/paths.json", env!("CARGO_MANIFEST_DIR"));
|
||||
crate::tests::runner::run_test_case(&path, 0, 3).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paths_0_4() {
|
||||
let path = format!("{}/fixtures/paths.json", env!("CARGO_MANIFEST_DIR"));
|
||||
crate::tests::runner::run_test_case(&path, 0, 4).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_one_of_0_0() {
|
||||
let path = format!("{}/fixtures/oneOf.json", env!("CARGO_MANIFEST_DIR"));
|
||||
|
||||
@ -91,12 +91,17 @@ impl<'a> ValidationContext<'a> {
|
||||
if let Some(ref prefix) = self.schema.prefix_items {
|
||||
for (i, sub_schema) in prefix.iter().enumerate() {
|
||||
if i < len {
|
||||
let path = format!("{}/{}", self.path, i);
|
||||
if let Some(child_instance) = arr.get(i) {
|
||||
let mut item_path = format!("{}/{}", self.path, i);
|
||||
if let Some(obj) = child_instance.as_object() {
|
||||
if let Some(id_str) = obj.get("id").and_then(|v| v.as_str()) {
|
||||
item_path = format!("{}/{}", self.path, id_str);
|
||||
}
|
||||
}
|
||||
let derived = self.derive(
|
||||
sub_schema,
|
||||
child_instance,
|
||||
&path,
|
||||
&item_path,
|
||||
HashSet::new(),
|
||||
self.extensible,
|
||||
false,
|
||||
@ -112,12 +117,17 @@ impl<'a> ValidationContext<'a> {
|
||||
|
||||
if let Some(ref items_schema) = self.schema.items {
|
||||
for i in validation_index..len {
|
||||
let path = format!("{}/{}", self.path, i);
|
||||
if let Some(child_instance) = arr.get(i) {
|
||||
let mut item_path = format!("{}/{}", self.path, i);
|
||||
if let Some(obj) = child_instance.as_object() {
|
||||
if let Some(id_str) = obj.get("id").and_then(|v| v.as_str()) {
|
||||
item_path = format!("{}/{}", self.path, id_str);
|
||||
}
|
||||
}
|
||||
let derived = self.derive(
|
||||
items_schema,
|
||||
child_instance,
|
||||
&path,
|
||||
&item_path,
|
||||
HashSet::new(),
|
||||
self.extensible,
|
||||
false,
|
||||
|
||||
@ -53,10 +53,18 @@ impl<'a> ValidationContext<'a> {
|
||||
if let Some(arr) = self.instance.as_array() {
|
||||
for i in 0..arr.len() {
|
||||
if !result.evaluated_indices.contains(&i) {
|
||||
let mut item_path = format!("{}/{}", self.path, i);
|
||||
if let Some(child_instance) = arr.get(i) {
|
||||
if let Some(obj) = child_instance.as_object() {
|
||||
if let Some(id_str) = obj.get("id").and_then(|v| v.as_str()) {
|
||||
item_path = format!("{}/{}", self.path, id_str);
|
||||
}
|
||||
}
|
||||
}
|
||||
result.errors.push(ValidationError {
|
||||
code: "STRICT_ITEM_VIOLATION".to_string(),
|
||||
message: format!("Unexpected item at index {}", i),
|
||||
path: format!("{}/{}", self.path, i),
|
||||
path: item_path,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user