diff --git a/engine/verifier/src/orm/handle_frontend_update.rs b/engine/verifier/src/orm/handle_frontend_update.rs index 6c6ce4c8..26a1ebcb 100644 --- a/engine/verifier/src/orm/handle_frontend_update.rs +++ b/engine/verifier/src/orm/handle_frontend_update.rs @@ -40,12 +40,15 @@ impl Verifier { let (mut sender, _orm_subscription) = self.get_first_orm_subscription_sender_for(scope, Some(&shape_iri), Some(&session_id))?; + log_info!("[orm_update_self] got subscription"); + // Revert changes, if there. if revert_inserts.len() > 0 || revert_removes.len() > 0 { let revert_changes = GraphQuadsPatch { 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) @@ -63,7 +66,7 @@ impl Verifier { diff: OrmPatches, ) -> Result<(), String> { log_info!( - "frontend_update_orm session={} shape={} diff={:?}", + "[orm_frontend_update] session={} shape={} diff={:?}", session_id, shape_iri, diff @@ -74,12 +77,17 @@ impl Verifier { self.get_first_orm_subscription_for(scope, Some(&shape_iri), Some(&session_id)); let doc_nuri = orm_subscription.nuri.clone(); + log_info!("[orm_frontend_update] got subscription"); + let sparql_update = create_sparql_update_query_for_diff(orm_subscription, diff); + log_info!( + "[orm_frontend_update] created sparql_update query:\n{}", + sparql_update + ); (doc_nuri, sparql_update) }; - log_debug!("Created SPARQL query for patches:\n{}", sparql_update); match self .process_sparql_update( &doc_nuri, @@ -90,8 +98,17 @@ impl Verifier { ) .await { - Err(e) => Err(e), + Err(e) => { + log_info!("[orm_frontend_update] query failed"); + + Err(e) + } Ok((_, revert_inserts, revert_removes, skolemnized_blank_nodes)) => { + log_info!( + "[orm_frontend_update] query successful. Reverts? {}", + revert_inserts.len() + ); + if !revert_inserts.is_empty() || !revert_removes.is_empty() || !skolemnized_blank_nodes.is_empty() @@ -117,6 +134,11 @@ fn create_sparql_update_query_for_diff( orm_subscription: &OrmSubscription, diff: OrmPatches, ) -> String { + log_info!( + "[create_sparql_update_query_for_diff] Starting with {} patches", + diff.len() + ); + // First sort patches. // - Process delete patches first. // - Process object creation add operations before rest, to ensure potential blank nodes are created. @@ -124,6 +146,11 @@ fn create_sparql_update_query_for_diff( .iter() .filter(|patch| patch.op == OrmPatchOp::remove) .collect(); + log_info!( + "[create_sparql_update_query_for_diff] Found {} delete patches", + delete_patches.len() + ); + let add_object_patches: Vec<_> = diff .iter() .filter(|patch| { @@ -134,7 +161,12 @@ fn create_sparql_update_query_for_diff( } }) .collect(); - let add_literal_patches: Vec<_> = diff + log_info!( + "[create_sparql_update_query_for_diff] Found {} add object patches", + add_object_patches.len() + ); + + let add_primitive_patches: Vec<_> = diff .iter() .filter(|patch| { patch.op == OrmPatchOp::add @@ -144,19 +176,33 @@ fn create_sparql_update_query_for_diff( } }) .collect(); + log_info!( + "[create_sparql_update_query_for_diff] Found {} add primitive patches", + add_primitive_patches.len() + ); // For each diff op, we create a separate INSERT or DELETE block. let mut sparql_sub_queries: Vec = vec![]; // Create delete statements. // - for del_patch in delete_patches.iter() { + for (idx, del_patch) in delete_patches.iter().enumerate() { + log_info!( + "[create_sparql_update_query_for_diff] Processing delete patch {}/{}: path={}", + idx + 1, + delete_patches.len(), + del_patch.path + ); + let mut var_counter: i32 = 0; let (where_statements, target, _pred_schema) = create_where_statements_for_patch(&del_patch, &mut var_counter, &orm_subscription); let (subject_var, target_predicate, target_object) = target; + log_info!("[create_sparql_update_query_for_diff] Delete patch where_statements: {:?}, subject_var={}, target_predicate={}, target_object={:?}", + where_statements, subject_var, target_predicate, target_object); + let delete_statement; if let Some(target_object) = target_object { // Delete the link to exactly one object (IRI referenced in path, i.e. target_object) @@ -183,29 +229,47 @@ fn create_sparql_update_query_for_diff( delete_statement, where_statements.join(" .\n ") )); + log_info!( + "[create_sparql_update_query_for_diff] Added delete query #{}", + sparql_sub_queries.len() + ); } // Process add object patches (might need blank nodes) // - for _add_obj_patch in add_object_patches { + 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 literal add patches + // Process primitive add patches // - for add_patch in add_literal_patches { + for (idx, add_patch) in add_primitive_patches.iter().enumerate() { + log_info!( + "[create_sparql_update_query_for_diff] Processing add primitive patch {}/{}: path={}", + idx + 1, + add_primitive_patches.len(), + add_patch.path + ); + 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; + log_info!("[create_sparql_update_query_for_diff] Add patch where_statements: {:?}, subject_var={}, target_predicate={}, target_object={:?}", + where_statements, subject_var, target_predicate, target_object); + if let Some(_target_object) = target_object { // Reference to exactly one object found. This is invalid when inserting literals. + log_info!("[create_sparql_update_query_for_diff] SKIPPING: target_object found for literal add (invalid)"); // TODO: Return error? continue; } else { @@ -215,6 +279,7 @@ fn create_sparql_update_query_for_diff( Some(val) => json_to_sparql_val(&val), // Can be one or more (joined with ", "). None => { // A value must be set. This patch is invalid. + log_info!("[create_sparql_update_query_for_diff] SKIPPING: No value in add patch (invalid)"); // TODO: Return error? continue; } @@ -225,6 +290,7 @@ fn create_sparql_update_query_for_diff( // If the schema only has max one value, // then `add` can also overwrite values, so we need to delete the previous one if !pred_schema.unwrap().is_multi() { + log_info!("[create_sparql_update_query_for_diff] Single-value predicate, adding DELETE before INSERT"); let remove_statement = format!(" {} <{}> ?o{}", subject_var, target_predicate, var_counter); @@ -236,6 +302,10 @@ fn create_sparql_update_query_for_diff( remove_statement, wheres.join(" .\n ") )); + log_info!( + "[create_sparql_update_query_for_diff] Added delete query #{}", + sparql_sub_queries.len() + ); // var_counter += 1; // Not necessary because not used afterwards. } // The actual INSERT. @@ -245,9 +315,17 @@ fn create_sparql_update_query_for_diff( add_statement, where_statements.join(". \n ") )); + log_info!( + "[create_sparql_update_query_for_diff] Added insert query #{}", + sparql_sub_queries.len() + ); } } + log_info!( + "[create_sparql_update_query_for_diff] Finished. Generated {} sub-queries", + sparql_sub_queries.len() + ); return sparql_sub_queries.join(";\n"); } @@ -294,6 +372,12 @@ fn create_where_statements_for_patch( (String, String, Option), Option>, ) { + log_info!( + "[create_where_statements_for_patch] Starting. patch.path={}, patch.op={:?}", + patch.path, + patch.op + ); + let mut body_statements: Vec = vec![]; let mut where_statements: Vec = vec![]; @@ -303,9 +387,20 @@ fn create_where_statements_for_patch( .map(|s| decode_json_pointer(&s.to_string())) .collect(); + log_info!( + "[create_where_statements_for_patch] Decoded path into {} segments: {:?}", + path.len(), + path + ); + path.remove(0); + // Handle special case: The whole object is deleted. if path.len() == 1 { let root_iri = &path[0]; + log_info!( + "[create_where_statements_for_patch] Special case: whole object deletion for root_iri={}", + root_iri + ); body_statements.push(format!("<{}> ?p ?o", root_iri)); where_statements.push(format!("<{}> ?p ?o", root_iri)); return ( @@ -315,24 +410,56 @@ fn create_where_statements_for_patch( ); } + log_info!( + "[create_where_statements_for_patch] Getting root schema for shape={}", + orm_subscription.shape_type.shape + ); let subj_schema: &Arc = orm_subscription .shape_type .schema .get(&orm_subscription.shape_type.shape) .unwrap(); + log_info!("[create_where_statements_for_patch] Root schema found"); let mut current_subj_schema: Arc = subj_schema.clone(); // The root IRI might change, if the parent path segment was an IRI. let root_iri = path.remove(0); let mut subject_ref = format!("<{}>", root_iri); + log_info!( + "[create_where_statements_for_patch] Starting traversal from root_iri={}, remaining path segments={}", + root_iri, + path.len() + ); while path.len() > 0 { let pred_name = path.remove(0); + log_info!( + "[create_where_statements_for_patch] Processing path segment: pred_name={}, remaining={}", + pred_name, + 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 + ); let pred_schema = find_pred_schema_by_name(&pred_name, ¤t_subj_schema); + log_info!( + "[create_where_statements_for_patch] Found predicate schema: iri={}, is_object={}, is_multi={}", + pred_schema.iri, + pred_schema.is_object(), + pred_schema.is_multi() + ); // Case: We arrived at a leaf value. if path.len() == 0 { + log_info!( + "[create_where_statements_for_patch] Reached leaf value. Returning target: subject_ref={}, predicate={}", + subject_ref, + pred_schema.iri + ); return ( where_statements, (subject_ref, pred_schema.iri.clone(), None), @@ -346,6 +473,12 @@ fn create_where_statements_for_patch( "{} <{}> ?o{}", subject_ref, pred_schema.iri, var_counter, )); + log_info!( + "[create_where_statements_for_patch] Added where statement for nested object: {} <{}> ?o{}", + subject_ref, + pred_schema.iri, + var_counter + ); // Update the subject_ref for traversal (e.g. ?o1 . ?o1 Cat); subject_ref = format!("?o{}", var_counter); @@ -358,9 +491,19 @@ fn create_where_statements_for_patch( ); } if pred_schema.is_multi() { + log_info!("[create_where_statements_for_patch] Predicate is multi-valued, expecting object IRI in path"); let object_iri = path.remove(0); + log_info!( + "[create_where_statements_for_patch] Got object_iri={}, remaining path={}", + object_iri, + path.len() + ); // Path ends on an object IRI, which we return here as well. if path.len() == 0 { + log_info!( + "[create_where_statements_for_patch] Path ends on object IRI. Returning target with object={}", + object_iri + ); return ( where_statements, (subject_ref, pred_schema.iri.clone(), Some(object_iri)), @@ -368,21 +511,36 @@ 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 + ); current_subj_schema = get_first_child_schema(Some(&object_iri), &pred_schema, &orm_subscription); + log_info!("[create_where_statements_for_patch] Child schema found"); // Since we have new IRI that we can use as root, we replace the current one with it. subject_ref = format!("<{object_iri}>"); // And can clear all, now unnecessary where statements. where_statements.clear(); + log_info!( + "[create_where_statements_for_patch] Reset subject_ref to <{}> and cleared where statements", + object_iri + ); } else { // 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. + 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"); } } // Can't happen. + log_err!("[create_where_statements_for_patch] PANIC: Reached end of function unexpectedly (should be impossible)"); panic!(); } diff --git a/engine/verifier/src/orm/query.rs b/engine/verifier/src/orm/query.rs index 3cbc9b15..ea228ce0 100644 --- a/engine/verifier/src/orm/query.rs +++ b/engine/verifier/src/orm/query.rs @@ -35,15 +35,14 @@ impl Verifier { // &update.overlay_id, // ); //let base = NuriV0::repo_id(&repo.id); - let binding = nuri.unwrap(); - let nuri = binding.split_at(53).0; - log_info!("querying construct\n{}\n{}\n", nuri, query); + let nuri_str = nuri.as_ref().map(|s| s.as_str()); + log_debug!("querying construct\n{}\n{}\n", nuri_str.unwrap(), query); - let parsed = Query::parse(&query, Some(nuri.clone())) - .map_err(|e| NgError::OxiGraphError(e.to_string()))?; + let parsed = + Query::parse(&query, nuri_str).map_err(|e| NgError::OxiGraphError(e.to_string()))?; let results = oxistore - .query(parsed, Some(nuri.to_string())) + .query(parsed, nuri) .map_err(|e| NgError::OxiGraphError(e.to_string()))?; match results { QueryResults::Graph(triples) => { @@ -51,8 +50,7 @@ impl Verifier { for t in triples { match t { Err(e) => { - log_info!("Error: {:?}n", e); - + log_err!("{}", e.to_string()); return Err(NgError::SparqlError(e.to_string())); } Ok(triple) => { diff --git a/engine/verifier/src/request_processor.rs b/engine/verifier/src/request_processor.rs index ab249524..b10db4cf 100644 --- a/engine/verifier/src/request_processor.rs +++ b/engine/verifier/src/request_processor.rs @@ -15,11 +15,9 @@ use std::sync::Arc; use futures::channel::mpsc; use futures::SinkExt; use futures::StreamExt; -use ng_net::actor::SoS; use ng_net::types::InboxPost; use ng_net::types::NgQRCode; use ng_net::types::NgQRCodeProfileSharingV0; -use ng_oxigraph::oxigraph::sparql::EvaluationError; use ng_oxigraph::oxigraph::sparql::{results::*, Query, QueryResults}; use ng_oxigraph::oxrdf::{Literal, NamedNode, Quad, Term}; use ng_oxigraph::oxsdatatypes::DateTime; diff --git a/sdk/js/alien-deepsignals/src/deepSignal.ts b/sdk/js/alien-deepsignals/src/deepSignal.ts index b37f99d3..f1f08b20 100644 --- a/sdk/js/alien-deepsignals/src/deepSignal.ts +++ b/sdk/js/alien-deepsignals/src/deepSignal.ts @@ -11,6 +11,16 @@ import { computed, signal, isSignal } from "./core"; /** A batched deep mutation (set/add/remove) from a deepSignal root. */ export type DeepPatch = { + /** Property path (array indices, object keys, synthetic Set entry ids) from the root to the mutated location. */ + path: (string | number)[]; +} & ( + | DeepSetAddPatch + | DeepSetRemovePatch + | DeepObjectAddPatch + | DeepRemovePatch + | DeepLiteralAddPatch +); +export type DeepPatchInternal = { /** Unique identifier for the deep signal root which produced this patch. */ root: symbol; /** Property path (array indices, object keys, synthetic Set entry ids) from the root to the mutated location. */ @@ -22,6 +32,7 @@ export type DeepPatch = { | DeepRemovePatch | DeepLiteralAddPatch ); + export interface DeepSetAddPatch { /** Mutation kind applied at the resolved `path`. */ op: "add"; @@ -105,7 +116,7 @@ function buildPath( return path; } -function queuePatch(patch: DeepPatch) { +function queuePatch(patch: DeepPatchInternal) { if (!pendingPatches) pendingPatches = new Map(); const root = patch.root; let list = pendingPatches.get(root); @@ -113,6 +124,9 @@ function queuePatch(patch: DeepPatch) { list = []; pendingPatches.set(root, list); } + // Remove root, we do not send that back. + // @ts-ignore + delete patch.root; list.push(patch); if (!microtaskScheduled) { microtaskScheduled = true; @@ -124,7 +138,7 @@ function queuePatch(patch: DeepPatch) { for (const [rootId, patches] of groups) { if (!patches.length) continue; const subs = mutationSubscribers.get(rootId); - if (subs) subs.forEach((cb) => cb(patches)); + if (subs) subs.forEach((callback) => callback(patches)); } }); } diff --git a/sdk/js/alien-deepsignals/src/test/deepSignalOptions.test.ts b/sdk/js/alien-deepsignals/src/test/deepSignalOptions.test.ts index 5b3f7c76..d9caa86c 100644 --- a/sdk/js/alien-deepsignals/src/test/deepSignalOptions.test.ts +++ b/sdk/js/alien-deepsignals/src/test/deepSignalOptions.test.ts @@ -1,5 +1,9 @@ import { describe, it, expect } from "vitest"; -import { deepSignal, DeepPatch, DeepSignalOptions } from "../deepSignal"; +import { + deepSignal, + DeepPatchInternal, + DeepSignalOptions, +} from "../deepSignal"; import { watch } from "../watch"; describe("deepSignal options", () => { @@ -12,7 +16,7 @@ describe("deepSignal options", () => { }; const state = deepSignal({ data: {} as any }, options); - const patches: DeepPatch[][] = []; + const patches: DeepPatchInternal[][] = []; const { stopListening: stop } = watch(state, ({ patches: batch }) => patches.push(batch) ); @@ -51,7 +55,7 @@ describe("deepSignal options", () => { }; const state = deepSignal({ s: new Set() }, options); - const patches: DeepPatch[][] = []; + const patches: DeepPatchInternal[][] = []; const { stopListening: stop } = watch(state, ({ patches: batch }) => patches.push(batch) ); @@ -76,7 +80,7 @@ describe("deepSignal options", () => { }; const state = deepSignal({ root: {} as any }, options); - const patches: DeepPatch[][] = []; + const patches: DeepPatchInternal[][] = []; const { stopListening: stop } = watch(state, ({ patches: batch }) => patches.push(batch) ); @@ -119,7 +123,7 @@ describe("deepSignal options", () => { }; const state = deepSignal({ items: [] as any[] }, options); - const patches: DeepPatch[][] = []; + const patches: DeepPatchInternal[][] = []; const { stopListening: stop } = watch(state, ({ patches: batch }) => patches.push(batch) ); @@ -148,7 +152,7 @@ describe("deepSignal options", () => { }; const state = deepSignal({ s: new Set() }, options); - const patches: DeepPatch[][] = []; + const patches: DeepPatchInternal[][] = []; const { stopListening: stop } = watch(state, ({ patches: batch }) => patches.push(batch) ); @@ -210,7 +214,7 @@ describe("deepSignal options", () => { }; const state = deepSignal({ container: {} as any }, options); - const patches: DeepPatch[][] = []; + const patches: DeepPatchInternal[][] = []; const { stopListening: stop } = watch(state, ({ patches: batch }) => patches.push(batch) ); @@ -283,7 +287,7 @@ describe("deepSignal options", () => { describe("backward compatibility", () => { it("still works without options", async () => { const state = deepSignal({ data: { value: 1 } }); - const patches: DeepPatch[][] = []; + const patches: DeepPatchInternal[][] = []; const { stopListening: stop } = watch(state, ({ patches: batch }) => patches.push(batch) ); @@ -298,7 +302,7 @@ describe("deepSignal options", () => { // TODO: Delete duplicate logic for `id`. Only accept @id. it("objects with id property still work for Sets", async () => { const state = deepSignal({ s: new Set() }); - const patches: DeepPatch[][] = []; + const patches: DeepPatchInternal[][] = []; const { stopListening: stop } = watch(state, ({ patches: batch }) => patches.push(batch) ); @@ -315,7 +319,7 @@ describe("deepSignal options", () => { it("@id takes precedence over id property", async () => { const state = deepSignal({ s: new Set() }); - const patches: DeepPatch[][] = []; + const patches: DeepPatchInternal[][] = []; const { stopListening: stop } = watch(state, ({ patches: batch }) => patches.push(batch) ); diff --git a/sdk/js/alien-deepsignals/src/test/watchPatches.test.ts b/sdk/js/alien-deepsignals/src/test/watchPatches.test.ts index 26583406..efa8a86d 100644 --- a/sdk/js/alien-deepsignals/src/test/watchPatches.test.ts +++ b/sdk/js/alien-deepsignals/src/test/watchPatches.test.ts @@ -3,14 +3,14 @@ import { deepSignal, setSetEntrySyntheticId, addWithId, - DeepPatch, + DeepPatchInternal, } from "../deepSignal"; import { watch, observe } from "../watch"; describe("watch (patch mode)", () => { it("emits set patches with correct paths and batching", async () => { const state = deepSignal({ a: { b: 1 }, arr: [1, { x: 2 }] }); - const received: DeepPatch[][] = []; + const received: DeepPatchInternal[][] = []; const { stopListening: stop } = watch(state, ({ patches }) => { received.push(patches); }); @@ -34,7 +34,7 @@ describe("watch (patch mode)", () => { a: { b: 1 }, c: 2, }); - const out: DeepPatch[][] = []; + const out: DeepPatchInternal[][] = []; const { stopListening: stop } = watch(state, ({ patches }) => out.push(patches) ); @@ -52,8 +52,8 @@ describe("watch (patch mode)", () => { it("observe patch mode mirrors watch patch mode", async () => { const state = deepSignal({ a: 1 }); - const wp: DeepPatch[][] = []; - const ob: DeepPatch[][] = []; + const wp: DeepPatchInternal[][] = []; + const ob: DeepPatchInternal[][] = []; const { stopListening: stop1 } = watch(state, ({ patches }) => wp.push(patches) ); @@ -72,7 +72,7 @@ describe("watch (patch mode)", () => { it("filters out patches from other roots", async () => { const a = deepSignal({ x: 1 }); const b = deepSignal({ y: 2 }); - const out: DeepPatch[][] = []; + const out: DeepPatchInternal[][] = []; const { stopListening: stop } = watch(a, ({ patches }) => out.push(patches) ); @@ -86,7 +86,7 @@ describe("watch (patch mode)", () => { it("emits patches for Set structural mutations (add/delete)", async () => { const state = deepSignal<{ s: Set }>({ s: new Set([1, 2]) }); - const batches: DeepPatch[][] = []; + const batches: DeepPatchInternal[][] = []; const { stopListening: stop } = watch(state, ({ patches }) => batches.push(patches) ); @@ -110,7 +110,7 @@ describe("watch (patch mode)", () => { it("emits patches for nested objects added after initialization", async () => { const state = deepSignal<{ root: any }>({ root: {} }); - const patches: DeepPatch[][] = []; + const patches: DeepPatchInternal[][] = []; const { stopListening: stop } = watch(state, ({ patches: batch }) => patches.push(batch) ); @@ -124,7 +124,7 @@ describe("watch (patch mode)", () => { it("emits patches for deeply nested arrays and objects", async () => { const state = deepSignal<{ data: any }>({ data: null }); - const patches: DeepPatch[][] = []; + const patches: DeepPatchInternal[][] = []; const { stopListening: stop } = watch(state, ({ patches: batch }) => patches.push(batch) ); @@ -161,7 +161,7 @@ describe("watch (patch mode)", () => { it("emits patches for Set with nested objects added as one operation", async () => { const state = deepSignal<{ container: any }>({ container: {} }); - const patches: DeepPatch[][] = []; + const patches: DeepPatchInternal[][] = []; const { stopListening: stop } = watch(state, ({ patches: batch }) => patches.push(batch) ); @@ -188,7 +188,7 @@ describe("watch (patch mode)", () => { const innerA = new Set([{ id: "node1", x: 1 }]); const s = new Set([innerA]); const state = deepSignal<{ graph: Set }>({ graph: s }); - const batches: DeepPatch[][] = []; + const batches: DeepPatchInternal[][] = []; const { stopListening: stop } = watch(state, ({ patches }) => batches.push(patches) ); @@ -204,7 +204,7 @@ describe("watch (patch mode)", () => { it("tracks deep nested object mutation inside a Set entry after iteration", async () => { const rawEntry = { id: "n1", data: { val: 1 } }; const st = deepSignal({ bag: new Set([rawEntry]) }); - const collected: DeepPatch[][] = []; + const collected: DeepPatchInternal[][] = []; const { stopListening: stop } = watch(st, ({ patches }) => collected.push(patches) ); @@ -215,7 +215,9 @@ describe("watch (patch mode)", () => { } proxied.data.val = 2; await Promise.resolve(); - const flat = collected.flat().map((p: DeepPatch) => p.path.join(".")); + const flat = collected + .flat() + .map((p: DeepPatchInternal) => p.path.join(".")); expect(flat.some((p: string) => p.endsWith("n1.data.val"))).toBe(true); stop(); }); @@ -223,13 +225,15 @@ describe("watch (patch mode)", () => { it("allows custom synthetic id for Set entry", async () => { const node = { name: "x" }; const state = deepSignal({ s: new Set() }); - const collected2: DeepPatch[][] = []; + const collected2: DeepPatchInternal[][] = []; const { stopListening: stop } = watch(state, ({ patches }) => collected2.push(patches) ); addWithId(state.s as any, node, "custom123"); await Promise.resolve(); - const flat = collected2.flat().map((p: DeepPatch) => p.path.join(".")); + const flat = collected2 + .flat() + .map((p: DeepPatchInternal) => p.path.join(".")); expect(flat.some((p: string) => p === "s.custom123")).toBe(true); stop(); }); @@ -237,7 +241,7 @@ describe("watch (patch mode)", () => { describe("Set", () => { it("emits patches for primitive adds", async () => { const st = deepSignal({ s: new Set() }); - const batches: DeepPatch[][] = []; + const batches: DeepPatchInternal[][] = []; const { stopListening: stop } = watch(st, ({ patches }) => batches.push(patches) ); @@ -266,7 +270,7 @@ describe("watch (patch mode)", () => { }); it("emits patches for primitive deletes", async () => { const st = deepSignal({ s: new Set([true, 2, "3"]) }); - const batches: DeepPatch[][] = []; + const batches: DeepPatchInternal[][] = []; const { stopListening: stop } = watch(st, ({ patches }) => batches.push(patches) ); @@ -293,7 +297,7 @@ describe("watch (patch mode)", () => { }); it("does not emit patches for non-existent primitives", async () => { const st = deepSignal({ s: new Set([1, 2]) }); - const batches: DeepPatch[][] = []; + const batches: DeepPatchInternal[][] = []; const { stopListening: stop } = watch(st, ({ patches }) => batches.push(patches) ); @@ -306,7 +310,7 @@ describe("watch (patch mode)", () => { }); it("does not emit patches for already added primitive", async () => { const st = deepSignal({ s: new Set([1, "test", true]) }); - const batches: DeepPatch[][] = []; + const batches: DeepPatchInternal[][] = []; const { stopListening: stop } = watch(st, ({ patches }) => batches.push(patches) ); @@ -322,7 +326,7 @@ describe("watch (patch mode)", () => { const st = deepSignal({ s: new Set() }); addWithId(st.s as any, { id: "a", x: 1 }, "a"); addWithId(st.s as any, { id: "b", x: 2 }, "b"); - const batches: DeepPatch[][] = []; + const batches: DeepPatchInternal[][] = []; const { stopListening: stop } = watch(st, ({ patches }) => batches.push(patches) ); @@ -340,7 +344,7 @@ describe("watch (patch mode)", () => { it("emits delete patch for object entry", async () => { const st = deepSignal({ s: new Set() }); const obj = { id: "n1", x: 1 }; - const patches: DeepPatch[][] = []; + const patches: DeepPatchInternal[][] = []; const { stopListening: stop } = watch(st, ({ patches: batch }) => patches.push(batch) ); @@ -356,7 +360,7 @@ describe("watch (patch mode)", () => { }); it("does not emit patch for duplicate add", async () => { const st = deepSignal({ s: new Set([1]) }); - const patches: DeepPatch[][] = []; + const patches: DeepPatchInternal[][] = []; const { stopListening: stop } = watch(st, ({ patches: batch }) => patches.push(batch) ); @@ -367,7 +371,7 @@ describe("watch (patch mode)", () => { }); it("does not emit patch deleting non-existent entry", async () => { const st = deepSignal({ s: new Set([1]) }); - const patches: DeepPatch[][] = []; + const patches: DeepPatchInternal[][] = []; const { stopListening: stop } = watch(st, ({ patches: batch }) => patches.push(batch) ); @@ -378,7 +382,7 @@ describe("watch (patch mode)", () => { }); it("addWithId primitive returns primitive and emits patch with primitive key", async () => { const st = deepSignal({ s: new Set() }); - const patches: DeepPatch[][] = []; + const patches: DeepPatchInternal[][] = []; const { stopListening: stop } = watch(st, ({ patches: batch }) => patches.push(batch) ); @@ -396,7 +400,7 @@ describe("watch (patch mode)", () => { const st = deepSignal({ s: new Set() }); const obj = { name: "x" }; setSetEntrySyntheticId(obj, "customX"); - const patches: DeepPatch[][] = []; + const patches: DeepPatchInternal[][] = []; const { stopListening: stop } = watch(st, ({ patches: batch }) => patches.push(batch) ); @@ -413,7 +417,7 @@ describe("watch (patch mode)", () => { { id: "e1", inner: { v: 1 } }, "e1" ); - const batches: DeepPatch[][] = []; + const batches: DeepPatchInternal[][] = []; const { stopListening: stop } = watch(st, ({ patches }) => batches.push(patches) ); @@ -429,7 +433,7 @@ describe("watch (patch mode)", () => { it("raw reference mutation produces no deep patch while proxied does", async () => { const raw = { id: "id1", data: { x: 1 } }; const st = deepSignal({ s: new Set([raw]) }); - const batches: DeepPatch[][] = []; + const batches: DeepPatchInternal[][] = []; const { stopListening: stop } = watch(st, ({ patches }) => batches.push(patches) ); @@ -451,7 +455,7 @@ describe("watch (patch mode)", () => { const st = deepSignal({ s: new Set() }); const a1 = { id: "dup", v: 1 }; const a2 = { id: "dup", v: 2 }; - const patches: DeepPatch[][] = []; + const patches: DeepPatchInternal[][] = []; const { stopListening: stop } = watch(st, ({ patches: batch }) => patches.push(batch) ); @@ -483,7 +487,7 @@ describe("watch (patch mode)", () => { expect(arr[0].inner.v).toBe(1); const spread = [...st.s]; expect(spread[0].inner.v).toBe(1); - const batches: DeepPatch[][] = []; + const batches: DeepPatchInternal[][] = []; const { stopListening: stop } = watch(st, ({ patches }) => batches.push(patches) ); @@ -498,7 +502,7 @@ describe("watch (patch mode)", () => { describe("Arrays & mixed batch", () => { it("emits patches for splice/unshift/shift in single batch", async () => { const st = deepSignal({ arr: [1, 2, 3] }); - const batches: DeepPatch[][] = []; + const batches: DeepPatchInternal[][] = []; const { stopListening: stop } = watch(st, ({ patches }) => batches.push(patches) ); @@ -512,7 +516,7 @@ describe("watch (patch mode)", () => { }); it("mixed object/array/Set mutations batch together", async () => { const st = deepSignal({ o: { a: 1 }, arr: [1], s: new Set() }); - const batches: DeepPatch[][] = []; + const batches: DeepPatchInternal[][] = []; const { stopListening: stop } = watch(st, ({ patches }) => batches.push(patches) ); 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 71993e98..e25e7eae 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 @@ -28,21 +28,24 @@ const title = "Multi-framework app"; let info = await ng.client_info(); console.log(info.V0.details); initNg(ng, event.session); + + window.ng = ng; }, true, [] ); + - + - + 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 905cbf9d..54477baf 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 @@ -2,9 +2,10 @@ import React 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"; export function HelloWorldReact() { - const state = useShape(TestObjectShapeType)?.entries().next(); + const state = [...(useShape(BasicShapeType)?.entries() || [])][0]; // @ts-expect-error window.reactState = state; @@ -17,14 +18,14 @@ export function HelloWorldReact() {

Rendered in React

- + */} 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 24a9c876..8a95a274 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 @@ -21,7 +21,7 @@ } const flatEntries = $derived( $shapeObject - ? flattenObject($shapeObject.entries().next() || ({} as any)) + ? $shapeObject.entries().map((o) => flattenObject(o)[0] || ({} as any)) : [] ); $effect(() => { diff --git a/sdk/js/examples/multi-framework-signals/src/shapes/orm/basic.schema.ts b/sdk/js/examples/multi-framework-signals/src/shapes/orm/basic.schema.ts new file mode 100644 index 00000000..490c11a2 --- /dev/null +++ b/sdk/js/examples/multi-framework-signals/src/shapes/orm/basic.schema.ts @@ -0,0 +1,37 @@ +import type { Schema } from "@ng-org/shex-orm"; + +/** + * ============================================================================= + * basicSchema: Schema for basic + * ============================================================================= + */ +export const basicSchema: Schema = { + "http://example.org/Basic": { + iri: "http://example.org/Basic", + predicates: [ + { + dataTypes: [ + { + valType: "literal", + literals: ["http://example.org/Basic"], + }, + ], + maxCardinality: 1, + minCardinality: 1, + iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type", + readablePredicate: "@type", + }, + { + dataTypes: [ + { + valType: "string", + }, + ], + maxCardinality: 1, + minCardinality: 1, + iri: "http://example.org/basicString", + readablePredicate: "basicString", + }, + ], + }, +}; diff --git a/sdk/js/examples/multi-framework-signals/src/shapes/orm/basic.shapeTypes.ts b/sdk/js/examples/multi-framework-signals/src/shapes/orm/basic.shapeTypes.ts new file mode 100644 index 00000000..e389ec20 --- /dev/null +++ b/sdk/js/examples/multi-framework-signals/src/shapes/orm/basic.shapeTypes.ts @@ -0,0 +1,9 @@ +import type { ShapeType } from "@ng-org/shex-orm"; +import { basicSchema } from "./basic.schema"; +import type { Basic } from "./basic.typings"; + +// ShapeTypes for basic +export const BasicShapeType: ShapeType = { + schema: basicSchema, + shape: "http://example.org/Basic", +}; diff --git a/sdk/js/examples/multi-framework-signals/src/shapes/orm/basic.typings.ts b/sdk/js/examples/multi-framework-signals/src/shapes/orm/basic.typings.ts new file mode 100644 index 00000000..9f0329e2 --- /dev/null +++ b/sdk/js/examples/multi-framework-signals/src/shapes/orm/basic.typings.ts @@ -0,0 +1,22 @@ +export type IRI = string; + +/** + * ============================================================================= + * Typescript Typings for basic + * ============================================================================= + */ + +/** + * Basic Type + */ +export interface Basic { + readonly "@id": IRI; + /** + * Original IRI: http://www.w3.org/1999/02/22-rdf-syntax-ns#type + */ + "@type": string; + /** + * Original IRI: http://example.org/basicString + */ + basicString: string; +} diff --git a/sdk/js/examples/multi-framework-signals/src/shapes/shex/basic.shex b/sdk/js/examples/multi-framework-signals/src/shapes/shex/basic.shex new file mode 100644 index 00000000..c1f9fd91 --- /dev/null +++ b/sdk/js/examples/multi-framework-signals/src/shapes/shex/basic.shex @@ -0,0 +1,8 @@ +PREFIX ex: +PREFIX xsd: + +ex:Basic { + a [ ex:Basic ] ; + ex:basicString xsd:string ; +} + diff --git a/sdk/js/lib-wasm/src/lib.rs b/sdk/js/lib-wasm/src/lib.rs index 65ea0e85..e7abf813 100644 --- a/sdk/js/lib-wasm/src/lib.rs +++ b/sdk/js/lib-wasm/src/lib.rs @@ -21,6 +21,7 @@ use std::sync::Arc; use nextgraph::net::app_protocol::AppRequest; use ng_net::orm::OrmPatch; +use ng_repo::log_info; use ng_wallet::types::SensitiveWallet; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; @@ -1314,7 +1315,9 @@ async fn app_request_stream_( // list. // }; // }; - let response_js = serde_wasm_bindgen::to_value(&app_response).unwrap(); + let response_js = app_response + .serialize(&serde_wasm_bindgen::Serializer::new().serialize_maps_as_objects(true)) + .unwrap(); // if let Some(graph_triples) = graph_triples_js { // let response: Object = response_js.try_into().map_err(|_| { // "Error while adding triples to AppResponse.V0.State".to_string() @@ -1818,7 +1821,6 @@ pub async fn orm_start( ) -> Result { let shape_type: OrmShapeType = serde_wasm_bindgen::from_value::(shapeType) .map_err(|e| format!("Deserialization error of shapeType {e}"))?; - log_info!("frontend_orm_start {:?}", shape_type); let session_id: u64 = serde_wasm_bindgen::from_value::(session_id) .map_err(|_| "Deserialization error of session_id".to_string())?; let scope = if scope.is_empty() { @@ -1826,6 +1828,7 @@ pub async fn orm_start( } else { NuriV0::new_from(&scope).map_err(|_| "Deserialization error of scope".to_string())? }; + log_info!("[orm_start] parameters parsed, calling new_orm_start"); let mut request = AppRequest::new_orm_start(scope, shape_type); request.set_session_id(session_id); app_request_stream_(request, callback).await @@ -1833,21 +1836,24 @@ pub async fn orm_start( #[wasm_bindgen] pub async fn orm_update( - scope: JsValue, + scope: String, shapeTypeName: String, diff: JsValue, session_id: JsValue, ) -> Result<(), String> { let diff: OrmPatches = serde_wasm_bindgen::from_value::(diff) .map_err(|e| format!("Deserialization error of diff {e}"))?; - log_info!("frontend_update_orm {:?}", diff); - let scope: NuriV0 = serde_wasm_bindgen::from_value::(scope) - .map_err(|_| "Deserialization error of scope".to_string())?; + let scope = if scope.is_empty() { + NuriV0::new_entire_user_site() + } else { + NuriV0::new_from(&scope).map_err(|_| "Deserialization error of scope".to_string())? + }; let mut request = AppRequest::new_orm_update(scope, shapeTypeName, diff); let session_id: u64 = serde_wasm_bindgen::from_value::(session_id) .map_err(|_| "Deserialization error of session_id".to_string())?; request.set_session_id(session_id); + log_info!("[orm_update] calling orm_update"); let response = nextgraph::local_broker::app_request(request) .await .map_err(|e: NgError| e.to_string())?; diff --git a/sdk/js/signals/src/connector/ormConnectionHandler.ts b/sdk/js/signals/src/connector/ormConnectionHandler.ts index 77d9333c..7b1c5f9b 100644 --- a/sdk/js/signals/src/connector/ormConnectionHandler.ts +++ b/sdk/js/signals/src/connector/ormConnectionHandler.ts @@ -72,14 +72,10 @@ export class OrmConnection { ngSession.then(({ ng, session }) => { console.log("ng and session", ng, session); try { - const sc = ("did:ng:" + session.private_store_id).substring( - 0, - 53 - ); - console.log("calling orm_start with nuri", sc); - ng.orm_start( - sc, + (scope.length == 0 + ? "did:ng:" + session.private_store_id + : scope) as string, shapeType, session.session_id, this.onBackendMessage @@ -97,10 +93,10 @@ export class OrmConnection { * @param ng * @returns */ - public static getConnection( + public static getConnection = ( shapeType: ShapeType, scope: Scope - ): OrmConnection { + ): OrmConnection => { const scopeKey = canonicalScope(scope); // Unique identifier for a given shape type and scope. @@ -118,50 +114,51 @@ export class OrmConnection { OrmConnection.idToEntry.set(identifier, newConnection); return newConnection; } - } + }; - public release() { + public release = () => { if (this.refCount > 0) this.refCount--; if (this.refCount === 0) { OrmConnection.idToEntry.delete(this.identifier); OrmConnection.cleanupSignalRegistry?.unregister(this.signalObject); } - } + }; - private onSignalObjectUpdate({ patches }: WatchPatchEvent>) { + private onSignalObjectUpdate = ({ patches }: WatchPatchEvent>) => { if (this.suspendDeepWatcher || !this.ready || !patches.length) return; const ormPatches = deepPatchesToDiff(patches); ngSession.then(({ ng, session }) => { ng.orm_update( - ("did:ng:" + session.private_store_id).substring(0, 53), + (this.scope.length == 0 + ? "did:ng:" + session.private_store_id + : this.scope) as string, this.shapeType.shape, ormPatches, session.session_id ); }); - } - - private onBackendMessage(...message: any) { - this.handleInitialResponse(message); - } - - private handleInitialResponse(...param: any) { - console.log("RESPONSE FROM BACKEND", param); + }; - // TODO: This will break, just provisionary. - const wasmMessage: any = param; - const { initialData } = wasmMessage; + private onBackendMessage = ({ V0: data }: any) => { + if (data.OrmInitial) { + this.handleInitialResponse(data.OrmInitial); + } + }; + private handleInitialResponse = (initialData: any) => { // Assign initial data to empty signal object without triggering watcher at first. this.suspendDeepWatcher = true; batch(() => { + // Do this in case the there was any (incorrect) data added before initialization. + this.signalObject.clear(); // Convert arrays to sets and apply to signalObject (we only have sets but can only transport arrays). for (const newItem of recurseArrayToSet(initialData)) { this.signalObject.add(newItem); } + console.log("data received", this.signalObject); }); queueMicrotask(() => { @@ -171,13 +168,13 @@ export class OrmConnection { }); this.ready = true; - } - private onBackendUpdate(...params: any) { + }; + private onBackendUpdate = (...params: any) => { // Apply diff - } + }; /** Function to create random subject IRIs for newly created nested objects. */ - private generateSubjectIri(path: (string | number)[]): string { + private generateSubjectIri = (path: (string | number)[]): string => { // Generate random string. let b = Buffer.alloc(33); crypto.getRandomValues(b); @@ -192,7 +189,7 @@ export class OrmConnection { // Else, just generate a random IRI. return "did:ng:q:" + randomString; } - } + }; } // @@ -214,6 +211,8 @@ export function deepPatchesToDiff(patches: DeepPatch[]): Patches { const recurseArrayToSet = (obj: any): any => { if (Array.isArray(obj)) { return new Set(obj.map(recurseArrayToSet)); + } else if (obj && typeof obj === "object" && obj instanceof Map) { + return Object.fromEntries(obj.entries()); } else if (obj && typeof obj === "object") { for (const key of Object.keys(obj)) { obj[key] = recurseArrayToSet(obj[key]); diff --git a/sdk/js/signals/src/frontendAdapters/react/useShape.ts b/sdk/js/signals/src/frontendAdapters/react/useShape.ts index df0aef8c..f7652c60 100644 --- a/sdk/js/signals/src/frontendAdapters/react/useShape.ts +++ b/sdk/js/signals/src/frontendAdapters/react/useShape.ts @@ -9,14 +9,16 @@ const useShape = ( shape: ShapeType, scope: Scope = "" ) => { - const shapeSignalRef = useRef< - ReturnType> - >(createSignalObjectForShape(shape, scope)); + const shapeSignalRef = useRef + > | null>(null); const [, setTick] = useState(0); useEffect(() => { + shapeSignalRef.current = createSignalObjectForShape(shape, scope); const handle = shapeSignalRef.current; const deepSignalObj = handle.signalObject; + const { stopListening } = watch(deepSignalObj, () => { // trigger a React re-render when the deep signal updates setTick((t) => t + 1); @@ -31,9 +33,7 @@ const useShape = ( }; }, []); - if ("@id" in shapeSignalRef.current.signalObject) - return shapeSignalRef.current.signalObject; - else return null; + return shapeSignalRef.current?.signalObject; }; export default useShape; diff --git a/sdk/rust/src/tests/orm_apply_patches.rs b/sdk/rust/src/tests/orm_apply_patches.rs index d08a3793..ea2881b0 100644 --- a/sdk/rust/src/tests/orm_apply_patches.rs +++ b/sdk/rust/src/tests/orm_apply_patches.rs @@ -130,7 +130,7 @@ INSERT DATA { // Apply ORM patch: Add name let diff = vec![OrmPatch { op: OrmPatchOp::add, - path: "urn:test:person1/name".to_string(), + path: "/urn:test:person1/name".to_string(), valType: None, value: Some(json!("Alice")), }]; @@ -229,7 +229,7 @@ INSERT DATA { // Apply ORM patch: Remove name let diff = vec![OrmPatch { op: OrmPatchOp::remove, - path: "urn:test:person2/name".to_string(), + path: "/urn:test:person2/name".to_string(), valType: None, value: Some(json!("Bob")), }]; @@ -329,13 +329,13 @@ INSERT DATA { let diff = vec![ // OrmDiffOp { // op: OrmDiffOpType::remove, - // path: "urn:test:person3/name".to_string(), + // path: "/urn:test:person3/name".to_string(), // valType: None, // value: Some(json!("Charlie")), // }, OrmPatch { op: OrmPatchOp::add, - path: "urn:test:person3/name".to_string(), + path: "/urn:test:person3/name".to_string(), valType: None, value: Some(json!("Charles")), }, @@ -443,7 +443,7 @@ INSERT DATA { let diff = vec![OrmPatch { op: OrmPatchOp::add, valType: Some(OrmPatchType::set), - path: "urn:test:person4/hobby".to_string(), + path: "/urn:test:person4/hobby".to_string(), value: Some(json!("Swimming")), }]; @@ -543,7 +543,7 @@ INSERT DATA { // Apply ORM patch: Remove hobby let diff = vec![OrmPatch { op: OrmPatchOp::remove, - path: "urn:test:person5/hobby".to_string(), + path: "/urn:test:person5/hobby".to_string(), valType: None, value: Some(json!("Swimming")), }]; @@ -712,7 +712,7 @@ INSERT DATA { // Apply ORM patch: Change city in nested address let diff = vec![OrmPatch { op: OrmPatchOp::add, - path: "urn:test:person6/address/city".to_string(), + path: "/urn:test:person6/address/city".to_string(), valType: None, value: Some(json!("Shelbyville")), }]; @@ -931,7 +931,7 @@ INSERT DATA { // Apply ORM patch: Change street in company's headquarter address (3 levels deep) let diff = vec![OrmPatch { op: OrmPatchOp::add, - path: "urn:test:person7/company/urn:test:company1/headquarter/street".to_string(), + path: "/urn:test:person7/company/urn:test:company1/headquarter/street".to_string(), valType: None, value: Some(json!("Rich Street")), }]; @@ -1038,7 +1038,7 @@ INSERT DATA { let diff = vec![ OrmPatch { op: OrmPatchOp::add, - path: "urn:test:person8".to_string(), + path: "/urn:test:person8".to_string(), valType: Some(OrmPatchType::object), value: None, }, @@ -1046,19 +1046,19 @@ INSERT DATA { // 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(), + path: "/urn:test:person8/@id".to_string(), valType: Some(OrmPatchType::object), value: None, }, OrmPatch { op: OrmPatchOp::add, - path: "urn:test:person8/type".to_string(), + 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(), + path: "/urn:test:person8/name".to_string(), valType: None, value: Some(json!("Alice")), }, @@ -1218,13 +1218,13 @@ INSERT DATA { let diff = vec![ OrmPatch { op: OrmPatchOp::add, - path: "urn:test:person9/address/http:~1~1example.org~1exampleAddress/type".to_string(), + 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" + path: "/urn:test:person9/address/http:~1~1example.org~1exampleAddress/street" .to_string(), valType: None, value: Some(json!("Heaven Avenue")),