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"
}
}