From ce82867563ddab6ea2c42dff9647967bb0f1bb6b Mon Sep 17 00:00:00 2001 From: Laurin Weger Date: Fri, 24 Oct 2025 00:29:54 +0200 Subject: [PATCH] more unit tests & fixes --- engine/net/src/orm.rs | 2 + engine/verifier/src/orm/add_remove_triples.rs | 42 +- .../verifier/src/orm/handle_backend_update.rs | 197 ++++--- .../src/orm/handle_frontend_update.rs | 56 +- engine/verifier/src/orm/process_changes.rs | 188 ++++-- engine/verifier/src/orm/shape_validation.rs | 49 +- engine/verifier/src/orm/types.rs | 11 +- .../src/app/pages/index.astro | 4 +- .../src/frontends/react/HelloWorld.tsx | 25 +- .../src/frontends/svelte/HelloWorld.svelte | 39 +- .../src/frontends/vue/HelloWorld.vue | 5 +- .../src/shapes/orm/basic.schema.ts | 4 +- .../src/shapes/orm/basic.shapeTypes.ts | 2 +- .../src/shapes/orm/basic.typings.ts | 2 +- .../src/shapes/orm/catShape.schema.ts | 10 +- .../src/shapes/orm/catShape.shapeTypes.ts | 2 +- .../src/shapes/orm/catShape.typings.ts | 2 +- .../src/shapes/orm/personShape.schema.ts | 10 +- .../src/shapes/orm/personShape.shapeTypes.ts | 2 +- .../src/shapes/orm/personShape.typings.ts | 2 +- .../src/shapes/orm/testShape.schema.ts | 16 +- .../src/shapes/orm/testShape.shapeTypes.ts | 2 +- .../src/shapes/orm/testShape.typings.ts | 2 +- .../src/shapes/shex/basic.shex | 2 +- .../src/shapes/shex/catShape.shex | 2 +- .../src/shapes/shex/personShape.shex | 2 +- .../src/shapes/shex/testShape.shex | 2 +- sdk/js/shex-orm/dist/ShexJTypes.d.ts | 542 ----------------- sdk/js/shex-orm/dist/ShexJTypes.d.ts.map | 1 - sdk/js/shex-orm/dist/ShexJTypes.js | 1 - sdk/js/shex-orm/dist/build.d.ts | 8 - sdk/js/shex-orm/dist/build.d.ts.map | 1 - sdk/js/shex-orm/dist/build.js | 62 -- sdk/js/shex-orm/dist/cli.d.ts | 3 - sdk/js/shex-orm/dist/cli.d.ts.map | 1 - sdk/js/shex-orm/dist/cli.js | 15 - sdk/js/shex-orm/dist/index.d.ts | 2 - sdk/js/shex-orm/dist/index.d.ts.map | 1 - sdk/js/shex-orm/dist/index.js | 1 - .../dist/schema-converter/converter.d.ts | 12 - .../dist/schema-converter/converter.d.ts.map | 1 - .../dist/schema-converter/converter.js | 69 --- .../schema-converter/templates/schema.ejs | 8 - .../schema-converter/templates/shapeTypes.ejs | 14 - .../schema-converter/templates/typings.ejs | 14 - .../transformers/ShexJSchemaTransformer.d.ts | 348 ----------- .../ShexJSchemaTransformer.d.ts.map | 1 - .../transformers/ShexJSchemaTransformer.js | 208 ------- .../transformers/ShexJTypingTransformer.d.ts | 366 ------------ .../ShexJTypingTransformer.d.ts.map | 1 - .../transformers/ShexJTypingTransformer.js | 550 ------------------ .../util/ShapeInterfaceDeclaration.d.ts | 5 - .../util/ShapeInterfaceDeclaration.d.ts.map | 1 - .../util/ShapeInterfaceDeclaration.js | 1 - .../util/annotateReadablePredicates.d.ts | 8 - .../util/annotateReadablePredicates.d.ts.map | 1 - .../util/annotateReadablePredicates.js | 129 ---- .../util/dedupeObjectTypeMembers.d.ts | 3 - .../util/dedupeObjectTypeMembers.d.ts.map | 1 - .../util/dedupeObjectTypeMembers.js | 38 -- .../util/getRdfTypesForTripleConstraint.d.ts | 4 - .../getRdfTypesForTripleConstraint.d.ts.map | 1 - .../util/getRdfTypesForTripleConstraint.js | 89 --- sdk/js/shex-orm/dist/types.d.ts | 37 -- sdk/js/shex-orm/dist/types.d.ts.map | 1 - sdk/js/shex-orm/dist/types.js | 1 - sdk/js/shex-orm/dist/util/forAllShapes.d.ts | 2 - .../shex-orm/dist/util/forAllShapes.d.ts.map | 1 - sdk/js/shex-orm/dist/util/forAllShapes.js | 17 - .../transformers/ShexJSchemaTransformer.ts | 26 +- .../src/connector/ormConnectionHandler.ts | 10 +- sdk/rust/src/tests/orm_apply_patches.rs | 119 +++- sdk/rust/src/tests/orm_create_patches.rs | 58 +- 73 files changed, 524 insertions(+), 2941 deletions(-) delete mode 100644 sdk/js/shex-orm/dist/ShexJTypes.d.ts delete mode 100644 sdk/js/shex-orm/dist/ShexJTypes.d.ts.map delete mode 100644 sdk/js/shex-orm/dist/ShexJTypes.js delete mode 100644 sdk/js/shex-orm/dist/build.d.ts delete mode 100644 sdk/js/shex-orm/dist/build.d.ts.map delete mode 100644 sdk/js/shex-orm/dist/build.js delete mode 100644 sdk/js/shex-orm/dist/cli.d.ts delete mode 100644 sdk/js/shex-orm/dist/cli.d.ts.map delete mode 100755 sdk/js/shex-orm/dist/cli.js delete mode 100644 sdk/js/shex-orm/dist/index.d.ts delete mode 100644 sdk/js/shex-orm/dist/index.d.ts.map delete mode 100644 sdk/js/shex-orm/dist/index.js delete mode 100644 sdk/js/shex-orm/dist/schema-converter/converter.d.ts delete mode 100644 sdk/js/shex-orm/dist/schema-converter/converter.d.ts.map delete mode 100644 sdk/js/shex-orm/dist/schema-converter/converter.js delete mode 100644 sdk/js/shex-orm/dist/schema-converter/templates/schema.ejs delete mode 100644 sdk/js/shex-orm/dist/schema-converter/templates/shapeTypes.ejs delete mode 100644 sdk/js/shex-orm/dist/schema-converter/templates/typings.ejs delete mode 100644 sdk/js/shex-orm/dist/schema-converter/transformers/ShexJSchemaTransformer.d.ts delete mode 100644 sdk/js/shex-orm/dist/schema-converter/transformers/ShexJSchemaTransformer.d.ts.map delete mode 100644 sdk/js/shex-orm/dist/schema-converter/transformers/ShexJSchemaTransformer.js delete mode 100644 sdk/js/shex-orm/dist/schema-converter/transformers/ShexJTypingTransformer.d.ts delete mode 100644 sdk/js/shex-orm/dist/schema-converter/transformers/ShexJTypingTransformer.d.ts.map delete mode 100644 sdk/js/shex-orm/dist/schema-converter/transformers/ShexJTypingTransformer.js delete mode 100644 sdk/js/shex-orm/dist/schema-converter/util/ShapeInterfaceDeclaration.d.ts delete mode 100644 sdk/js/shex-orm/dist/schema-converter/util/ShapeInterfaceDeclaration.d.ts.map delete mode 100644 sdk/js/shex-orm/dist/schema-converter/util/ShapeInterfaceDeclaration.js delete mode 100644 sdk/js/shex-orm/dist/schema-converter/util/annotateReadablePredicates.d.ts delete mode 100644 sdk/js/shex-orm/dist/schema-converter/util/annotateReadablePredicates.d.ts.map delete mode 100644 sdk/js/shex-orm/dist/schema-converter/util/annotateReadablePredicates.js delete mode 100644 sdk/js/shex-orm/dist/schema-converter/util/dedupeObjectTypeMembers.d.ts delete mode 100644 sdk/js/shex-orm/dist/schema-converter/util/dedupeObjectTypeMembers.d.ts.map delete mode 100644 sdk/js/shex-orm/dist/schema-converter/util/dedupeObjectTypeMembers.js delete mode 100644 sdk/js/shex-orm/dist/schema-converter/util/getRdfTypesForTripleConstraint.d.ts delete mode 100644 sdk/js/shex-orm/dist/schema-converter/util/getRdfTypesForTripleConstraint.d.ts.map delete mode 100644 sdk/js/shex-orm/dist/schema-converter/util/getRdfTypesForTripleConstraint.js delete mode 100644 sdk/js/shex-orm/dist/types.d.ts delete mode 100644 sdk/js/shex-orm/dist/types.d.ts.map delete mode 100644 sdk/js/shex-orm/dist/types.js delete mode 100644 sdk/js/shex-orm/dist/util/forAllShapes.d.ts delete mode 100644 sdk/js/shex-orm/dist/util/forAllShapes.d.ts.map delete mode 100644 sdk/js/shex-orm/dist/util/forAllShapes.js diff --git a/engine/net/src/orm.rs b/engine/net/src/orm.rs index d36b0f60..7e2c166e 100644 --- a/engine/net/src/orm.rs +++ b/engine/net/src/orm.rs @@ -41,8 +41,10 @@ pub enum OrmPatchType { #[derive(Clone, Debug, Serialize, Deserialize)] pub struct OrmPatch { pub op: OrmPatchOp, + #[serde(skip_serializing_if = "Option::is_none")] pub valType: Option, pub path: String, + #[serde(skip_serializing_if = "Option::is_none")] pub value: Option, // TODO: Improve type } diff --git a/engine/verifier/src/orm/add_remove_triples.rs b/engine/verifier/src/orm/add_remove_triples.rs index 4823816b..378bdaff 100644 --- a/engine/verifier/src/orm/add_remove_triples.rs +++ b/engine/verifier/src/orm/add_remove_triples.rs @@ -44,7 +44,6 @@ pub fn add_remove_triples( tracked_predicates: HashMap::new(), parents: HashMap::new(), valid: OrmTrackedSubjectValidity::Pending, - prev_valid: OrmTrackedSubjectValidity::Pending, subject_iri: subject_iri.to_string(), shape: shape.clone(), })) @@ -208,47 +207,8 @@ pub fn add_remove_triples( } else { panic!("tracked_predicate.current_literals must not be None."); } - } else if tracked_predicate - .schema - .dataTypes - .iter() - .any(|dt| dt.valType == OrmSchemaValType::shape) - { - // Remove parent from child and child from tracked children. - // If predicate is of type shape, register (parent -> child) links so that - // nested subjects can later be (lazily) fetched / validated. - let shapes_to_process: Vec<_> = tracked_predicate - .schema - .dataTypes - .iter() - .filter_map(|dt| { - if dt.valType == OrmSchemaValType::shape { - dt.shape.clone() - } else { - None - } - }) - .collect(); - - if let BasicType::Str(obj_iri) = &val_removed { - // Remove link to children - tracked_predicate - .tracked_children - .retain(|ts| *obj_iri != ts.read().unwrap().subject_iri); - - for shape_iri in shapes_to_process { - // Get or create object's tracked subject struct. - let child_shape = schema.get(&shape_iri).unwrap(); - - // Remove self from parent - get_or_create_tracked_subject(&obj_iri, child_shape, tracked_subjects) - .write() - .unwrap() - .parents - .remove(subject_iri); - } - } } + // Parent-child link removal is handled during cleanup since we need to keep them for creating patches. } Ok(()) } diff --git a/engine/verifier/src/orm/handle_backend_update.rs b/engine/verifier/src/orm/handle_backend_update.rs index 41760e26..395091ea 100644 --- a/engine/verifier/src/orm/handle_backend_update.rs +++ b/engine/verifier/src/orm/handle_backend_update.rs @@ -123,8 +123,8 @@ impl Verifier { ); } - let subs = self.orm_subscriptions.get(&scope).unwrap(); - for sub in subs.iter() { + let subs = self.orm_subscriptions.get_mut(&scope).unwrap(); + for sub in subs.iter_mut() { log_debug!( "Applying changes to subscription with nuri {} and shape {}", sub.nuri.repo(), @@ -161,48 +161,65 @@ impl Verifier { for (shape_iri, subject_changes) in &orm_changes { for (subject_iri, change) in subject_changes { log_debug!( - "Patch creating for subject change {}. #changed preds: {}", + "Patch creating for subject change x shape {} x {}. #changed preds: {}", subject_iri, + shape_iri, change.predicates.len() ); // Get the tracked subject for this (subject, shape) pair - let tracked_subject = sub + let Some(tracked_subject) = sub .tracked_subjects .get(subject_iri) .and_then(|shapes| shapes.get(shape_iri)) .map(|ts| ts.read().unwrap()) - .unwrap(); + else { + // We might not be tracking this subject x shape combination. Then, there is nothing to do. + continue; + }; + + log_debug!( + " - Validity check: prev_valid={:?}, valid={:?}", + change.prev_valid, + tracked_subject.valid + ); // Now we have the tracked predicate (containing the shape) and the change. // Check validity changes - if tracked_subject.prev_valid == OrmTrackedSubjectValidity::Invalid + if change.prev_valid == OrmTrackedSubjectValidity::Invalid && tracked_subject.valid == OrmTrackedSubjectValidity::Invalid { // Is the subject invalid and was it before? There is nothing we need to inform about. continue; - } else if tracked_subject.prev_valid == OrmTrackedSubjectValidity::Valid - && tracked_subject.valid == OrmTrackedSubjectValidity::Invalid - || tracked_subject.valid == OrmTrackedSubjectValidity::Untracked + } else if change.prev_valid == OrmTrackedSubjectValidity::Valid + && tracked_subject.valid != OrmTrackedSubjectValidity::Valid { // Has the subject become invalid or untracked? - // We add a patch, deleting the object at its root. - let mut path: Vec; - if tracked_subject.parents.is_empty() { - // If this is a root object, we need to add the object's id itself. - path = vec![tracked_subject.subject_iri.clone()]; - } else { - path = vec![]; + // Check if any parent is also being deleted - if so, skip this deletion patch + // because the parent deletion will implicitly delete the children + let has_parent_being_deleted = + tracked_subject.parents.values().any(|parent_arc| { + let parent_ts = parent_arc.read().unwrap(); + parent_ts.valid == OrmTrackedSubjectValidity::ToDelete + }); + + if !has_parent_being_deleted { + // We add a patch, deleting the object at its root. + // Start with an empty path - the subject IRI will be added in build_path_to_root_and_create_patches + let mut path = vec![]; + + build_path_to_root_and_create_patches( + &tracked_subject, + &sub.tracked_subjects, + &sub.shape_type.shape, + &mut path, + (OrmPatchOp::remove, Some(OrmPatchType::object), None, None), + &mut patches, + &mut objects_to_create, + &change.prev_valid, + &orm_changes, + &tracked_subject.subject_iri, + ); } - - build_path_to_root_and_create_patches( - &tracked_subject, - &sub.tracked_subjects, - &sub.shape_type.shape, - &mut path, - (OrmPatchOp::remove, Some(OrmPatchType::object), None, None), - &mut patches, - &mut objects_to_create, - ); } else { // The subject is valid or has become valid. // Process each predicate change @@ -234,6 +251,9 @@ impl Verifier { diff_op, &mut patches, &mut objects_to_create, + &change.prev_valid, + &orm_changes, + &tracked_subject.subject_iri, ); } } @@ -246,7 +266,7 @@ impl Verifier { // Sort by path length (shorter first) to ensure parent objects are created before children let mut sorted_objects: Vec<_> = objects_to_create.iter().collect(); sorted_objects.sort_by_key(|(path_segments, _)| path_segments.len()); - + let mut object_create_patches = vec![]; for (path_segments, maybe_iri) in sorted_objects { let escaped_path: Vec = path_segments .iter() @@ -254,8 +274,8 @@ impl Verifier { .collect(); let json_pointer = format!("/{}", escaped_path.join("/")); - // Always create the object itself - patches.push(OrmPatch { + // Always create the object itself. + object_create_patches.push(OrmPatch { op: OrmPatchOp::add, valType: Some(OrmPatchType::object), path: json_pointer.clone(), @@ -264,7 +284,7 @@ impl Verifier { // If this object has an IRI (it's a real subject), add the id field if let Some(iri) = maybe_iri { - patches.push(OrmPatch { + object_create_patches.push(OrmPatch { op: OrmPatchOp::add, valType: None, path: format!("{}/@id", json_pointer), @@ -277,8 +297,13 @@ impl Verifier { let _ = sub .sender .clone() - .send(AppResponse::V0(AppResponseV0::OrmUpdate(patches.to_vec()))) + .send(AppResponse::V0(AppResponseV0::OrmUpdate( + [object_create_patches, patches].concat(), + ))) .await; + + // Cleanup (remove tracked subjects to be deleted). + Verifier::cleanup_tracked_subjects(sub); } } } @@ -286,40 +311,32 @@ impl Verifier { /// Queue patches for a newly valid tracked subject. /// This handles creating object patches and id field patches for subjects that have become valid. -fn queue_patches_for_newly_valid_subject( - tracked_subject: &OrmTrackedSubject, +fn queue_objects_to_create( + current_ts: &OrmTrackedSubject, tracked_subjects: &HashMap>>>, root_shape: &String, path: &[String], - patches: &mut Vec, objects_to_create: &mut HashSet<(Vec, Option)>, + orm_changes: &OrmChanges, + child_iri: &String, ) { // Check if we're at a root subject or need to traverse to parents - if tracked_subject.parents.is_empty() || tracked_subject.shape.iri == *root_shape { - // Register object for creation. - // Path to object consists of this subject's iri and the path except for the last element. - let mut path_to_subject = vec![tracked_subject.subject_iri.clone()]; - if path.len() > 1 { - path_to_subject.extend_from_slice(&path[..path.len() - 1]); - } - - // log_debug!("Queuing object creation for path: {:?}", path_to_subject); - - // Always create the object itself with its IRI - objects_to_create.insert(( - path_to_subject.clone(), - Some(tracked_subject.subject_iri.clone()), - )); + if current_ts.parents.is_empty() || current_ts.shape.iri == *root_shape { + // We are at the root. Insert without the last element (which is the property name). + objects_to_create.insert((path[..path.len() - 1].to_vec(), Some(child_iri.clone()))); } else { // Not at root: traverse to parents and create object patches along the way - for (_parent_iri, parent_tracked_subject) in tracked_subject.parents.iter() { + for (_parent_iri, parent_tracked_subject) in current_ts.parents.iter() { let parent_ts = parent_tracked_subject.read().unwrap(); - if let Some(new_path) = build_path_segment_for_parent(tracked_subject, &parent_ts, path) - { + if let Some(new_path) = build_path_segment_for_parent(current_ts, &parent_ts, path) { // Check if the parent's predicate is multi-valued and if no siblings were previously valid let should_create_parent_predicate_object = - check_should_create_parent_predicate_object(tracked_subject, &parent_ts); + check_should_create_parent_predicate_object( + current_ts, + &parent_ts, + orm_changes, + ); if should_create_parent_predicate_object { // Need to create an intermediate object for the multi-valued predicate @@ -331,18 +348,18 @@ fn queue_patches_for_newly_valid_subject( } // Recurse to the parent first - queue_patches_for_newly_valid_subject( + queue_objects_to_create( &parent_ts, tracked_subjects, root_shape, &new_path, - patches, objects_to_create, + orm_changes, + child_iri, ); // Register this object for creation with its IRI - objects_to_create - .insert((new_path.clone(), Some(tracked_subject.subject_iri.clone()))); + objects_to_create.insert((new_path.clone(), Some(current_ts.subject_iri.clone()))); } } } @@ -353,6 +370,7 @@ fn queue_patches_for_newly_valid_subject( fn check_should_create_parent_predicate_object( tracked_subject: &OrmTrackedSubject, parent_ts: &OrmTrackedSubject, + orm_changes: &OrmChanges, ) -> bool { // Find the predicate schema linking parent to this subject for pred_arc in &parent_ts.shape.predicates { @@ -369,11 +387,22 @@ fn check_should_create_parent_predicate_object( let is_multi = pred_arc.maxCardinality > 1 || pred_arc.maxCardinality == -1; if is_multi { - // Check if any siblings were previously valid + // Check if any siblings were previously valid. + // If not, the intermediate object does not exist yet. let any_sibling_was_valid = tp.tracked_children.iter().any(|child| { let child_read = child.read().unwrap(); - child_read.subject_iri != tracked_subject.subject_iri - && child_read.prev_valid == OrmTrackedSubjectValidity::Valid + if child_read.subject_iri == tracked_subject.subject_iri { + return false; + } + + // Look up the prev_valid from orm_changes + let prev_valid = orm_changes + .get(&child_read.shape.iri) + .and_then(|subjects| subjects.get(&child_read.subject_iri)) + .map(|change| &change.prev_valid) + .unwrap_or(&OrmTrackedSubjectValidity::Valid); + + *prev_valid == OrmTrackedSubjectValidity::Valid }); return !any_sibling_was_valid; @@ -442,14 +471,21 @@ fn build_path_to_root_and_create_patches( ), patches: &mut Vec, objects_to_create: &mut HashSet<(Vec, Option)>, + prev_valid: &OrmTrackedSubjectValidity, + orm_changes: &OrmChanges, + child_iri: &String, ) { log_debug!( - " - build path, ts: {}, path {:?}", + " - build path, ts: {}, path {:?}, #parents: {}, shape: {}", tracked_subject.subject_iri, - path + path, + tracked_subject.parents.len(), + tracked_subject.shape.iri ); // If the tracked subject is not valid, we don't create patches for it - if tracked_subject.valid != OrmTrackedSubjectValidity::Valid { + // EXCEPT when we're removing the object itself (indicated by op == remove and valType == object) + let is_delete_op = diff_op.0 == OrmPatchOp::remove && diff_op.1 == Some(OrmPatchType::object); + if tracked_subject.valid != OrmTrackedSubjectValidity::Valid && !is_delete_op { return; } @@ -457,12 +493,19 @@ fn build_path_to_root_and_create_patches( if tracked_subject.parents.is_empty() || tracked_subject.shape.iri == *root_shape { // Build the final JSON Pointer path let escaped_path: Vec = path.iter().map(|seg| escape_json_pointer(seg)).collect(); - // Always add the root subject to the path. - let json_pointer = format!( - "/{}/{}", - escape_json_pointer(&tracked_subject.subject_iri), - escaped_path.join("/") - ); + + // Create the JSON pointer path + let json_pointer = if escaped_path.is_empty() { + // For root object operations (no path elements), just use the subject IRI + format!("/{}", escape_json_pointer(&tracked_subject.subject_iri)) + } else { + // For nested operations, include both subject and path + format!( + "/{}/{}", + escape_json_pointer(&tracked_subject.subject_iri), + escaped_path.join("/") + ) + }; // Create the patch for the actual value change patches.push(OrmPatch { @@ -473,16 +516,17 @@ fn build_path_to_root_and_create_patches( }); // If the subject is newly valid, now we have the full path to queue its creation. - if tracked_subject.prev_valid != OrmTrackedSubjectValidity::Valid { + if *prev_valid != OrmTrackedSubjectValidity::Valid { let mut final_path = vec![tracked_subject.subject_iri.clone()]; final_path.extend_from_slice(path); - queue_patches_for_newly_valid_subject( + queue_objects_to_create( tracked_subject, tracked_subjects, root_shape, &final_path, - patches, objects_to_create, + orm_changes, + child_iri, ); } @@ -505,6 +549,15 @@ fn build_path_to_root_and_create_patches( diff_op.clone(), patches, objects_to_create, + prev_valid, + orm_changes, + child_iri, + ); + } else { + log_debug!( + " - build_path_segment_for_parent returned None for parent: {}, child: {}", + parent_ts.subject_iri, + tracked_subject.subject_iri ); } } diff --git a/engine/verifier/src/orm/handle_frontend_update.rs b/engine/verifier/src/orm/handle_frontend_update.rs index 358c26d9..aba4516b 100644 --- a/engine/verifier/src/orm/handle_frontend_update.rs +++ b/engine/verifier/src/orm/handle_frontend_update.rs @@ -24,10 +24,7 @@ use crate::types::GraphQuadsPatch; use crate::verifier::*; impl Verifier { - /// After creating new objects (without an id) in JS-land, - /// we send the generated id for those back. - /// If something went wrong (revert_inserts / revert_removes not empty), - /// we send a JSON patch back to revert the made changes. + /// pub(crate) async fn orm_update_self( &mut self, scope: &NuriV0, @@ -48,10 +45,10 @@ impl Verifier { inserts: revert_removes, removes: revert_inserts, }; - log_info!("[orm_frontend_update] Reverting"); - - // TODO: Call with correct params. - // self.orm_backend_update(session_id, scope, "", revert_changes) + log_info!("[orm_update_self] Reverting triples, calling orm_backend_update. TODO"); + // TODO + // self.orm_backend_update(session_id, scope, "", revert_changes); + log_info!("[orm_update_self] Triples reverted."); } Ok(()) @@ -151,20 +148,20 @@ fn create_sparql_update_query_for_diff( delete_patches.len() ); - let add_object_patches: Vec<_> = diff - .iter() - .filter(|patch| { - patch.op == OrmPatchOp::add - && match &patch.valType { - Some(vt) => *vt == OrmPatchType::object, - _ => false, - } - }) - .collect(); - log_info!( - "[create_sparql_update_query_for_diff] Found {} add object patches", - add_object_patches.len() - ); + // let add_object_patches: Vec<_> = diff + // .iter() + // .filter(|patch| { + // patch.op == OrmPatchOp::add + // && match &patch.valType { + // Some(vt) => *vt == OrmPatchType::object, + // _ => false, + // } + // }) + // .collect(); + // log_info!( + // "[create_sparql_update_query_for_diff] Found {} add object patches", + // add_object_patches.len() + // ); let add_primitive_patches: Vec<_> = diff .iter() @@ -235,17 +232,6 @@ fn create_sparql_update_query_for_diff( ); } - // Process add object patches (might need blank nodes) - // - for (idx, _add_obj_patch) in add_object_patches.iter().enumerate() { - log_info!("[create_sparql_update_query_for_diff] Processing add object patch {}/{} (NOT YET IMPLEMENTED)", idx + 1, add_object_patches.len()); - // Creating objects without an id field is only supported in one circumstance: - // An object is added to a property which has a max cardinality of one, e.g. `painting.artist`. - // In that case, we create a blank node. - // TODO: We need to set up a list of created blank nodes and where they belong to. - // POTENTIAL PANIC SOURCE: This is not implemented yet - } - // Process primitive add patches // for (idx, add_patch) in add_primitive_patches.iter().enumerate() { @@ -259,7 +245,6 @@ fn create_sparql_update_query_for_diff( let mut var_counter: i32 = 0; // Create WHERE statements from path. - // POTENTIAL PANIC SOURCE: create_where_statements_for_patch can panic in several places let (where_statements, target, pred_schema) = create_where_statements_for_patch(&add_patch, &mut var_counter, &orm_subscription); let (subject_var, target_predicate, target_object) = target; @@ -434,7 +419,6 @@ fn create_where_statements_for_patch( path.len() ); - // POTENTIAL PANIC SOURCE: find_pred_schema_by_name can panic log_info!( "[create_where_statements_for_patch] Looking up predicate schema for name={}", pred_name @@ -505,7 +489,6 @@ fn create_where_statements_for_patch( ); } - // POTENTIAL PANIC SOURCE: get_first_child_schema can panic log_info!( "[create_where_statements_for_patch] Getting child schema for object_iri={}", object_iri @@ -528,7 +511,6 @@ fn create_where_statements_for_patch( // As long as there is only one allowed shape or the first one is valid, this is fine. log_info!("[create_where_statements_for_patch] Predicate is single-valued, getting child schema"); - // POTENTIAL PANIC SOURCE: get_first_child_schema can panic current_subj_schema = get_first_child_schema(None, &pred_schema, &orm_subscription); log_info!("[create_where_statements_for_patch] Child schema found"); } diff --git a/engine/verifier/src/orm/process_changes.rs b/engine/verifier/src/orm/process_changes.rs index eee48788..e0617489 100644 --- a/engine/verifier/src/orm/process_changes.rs +++ b/engine/verifier/src/orm/process_changes.rs @@ -162,7 +162,7 @@ impl Verifier { // Mark as currently validating currently_validating.insert(validation_key.clone()); - // Get triples of subject (added & removed). + // Get triple changes for subject (added & removed). let triples_added_for_subj = added_triples_by_subject .get(*subject_iri) .map(|v| v.as_slice()) @@ -177,31 +177,36 @@ impl Verifier { .entry(shape.iri.clone()) .or_insert_with(HashMap::new) .entry((*subject_iri).clone()) - .or_insert_with(|| OrmTrackedSubjectChange { - subject_iri: (*subject_iri).clone(), - predicates: HashMap::new(), - data_applied: false, - }); + .or_insert_with(|| { + // Create a new change record. + // This includes the previous validity and triple changes. + let orm_subscription = self + .orm_subscriptions + .get_mut(nuri) + .unwrap() + .iter_mut() + .find(|sub| { + sub.shape_type.shape == *root_shape_iri + && sub.session_id == session_id + }) + .unwrap(); - // Apply all triples for that subject to the tracked (shape, subject) pair. - // Record the changes. - { - let orm_subscription = self - .orm_subscriptions - .get_mut(nuri) - .unwrap() - .iter_mut() - .find(|sub| { - sub.shape_type.shape == *root_shape_iri && sub.session_id == session_id - }) - .unwrap(); + log_debug!("[process_changes_for_shape_and_session] Creating change object for {}, {}", subject_iri, shape.iri); + let prev_valid = match orm_subscription + .tracked_subjects + .get(*subject_iri) + .and_then(|shapes| shapes.get(&shape.iri)) + { + Some(tracked_subject) => tracked_subject.read().unwrap().valid.clone(), + None => OrmTrackedSubjectValidity::Pending, + }; - // Update tracked subjects and modify change objects. - if !change.data_applied { - log_debug!( - "Adding triples to change tracker for subject {}", - subject_iri - ); + let mut change = OrmTrackedSubjectChange { + subject_iri: (*subject_iri).clone(), + predicates: HashMap::new(), + is_validated: false, + prev_valid, + }; if let Err(e) = add_remove_triples( shape.clone(), @@ -209,46 +214,31 @@ impl Verifier { triples_added_for_subj, triples_removed_for_subj, orm_subscription, - change, + &mut change, ) { log_err!("apply_changes_from_triples add/remove error: {:?}", e); panic!(); } - change.data_applied = true; - } - - // Check if this is the first evaluation round - In that case, set old validity to new one. - // if the object was already validated, don't do so again. - { - let tracked_subject = &mut orm_subscription - .tracked_subjects - .get(*subject_iri) - .unwrap() - .get(&shape.iri) - .unwrap() - .write() - .unwrap(); - - // First run - if !change.data_applied - && tracked_subject.valid != OrmTrackedSubjectValidity::Pending - { - tracked_subject.prev_valid = tracked_subject.valid.clone(); - } - if change.data_applied { - log_debug!("not applying triples again for subject {subject_iri}"); + change + }); - // Has this subject already been validated? - if change.data_applied - && tracked_subject.valid != OrmTrackedSubjectValidity::Pending - { - log_debug!("Not evaluating subject again {subject_iri}"); + // If validation took place already, there's nothing more to do... + if change.is_validated { + continue; + } - continue; - } - } - } + // Run validation and record objects that need to be re-evaluated. + { + let orm_subscription = self + .orm_subscriptions + .get_mut(nuri) + .unwrap() + .iter_mut() + .find(|sub| { + sub.shape_type.shape == *root_shape_iri && sub.session_id == session_id + }) + .unwrap(); // Validate the subject. // need_eval contains elements in reverse priority (last element to be validated first) @@ -415,4 +405,88 @@ impl Verifier { Some(subscription) => Ok((subscription.sender.clone(), subscription)), } } + + pub fn cleanup_tracked_subjects(orm_subscription: &mut OrmSubscription) { + let tracked_subjects = &mut orm_subscription.tracked_subjects; + + // First pass: Clean up relationships for subjects being deleted + for (subject_iri, subjects_for_shape) in tracked_subjects.iter() { + for (_shape_iri, tracked_subject_lock) in subjects_for_shape.iter() { + let tracked_subject = tracked_subject_lock.read().unwrap(); + + // Only process subjects that are marked for deletion + if tracked_subject.valid != OrmTrackedSubjectValidity::ToDelete { + continue; + } + + let has_parents = !tracked_subject.parents.is_empty(); + + // Set all children to `untracked` that don't have other parents + for tracked_predicate in tracked_subject.tracked_predicates.values() { + let tracked_pred_read = tracked_predicate.read().unwrap(); + for child in &tracked_pred_read.tracked_children { + let mut tracked_child = child.write().unwrap(); + if tracked_child.parents.is_empty() + || (tracked_child.parents.len() == 1 + && tracked_child + .parents + .contains_key(&tracked_subject.subject_iri)) + { + if tracked_child.valid != OrmTrackedSubjectValidity::ToDelete { + tracked_child.valid = OrmTrackedSubjectValidity::Untracked; + } + } + } + } + + // Remove this subject from its children's parent lists + // (Only if this is not a root subject - root subjects keep child relationships) + if has_parents { + for tracked_pred in tracked_subject.tracked_predicates.values() { + let tracked_pred_read = tracked_pred.read().unwrap(); + for child in &tracked_pred_read.tracked_children { + child.write().unwrap().parents.remove(subject_iri); + } + } + } + + // Also remove this subject from its parents' children lists + for (_parent_iri, parent_tracked_subject) in &tracked_subject.parents { + let mut parent_ts = parent_tracked_subject.write().unwrap(); + for tracked_pred in parent_ts.tracked_predicates.values_mut() { + let mut tracked_pred_mut = tracked_pred.write().unwrap(); + tracked_pred_mut + .tracked_children + .retain(|child| child.read().unwrap().subject_iri != *subject_iri); + } + } + } + } + + // Second pass: Collect subjects to remove (we can't remove while iterating) + let mut subjects_to_remove: Vec<(String, String)> = vec![]; + + for (subject_iri, subjects_for_shape) in tracked_subjects.iter() { + for (shape_iri, tracked_subject) in subjects_for_shape.iter() { + let tracked_subject = tracked_subject.read().unwrap(); + + // Only cleanup subjects that are marked for deletion + if tracked_subject.valid == OrmTrackedSubjectValidity::ToDelete { + subjects_to_remove.push((subject_iri.clone(), shape_iri.clone())); + } + } + } + + // Third pass: Remove the subjects marked for deletion + for (subject_iri, shape_iri) in subjects_to_remove { + if let Some(shapes_map) = tracked_subjects.get_mut(&subject_iri) { + shapes_map.remove(&shape_iri); + + // If this was the last shape for this subject, remove the subject entry entirely + if shapes_map.is_empty() { + tracked_subjects.remove(&subject_iri); + } + } + } + } } diff --git a/engine/verifier/src/orm/shape_validation.rs b/engine/verifier/src/orm/shape_validation.rs index 6465f005..cee0ab85 100644 --- a/engine/verifier/src/orm/shape_validation.rs +++ b/engine/verifier/src/orm/shape_validation.rs @@ -19,7 +19,7 @@ impl Verifier { /// Might return nested objects that need to be validated. /// Assumes all triples to be of same subject. pub fn update_subject_validity( - s_change: &OrmTrackedSubjectChange, + s_change: &mut OrmTrackedSubjectChange, shape: &OrmSchemaShape, orm_subscription: &mut OrmSubscription, ) -> Vec<(SubjectIri, ShapeIri, NeedsFetchBool)> { @@ -32,7 +32,7 @@ impl Verifier { return vec![]; }; let mut tracked_subject = tracked_subject.write().unwrap(); - let previous_validity = tracked_subject.prev_valid.clone(); + let previous_validity = s_change.prev_valid.clone(); // Keep track of objects that need to be validated against a shape to fetch and validate. let mut need_evaluation: Vec<(String, String, bool)> = vec![]; @@ -348,42 +348,14 @@ impl Verifier { tracked_subject.valid = new_validity.clone(); - if new_validity == OrmTrackedSubjectValidity::Invalid { - // For invalid subjects, we need to to cleanup. - - let has_parents = !tracked_subject.parents.is_empty(); - if has_parents { - // This object is not a root object. Tracked child objects can be dropped. - // We therefore delete the child -> parent links. - // Untracked objects (with no parents) will be deleted in the subsequent child validation. - for tracked_predicate in tracked_subject.tracked_predicates.values() { - for child in &tracked_predicate.write().unwrap().tracked_children { - child - .write() - .unwrap() - .parents - .remove(&tracked_subject.subject_iri); - } - } - } else { - // This is a root objects, we will set the children to untracked - // but don't delete the child > parent relationship. - } + // First, if we have a definite decision, we set is_validated to true. + if new_validity != OrmTrackedSubjectValidity::Pending { + s_change.is_validated = true; + } - // Set all children to `untracked` that don't have other parents. - for tracked_predicate in tracked_subject.tracked_predicates.values() { - for child in &tracked_predicate.write().unwrap().tracked_children { - let mut tracked_child = child.write().unwrap(); - if tracked_child.parents.is_empty() - || (tracked_child.parents.len() == 1 - && tracked_child - .parents - .contains_key(&tracked_subject.subject_iri)) - { - tracked_child.valid = OrmTrackedSubjectValidity::Untracked; - } - } - } + if new_validity == OrmTrackedSubjectValidity::Invalid { + // For invalid subjects, we schedule cleanup. + tracked_subject.valid = OrmTrackedSubjectValidity::ToDelete; // Add all children to need_evaluation for their cleanup. for tracked_predicate in tracked_subject.tracked_predicates.values() { @@ -396,9 +368,6 @@ impl Verifier { )); } } - - // Remove all tracked_predicates. - tracked_subject.tracked_predicates.clear(); } else if new_validity == OrmTrackedSubjectValidity::Valid && previous_validity != OrmTrackedSubjectValidity::Valid { diff --git a/engine/verifier/src/orm/types.rs b/engine/verifier/src/orm/types.rs index 5021934f..9e93b75c 100644 --- a/engine/verifier/src/orm/types.rs +++ b/engine/verifier/src/orm/types.rs @@ -25,8 +25,6 @@ pub struct OrmTrackedSubject { pub parents: HashMap>>, /// Validity. When untracked, triple updates are not processed for this tracked subject. pub valid: OrmTrackedSubjectValidity, - /// Previous validity. Used for validation and creating JSON Patch diffs from changes. - pub prev_valid: OrmTrackedSubjectValidity, /// Subject IRI pub subject_iri: String, /// The shape for which the predicates are tracked. @@ -39,6 +37,7 @@ pub enum OrmTrackedSubjectValidity { Invalid, Pending, Untracked, + ToDelete, } #[derive(Clone, Debug)] @@ -60,10 +59,10 @@ pub struct OrmTrackedSubjectChange { pub subject_iri: String, /// Predicates that were changed. pub predicates: HashMap, - /// If the new triples have been added to the tracked predicates - /// (values_added / values_removed) already. This is to prevent - /// double-application. - pub data_applied: bool, + /// If the validation has taken place + pub is_validated: bool, + /// The validity before the new validation. + pub prev_valid: OrmTrackedSubjectValidity, } #[derive(Debug)] pub struct OrmTrackedPredicateChanges { diff --git a/sdk/js/examples/multi-framework-signals/src/app/pages/index.astro b/sdk/js/examples/multi-framework-signals/src/app/pages/index.astro index e36eacdf..c319cb98 100644 --- a/sdk/js/examples/multi-framework-signals/src/app/pages/index.astro +++ b/sdk/js/examples/multi-framework-signals/src/app/pages/index.astro @@ -46,7 +46,7 @@ const title = "Multi-framework app"; - + diff --git a/sdk/js/examples/multi-framework-signals/src/frontends/react/HelloWorld.tsx b/sdk/js/examples/multi-framework-signals/src/frontends/react/HelloWorld.tsx index 6751f77f..da729d87 100644 --- a/sdk/js/examples/multi-framework-signals/src/frontends/react/HelloWorld.tsx +++ b/sdk/js/examples/multi-framework-signals/src/frontends/react/HelloWorld.tsx @@ -1,10 +1,11 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; import { useShape } from "@ng-org/signals/react"; import flattenObject from "../utils/flattenObject"; import { TestObjectShapeType } from "../../shapes/orm/testShape.shapeTypes"; import { BasicShapeType } from "../../shapes/orm/basic.shapeTypes"; import type { ShapeType } from "@ng-org/shex-orm"; import type { Basic } from "../../shapes/orm/basic.typings"; +import { deepSignal, watch } from "@ng-org/alien-deepsignals"; const sparqlExampleData = ` PREFIX ex: @@ -72,10 +73,10 @@ INSERT DATA { export function HelloWorldReact() { const state = useShape(BasicShapeType); + const objects = [...(state || [])]; // @ts-expect-error window.reactState = state; - console.log("react state", state); if (!state) return
Loading...
; // Create a table from the state object: One column for keys, one for values, one with an input to change the value. @@ -97,7 +98,7 @@ export function HelloWorldReact() {
- {state.values()?.map((ormObj) => ( + {objects.map((ormObj) => ( @@ -155,7 +156,7 @@ export function HelloWorldReact() { value={value} onChange={(e) => { setNestedValue( - state, + ormObj, key, e.target.value ); @@ -168,7 +169,7 @@ export function HelloWorldReact() { value={value} onChange={(e) => { setNestedValue( - state, + ormObj, key, Number( e.target @@ -184,7 +185,7 @@ export function HelloWorldReact() { checked={value} onChange={(e) => { setNestedValue( - state, + ormObj, key, e.target.checked ); @@ -196,11 +197,11 @@ export function HelloWorldReact() { onClick={() => { const currentArray = getNestedValue( - state, + ormObj, key ); setNestedValue( - state, + ormObj, key, [ ...currentArray, @@ -216,7 +217,7 @@ export function HelloWorldReact() { onClick={() => { const currentArray = getNestedValue( - state, + ormObj, key ); if ( @@ -224,7 +225,7 @@ export function HelloWorldReact() { 0 ) { setNestedValue( - state, + ormObj, key, currentArray.slice( 0, @@ -243,7 +244,7 @@ export function HelloWorldReact() { onClick={() => { const currentSet = getNestedValue( - state, + ormObj, key ); currentSet.add( @@ -257,7 +258,7 @@ export function HelloWorldReact() { onClick={() => { const currentSet = getNestedValue( - state, + ormObj, key ); const lastItem = diff --git a/sdk/js/examples/multi-framework-signals/src/frontends/svelte/HelloWorld.svelte b/sdk/js/examples/multi-framework-signals/src/frontends/svelte/HelloWorld.svelte index 86721f41..2246b9ba 100644 --- a/sdk/js/examples/multi-framework-signals/src/frontends/svelte/HelloWorld.svelte +++ b/sdk/js/examples/multi-framework-signals/src/frontends/svelte/HelloWorld.svelte @@ -2,8 +2,9 @@ import { TestObjectShapeType } from "../../shapes/orm/testShape.shapeTypes"; import { useShape } from "@ng-org/signals/svelte"; import flattenObject from "../utils/flattenObject"; + import { BasicShapeType } from "../../shapes/orm/basic.shapeTypes"; - const shapeObject = useShape(TestObjectShapeType); + const shapeObjects = useShape(BasicShapeType); function getNestedValue(obj: any, path: string) { return path @@ -20,16 +21,16 @@ cur[keys[keys.length - 1]] = value; } const flattenedObjects = $derived( - $shapeObject - ? $shapeObject.values().map((o) => flattenObject(o)[0] || ({} as any)) + $shapeObjects + ? $shapeObjects.values().map((o) => flattenObject(o)[0] || ({} as any)) : [] ); $effect(() => { - (window as any).svelteState = $shapeObject; + (window as any).svelteState = $shapeObjects; }); -{#if $shapeObject} +{#if $shapeObjects}

Rendered in Svelte

@@ -61,28 +62,32 @@ type="text" {value} oninput={(e: any) => - setNestedValue($shapeObject, key, e.target.value)} + setNestedValue($shapeObjects, key, e.target.value)} /> {:else if typeof value === "number"} - setNestedValue($shapeObject, key, Number(e.target.value))} + setNestedValue( + $shapeObjects, + key, + Number(e.target.value) + )} /> {:else if typeof value === "boolean"} - setNestedValue($shapeObject, key, e.target.checked)} + setNestedValue($shapeObjects, key, e.target.checked)} /> {:else if Array.isArray(value)}
@@ -100,13 +105,19 @@