diff --git a/nextgraph/src/tests/orm.rs b/nextgraph/src/tests/orm.rs index b829893..004051a 100644 --- a/nextgraph/src/tests/orm.rs +++ b/nextgraph/src/tests/orm.rs @@ -330,6 +330,29 @@ INSERT DATA { async fn test_orm_creation() { // Setup wallet and document let (_wallet, session_id) = create_or_open_wallet().await; + + // Tests below all in this test, to prevent waiting times through wallet creation. + + // === + test_orm_big_object(session_id).await; + + // === + test_orm_root_array(session_id).await; + + // === + test_orm_with_optional(session_id).await; + + // === + test_orm_literal(session_id).await; + + // === + test_orm_multi_type(session_id).await; + + // === + test_orm_nested(session_id).await; +} + +async fn test_orm_big_object(session_id: u64) { let doc_nuri = create_doc_with_data( session_id, r#" @@ -413,16 +436,6 @@ INSERT DATA { } } cancel_fn(); - // - - // === - test_orm_root_array(session_id).await; - - // === - test_orm_with_optional(session_id).await; - - // === - test_orm_literal(session_id).await; } async fn test_orm_root_array(session_id: u64) { @@ -655,9 +668,6 @@ INSERT DATA { .into(), ); - // TODO ======= - // obj3 valid even though it should not. - let shape_type = OrmShapeType { schema, shape: "http://example.org/OptionShape".to_string(), @@ -683,6 +693,274 @@ INSERT DATA { } cancel_fn(); } + +async fn test_orm_multi_type(session_id: u64) { + let doc_nuri = create_doc_with_data( + session_id, + r#" +PREFIX ex: +INSERT DATA { + + ex:strOrNum "a string" ; + ex:strOrNum "another string" ; + ex:strOrNum 2 . + + # Invalid because false is not string or number. + + ex:strOrNum "a string2" ; + ex:strOrNum 2 ; + ex:strOrNum false . +} +"# + .to_string(), + ) + .await; + + // Define the ORM schema + let mut schema = HashMap::new(); + schema.insert( + "http://example.org/MultiTypeShape".to_string(), + OrmSchemaShape { + iri: "http://example.org/MultiTypeShape".to_string(), + predicates: vec![OrmSchemaPredicate { + iri: "http://example.org/strOrNum".to_string(), + extra: Some(true), + maxCardinality: -1, + minCardinality: 1, + readablePredicate: "strOrNum".to_string(), + dataTypes: vec![ + OrmSchemaDataType { + valType: OrmSchemaLiteralType::string, + literals: None, + shape: None, + }, + OrmSchemaDataType { + valType: OrmSchemaLiteralType::number, + literals: None, + shape: None, + }, + ], + } + .into()], + } + .into(), + ); + + let shape_type = OrmShapeType { + schema, + shape: "http://example.org/MultiTypeShape".to_string(), + }; + + let nuri = NuriV0::new_from(&doc_nuri).expect("parse nuri"); + let (mut receiver, cancel_fn) = orm_start(nuri, shape_type, session_id) + .await + .expect("orm_start"); + + while let Some(app_response) = receiver.next().await { + let orm_json = match app_response { + AppResponse::V0(v) => match v { + AppResponseV0::OrmInitial(json) => Some(json), + _ => None, + }, + } + .unwrap(); + + log_info!("ORM JSON arrived for multi type test\n: {:?}", orm_json); + + break; + } + cancel_fn(); +} + +async fn test_orm_nested(session_id: u64) { + let doc_nuri = create_doc_with_data( + session_id, + r#" +PREFIX ex: +INSERT DATA { + # Valid + + ex:str "obj1 str" ; + ex:nestedWithExtra [ + ex:nestedStr "obj1 nested with extra valid" ; + ex:nestedNum 2 + ] , [ + # Invalid, nestedNum is missing but okay because extra. + ex:nestedStr "obj1 nested with extra invalid" + ] ; + ex:nestedWithoutExtra [ + ex:nestedStr "obj1 nested without extra valid" ; + ex:nestedNum 2 + ] . + + # Invalid because nestedWithoutExtra has an invalid child. + + ex:str "obj2 str" ; + ex:nestedWithExtra [ + ex:nestedStr "obj2: a nested string valid" ; + ex:nestedNum 2 + ] ; + ex:nestedWithoutExtra [ + ex:nestedStr "obj2 nested without extra valid" ; + ex:nestedNum 2 + ] , + # Invalid because nestedNum is missing. + [ + ex:nestedStr "obj2 nested without extra invalid" + ] . +} +"# + .to_string(), + ) + .await; + + // Define the ORM schema + let mut schema = HashMap::new(); + schema.insert( + "http://example.org/RootShape".to_string(), + OrmSchemaShape { + iri: "http://example.org/RootShape".to_string(), + predicates: vec![ + OrmSchemaPredicate { + iri: "http://example.org/str".to_string(), + extra: None, + maxCardinality: 1, + minCardinality: 1, + readablePredicate: "str".to_string(), + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaLiteralType::string, + literals: None, + shape: None, + }], + } + .into(), + OrmSchemaPredicate { + iri: "http://example.org/nestedWithExtra".to_string(), + extra: Some(true), + maxCardinality: 1, + minCardinality: 1, + readablePredicate: "nestedWithExtra".to_string(), + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaLiteralType::shape, + literals: None, + shape: Some("http://example.org/NestedShapeWithExtra".to_string()), + }], + } + .into(), + OrmSchemaPredicate { + iri: "http://example.org/nestedWithoutExtra".to_string(), + extra: Some(false), + maxCardinality: 1, + minCardinality: 1, + readablePredicate: "nestedWithoutExtra".to_string(), + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaLiteralType::shape, + literals: None, + shape: Some("http://example.org/NestedShapeWithoutExtra".to_string()), + }], + } + .into(), + ], + } + .into(), + ); + schema.insert( + "http://example.org/NestedShapeWithExtra".to_string(), + OrmSchemaShape { + iri: "http://example.org/NestedShapeWithExtra".to_string(), + predicates: vec![ + OrmSchemaPredicate { + iri: "http://example.org/nestedStr".to_string(), + extra: None, + readablePredicate: "nestedStr".to_string(), + maxCardinality: 1, + minCardinality: 1, + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaLiteralType::string, + literals: None, + shape: None, + }], + } + .into(), + OrmSchemaPredicate { + iri: "http://example.org/nestedNum".to_string(), + extra: None, + readablePredicate: "nestedNum".to_string(), + maxCardinality: 1, + minCardinality: 1, + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaLiteralType::number, + literals: None, + shape: None, + }], + } + .into(), + ], + } + .into(), + ); + schema.insert( + "http://example.org/NestedShapeWithoutExtra".to_string(), + OrmSchemaShape { + iri: "http://example.org/NestedShapeWithoutExtra".to_string(), + predicates: vec![ + OrmSchemaPredicate { + iri: "http://example.org/nestedStr".to_string(), + extra: None, + readablePredicate: "nestedStr".to_string(), + maxCardinality: 1, + minCardinality: 1, + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaLiteralType::string, + literals: None, + shape: None, + }], + } + .into(), + OrmSchemaPredicate { + iri: "http://example.org/nestedNum".to_string(), + extra: None, + readablePredicate: "nestedNum".to_string(), + maxCardinality: 1, + minCardinality: 1, + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaLiteralType::number, + literals: None, + shape: None, + }], + } + .into(), + ], + } + .into(), + ); + + let shape_type = OrmShapeType { + schema, + shape: "http://example.org/RootShape".to_string(), + }; + + let nuri = NuriV0::new_from(&doc_nuri).expect("parse nuri"); + let (mut receiver, cancel_fn) = orm_start(nuri, shape_type, session_id) + .await + .expect("orm_start"); + + while let Some(app_response) = receiver.next().await { + let orm_json = match app_response { + AppResponse::V0(v) => match v { + AppResponseV0::OrmInitial(json) => Some(json), + _ => None, + }, + } + .unwrap(); + + log_info!("ORM JSON arrived for nested test\n: {:?}", orm_json); + + break; + } + cancel_fn(); +} + // // Helpers fn create_big_schema() -> OrmSchema { diff --git a/ng-verifier/src/orm/utils.rs b/ng-verifier/src/orm/utils.rs index 54aad45..3a1662f 100644 --- a/ng-verifier/src/orm/utils.rs +++ b/ng-verifier/src/orm/utils.rs @@ -88,27 +88,26 @@ pub fn shape_type_to_sparql( where_statements: &mut Vec, var_counter: &mut i32, visited_shapes: &mut HashSet, + in_recursion: bool, ) { // Prevent infinite recursion on cyclic schemas. // TODO: We could handle this as IRI string reference. if visited_shapes.contains(&shape.iri) { return; } + + let mut new_where_statements: Vec = vec![]; + let mut new_construct_statements: Vec = vec![]; + visited_shapes.insert(shape.iri.clone()); // Add statements for each predicate. for predicate in &shape.predicates { let mut union_branches = Vec::new(); - let mut allowed_literals = Vec::new(); - // Predicate constraints might have more than one acceptable data type. Traverse each. - // It is assumed that constant literals, nested shapes and regular types are not mixed. + // Predicate constraints might have more than one acceptable nested shape. Traverse each. for datatype in &predicate.dataTypes { - if datatype.valType == OrmSchemaLiteralType::literal { - // Collect allowed literals and as strings - // (already in SPARQL-format, e.g. `"a astring"`, ``, `true`, or `42`). - allowed_literals.extend(literal_to_sparql_str(datatype.clone())); - } else if datatype.valType == OrmSchemaLiteralType::shape { + if datatype.valType == OrmSchemaLiteralType::shape { let shape_iri = &datatype.shape.clone().unwrap(); let nested_shape = schema.get(shape_iri).unwrap(); @@ -117,7 +116,7 @@ pub fn shape_type_to_sparql( // Each shape option gets its own var. let obj_var_name = get_new_var_name(var_counter); - construct_statements.push(format!( + new_construct_statements.push(format!( " ?{} <{}> ?{}", subject_var_name, predicate.iri, obj_var_name )); @@ -136,33 +135,15 @@ pub fn shape_type_to_sparql( where_statements, var_counter, visited_shapes, + true, ); } } - // The where statement which might be wrapped in OPTIONAL. + // The where statement (which may be wrapped in OPTIONAL). let where_body: String; - if !allowed_literals.is_empty() - && !predicate.extra.unwrap_or(false) - && predicate.minCardinality > 0 - { - // If we have literal requirements and they are not optional ("extra"), - // Add CONSTRUCT, WHERE, and FILTER. - - let pred_var_name = get_new_var_name(var_counter); - construct_statements.push(format!( - " ?{} <{}> ?{}", - subject_var_name, predicate.iri, pred_var_name - )); - where_body = format!( - " ?{s} <{p}> ?{o} . \n FILTER(?{o} IN ({lits}))", - s = subject_var_name, - p = predicate.iri, - o = pred_var_name, - lits = allowed_literals.join(", ") - ); - } else if !union_branches.is_empty() { + if !union_branches.is_empty() { // We have nested shape(s) which were already added to CONSTRUCT above. // Join them with UNION. @@ -174,25 +155,50 @@ pub fn shape_type_to_sparql( } else { // Regular predicate data type. Just add basic CONSTRUCT and WHERE statements. - let pred_var_name = get_new_var_name(var_counter); - construct_statements.push(format!( + let obj_var_name = get_new_var_name(var_counter); + new_construct_statements.push(format!( " ?{} <{}> ?{}", - subject_var_name, predicate.iri, pred_var_name + subject_var_name, predicate.iri, obj_var_name )); where_body = format!( " ?{} <{}> ?{}", - subject_var_name, predicate.iri, pred_var_name + subject_var_name, predicate.iri, obj_var_name ); } - // Wrap in optional, if necessary. + // Wrap in optional, if predicate is optional if predicate.minCardinality < 1 { - where_statements.push(format!(" OPTIONAL {{\n{}\n }}", where_body)); + new_where_statements.push(format!(" OPTIONAL {{\n{}\n }}", where_body)); } else { - where_statements.push(where_body); + new_where_statements.push(where_body); }; } + if in_recursion { + // All statements in recursive objects need to be optional + // because we want to fetch _all_ nested objects, + // invalid ones too, for later validation. + let pred_var_name = get_new_var_name(var_counter); + let obj_var_name = get_new_var_name(var_counter); + + // The "catch any triple in subject" where statement + construct_statements.push(format!( + " ?{} ?{} ?{}", + subject_var_name, pred_var_name, obj_var_name + )); + + let joined_where_statements = new_where_statements.join(" .\n"); + + // We do a join of the where statements (which will take care of querying further nested objects) + // and the "catch any triple in subject" where statement. + where_statements.push(format!( + " {{?{} ?{} ?{}}}\n UNION {{\n {}\n }}", + subject_var_name, pred_var_name, obj_var_name, joined_where_statements + )); + } else { + where_statements.append(&mut new_where_statements); + construct_statements.append(&mut new_construct_statements); + } visited_shapes.remove(&shape.iri); } @@ -209,11 +215,12 @@ pub fn shape_type_to_sparql( &mut where_statements, &mut var_counter, &mut visited_shapes, + false, ); // Filter subjects, if present. if let Some(subjects) = filter_subjects { - log_debug!("filter_subjects: {:?}", subjects); + // log_debug!("filter_subjects: {:?}", subjects); let subjects_str = subjects .iter() .map(|s| format!("<{}>", s)) diff --git a/package.json b/package.json index cd0f902..becc57e 100644 --- a/package.json +++ b/package.json @@ -15,5 +15,8 @@ "prettier": "^3.6.2", "prettier-plugin-svelte": "^3.4.0" }, + "engines": { + "node": ">=22.18" + }, "packageManager": "pnpm@10.15.0+sha512.486ebc259d3e999a4e8691ce03b5cac4a71cbeca39372a9b762cb500cfdf0873e2cb16abe3d951b1ee2cf012503f027b98b6584e4df22524e0c7450d9ec7aa7b" } diff --git a/sdk/ng-sdk-js/examples/multi-framework-signals/package.json b/sdk/ng-sdk-js/examples/multi-framework-signals/package.json index 3feb6d9..84b3371 100644 --- a/sdk/ng-sdk-js/examples/multi-framework-signals/package.json +++ b/sdk/ng-sdk-js/examples/multi-framework-signals/package.json @@ -43,5 +43,8 @@ "@types/react-dom": "19.1.7", "vite": "7.1.3", "vitest": "^3.2.4" + }, + "engines": { + "node": ">=22.18" } }