From 83e07344a1106e72d8c41a9ee86593ae7c1d106b Mon Sep 17 00:00:00 2001 From: Niko PLP Date: Sun, 21 Sep 2025 19:24:09 +0300 Subject: [PATCH] ORM APIs for JS SDK --- nextgraph/src/lib.rs | 3 + ng-net/src/app_protocol.rs | 39 ++++- ng-net/src/lib.rs | 2 + ng-net/src/orm.rs | 24 +++ ng-verifier/src/commits/transaction.rs | 5 + ng-verifier/src/lib.rs | 2 + ng-verifier/src/orm.rs | 144 ++++++++++++++++++ ng-verifier/src/request_processor.rs | 17 +++ ng-verifier/src/types.rs | 11 ++ ng-verifier/src/verifier.rs | 5 +- pnpm-lock.yaml | 2 + .../src/ng-mock/wasm-land/shapeHandler.ts | 24 ++- sdk/ng-sdk-js/src/lib.rs | 40 +++++ 13 files changed, 311 insertions(+), 7 deletions(-) create mode 100644 ng-net/src/orm.rs create mode 100644 ng-verifier/src/orm.rs diff --git a/nextgraph/src/lib.rs b/nextgraph/src/lib.rs index 2052b25..d156190 100644 --- a/nextgraph/src/lib.rs +++ b/nextgraph/src/lib.rs @@ -93,6 +93,9 @@ pub mod verifier { pub mod protocol { pub use ng_net::app_protocol::*; } + pub mod orm { + pub use ng_verifier::orm::*; + } pub use ng_verifier::prepare_app_response_for_js; pub use ng_verifier::read_triples_in_app_response_from_rust; pub use ng_verifier::triples_ser_to_json_string; diff --git a/ng-net/src/app_protocol.rs b/ng-net/src/app_protocol.rs index bc306cf..cefd749 100644 --- a/ng-net/src/app_protocol.rs +++ b/ng-net/src/app_protocol.rs @@ -18,7 +18,9 @@ use ng_repo::repo::CommitInfo; use ng_repo::types::*; 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}; use crate::types::*; #[derive(Clone, Debug, Serialize, Deserialize)] @@ -48,7 +50,7 @@ impl AppFetchContentV0 { } } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] pub enum NgAccessV0 { ReadCap(ReadCap), Token(Digest), @@ -59,7 +61,7 @@ pub enum NgAccessV0 { Topic(PrivKey), } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] pub enum TargetBranchV0 { Chat, Stream, @@ -93,7 +95,7 @@ impl TargetBranchV0 { } } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq)] pub enum NuriTargetV0 { UserSite, // targets the whole data set of the user @@ -175,7 +177,7 @@ impl From<&CommitInfo> for CommitInfoJs { const DID_PREFIX: &str = "did:ng"; -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] pub struct NuriV0 { pub identity: Option, // None for personal identity pub target: NuriTargetV0, @@ -711,12 +713,15 @@ pub enum AppRequestCommandV0 { SocialQueryCancel, QrCodeProfile, QrCodeProfileImport, + OrmStart, + OrmUpdate, + OrmStop, } impl AppRequestCommandV0 { pub fn is_stream(&self) -> bool { match self { - Self::Fetch(AppFetchContentV0::Subscribe) | Self::FileGet => true, + Self::Fetch(AppFetchContentV0::Subscribe) | Self::FileGet | Self::OrmStart => true, _ => false, } } @@ -805,6 +810,24 @@ impl AppRequest { session_id: 0, }) } + + pub fn new_orm_start(scope: NuriV0, shape_type: OrmShapeType) -> Self { + AppRequest::new( + AppRequestCommandV0::OrmStart, + scope, + Some(AppRequestPayload::V0(AppRequestPayloadV0::OrmStart(shape_type))), + ) + } + + pub fn new_orm_update(scope: NuriV0, shape_type_name: String, diff: OrmDiff) -> Self { + AppRequest::new( + AppRequestCommandV0::OrmUpdate, + scope, + Some(AppRequestPayload::V0(AppRequestPayloadV0::OrmUpdate((diff,shape_type_name)))), + ) + } + + pub fn inbox_post(post: InboxPost) -> Self { AppRequest::new( @@ -1039,6 +1062,9 @@ pub enum AppRequestPayloadV0 { //Invoke(InvokeArguments), QrCodeProfile(u32), QrCodeProfileImport(String), + OrmStart(OrmShapeType), + OrmUpdate((OrmDiff, String)), // ShapeID + OrmStop(String), //ShapeID } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -1288,6 +1314,9 @@ pub enum AppResponseV0 { Nuri(String), Header(AppHeader), Commits(Vec), + OrmInitial(Value), + OrmUpdate(OrmDiff), + OrmError(String), } #[derive(Clone, Debug, Serialize, Deserialize)] diff --git a/ng-net/src/lib.rs b/ng-net/src/lib.rs index 2c58b79..e4143fb 100644 --- a/ng-net/src/lib.rs +++ b/ng-net/src/lib.rs @@ -16,6 +16,8 @@ pub mod app_protocol; pub mod broker; +pub mod orm; + pub mod server_broker; #[doc(hidden)] diff --git a/ng-net/src/orm.rs b/ng-net/src/orm.rs new file mode 100644 index 0000000..84376b7 --- /dev/null +++ b/ng-net/src/orm.rs @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers + * All rights reserved. + * Licensed under the Apache License, Version 2.0 + * + * or the MIT license , + * at your option. All files in the project carrying such + * notice may not be copied, modified, or distributed except + * according to those terms. +*/ + +use serde::{Deserialize, Serialize}; + +use serde_json::Value; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct OrmShapeType { + pub iri: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct OrmDiff { + +} \ No newline at end of file diff --git a/ng-verifier/src/commits/transaction.rs b/ng-verifier/src/commits/transaction.rs index 1fe7c44..adcbff7 100644 --- a/ng-verifier/src/commits/transaction.rs +++ b/ng-verifier/src/commits/transaction.rs @@ -748,6 +748,11 @@ impl Verifier { })), ) .await; + let graph_nuri = NuriV0::repo_graph_name( + &update.repo_id, + &update.overlay_id, + ); + self.orm_update(&NuriV0::new_empty(), update.transaction.as_quads_patch(graph_nuri)).await; } } Ok(commit_nuris) diff --git a/ng-verifier/src/lib.rs b/ng-verifier/src/lib.rs index dfe5d12..d344386 100644 --- a/ng-verifier/src/lib.rs +++ b/ng-verifier/src/lib.rs @@ -18,6 +18,8 @@ mod user_storage; mod commits; +pub mod orm; + mod request_processor; mod inbox_processor; diff --git a/ng-verifier/src/orm.rs b/ng-verifier/src/orm.rs new file mode 100644 index 0000000..2e1e806 --- /dev/null +++ b/ng-verifier/src/orm.rs @@ -0,0 +1,144 @@ +// Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers +// All rights reserved. +// Licensed under the Apache License, Version 2.0 +// +// or the MIT license , +// at your option. All files in the project carrying such +// notice may not be copied, modified, or distributed except +// according to those terms. + +use std::collections::HashMap; + +use futures::channel::mpsc; + +use futures::SinkExt; +pub use ng_net::orm::OrmShapeType; +pub use ng_net::orm::OrmDiff; +use ng_oxigraph::oxigraph::sparql::{results::*, Query, QueryResults}; +use ng_oxigraph::oxrdf::Term; +use ng_oxigraph::oxrdf::Triple; +use ng_repo::log::*; +use ng_net::app_protocol::*; +use ng_net::{ + connection::NoiseFSM, + types::*, + utils::{Receiver, Sender}, +}; +use ng_repo::errors::NgError; + +use crate::types::*; +use crate::verifier::*; + +impl Verifier { + + fn sparql_construct(&self, query: String) -> Result, NgError> { + let oxistore = self.graph_dataset.as_ref().unwrap(); + + // let graph_nuri = NuriV0::repo_graph_name( + // &update.repo_id, + // &update.overlay_id, + // ); + + //let base = NuriV0::repo_id(&repo.id); + let parsed = Query::parse(&query, None).map_err(|e| NgError::OxiGraphError(e.to_string()))?; + let results = oxistore + .query(parsed, None) + .map_err(|e| NgError::OxiGraphError(e.to_string()))?; + match results { + QueryResults::Graph(triples) => { + let mut results = vec![]; + for t in triples { + match t { + Err(e) => { log_err!("{}",e.to_string()); return Err(NgError::SparqlError(e.to_string()))}, + Ok(triple) => results.push(triple), + } + } + Ok(results) + } + _ => return Err(NgError::InvalidResponse), + } + } + + fn sparql_select(&self, query: String) -> Result>>, NgError> { + let oxistore = self.graph_dataset.as_ref().unwrap(); + + // let graph_nuri = NuriV0::repo_graph_name( + // &update.repo_id, + // &update.overlay_id, + // ); + + //let base = NuriV0::repo_id(&repo.id); + let parsed = Query::parse(&query, None).map_err(|e| NgError::OxiGraphError(e.to_string()))?; + let results = oxistore + .query(parsed, None) + .map_err(|e| NgError::OxiGraphError(e.to_string()))?; + match results { + QueryResults::Solutions(sols) => { + let mut results = vec![]; + for t in sols { + match t { + Err(e) => { log_err!("{}",e.to_string()); return Err(NgError::SparqlError(e.to_string()))}, + Ok(querysol) => results.push(querysol.values().to_vec()), + } + } + Ok(results) + } + _ => return Err(NgError::InvalidResponse), + } + } + + pub(crate) async fn orm_update(&mut self, scope: &NuriV0, patch: GraphQuadsPatch) { + + } + + pub(crate) async fn frontend_update_orm(&mut self, scope: &NuriV0, shape_id: String, diff: OrmDiff) { + + } + + pub(crate) async fn push_orm_response(&mut self, scope: &NuriV0, schema_iri: &String, response: AppResponse) { + log_info!( + "push_orm_response {:?} {} {:?}", + scope, + schema_iri, + self.orm_subscriptions + ); + if let Some(shapes) = self.orm_subscriptions.get_mut(scope) { + if let Some(sessions) = shapes.get_mut(schema_iri) { + let mut sessions_to_close : Vec = vec![]; + for (session_id, sender) in sessions.iter_mut() { + if sender.is_closed() { + log_debug!("closed so removing session {}", session_id); + sessions_to_close.push(*session_id); + } else { + let _ = sender.send(response.clone()).await; + } + } + for session_id in sessions_to_close.iter() { + sessions.remove(session_id); + } + } + } + } + + pub(crate) async fn start_orm( + &mut self, + nuri: &NuriV0, + schema: &OrmShapeType, + session_id: u64, + ) -> Result<(Receiver, CancelFn), NgError> { + + let (tx, rx) = mpsc::unbounded::(); + + self.orm_subscriptions.insert(nuri.clone(), HashMap::from([(schema.iri.clone(), HashMap::from([(session_id, tx.clone())]))])); + + //self.push_orm_response().await; + + let close = Box::new(move || { + //log_debug!("CLOSE_CHANNEL of subscription for branch {}", branch_id); + if !tx.is_closed() { + tx.close_channel(); + } + }); + Ok((rx, close)) + } +} \ No newline at end of file diff --git a/ng-verifier/src/request_processor.rs b/ng-verifier/src/request_processor.rs index 8d92a07..13c6d5e 100644 --- a/ng-verifier/src/request_processor.rs +++ b/ng-verifier/src/request_processor.rs @@ -49,8 +49,17 @@ impl Verifier { command: &AppRequestCommandV0, nuri: &NuriV0, _payload: &Option, + session_id: u64 ) -> Result<(Receiver, CancelFn), NgError> { match command { + AppRequestCommandV0::OrmStart => { + match _payload { + Some(AppRequestPayload::V0(AppRequestPayloadV0::OrmStart(shape_type))) => { + self.start_orm(nuri, shape_type, session_id).await + }, + _ => return Err(NgError::InvalidArgument) + } + }, AppRequestCommandV0::Fetch(fetch) => match fetch { AppFetchContentV0::Subscribe => { let (repo_id, branch_id, store_repo) = @@ -858,6 +867,14 @@ impl Verifier { payload: Option, ) -> Result { match command { + AppRequestCommandV0::OrmUpdate => { + match payload { + Some(AppRequestPayload::V0(AppRequestPayloadV0::OrmUpdate((diff,shape_id)))) => { + self.frontend_update_orm(&nuri, shape_id, diff).await + }, + _ => return Err(NgError::InvalidArgument) + } + }, AppRequestCommandV0::SocialQueryStart => { let (from_profile, contacts_string, degree) = if let Some(AppRequestPayload::V0(AppRequestPayloadV0::SocialQueryStart{ from_profile, contacts, degree diff --git a/ng-verifier/src/types.rs b/ng-verifier/src/types.rs index 197e1e8..b2209aa 100644 --- a/ng-verifier/src/types.rs +++ b/ng-verifier/src/types.rs @@ -43,6 +43,11 @@ pub struct GraphTransaction { pub removes: Vec, } +pub struct GraphQuadsPatch { + pub inserts: Vec, + pub removes: Vec, +} + const TOKENIZED_COMMIT: &str = "did:ng:_"; impl GraphTransaction { @@ -52,6 +57,12 @@ impl GraphTransaction { removes: serde_bare::to_vec(&self.removes).unwrap(), } } + pub(crate) fn as_quads_patch(&self, graph_nuri: String) -> GraphQuadsPatch { + GraphQuadsPatch { + inserts: self.inserts.iter().map(|triple| triple.clone().in_graph(NamedNode::new(graph_nuri.clone()).unwrap())).collect(), + removes: self.removes.iter().map(|triple| triple.clone().in_graph(NamedNode::new(graph_nuri.clone()).unwrap())).collect(), + } + } pub(crate) fn tokenize_with_commit_id(&mut self, commit_id: ObjectId, repo_id: &RepoId) { for triple in self.inserts.iter_mut() { if let Subject::NamedNode(nn) = &triple.subject { diff --git a/ng-verifier/src/verifier.rs b/ng-verifier/src/verifier.rs index 494337e..964269b 100644 --- a/ng-verifier/src/verifier.rs +++ b/ng-verifier/src/verifier.rs @@ -111,6 +111,7 @@ pub struct Verifier { in_memory_outbox: Vec, uploads: BTreeMap, branch_subscriptions: HashMap>, + pub(crate) orm_subscriptions: HashMap>>>, pub(crate) temporary_repo_certificates: HashMap, } @@ -516,6 +517,7 @@ impl Verifier { inner_to_outer: HashMap::new(), uploads: BTreeMap::new(), branch_subscriptions: HashMap::new(), + orm_subscriptions: HashMap::new(), temporary_repo_certificates: HashMap::new(), } } @@ -2779,6 +2781,7 @@ impl Verifier { inner_to_outer: HashMap::new(), uploads: BTreeMap::new(), branch_subscriptions: HashMap::new(), + orm_subscriptions: HashMap::new(), temporary_repo_certificates: HashMap::new(), }; // this is important as it will load the last seq from storage @@ -2796,7 +2799,7 @@ impl Verifier { ) -> Result<(Receiver, CancelFn), NgError> { match req { AppRequest::V0(v0) => { - self.process_stream(&v0.command, &v0.nuri, &v0.payload) + self.process_stream(&v0.command, &v0.nuri, &v0.payload, v0.session_id) .await } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 52e9d09..f2a7be0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -280,6 +280,8 @@ importers: specifier: ^3.2.4 version: 3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.5.1)(jsdom@26.1.0) + sdk/ng-sdk-js/pkg: {} + packages: '@ampproject/remapping@2.3.0': diff --git a/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/shapeHandler.ts b/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/shapeHandler.ts index 8992b7b..61eafab 100644 --- a/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/shapeHandler.ts +++ b/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/shapeHandler.ts @@ -1,6 +1,7 @@ import * as shapeManager from "./shapeManager"; import type { WasmConnection, Diff, Scope } from "./types"; import type { ShapeType, BaseType } from "@nextgraph-monorepo/ng-shex-orm"; +import * as ng from "@nextgraph-monorepo/ng-sdk-js"; import type { Person } from "../../shapes/ldo/personShape.typings"; import type { Cat } from "../../shapes/ldo/catShape.typings"; import type { TestObject } from "../../shapes/ldo/testShape.typings"; @@ -94,9 +95,29 @@ communicationChannel.addEventListener( "message", (event: MessageEvent) => { console.log("BACKEND: Received message", event.data); + // call WASM ng-sdk-js const { type, connectionId, shapeType } = event.data; if (type === "Request") { + + /* unsub = await ng.orm_start(scope, shapeType, session_id, + async (response) => { + //console.log("GOT APP RESPONSE", response); + if (response.V0.OrmInitial) { + + } else if (response.V0.OrmUpdate) { + let diff = response.V0.OrmUpdate.diff + const msg: WasmMessage = { + type: "BackendUpdate", + connectionId, + diff, + }; + communicationChannel.postMessage(msg); + } else if (response.V0.OrmError) { + + } + */ + const shapeId = shapeType?.shape; const initialData = getInitialObjectByShapeId(shapeId); @@ -110,7 +131,7 @@ communicationChannel.addEventListener( // Notify js-land about backend updates const msg: WasmMessage = { type: "BackendUpdate", - connectionId: conId, + connectionId, diff, }; communicationChannel.postMessage(msg); @@ -128,6 +149,7 @@ communicationChannel.addEventListener( if (type === "Stop") { shapeManager.connections.delete(connectionId); + // await ng.app_request ( OrmStop ) return; } diff --git a/sdk/ng-sdk-js/src/lib.rs b/sdk/ng-sdk-js/src/lib.rs index aaddf64..fe2445e 100644 --- a/sdk/ng-sdk-js/src/lib.rs +++ b/sdk/ng-sdk-js/src/lib.rs @@ -56,6 +56,7 @@ use ng_wallet::*; use nextgraph::local_broker::*; use nextgraph::verifier::CancelFn; +use nextgraph::verifier::orm::{OrmShapeType, OrmDiff}; use crate::model::*; @@ -1782,6 +1783,45 @@ pub async fn doc_subscribe( app_request_stream_(request, callback).await } +#[wasm_bindgen] +pub async fn orm_start( + scope: JsValue, + shapeType: JsValue, + session_id: JsValue, + callback: &js_sys::Function, +) -> Result { + let session_id: u64 = serde_wasm_bindgen::from_value::(session_id) + .map_err(|_| "Deserialization error of session_id".to_string())?; + let scope: NuriV0 = serde_wasm_bindgen::from_value::(scope) + .map_err(|_| "Deserialization error of scope".to_string())?; + let shapeType: OrmShapeType = serde_wasm_bindgen::from_value::(shapeType) + .map_err(|e| format!("Deserialization error of shapeType {e}"))?; + let mut request = AppRequest::new_orm_start(scope, shapeType); + request.set_session_id(session_id); + app_request_stream_(request, callback).await +} + +#[wasm_bindgen] +pub async fn orm_update( + scope: JsValue, + shapeTypeName: String, + diff: JsValue, + session_id: JsValue, +) -> Result<(), String> { + let session_id: u64 = serde_wasm_bindgen::from_value::(session_id) + .map_err(|_| "Deserialization error of session_id".to_string())?; + let diff: OrmDiff = serde_wasm_bindgen::from_value::(diff) + .map_err(|e| format!("Deserialization error of diff {e}"))?; + let scope: NuriV0 = serde_wasm_bindgen::from_value::(scope) + .map_err(|_| "Deserialization error of scope".to_string())?; + let mut request = AppRequest::new_orm_update(scope, shapeTypeName, diff); + request.set_session_id(session_id); + let response = nextgraph::local_broker::app_request(request) + .await + .map_err(|e: NgError| e.to_string())?; + Ok(()) +} + // // #[wasm_bindgen] // pub async fn get_readcap() -> Result { // let request = ObjectRef::nil();