diff --git a/engine/net/src/app_protocol.rs b/engine/net/src/app_protocol.rs index 2def62a..4c6b26b 100644 --- a/engine/net/src/app_protocol.rs +++ b/engine/net/src/app_protocol.rs @@ -20,7 +20,7 @@ use ng_repo::utils::{decode_digest, decode_key, decode_sym_key}; use ng_repo::utils::{decode_overlayid, display_timestamp_local}; use serde_json::Value; -use crate::orm::{OrmDiff, OrmShapeType, OrmUpdateBlankNodeIds}; +use crate::orm::{OrmPatches, OrmShapeType, OrmUpdateBlankNodeIds}; use crate::types::*; #[derive(Clone, Debug, Serialize, Deserialize)] @@ -815,7 +815,7 @@ impl AppRequest { ) } - pub fn new_orm_update(scope: NuriV0, shape_type_name: String, diff: OrmDiff) -> Self { + pub fn new_orm_update(scope: NuriV0, shape_type_name: String, diff: OrmPatches) -> Self { AppRequest::new( AppRequestCommandV0::OrmUpdate, scope, @@ -1056,8 +1056,8 @@ pub enum AppRequestPayloadV0 { QrCodeProfile(u32), QrCodeProfileImport(String), OrmStart(OrmShapeType), - OrmUpdate((OrmDiff, String)), // ShapeID - OrmStop(String), //ShapeID + OrmUpdate((OrmPatches, String)), // ShapeID + OrmStop(String), //ShapeID } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -1308,7 +1308,7 @@ pub enum AppResponseV0 { Header(AppHeader), Commits(Vec), OrmInitial(Value), - OrmUpdate(OrmDiff), + OrmUpdate(OrmPatches), OrmUpdateBlankNodeIds(OrmUpdateBlankNodeIds), OrmError(String), } diff --git a/engine/net/src/orm.rs b/engine/net/src/orm.rs index d0d1fe5..d36b0f6 100644 --- a/engine/net/src/orm.rs +++ b/engine/net/src/orm.rs @@ -23,30 +23,30 @@ pub struct OrmShapeType { pub shape: String, } -/* == Diff Types == */ +/* == Patch Types == */ #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] #[allow(non_camel_case_types)] -pub enum OrmDiffOpType { +pub enum OrmPatchOp { add, remove, } #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] #[allow(non_camel_case_types)] -pub enum OrmDiffType { +pub enum OrmPatchType { set, object, } #[derive(Clone, Debug, Serialize, Deserialize)] -pub struct OrmDiffOp { - pub op: OrmDiffOpType, - pub valType: Option, +pub struct OrmPatch { + pub op: OrmPatchOp, + pub valType: Option, pub path: String, pub value: Option, // TODO: Improve type } -pub type OrmDiff = Vec; +pub type OrmPatches = Vec; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct OrmUpdateBlankNodeId { diff --git a/engine/verifier/src/orm/handle_backend_update.rs b/engine/verifier/src/orm/handle_backend_update.rs index 3d4497b..41760e2 100644 --- a/engine/verifier/src/orm/handle_backend_update.rs +++ b/engine/verifier/src/orm/handle_backend_update.rs @@ -11,7 +11,7 @@ use std::collections::HashMap; use std::u64; use futures::SinkExt; -pub use ng_net::orm::{OrmDiff, OrmShapeType}; +pub use ng_net::orm::{OrmPatches, OrmShapeType}; use ng_net::{app_protocol::*, orm::*}; use ng_repo::log::*; @@ -132,7 +132,7 @@ impl Verifier { ); // The JSON patches to send to JS land. - let mut patches: Vec = vec![]; + let mut patches: Vec = vec![]; // Keep track of objects to create: (path, Option) // The IRI is Some for real subjects, None for intermediate objects (e.g., multi-valued predicate containers) @@ -199,7 +199,7 @@ impl Verifier { &sub.tracked_subjects, &sub.shape_type.shape, &mut path, - (OrmDiffOpType::remove, Some(OrmDiffType::object), None, None), + (OrmPatchOp::remove, Some(OrmPatchType::object), None, None), &mut patches, &mut objects_to_create, ); @@ -255,17 +255,17 @@ impl Verifier { let json_pointer = format!("/{}", escaped_path.join("/")); // Always create the object itself - patches.push(OrmDiffOp { - op: OrmDiffOpType::add, - valType: Some(OrmDiffType::object), + patches.push(OrmPatch { + op: OrmPatchOp::add, + valType: Some(OrmPatchType::object), path: json_pointer.clone(), value: None, }); // If this object has an IRI (it's a real subject), add the id field if let Some(iri) = maybe_iri { - patches.push(OrmDiffOp { - op: OrmDiffOpType::add, + patches.push(OrmPatch { + op: OrmPatchOp::add, valType: None, path: format!("{}/@id", json_pointer), value: Some(json!(iri)), @@ -291,7 +291,7 @@ fn queue_patches_for_newly_valid_subject( tracked_subjects: &HashMap>>>, root_shape: &String, path: &[String], - patches: &mut Vec, + patches: &mut Vec, objects_to_create: &mut HashSet<(Vec, Option)>, ) { // Check if we're at a root subject or need to traverse to parents @@ -435,12 +435,12 @@ fn build_path_to_root_and_create_patches( root_shape: &String, path: &mut Vec, diff_op: ( - OrmDiffOpType, - Option, + OrmPatchOp, + Option, Option, // The value added / removed Option, // The IRI, if change is an added / removed object. ), - patches: &mut Vec, + patches: &mut Vec, objects_to_create: &mut HashSet<(Vec, Option)>, ) { log_debug!( @@ -465,7 +465,7 @@ fn build_path_to_root_and_create_patches( ); // Create the patch for the actual value change - patches.push(OrmDiffOp { + patches.push(OrmPatch { op: diff_op.0.clone(), valType: diff_op.1.clone(), path: json_pointer.clone(), @@ -515,8 +515,8 @@ fn build_path_to_root_and_create_patches( fn create_diff_ops_from_predicate_change( pred_change: &OrmTrackedPredicateChanges, ) -> Vec<( - OrmDiffOpType, - Option, + OrmPatchOp, + Option, Option, // The value added / removed Option, // The IRI, if change is an added / removed object. )> { @@ -537,7 +537,7 @@ fn create_diff_ops_from_predicate_change( // A value was added. Another one might have been removed // but the add patch overwrite previous values. return [( - OrmDiffOpType::add, + OrmPatchOp::add, None, Some(json!(pred_change.values_added[0])), None, @@ -545,21 +545,21 @@ fn create_diff_ops_from_predicate_change( .to_vec(); } else { // Since there is only one possible value, removing the path is enough. - return [(OrmDiffOpType::remove, None, None, None)].to_vec(); + return [(OrmPatchOp::remove, None, None, None)].to_vec(); } } else if is_multi && !is_object { if pred_change.values_added.len() > 0 { ops.push(( - OrmDiffOpType::add, - Some(OrmDiffType::set), + OrmPatchOp::add, + Some(OrmPatchType::set), Some(json!(pred_change.values_added)), None, )); } if pred_change.values_removed.len() > 0 { ops.push(( - OrmDiffOpType::remove, - Some(OrmDiffType::set), + OrmPatchOp::remove, + Some(OrmPatchType::set), Some(json!(pred_change.values_removed)), None, )); diff --git a/engine/verifier/src/orm/handle_frontend_update.rs b/engine/verifier/src/orm/handle_frontend_update.rs index fac4388..6c6ce4c 100644 --- a/engine/verifier/src/orm/handle_frontend_update.rs +++ b/engine/verifier/src/orm/handle_frontend_update.rs @@ -7,20 +7,20 @@ // notice may not be copied, modified, or distributed except // according to those terms. -use ng_net::orm::{OrmDiffOp, OrmDiffOpType, OrmDiffType, OrmSchemaPredicate, OrmSchemaShape}; +use ng_net::orm::{OrmPatch, OrmPatchOp, OrmPatchType, OrmSchemaPredicate, OrmSchemaShape}; use ng_oxigraph::oxrdf::Quad; use ng_repo::errors::VerifierError; use std::sync::{Arc, RwLock}; use std::u64; -use futures::SinkExt; use ng_net::app_protocol::*; -pub use ng_net::orm::{OrmDiff, OrmShapeType}; +pub use ng_net::orm::{OrmPatches, OrmShapeType}; use ng_repo::log::*; use crate::orm::types::*; use crate::orm::utils::{decode_json_pointer, json_to_sparql_val}; +use crate::types::GraphQuadsPatch; use crate::verifier::*; impl Verifier { @@ -34,25 +34,22 @@ impl Verifier { shape_iri: ShapeIri, session_id: u64, _skolemnized_blank_nodes: Vec, - _revert_inserts: Vec, - _revert_removes: Vec, + revert_inserts: Vec, + revert_removes: Vec, ) -> Result<(), VerifierError> { let (mut sender, _orm_subscription) = self.get_first_orm_subscription_sender_for(scope, Some(&shape_iri), Some(&session_id))?; - // TODO prepare OrmUpdateBlankNodeIds with skolemnized_blank_nodes - // use orm_subscription if needed - // note(niko): I think skolemnized blank nodes can still be many, in case of multi-level nested sub-objects. - let orm_bnids = vec![]; - let _ = sender - .send(AppResponse::V0(AppResponseV0::OrmUpdateBlankNodeIds( - orm_bnids, - ))) - .await; + // Revert changes, if there. + if revert_inserts.len() > 0 || revert_removes.len() > 0 { + let revert_changes = GraphQuadsPatch { + inserts: revert_removes, + removes: revert_inserts, + }; - // TODO (later) revert the inserts and removes - // let orm_diff = vec![]; - // let _ = sender.send(AppResponse::V0(AppResponseV0::OrmUpdate(orm_diff))).await; + // TODO: Call with correct params. + // self.orm_backend_update(session_id, scope, "", revert_changes) + } Ok(()) } @@ -63,7 +60,7 @@ impl Verifier { session_id: u64, scope: &NuriV0, shape_iri: ShapeIri, - diff: OrmDiff, + diff: OrmPatches, ) -> Result<(), String> { log_info!( "frontend_update_orm session={} shape={} diff={:?}", @@ -118,21 +115,21 @@ impl Verifier { fn create_sparql_update_query_for_diff( orm_subscription: &OrmSubscription, - diff: OrmDiff, + diff: OrmPatches, ) -> String { // First sort patches. // - Process delete patches first. // - Process object creation add operations before rest, to ensure potential blank nodes are created. let delete_patches: Vec<_> = diff .iter() - .filter(|patch| patch.op == OrmDiffOpType::remove) + .filter(|patch| patch.op == OrmPatchOp::remove) .collect(); let add_object_patches: Vec<_> = diff .iter() .filter(|patch| { - patch.op == OrmDiffOpType::add + patch.op == OrmPatchOp::add && match &patch.valType { - Some(vt) => *vt == OrmDiffType::object, + Some(vt) => *vt == OrmPatchType::object, _ => false, } }) @@ -140,9 +137,9 @@ fn create_sparql_update_query_for_diff( let add_literal_patches: Vec<_> = diff .iter() .filter(|patch| { - patch.op == OrmDiffOpType::add + patch.op == OrmPatchOp::add && match &patch.valType { - Some(vt) => *vt != OrmDiffType::object, + Some(vt) => *vt != OrmPatchType::object, _ => true, } }) @@ -289,7 +286,7 @@ fn find_pred_schema_by_name( /// - The Option subject, predicate, Option of the path's ending (to be used for DELETE) /// - The Option predicate schema of the tail of the target property. fn create_where_statements_for_patch( - patch: &OrmDiffOp, + patch: &OrmPatch, var_counter: &mut i32, orm_subscription: &OrmSubscription, ) -> ( @@ -334,6 +331,7 @@ fn create_where_statements_for_patch( let pred_name = path.remove(0); let pred_schema = find_pred_schema_by_name(&pred_name, ¤t_subj_schema); + // Case: We arrived at a leaf value. if path.len() == 0 { return ( where_statements, @@ -342,6 +340,8 @@ fn create_where_statements_for_patch( ); } + // Else, we have a nested object. + where_statements.push(format!( "{} <{}> ?o{}", subject_ref, pred_schema.iri, var_counter, @@ -369,7 +369,7 @@ fn create_where_statements_for_patch( } current_subj_schema = - get_first_valid_child_schema(&object_iri, &pred_schema, &orm_subscription); + get_first_child_schema(Some(&object_iri), &pred_schema, &orm_subscription); // Since we have new IRI that we can use as root, we replace the current one with it. subject_ref = format!("<{object_iri}>"); @@ -379,15 +379,18 @@ fn create_where_statements_for_patch( // Set to child subject schema. // TODO: Actually, we should get the tracked subject and check for the correct shape there. // As long as there is only one allowed shape or the first one is valid, this is fine. - current_subj_schema = get_first_child_schema(&pred_schema, &orm_subscription); + current_subj_schema = get_first_child_schema(None, &pred_schema, &orm_subscription); } } // Can't happen. panic!(); } -fn get_first_valid_child_schema( - subject_iri: &String, +/// Get the schema for a given subject and predicate schema. +/// It will return the first schema of which the tracked subject is valid. +/// If there is no subject found, return the first subject schema of the predicate schema. +fn get_first_child_schema( + subject_iri: Option<&String>, pred_schema: &OrmSchemaPredicate, orm_subscription: &OrmSubscription, ) -> Arc { @@ -396,18 +399,31 @@ fn get_first_valid_child_schema( continue; }; - let tracked_subject = orm_subscription - .tracked_subjects - .get(subject_iri) - .unwrap() - .get(schema_shape) - .unwrap(); - - if tracked_subject.read().unwrap().valid == OrmTrackedSubjectValidity::Valid { - return orm_subscription - .shape_type - .schema - .get(schema_shape) + let tracked_subject_opt = subject_iri + .and_then(|iri| orm_subscription.tracked_subjects.get(iri)) + .and_then(|ts_shapes| ts_shapes.get(schema_shape)); + + if let Some(tracked_subject) = tracked_subject_opt { + // The subject is already being tracked (it's not new). + if tracked_subject.read().unwrap().valid == OrmTrackedSubjectValidity::Valid { + return orm_subscription + .shape_type + .schema + .get(schema_shape) + .unwrap() + .clone(); + } + } else { + // New subject, we need to guess the schema, take the first one. + // TODO: We could do that by looking at a distinct property, e.g. @type which must be non-overlapping. + return pred_schema + .dataTypes + .iter() + .find_map(|dt| { + dt.shape + .as_ref() + .and_then(|shape_str| orm_subscription.shape_type.schema.get(shape_str)) + }) .unwrap() .clone(); } @@ -415,15 +431,3 @@ fn get_first_valid_child_schema( // TODO: Panicking might be too aggressive. panic!("No valid child schema found."); } - -fn get_first_child_schema( - pred_schema: &OrmSchemaPredicate, - orm_subscription: &OrmSubscription, -) -> Arc { - return orm_subscription - .shape_type - .schema - .get(pred_schema.dataTypes[0].shape.as_ref().unwrap()) - .unwrap() - .clone(); -} diff --git a/engine/verifier/src/orm/materialize.rs b/engine/verifier/src/orm/materialize.rs index 69d14da..5e158e4 100644 --- a/engine/verifier/src/orm/materialize.rs +++ b/engine/verifier/src/orm/materialize.rs @@ -9,7 +9,7 @@ use futures::SinkExt; use ng_net::orm::*; -pub use ng_net::orm::{OrmDiff, OrmShapeType}; +pub use ng_net::orm::{OrmPatches, OrmShapeType}; use ng_net::utils::Receiver; use serde_json::json; use serde_json::Value; @@ -141,7 +141,7 @@ pub(crate) fn materialize_orm_object( shape: &OrmSchemaShape, tracked_subjects: &HashMap>>>, ) -> Value { - let mut orm_obj = json!({"id": change.subject_iri}); + let mut orm_obj = json!({"@id": change.subject_iri}); let orm_obj_map = orm_obj.as_object_mut().unwrap(); for pred_schema in &shape.predicates { let property_name = &pred_schema.readablePredicate; diff --git a/engine/verifier/src/orm/mod.rs b/engine/verifier/src/orm/mod.rs index 1a2a842..f602fb3 100644 --- a/engine/verifier/src/orm/mod.rs +++ b/engine/verifier/src/orm/mod.rs @@ -17,13 +17,13 @@ pub mod shape_validation; pub mod types; pub mod utils; -pub use ng_net::orm::{OrmDiff, OrmShapeType}; +pub use ng_net::orm::{OrmPatches, OrmShapeType}; use crate::orm::types::*; use crate::verifier::*; impl Verifier { - pub(crate) fn clean_orm_subscriptions(&mut self) { + pub(crate) fn _clean_orm_subscriptions(&mut self) { self.orm_subscriptions.retain(|_, subscriptions| { subscriptions.retain(|sub| !sub.sender.is_closed()); !subscriptions.is_empty() diff --git a/engine/verifier/src/orm/process_changes.rs b/engine/verifier/src/orm/process_changes.rs index aa36b8b..eee4878 100644 --- a/engine/verifier/src/orm/process_changes.rs +++ b/engine/verifier/src/orm/process_changes.rs @@ -15,7 +15,7 @@ use std::collections::HashSet; use std::sync::Arc; use std::u64; -pub use ng_net::orm::{OrmDiff, OrmShapeType}; +pub use ng_net::orm::{OrmPatches, OrmShapeType}; use ng_net::{app_protocol::*, orm::*}; use ng_oxigraph::oxrdf::Triple; use ng_repo::errors::NgError; diff --git a/engine/verifier/src/orm/query.rs b/engine/verifier/src/orm/query.rs index 70d893d..ea228ce 100644 --- a/engine/verifier/src/orm/query.rs +++ b/engine/verifier/src/orm/query.rs @@ -11,7 +11,7 @@ use ng_repo::errors::VerifierError; use std::collections::HashSet; -pub use ng_net::orm::{OrmDiff, OrmShapeType}; +pub use ng_net::orm::{OrmPatches, OrmShapeType}; use crate::orm::types::*; use crate::orm::utils::{escape_literal, is_iri}; diff --git a/engine/verifier/src/orm/utils.rs b/engine/verifier/src/orm/utils.rs index f5e1ce8..5ce25d1 100644 --- a/engine/verifier/src/orm/utils.rs +++ b/engine/verifier/src/orm/utils.rs @@ -16,7 +16,7 @@ use std::collections::HashSet; use lazy_static::lazy_static; use regex::Regex; -pub use ng_net::orm::{OrmDiff, OrmShapeType}; +pub use ng_net::orm::{OrmPatches, OrmShapeType}; use ng_net::{app_protocol::*, orm::*}; use ng_oxigraph::oxrdf::Triple; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 946c30c..5d5f2c1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -877,6 +877,9 @@ importers: '@ng-org/alien-deepsignals': specifier: workspace:* version: link:../alien-deepsignals + '@ng-org/lib-wasm': + specifier: workspace:* + version: link:../lib-wasm/pkg '@ng-org/shex-orm': specifier: workspace:* version: link:../shex-orm 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 4457860..7022e12 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 @@ -6,9 +6,6 @@ import VueRoot from "../components/VueRoot.vue"; import ReactRoot from "../components/ReactRoot"; import SvelteRoot from "../components/SvelteRoot.svelte"; -// Hack to get mock backend started -//import { mockTestObject } from "../../ng-mock/wasm-land/shapeHandler"; - const title = "Multi-framework app"; --- diff --git a/sdk/js/lib-wasm/src/lib.rs b/sdk/js/lib-wasm/src/lib.rs index ab429c4..5cd0339 100644 --- a/sdk/js/lib-wasm/src/lib.rs +++ b/sdk/js/lib-wasm/src/lib.rs @@ -58,7 +58,7 @@ use ng_wallet::types::*; use ng_wallet::*; use nextgraph::local_broker::*; -use nextgraph::verifier::orm::{OrmDiff, OrmShapeType}; +use nextgraph::verifier::orm::{OrmPatches, OrmShapeType}; use nextgraph::verifier::CancelFn; use crate::model::*; diff --git a/sdk/js/signals/package.json b/sdk/js/signals/package.json index b7effb4..c7bce0d 100644 --- a/sdk/js/signals/package.json +++ b/sdk/js/signals/package.json @@ -62,6 +62,7 @@ "vue": "3.5.19" }, "devDependencies": { + "@ng-org/lib-wasm": "workspace:*", "@playwright/test": "^1.55.0", "@types/node": "24.3.0", "@types/react": "19.1.10", diff --git a/sdk/js/signals/src/connector/createSignalObjectForShape.ts b/sdk/js/signals/src/connector/createSignalObjectForShape.ts index 3a7e144..0665866 100644 --- a/sdk/js/signals/src/connector/createSignalObjectForShape.ts +++ b/sdk/js/signals/src/connector/createSignalObjectForShape.ts @@ -1,17 +1,10 @@ import type { Diff, Scope } from "../types.js"; import { applyDiff } from "./applyDiff.js"; -import ng from "@ng-org/lib-wasm"; - -import { - deepSignal, - watch, - batch, -} from "@ng-org/alien-deepsignals"; -import type { - DeepPatch, - DeepSignalObject, -} from "@ng-org/alien-deepsignals"; +import type * as NG from "@ng-org/lib-wasm"; + +import { deepSignal, watch, batch } from "@ng-org/alien-deepsignals"; +import type { DeepPatch, DeepSignalObject } from "@ng-org/alien-deepsignals"; import type { ShapeType, BaseType } from "@ng-org/shex-orm"; interface PoolEntry { diff --git a/sdk/rust/src/local_broker.rs b/sdk/rust/src/local_broker.rs index f1d2877..75f9f23 100644 --- a/sdk/rust/src/local_broker.rs +++ b/sdk/rust/src/local_broker.rs @@ -17,7 +17,7 @@ use async_std::sync::{Arc, Condvar, Mutex, RwLock}; use futures::channel::mpsc; use futures::{SinkExt, StreamExt}; use lazy_static::lazy_static; -use ng_net::orm::{OrmDiff, OrmShapeType}; +use ng_net::orm::{OrmPatches, OrmShapeType}; use ng_oxigraph::oxrdf::Triple; use once_cell::sync::Lazy; use pdf_writer::{Content, Finish, Name, Pdf, Rect, Ref, Str}; @@ -2767,7 +2767,7 @@ pub async fn orm_start( pub async fn orm_update( scope: NuriV0, shape_type_name: String, - diff: OrmDiff, + diff: OrmPatches, session_id: u64, ) -> Result<(), NgError> { let mut request = AppRequest::new_orm_update(scope, shape_type_name, diff); diff --git a/sdk/rust/src/tests/orm_apply_patches.rs b/sdk/rust/src/tests/orm_apply_patches.rs index 1392355..d08a379 100644 --- a/sdk/rust/src/tests/orm_apply_patches.rs +++ b/sdk/rust/src/tests/orm_apply_patches.rs @@ -13,7 +13,7 @@ use crate::tests::create_or_open_wallet::create_or_open_wallet; use async_std::stream::StreamExt; use ng_net::app_protocol::{AppResponse, AppResponseV0, NuriV0}; use ng_net::orm::{ - BasicType, OrmDiffOp, OrmDiffOpType, OrmDiffType, OrmSchemaDataType, OrmSchemaPredicate, + BasicType, OrmPatch, OrmPatchOp, OrmPatchType, OrmSchemaDataType, OrmSchemaPredicate, OrmSchemaShape, OrmSchemaValType, OrmShapeType, }; @@ -44,11 +44,17 @@ async fn test_orm_apply_patches() { // Test 5: Remove from multi-value literal array test_patch_remove_from_array(session_id).await; - // // Test 6: Nested object - modify nested literal + // Test 6: Nested object - modify nested literal test_patch_nested_literal(session_id).await; // Test 7: Multi-level nesting test_patch_multilevel_nested(session_id).await; + + // Test 8: Root object creation + test_patch_create_root_object(session_id).await; + + // Test 9: Nested object creation + test_patch_create_nested_object(session_id).await; } /// Test adding a single literal value via ORM patch @@ -122,8 +128,8 @@ INSERT DATA { } // Apply ORM patch: Add name - let diff = vec![OrmDiffOp { - op: OrmDiffOpType::add, + let diff = vec![OrmPatch { + op: OrmPatchOp::add, path: "urn:test:person1/name".to_string(), valType: None, value: Some(json!("Alice")), @@ -221,8 +227,8 @@ INSERT DATA { } // Apply ORM patch: Remove name - let diff = vec![OrmDiffOp { - op: OrmDiffOpType::remove, + let diff = vec![OrmPatch { + op: OrmPatchOp::remove, path: "urn:test:person2/name".to_string(), valType: None, value: Some(json!("Bob")), @@ -327,8 +333,8 @@ INSERT DATA { // valType: None, // value: Some(json!("Charlie")), // }, - OrmDiffOp { - op: OrmDiffOpType::add, + OrmPatch { + op: OrmPatchOp::add, path: "urn:test:person3/name".to_string(), valType: None, value: Some(json!("Charles")), @@ -434,9 +440,9 @@ INSERT DATA { } // Apply ORM patch: Add hobby - let diff = vec![OrmDiffOp { - op: OrmDiffOpType::add, - valType: Some(OrmDiffType::set), + let diff = vec![OrmPatch { + op: OrmPatchOp::add, + valType: Some(OrmPatchType::set), path: "urn:test:person4/hobby".to_string(), value: Some(json!("Swimming")), }]; @@ -535,8 +541,8 @@ INSERT DATA { } // Apply ORM patch: Remove hobby - let diff = vec![OrmDiffOp { - op: OrmDiffOpType::remove, + let diff = vec![OrmPatch { + op: OrmPatchOp::remove, path: "urn:test:person5/hobby".to_string(), valType: None, value: Some(json!("Swimming")), @@ -704,8 +710,8 @@ INSERT DATA { } // Apply ORM patch: Change city in nested address - let diff = vec![OrmDiffOp { - op: OrmDiffOpType::add, + let diff = vec![OrmPatch { + op: OrmPatchOp::add, path: "urn:test:person6/address/city".to_string(), valType: None, value: Some(json!("Shelbyville")), @@ -923,8 +929,8 @@ INSERT DATA { } // Apply ORM patch: Change street in company's headquarter address (3 levels deep) - let diff = vec![OrmDiffOp { - op: OrmDiffOpType::add, + let diff = vec![OrmPatch { + op: OrmPatchOp::add, path: "urn:test:person7/company/urn:test:company1/headquarter/street".to_string(), valType: None, value: Some(json!("Rich Street")), @@ -957,3 +963,304 @@ INSERT DATA { log_info!("✓ Test passed: Multi-level nested modification"); } + +/// Test multi-level nested object modifications via ORM patch +async fn test_patch_create_root_object(session_id: u64) { + log_info!("\n\n=== TEST: Creation of root object ===\n"); + + let doc_nuri = create_doc_with_data( + session_id, + r#" +PREFIX ex: +INSERT DATA { + a ex:Person ; + ex:name "Eve" . +} +"# + .to_string(), + ) + .await; + + let mut schema = HashMap::new(); + schema.insert( + "http://example.org/PersonShape".to_string(), + Arc::new(OrmSchemaShape { + iri: "http://example.org/PersonShape".to_string(), + predicates: vec![ + Arc::new(OrmSchemaPredicate { + iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".to_string(), + extra: Some(false), + maxCardinality: 1, + minCardinality: 1, + readablePredicate: "type".to_string(), + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaValType::literal, + literals: Some(vec![BasicType::Str( + "http://example.org/Person".to_string(), + )]), + shape: None, + }], + }), + Arc::new(OrmSchemaPredicate { + iri: "http://example.org/name".to_string(), + extra: Some(false), + readablePredicate: "name".to_string(), + minCardinality: 0, + maxCardinality: 1, + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaValType::string, + literals: None, + shape: None, + }], + }), + ], + }), + ); + + let shape_type = OrmShapeType { + shape: "http://example.org/PersonShape".to_string(), + schema, + }; + + let nuri = NuriV0::new_from(&doc_nuri).expect("parse nuri"); + let (mut receiver, _cancel_fn) = orm_start(nuri.clone(), shape_type.clone(), session_id) + .await + .expect("orm_start failed"); + + // Get initial state + while let Some(app_response) = receiver.next().await { + if let AppResponse::V0(AppResponseV0::OrmInitial(initial)) = app_response { + break; + } + } + + // Apply ORM patch: Create a new object + let diff = vec![ + OrmPatch { + op: OrmPatchOp::add, + path: "urn:test:person8".to_string(), + valType: Some(OrmPatchType::object), + value: None, + }, + OrmPatch { + // This does nothing as it does not represent a triple. + // A subject is created when inserting data. + op: OrmPatchOp::add, + path: "urn:test:person8/@id".to_string(), + valType: Some(OrmPatchType::object), + value: None, + }, + OrmPatch { + op: OrmPatchOp::add, + path: "urn:test:person8/type".to_string(), + valType: None, + value: Some(json!("http://example.org/Person")), + }, + OrmPatch { + op: OrmPatchOp::add, + path: "urn:test:person8/name".to_string(), + valType: None, + value: Some(json!("Alice")), + }, + ]; + + orm_update(nuri.clone(), shape_type.shape.clone(), diff, session_id) + .await + .expect("orm_update failed"); + + // Verify the change was applied + let triples = doc_sparql_construct( + session_id, + "CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }".to_string(), + Some(doc_nuri.clone()), + ) + .await + .expect("SPARQL query failed"); + + let has_name = triples.iter().any(|t| { + t.to_string() == "urn:test:person8" + && t.predicate.as_str() == "http://example.org/name" + && t.object.to_string().contains("Alice") + }); + let has_type = triples.iter().any(|t| { + t.predicate.as_str() == "http://www.w3.org/1999/02/22-rdf-syntax-ns#type" + && t.object.to_string().contains("http://example.org/Person") + }); + + assert!(!has_name, "New person has name"); + assert!(has_type, "New person has type"); + + log_info!("✓ Test passed: Creation of root object"); +} + +/// Test adding a second address object. +async fn test_patch_create_nested_object(session_id: u64) { + log_info!("\n\n=== TEST: Nested object creation ===\n"); + + let doc_nuri = create_doc_with_data( + session_id, + r#" +PREFIX ex: +INSERT DATA { + a ex:Person ; + ex:name "Dave" ; + ex:address . + + a ex:Address ; + ex:street "Main St" ; + ex:city "Springfield" . +} +"# + .to_string(), + ) + .await; + + let mut schema = HashMap::new(); + schema.insert( + "http://example.org/Person".to_string(), + Arc::new(OrmSchemaShape { + iri: "http://example.org/Person".to_string(), + predicates: vec![ + Arc::new(OrmSchemaPredicate { + iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".to_string(), + extra: Some(false), + maxCardinality: 1, + minCardinality: 1, + readablePredicate: "type".to_string(), + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaValType::literal, + literals: Some(vec![BasicType::Str( + "http://example.org/Person".to_string(), + )]), + shape: None, + }], + }), + Arc::new(OrmSchemaPredicate { + iri: "http://example.org/name".to_string(), + readablePredicate: "name".to_string(), + extra: Some(false), + minCardinality: 0, + maxCardinality: 1, + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaValType::string, + literals: None, + shape: None, + }], + }), + Arc::new(OrmSchemaPredicate { + iri: "http://example.org/address".to_string(), + readablePredicate: "address".to_string(), + extra: Some(false), + minCardinality: 0, + maxCardinality: 2, + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaValType::shape, + shape: Some("http://example.org/Address".to_string()), + literals: None, + }], + }), + ], + }), + ); + schema.insert( + "http://example.org/Address".to_string(), + Arc::new(OrmSchemaShape { + iri: "http://example.org/Address".to_string(), + predicates: vec![ + Arc::new(OrmSchemaPredicate { + iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".to_string(), + extra: Some(false), + maxCardinality: 1, + minCardinality: 1, + readablePredicate: "type".to_string(), + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaValType::literal, + literals: Some(vec![BasicType::Str( + "http://example.org/Address".to_string(), + )]), + shape: None, + }], + }), + Arc::new(OrmSchemaPredicate { + iri: "http://example.org/street".to_string(), + extra: Some(false), + readablePredicate: "street".to_string(), + minCardinality: 0, + maxCardinality: 1, + dataTypes: vec![OrmSchemaDataType { + valType: OrmSchemaValType::string, + literals: None, + shape: None, + }], + }), + ], + }), + ); + + let shape_type = OrmShapeType { + shape: "http://example.org/Person".to_string(), + schema, + }; + + let nuri = NuriV0::new_from(&doc_nuri).expect("parse nuri"); + let (mut receiver, _cancel_fn) = orm_start(nuri.clone(), shape_type.clone(), session_id) + .await + .expect("orm_start failed"); + + // Get initial state + while let Some(app_response) = receiver.next().await { + if let AppResponse::V0(AppResponseV0::OrmInitial(initial)) = app_response { + break; + } + } + + // Apply ORM patch: Add a second address. + let diff = vec![ + OrmPatch { + op: OrmPatchOp::add, + path: "urn:test:person9/address/http:~1~1example.org~1exampleAddress/type".to_string(), + valType: None, + value: Some(json!("http://example.org/Address")), + }, + OrmPatch { + op: OrmPatchOp::add, + path: "urn:test:person9/address/http:~1~1example.org~1exampleAddress/street" + .to_string(), + valType: None, + value: Some(json!("Heaven Avenue")), + }, + ]; + + orm_update(nuri.clone(), shape_type.shape.clone(), diff, session_id) + .await + .expect("orm_update failed"); + + // Verify the change was applied + let triples = doc_sparql_construct( + session_id, + "CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }".to_string(), + Some(doc_nuri.clone()), + ) + .await + .expect("SPARQL query failed"); + + let has_new_address_type = triples.iter().any(|t| { + t.subject + .to_string() + .contains("http://example.org/exampleAddress") + && t.predicate.as_str() == "http://www.w3.org/1999/02/22-rdf-syntax-ns#type" + && t.object.to_string().contains("http://example.org/Address") + }); + let has_new_address_street = triples.iter().any(|t| { + t.subject + .to_string() + .contains("http://example.org/exampleAddress") + && t.predicate.as_str() == "http://example.org/street" + && t.object.to_string().contains("Heaven Avenue") + }); + + assert!(has_new_address_type, "New address type should be added"); + assert!(has_new_address_street, "New street should be added"); + + log_info!("✓ Test passed: Nested object creation"); +}