applying patch tests working

feat/orm-diffs
Laurin Weger 1 day ago
parent d45ebc340f
commit a3ecc7358b
No known key found for this signature in database
GPG Key ID: 9B372BB0B792770F
  1. 8
      engine/net/src/app_protocol.rs
  2. 14
      engine/net/src/orm.rs
  3. 42
      engine/verifier/src/orm/handle_backend_update.rs
  4. 98
      engine/verifier/src/orm/handle_frontend_update.rs
  5. 4
      engine/verifier/src/orm/materialize.rs
  6. 4
      engine/verifier/src/orm/mod.rs
  7. 2
      engine/verifier/src/orm/process_changes.rs
  8. 2
      engine/verifier/src/orm/query.rs
  9. 2
      engine/verifier/src/orm/utils.rs
  10. 3
      pnpm-lock.yaml
  11. 3
      sdk/js/examples/multi-framework-signals/src/app/pages/index.astro
  12. 2
      sdk/js/lib-wasm/src/lib.rs
  13. 1
      sdk/js/signals/package.json
  14. 15
      sdk/js/signals/src/connector/createSignalObjectForShape.ts
  15. 4
      sdk/rust/src/local_broker.rs
  16. 341
      sdk/rust/src/tests/orm_apply_patches.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 ng_repo::utils::{decode_overlayid, display_timestamp_local};
use serde_json::Value; use serde_json::Value;
use crate::orm::{OrmDiff, OrmShapeType, OrmUpdateBlankNodeIds}; use crate::orm::{OrmPatches, OrmShapeType, OrmUpdateBlankNodeIds};
use crate::types::*; use crate::types::*;
#[derive(Clone, Debug, Serialize, Deserialize)] #[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( AppRequest::new(
AppRequestCommandV0::OrmUpdate, AppRequestCommandV0::OrmUpdate,
scope, scope,
@ -1056,7 +1056,7 @@ pub enum AppRequestPayloadV0 {
QrCodeProfile(u32), QrCodeProfile(u32),
QrCodeProfileImport(String), QrCodeProfileImport(String),
OrmStart(OrmShapeType), OrmStart(OrmShapeType),
OrmUpdate((OrmDiff, String)), // ShapeID OrmUpdate((OrmPatches, String)), // ShapeID
OrmStop(String), //ShapeID OrmStop(String), //ShapeID
} }
@ -1308,7 +1308,7 @@ pub enum AppResponseV0 {
Header(AppHeader), Header(AppHeader),
Commits(Vec<String>), Commits(Vec<String>),
OrmInitial(Value), OrmInitial(Value),
OrmUpdate(OrmDiff), OrmUpdate(OrmPatches),
OrmUpdateBlankNodeIds(OrmUpdateBlankNodeIds), OrmUpdateBlankNodeIds(OrmUpdateBlankNodeIds),
OrmError(String), OrmError(String),
} }

@ -23,30 +23,30 @@ pub struct OrmShapeType {
pub shape: String, pub shape: String,
} }
/* == Diff Types == */ /* == Patch Types == */
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
#[allow(non_camel_case_types)] #[allow(non_camel_case_types)]
pub enum OrmDiffOpType { pub enum OrmPatchOp {
add, add,
remove, remove,
} }
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
#[allow(non_camel_case_types)] #[allow(non_camel_case_types)]
pub enum OrmDiffType { pub enum OrmPatchType {
set, set,
object, object,
} }
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct OrmDiffOp { pub struct OrmPatch {
pub op: OrmDiffOpType, pub op: OrmPatchOp,
pub valType: Option<OrmDiffType>, pub valType: Option<OrmPatchType>,
pub path: String, pub path: String,
pub value: Option<Value>, // TODO: Improve type pub value: Option<Value>, // TODO: Improve type
} }
pub type OrmDiff = Vec<OrmDiffOp>; pub type OrmPatches = Vec<OrmPatch>;
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct OrmUpdateBlankNodeId { pub struct OrmUpdateBlankNodeId {

@ -11,7 +11,7 @@ use std::collections::HashMap;
use std::u64; use std::u64;
use futures::SinkExt; 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_net::{app_protocol::*, orm::*};
use ng_repo::log::*; use ng_repo::log::*;
@ -132,7 +132,7 @@ impl Verifier {
); );
// The JSON patches to send to JS land. // The JSON patches to send to JS land.
let mut patches: Vec<OrmDiffOp> = vec![]; let mut patches: Vec<OrmPatch> = vec![];
// Keep track of objects to create: (path, Option<IRI>) // Keep track of objects to create: (path, Option<IRI>)
// The IRI is Some for real subjects, None for intermediate objects (e.g., multi-valued predicate containers) // 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.tracked_subjects,
&sub.shape_type.shape, &sub.shape_type.shape,
&mut path, &mut path,
(OrmDiffOpType::remove, Some(OrmDiffType::object), None, None), (OrmPatchOp::remove, Some(OrmPatchType::object), None, None),
&mut patches, &mut patches,
&mut objects_to_create, &mut objects_to_create,
); );
@ -255,17 +255,17 @@ impl Verifier {
let json_pointer = format!("/{}", escaped_path.join("/")); let json_pointer = format!("/{}", escaped_path.join("/"));
// Always create the object itself // Always create the object itself
patches.push(OrmDiffOp { patches.push(OrmPatch {
op: OrmDiffOpType::add, op: OrmPatchOp::add,
valType: Some(OrmDiffType::object), valType: Some(OrmPatchType::object),
path: json_pointer.clone(), path: json_pointer.clone(),
value: None, value: None,
}); });
// If this object has an IRI (it's a real subject), add the id field // If this object has an IRI (it's a real subject), add the id field
if let Some(iri) = maybe_iri { if let Some(iri) = maybe_iri {
patches.push(OrmDiffOp { patches.push(OrmPatch {
op: OrmDiffOpType::add, op: OrmPatchOp::add,
valType: None, valType: None,
path: format!("{}/@id", json_pointer), path: format!("{}/@id", json_pointer),
value: Some(json!(iri)), value: Some(json!(iri)),
@ -291,7 +291,7 @@ fn queue_patches_for_newly_valid_subject(
tracked_subjects: &HashMap<String, HashMap<String, Arc<RwLock<OrmTrackedSubject>>>>, tracked_subjects: &HashMap<String, HashMap<String, Arc<RwLock<OrmTrackedSubject>>>>,
root_shape: &String, root_shape: &String,
path: &[String], path: &[String],
patches: &mut Vec<OrmDiffOp>, patches: &mut Vec<OrmPatch>,
objects_to_create: &mut HashSet<(Vec<String>, Option<SubjectIri>)>, objects_to_create: &mut HashSet<(Vec<String>, Option<SubjectIri>)>,
) { ) {
// Check if we're at a root subject or need to traverse to parents // 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, root_shape: &String,
path: &mut Vec<String>, path: &mut Vec<String>,
diff_op: ( diff_op: (
OrmDiffOpType, OrmPatchOp,
Option<OrmDiffType>, Option<OrmPatchType>,
Option<Value>, // The value added / removed Option<Value>, // The value added / removed
Option<String>, // The IRI, if change is an added / removed object. Option<String>, // The IRI, if change is an added / removed object.
), ),
patches: &mut Vec<OrmDiffOp>, patches: &mut Vec<OrmPatch>,
objects_to_create: &mut HashSet<(Vec<String>, Option<SubjectIri>)>, objects_to_create: &mut HashSet<(Vec<String>, Option<SubjectIri>)>,
) { ) {
log_debug!( log_debug!(
@ -465,7 +465,7 @@ fn build_path_to_root_and_create_patches(
); );
// Create the patch for the actual value change // Create the patch for the actual value change
patches.push(OrmDiffOp { patches.push(OrmPatch {
op: diff_op.0.clone(), op: diff_op.0.clone(),
valType: diff_op.1.clone(), valType: diff_op.1.clone(),
path: json_pointer.clone(), path: json_pointer.clone(),
@ -515,8 +515,8 @@ fn build_path_to_root_and_create_patches(
fn create_diff_ops_from_predicate_change( fn create_diff_ops_from_predicate_change(
pred_change: &OrmTrackedPredicateChanges, pred_change: &OrmTrackedPredicateChanges,
) -> Vec<( ) -> Vec<(
OrmDiffOpType, OrmPatchOp,
Option<OrmDiffType>, Option<OrmPatchType>,
Option<Value>, // The value added / removed Option<Value>, // The value added / removed
Option<String>, // The IRI, if change is an added / removed object. Option<String>, // 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 // A value was added. Another one might have been removed
// but the add patch overwrite previous values. // but the add patch overwrite previous values.
return [( return [(
OrmDiffOpType::add, OrmPatchOp::add,
None, None,
Some(json!(pred_change.values_added[0])), Some(json!(pred_change.values_added[0])),
None, None,
@ -545,21 +545,21 @@ fn create_diff_ops_from_predicate_change(
.to_vec(); .to_vec();
} else { } else {
// Since there is only one possible value, removing the path is enough. // 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 { } else if is_multi && !is_object {
if pred_change.values_added.len() > 0 { if pred_change.values_added.len() > 0 {
ops.push(( ops.push((
OrmDiffOpType::add, OrmPatchOp::add,
Some(OrmDiffType::set), Some(OrmPatchType::set),
Some(json!(pred_change.values_added)), Some(json!(pred_change.values_added)),
None, None,
)); ));
} }
if pred_change.values_removed.len() > 0 { if pred_change.values_removed.len() > 0 {
ops.push(( ops.push((
OrmDiffOpType::remove, OrmPatchOp::remove,
Some(OrmDiffType::set), Some(OrmPatchType::set),
Some(json!(pred_change.values_removed)), Some(json!(pred_change.values_removed)),
None, None,
)); ));

@ -7,20 +7,20 @@
// notice may not be copied, modified, or distributed except // notice may not be copied, modified, or distributed except
// according to those terms. // 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_oxigraph::oxrdf::Quad;
use ng_repo::errors::VerifierError; use ng_repo::errors::VerifierError;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use std::u64; use std::u64;
use futures::SinkExt;
use ng_net::app_protocol::*; use ng_net::app_protocol::*;
pub use ng_net::orm::{OrmDiff, OrmShapeType}; pub use ng_net::orm::{OrmPatches, OrmShapeType};
use ng_repo::log::*; use ng_repo::log::*;
use crate::orm::types::*; use crate::orm::types::*;
use crate::orm::utils::{decode_json_pointer, json_to_sparql_val}; use crate::orm::utils::{decode_json_pointer, json_to_sparql_val};
use crate::types::GraphQuadsPatch;
use crate::verifier::*; use crate::verifier::*;
impl Verifier { impl Verifier {
@ -34,25 +34,22 @@ impl Verifier {
shape_iri: ShapeIri, shape_iri: ShapeIri,
session_id: u64, session_id: u64,
_skolemnized_blank_nodes: Vec<Quad>, _skolemnized_blank_nodes: Vec<Quad>,
_revert_inserts: Vec<Quad>, revert_inserts: Vec<Quad>,
_revert_removes: Vec<Quad>, revert_removes: Vec<Quad>,
) -> Result<(), VerifierError> { ) -> Result<(), VerifierError> {
let (mut sender, _orm_subscription) = let (mut sender, _orm_subscription) =
self.get_first_orm_subscription_sender_for(scope, Some(&shape_iri), Some(&session_id))?; self.get_first_orm_subscription_sender_for(scope, Some(&shape_iri), Some(&session_id))?;
// TODO prepare OrmUpdateBlankNodeIds with skolemnized_blank_nodes // Revert changes, if there.
// use orm_subscription if needed if revert_inserts.len() > 0 || revert_removes.len() > 0 {
// note(niko): I think skolemnized blank nodes can still be many, in case of multi-level nested sub-objects. let revert_changes = GraphQuadsPatch {
let orm_bnids = vec![]; inserts: revert_removes,
let _ = sender removes: revert_inserts,
.send(AppResponse::V0(AppResponseV0::OrmUpdateBlankNodeIds( };
orm_bnids,
)))
.await;
// TODO (later) revert the inserts and removes // TODO: Call with correct params.
// let orm_diff = vec![]; // self.orm_backend_update(session_id, scope, "", revert_changes)
// let _ = sender.send(AppResponse::V0(AppResponseV0::OrmUpdate(orm_diff))).await; }
Ok(()) Ok(())
} }
@ -63,7 +60,7 @@ impl Verifier {
session_id: u64, session_id: u64,
scope: &NuriV0, scope: &NuriV0,
shape_iri: ShapeIri, shape_iri: ShapeIri,
diff: OrmDiff, diff: OrmPatches,
) -> Result<(), String> { ) -> Result<(), String> {
log_info!( log_info!(
"frontend_update_orm session={} shape={} diff={:?}", "frontend_update_orm session={} shape={} diff={:?}",
@ -118,21 +115,21 @@ impl Verifier {
fn create_sparql_update_query_for_diff( fn create_sparql_update_query_for_diff(
orm_subscription: &OrmSubscription, orm_subscription: &OrmSubscription,
diff: OrmDiff, diff: OrmPatches,
) -> String { ) -> String {
// First sort patches. // First sort patches.
// - Process delete patches first. // - Process delete patches first.
// - Process object creation add operations before rest, to ensure potential blank nodes are created. // - Process object creation add operations before rest, to ensure potential blank nodes are created.
let delete_patches: Vec<_> = diff let delete_patches: Vec<_> = diff
.iter() .iter()
.filter(|patch| patch.op == OrmDiffOpType::remove) .filter(|patch| patch.op == OrmPatchOp::remove)
.collect(); .collect();
let add_object_patches: Vec<_> = diff let add_object_patches: Vec<_> = diff
.iter() .iter()
.filter(|patch| { .filter(|patch| {
patch.op == OrmDiffOpType::add patch.op == OrmPatchOp::add
&& match &patch.valType { && match &patch.valType {
Some(vt) => *vt == OrmDiffType::object, Some(vt) => *vt == OrmPatchType::object,
_ => false, _ => false,
} }
}) })
@ -140,9 +137,9 @@ fn create_sparql_update_query_for_diff(
let add_literal_patches: Vec<_> = diff let add_literal_patches: Vec<_> = diff
.iter() .iter()
.filter(|patch| { .filter(|patch| {
patch.op == OrmDiffOpType::add patch.op == OrmPatchOp::add
&& match &patch.valType { && match &patch.valType {
Some(vt) => *vt != OrmDiffType::object, Some(vt) => *vt != OrmPatchType::object,
_ => true, _ => true,
} }
}) })
@ -289,7 +286,7 @@ fn find_pred_schema_by_name(
/// - The Option subject, predicate, Option<Object> of the path's ending (to be used for DELETE) /// - The Option subject, predicate, Option<Object> of the path's ending (to be used for DELETE)
/// - The Option predicate schema of the tail of the target property. /// - The Option predicate schema of the tail of the target property.
fn create_where_statements_for_patch( fn create_where_statements_for_patch(
patch: &OrmDiffOp, patch: &OrmPatch,
var_counter: &mut i32, var_counter: &mut i32,
orm_subscription: &OrmSubscription, orm_subscription: &OrmSubscription,
) -> ( ) -> (
@ -334,6 +331,7 @@ fn create_where_statements_for_patch(
let pred_name = path.remove(0); let pred_name = path.remove(0);
let pred_schema = find_pred_schema_by_name(&pred_name, &current_subj_schema); let pred_schema = find_pred_schema_by_name(&pred_name, &current_subj_schema);
// Case: We arrived at a leaf value.
if path.len() == 0 { if path.len() == 0 {
return ( return (
where_statements, where_statements,
@ -342,6 +340,8 @@ fn create_where_statements_for_patch(
); );
} }
// Else, we have a nested object.
where_statements.push(format!( where_statements.push(format!(
"{} <{}> ?o{}", "{} <{}> ?o{}",
subject_ref, pred_schema.iri, var_counter, subject_ref, pred_schema.iri, var_counter,
@ -369,7 +369,7 @@ fn create_where_statements_for_patch(
} }
current_subj_schema = 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. // Since we have new IRI that we can use as root, we replace the current one with it.
subject_ref = format!("<{object_iri}>"); subject_ref = format!("<{object_iri}>");
@ -379,15 +379,18 @@ fn create_where_statements_for_patch(
// Set to child subject schema. // Set to child subject schema.
// TODO: Actually, we should get the tracked subject and check for the correct shape there. // 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. // 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. // Can't happen.
panic!(); panic!();
} }
fn get_first_valid_child_schema( /// Get the schema for a given subject and predicate schema.
subject_iri: &String, /// 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, pred_schema: &OrmSchemaPredicate,
orm_subscription: &OrmSubscription, orm_subscription: &OrmSubscription,
) -> Arc<OrmSchemaShape> { ) -> Arc<OrmSchemaShape> {
@ -396,13 +399,12 @@ fn get_first_valid_child_schema(
continue; continue;
}; };
let tracked_subject = orm_subscription let tracked_subject_opt = subject_iri
.tracked_subjects .and_then(|iri| orm_subscription.tracked_subjects.get(iri))
.get(subject_iri) .and_then(|ts_shapes| ts_shapes.get(schema_shape));
.unwrap()
.get(schema_shape)
.unwrap();
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 { if tracked_subject.read().unwrap().valid == OrmTrackedSubjectValidity::Valid {
return orm_subscription return orm_subscription
.shape_type .shape_type
@ -411,19 +413,21 @@ fn get_first_valid_child_schema(
.unwrap() .unwrap()
.clone(); .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();
}
} }
// TODO: Panicking might be too aggressive. // TODO: Panicking might be too aggressive.
panic!("No valid child schema found."); panic!("No valid child schema found.");
} }
fn get_first_child_schema(
pred_schema: &OrmSchemaPredicate,
orm_subscription: &OrmSubscription,
) -> Arc<OrmSchemaShape> {
return orm_subscription
.shape_type
.schema
.get(pred_schema.dataTypes[0].shape.as_ref().unwrap())
.unwrap()
.clone();
}

@ -9,7 +9,7 @@
use futures::SinkExt; use futures::SinkExt;
use ng_net::orm::*; use ng_net::orm::*;
pub use ng_net::orm::{OrmDiff, OrmShapeType}; pub use ng_net::orm::{OrmPatches, OrmShapeType};
use ng_net::utils::Receiver; use ng_net::utils::Receiver;
use serde_json::json; use serde_json::json;
use serde_json::Value; use serde_json::Value;
@ -141,7 +141,7 @@ pub(crate) fn materialize_orm_object(
shape: &OrmSchemaShape, shape: &OrmSchemaShape,
tracked_subjects: &HashMap<String, HashMap<String, Arc<RwLock<OrmTrackedSubject>>>>, tracked_subjects: &HashMap<String, HashMap<String, Arc<RwLock<OrmTrackedSubject>>>>,
) -> Value { ) -> 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(); let orm_obj_map = orm_obj.as_object_mut().unwrap();
for pred_schema in &shape.predicates { for pred_schema in &shape.predicates {
let property_name = &pred_schema.readablePredicate; let property_name = &pred_schema.readablePredicate;

@ -17,13 +17,13 @@ pub mod shape_validation;
pub mod types; pub mod types;
pub mod utils; pub mod utils;
pub use ng_net::orm::{OrmDiff, OrmShapeType}; pub use ng_net::orm::{OrmPatches, OrmShapeType};
use crate::orm::types::*; use crate::orm::types::*;
use crate::verifier::*; use crate::verifier::*;
impl Verifier { impl Verifier {
pub(crate) fn clean_orm_subscriptions(&mut self) { pub(crate) fn _clean_orm_subscriptions(&mut self) {
self.orm_subscriptions.retain(|_, subscriptions| { self.orm_subscriptions.retain(|_, subscriptions| {
subscriptions.retain(|sub| !sub.sender.is_closed()); subscriptions.retain(|sub| !sub.sender.is_closed());
!subscriptions.is_empty() !subscriptions.is_empty()

@ -15,7 +15,7 @@ use std::collections::HashSet;
use std::sync::Arc; use std::sync::Arc;
use std::u64; 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_net::{app_protocol::*, orm::*};
use ng_oxigraph::oxrdf::Triple; use ng_oxigraph::oxrdf::Triple;
use ng_repo::errors::NgError; use ng_repo::errors::NgError;

@ -11,7 +11,7 @@ use ng_repo::errors::VerifierError;
use std::collections::HashSet; 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::types::*;
use crate::orm::utils::{escape_literal, is_iri}; use crate::orm::utils::{escape_literal, is_iri};

@ -16,7 +16,7 @@ use std::collections::HashSet;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use regex::Regex; 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_net::{app_protocol::*, orm::*};
use ng_oxigraph::oxrdf::Triple; use ng_oxigraph::oxrdf::Triple;

@ -877,6 +877,9 @@ importers:
'@ng-org/alien-deepsignals': '@ng-org/alien-deepsignals':
specifier: workspace:* specifier: workspace:*
version: link:../alien-deepsignals version: link:../alien-deepsignals
'@ng-org/lib-wasm':
specifier: workspace:*
version: link:../lib-wasm/pkg
'@ng-org/shex-orm': '@ng-org/shex-orm':
specifier: workspace:* specifier: workspace:*
version: link:../shex-orm version: link:../shex-orm

@ -6,9 +6,6 @@ import VueRoot from "../components/VueRoot.vue";
import ReactRoot from "../components/ReactRoot"; import ReactRoot from "../components/ReactRoot";
import SvelteRoot from "../components/SvelteRoot.svelte"; 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"; const title = "Multi-framework app";
--- ---

@ -58,7 +58,7 @@ use ng_wallet::types::*;
use ng_wallet::*; use ng_wallet::*;
use nextgraph::local_broker::*; use nextgraph::local_broker::*;
use nextgraph::verifier::orm::{OrmDiff, OrmShapeType}; use nextgraph::verifier::orm::{OrmPatches, OrmShapeType};
use nextgraph::verifier::CancelFn; use nextgraph::verifier::CancelFn;
use crate::model::*; use crate::model::*;

@ -62,6 +62,7 @@
"vue": "3.5.19" "vue": "3.5.19"
}, },
"devDependencies": { "devDependencies": {
"@ng-org/lib-wasm": "workspace:*",
"@playwright/test": "^1.55.0", "@playwright/test": "^1.55.0",
"@types/node": "24.3.0", "@types/node": "24.3.0",
"@types/react": "19.1.10", "@types/react": "19.1.10",

@ -1,17 +1,10 @@
import type { Diff, Scope } from "../types.js"; import type { Diff, Scope } from "../types.js";
import { applyDiff } from "./applyDiff.js"; import { applyDiff } from "./applyDiff.js";
import ng from "@ng-org/lib-wasm"; import type * as NG from "@ng-org/lib-wasm";
import { import { deepSignal, watch, batch } from "@ng-org/alien-deepsignals";
deepSignal, import type { DeepPatch, DeepSignalObject } from "@ng-org/alien-deepsignals";
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"; import type { ShapeType, BaseType } from "@ng-org/shex-orm";
interface PoolEntry<T extends BaseType> { interface PoolEntry<T extends BaseType> {

@ -17,7 +17,7 @@ use async_std::sync::{Arc, Condvar, Mutex, RwLock};
use futures::channel::mpsc; use futures::channel::mpsc;
use futures::{SinkExt, StreamExt}; use futures::{SinkExt, StreamExt};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use ng_net::orm::{OrmDiff, OrmShapeType}; use ng_net::orm::{OrmPatches, OrmShapeType};
use ng_oxigraph::oxrdf::Triple; use ng_oxigraph::oxrdf::Triple;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use pdf_writer::{Content, Finish, Name, Pdf, Rect, Ref, Str}; use pdf_writer::{Content, Finish, Name, Pdf, Rect, Ref, Str};
@ -2767,7 +2767,7 @@ pub async fn orm_start(
pub async fn orm_update( pub async fn orm_update(
scope: NuriV0, scope: NuriV0,
shape_type_name: String, shape_type_name: String,
diff: OrmDiff, diff: OrmPatches,
session_id: u64, session_id: u64,
) -> Result<(), NgError> { ) -> Result<(), NgError> {
let mut request = AppRequest::new_orm_update(scope, shape_type_name, diff); let mut request = AppRequest::new_orm_update(scope, shape_type_name, diff);

@ -13,7 +13,7 @@ use crate::tests::create_or_open_wallet::create_or_open_wallet;
use async_std::stream::StreamExt; use async_std::stream::StreamExt;
use ng_net::app_protocol::{AppResponse, AppResponseV0, NuriV0}; use ng_net::app_protocol::{AppResponse, AppResponseV0, NuriV0};
use ng_net::orm::{ use ng_net::orm::{
BasicType, OrmDiffOp, OrmDiffOpType, OrmDiffType, OrmSchemaDataType, OrmSchemaPredicate, BasicType, OrmPatch, OrmPatchOp, OrmPatchType, OrmSchemaDataType, OrmSchemaPredicate,
OrmSchemaShape, OrmSchemaValType, OrmShapeType, OrmSchemaShape, OrmSchemaValType, OrmShapeType,
}; };
@ -44,11 +44,17 @@ async fn test_orm_apply_patches() {
// Test 5: Remove from multi-value literal array // Test 5: Remove from multi-value literal array
test_patch_remove_from_array(session_id).await; 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_patch_nested_literal(session_id).await;
// Test 7: Multi-level nesting // Test 7: Multi-level nesting
test_patch_multilevel_nested(session_id).await; 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 /// Test adding a single literal value via ORM patch
@ -122,8 +128,8 @@ INSERT DATA {
} }
// Apply ORM patch: Add name // Apply ORM patch: Add name
let diff = vec![OrmDiffOp { let diff = vec![OrmPatch {
op: OrmDiffOpType::add, op: OrmPatchOp::add,
path: "urn:test:person1/name".to_string(), path: "urn:test:person1/name".to_string(),
valType: None, valType: None,
value: Some(json!("Alice")), value: Some(json!("Alice")),
@ -221,8 +227,8 @@ INSERT DATA {
} }
// Apply ORM patch: Remove name // Apply ORM patch: Remove name
let diff = vec![OrmDiffOp { let diff = vec![OrmPatch {
op: OrmDiffOpType::remove, op: OrmPatchOp::remove,
path: "urn:test:person2/name".to_string(), path: "urn:test:person2/name".to_string(),
valType: None, valType: None,
value: Some(json!("Bob")), value: Some(json!("Bob")),
@ -327,8 +333,8 @@ INSERT DATA {
// valType: None, // valType: None,
// value: Some(json!("Charlie")), // value: Some(json!("Charlie")),
// }, // },
OrmDiffOp { OrmPatch {
op: OrmDiffOpType::add, op: OrmPatchOp::add,
path: "urn:test:person3/name".to_string(), path: "urn:test:person3/name".to_string(),
valType: None, valType: None,
value: Some(json!("Charles")), value: Some(json!("Charles")),
@ -434,9 +440,9 @@ INSERT DATA {
} }
// Apply ORM patch: Add hobby // Apply ORM patch: Add hobby
let diff = vec![OrmDiffOp { let diff = vec![OrmPatch {
op: OrmDiffOpType::add, op: OrmPatchOp::add,
valType: Some(OrmDiffType::set), valType: Some(OrmPatchType::set),
path: "urn:test:person4/hobby".to_string(), path: "urn:test:person4/hobby".to_string(),
value: Some(json!("Swimming")), value: Some(json!("Swimming")),
}]; }];
@ -535,8 +541,8 @@ INSERT DATA {
} }
// Apply ORM patch: Remove hobby // Apply ORM patch: Remove hobby
let diff = vec![OrmDiffOp { let diff = vec![OrmPatch {
op: OrmDiffOpType::remove, op: OrmPatchOp::remove,
path: "urn:test:person5/hobby".to_string(), path: "urn:test:person5/hobby".to_string(),
valType: None, valType: None,
value: Some(json!("Swimming")), value: Some(json!("Swimming")),
@ -704,8 +710,8 @@ INSERT DATA {
} }
// Apply ORM patch: Change city in nested address // Apply ORM patch: Change city in nested address
let diff = vec![OrmDiffOp { let diff = vec![OrmPatch {
op: OrmDiffOpType::add, op: OrmPatchOp::add,
path: "urn:test:person6/address/city".to_string(), path: "urn:test:person6/address/city".to_string(),
valType: None, valType: None,
value: Some(json!("Shelbyville")), value: Some(json!("Shelbyville")),
@ -923,8 +929,8 @@ INSERT DATA {
} }
// Apply ORM patch: Change street in company's headquarter address (3 levels deep) // Apply ORM patch: Change street in company's headquarter address (3 levels deep)
let diff = vec![OrmDiffOp { let diff = vec![OrmPatch {
op: OrmDiffOpType::add, 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, valType: None,
value: Some(json!("Rich Street")), value: Some(json!("Rich Street")),
@ -957,3 +963,304 @@ INSERT DATA {
log_info!("✓ Test passed: Multi-level nested modification"); 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: <http://example.org/>
INSERT DATA {
<urn:test:person7> 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: <http://example.org/>
INSERT DATA {
<urn:test:person9> a ex:Person ;
ex:name "Dave" ;
ex:address <urn:test:address1> .
<urn:test:address1> 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");
}

Loading…
Cancel
Save