From 0287993dbf6bb567ea50662e658ef6787ceb818c Mon Sep 17 00:00:00 2001 From: Niko PLP Date: Fri, 17 May 2024 12:20:59 +0300 Subject: [PATCH] SPARQL endpoint for query and update --- Cargo.lock | 22 +- nextgraph/src/local_broker.rs | 60 +- ng-broker/src/server_broker.rs | 1 - ng-net/src/actors/app/request.rs | 3 +- ng-net/src/app_protocol.rs | 50 +- ng-sdk-js/Cargo.toml | 4 +- ng-sdk-js/app-node/index.js | 30 +- ng-sdk-js/src/lib.rs | 69 ++- ng-sdk-js/src/model.rs | 821 +++++++++++++++++++++++++++ ng-verifier/src/request_processor.rs | 87 +++ ng-verifier/src/verifier.rs | 3 +- 11 files changed, 1102 insertions(+), 48 deletions(-) create mode 100644 ng-sdk-js/src/model.rs diff --git a/Cargo.lock b/Cargo.lock index 5b7fc02..50e7a6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3451,9 +3451,11 @@ dependencies = [ "ng-repo", "ng-wallet", "once_cell", + "oxrdf", "rand 0.7.3", "serde", "serde-wasm-bindgen", + "serde_bare", "serde_bytes", "serde_json", "wasm-bindgen", @@ -3884,19 +3886,20 @@ checksum = "d05417ee46e2eb40dd9d590b4d67fc2408208b3a48a6b7f71d2bc1d7ce12a3e0" [[package]] name = "oxrdf" version = "0.2.0-alpha.4" -source = "git+https://git.nextgraph.org/NextGraph/oxigraph.git?branch=main#a4e2847810c384811c3efc0b844c1cbdb997cb71" +source = "git+https://git.nextgraph.org/NextGraph/oxigraph.git?branch=main#c7f873f904617c201e359196717eb2133d91cef5" dependencies = [ "oxilangtag", "oxiri", "oxsdatatypes", "rand 0.8.5", + "serde", "thiserror", ] [[package]] name = "oxrdfio" version = "0.1.0-alpha.5" -source = "git+https://git.nextgraph.org/NextGraph/oxigraph.git?branch=main#a4e2847810c384811c3efc0b844c1cbdb997cb71" +source = "git+https://git.nextgraph.org/NextGraph/oxigraph.git?branch=main#c7f873f904617c201e359196717eb2133d91cef5" dependencies = [ "oxrdf", "oxrdfxml", @@ -3907,7 +3910,7 @@ dependencies = [ [[package]] name = "oxrdfxml" version = "0.1.0-alpha.5" -source = "git+https://git.nextgraph.org/NextGraph/oxigraph.git?branch=main#a4e2847810c384811c3efc0b844c1cbdb997cb71" +source = "git+https://git.nextgraph.org/NextGraph/oxigraph.git?branch=main#c7f873f904617c201e359196717eb2133d91cef5" dependencies = [ "oxilangtag", "oxiri", @@ -3919,16 +3922,17 @@ dependencies = [ [[package]] name = "oxsdatatypes" version = "0.2.0-alpha.1" -source = "git+https://git.nextgraph.org/NextGraph/oxigraph.git?branch=main#a4e2847810c384811c3efc0b844c1cbdb997cb71" +source = "git+https://git.nextgraph.org/NextGraph/oxigraph.git?branch=main#c7f873f904617c201e359196717eb2133d91cef5" dependencies = [ "js-sys", + "serde", "thiserror", ] [[package]] name = "oxttl" version = "0.1.0-alpha.5" -source = "git+https://git.nextgraph.org/NextGraph/oxigraph.git?branch=main#a4e2847810c384811c3efc0b844c1cbdb997cb71" +source = "git+https://git.nextgraph.org/NextGraph/oxigraph.git?branch=main#c7f873f904617c201e359196717eb2133d91cef5" dependencies = [ "memchr", "oxilangtag", @@ -5238,7 +5242,7 @@ dependencies = [ [[package]] name = "sparesults" version = "0.2.0-alpha.4" -source = "git+https://git.nextgraph.org/NextGraph/oxigraph.git?branch=main#a4e2847810c384811c3efc0b844c1cbdb997cb71" +source = "git+https://git.nextgraph.org/NextGraph/oxigraph.git?branch=main#c7f873f904617c201e359196717eb2133d91cef5" dependencies = [ "json-event-parser", "memchr", @@ -5250,7 +5254,7 @@ dependencies = [ [[package]] name = "spargebra" version = "0.3.0-alpha.4" -source = "git+https://git.nextgraph.org/NextGraph/oxigraph.git?branch=main#a4e2847810c384811c3efc0b844c1cbdb997cb71" +source = "git+https://git.nextgraph.org/NextGraph/oxigraph.git?branch=main#c7f873f904617c201e359196717eb2133d91cef5" dependencies = [ "oxilangtag", "oxiri", @@ -5263,7 +5267,7 @@ dependencies = [ [[package]] name = "sparopt" version = "0.1.0-alpha.5-dev" -source = "git+https://git.nextgraph.org/NextGraph/oxigraph.git?branch=main#a4e2847810c384811c3efc0b844c1cbdb997cb71" +source = "git+https://git.nextgraph.org/NextGraph/oxigraph.git?branch=main#c7f873f904617c201e359196717eb2133d91cef5" dependencies = [ "oxrdf", "rand 0.8.5", @@ -6364,6 +6368,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" dependencies = [ "cfg-if", + "serde", + "serde_json", "wasm-bindgen-macro", ] diff --git a/nextgraph/src/local_broker.rs b/nextgraph/src/local_broker.rs index fa662e9..4537191 100644 --- a/nextgraph/src/local_broker.rs +++ b/nextgraph/src/local_broker.rs @@ -2071,21 +2071,26 @@ pub async fn app_request(request: AppRequest) -> Result { None | Some(Err(_)) => return Err(NgError::LocalBrokerNotInitialized), Some(Ok(broker)) => broker.write().await, }; - let (real_session_id, is_remote) = broker.get_real_session_id_for_mut(request.session_id())?; + match &broker.config { + LocalBrokerConfig::Headless(_) => { + broker.send_request_headless(request).await + }, + _ => { + let (real_session_id, is_remote) = broker.get_real_session_id_for_mut(request.session_id())?; - if is_remote { - let session = broker.remote_sessions_list[real_session_id] - .as_ref() - .ok_or(NgError::SessionNotFound)?; - session.send_request(request).await - } else { - let session = broker.opened_sessions_list[real_session_id] - .as_mut() - .ok_or(NgError::SessionNotFound)?; - session.verifier.app_request(request).await + if is_remote { + let session = broker.remote_sessions_list[real_session_id] + .as_ref() + .ok_or(NgError::SessionNotFound)?; + session.send_request(request).await + } else { + let session = broker.opened_sessions_list[real_session_id] + .as_mut() + .ok_or(NgError::SessionNotFound)?; + session.verifier.app_request(request).await + } + } } - - } /// process any type of app request that returns a stream of values @@ -2096,18 +2101,25 @@ pub async fn app_request_stream( None | Some(Err(_)) => return Err(NgError::LocalBrokerNotInitialized), Some(Ok(broker)) => broker.write().await, }; - let (real_session_id, is_remote) = broker.get_real_session_id_for_mut(request.session_id())?; + match &broker.config { + LocalBrokerConfig::Headless(_) => { + broker.send_request_stream_headless(request).await + }, + _ => { + let (real_session_id, is_remote) = broker.get_real_session_id_for_mut(request.session_id())?; - if is_remote { - let session = broker.remote_sessions_list[real_session_id] - .as_ref() - .ok_or(NgError::SessionNotFound)?; - session.send_request_stream(request).await - } else { - let session = broker.opened_sessions_list[real_session_id] - .as_mut() - .ok_or(NgError::SessionNotFound)?; - session.verifier.app_request_stream(request).await + if is_remote { + let session = broker.remote_sessions_list[real_session_id] + .as_ref() + .ok_or(NgError::SessionNotFound)?; + session.send_request_stream(request).await + } else { + let session = broker.opened_sessions_list[real_session_id] + .as_mut() + .ok_or(NgError::SessionNotFound)?; + session.verifier.app_request_stream(request).await + } + } } } diff --git a/ng-broker/src/server_broker.rs b/ng-broker/src/server_broker.rs index 9bb61c0..f1ce5c2 100644 --- a/ng-broker/src/server_broker.rs +++ b/ng-broker/src/server_broker.rs @@ -462,7 +462,6 @@ impl IServerBroker for ServerBroker { .app_request(req) .await .map_err(|e| e.into()); - fsm.lock() .await .send_in_reply_to(res.into(), request_id) diff --git a/ng-net/src/actors/app/request.rs b/ng-net/src/actors/app/request.rs index a51a387..78c26a3 100644 --- a/ng-net/src/actors/app/request.rs +++ b/ng-net/src/actors/app/request.rs @@ -101,7 +101,8 @@ impl From for AppMessage { impl From for ProtocolMessage { fn from(response: AppResponse) -> ProtocolMessage { - response.into() + let app_msg: AppMessage = response.into(); + app_msg.into() } } diff --git a/ng-net/src/app_protocol.rs b/ng-net/src/app_protocol.rs index df3ea38..bd1e454 100644 --- a/ng-net/src/app_protocol.rs +++ b/ng-net/src/app_protocol.rs @@ -115,6 +115,18 @@ impl NuriV0 { locator: vec![], } } + pub fn new_entire_user_site() -> Self { + Self { + target: NuriTargetV0::UserSite, + entire_store: false, + object: None, + branch: None, + overlay: None, + access: vec![], + topic: None, + locator: vec![], + } + } pub fn new(_from: String) -> Self { todo!(); } @@ -134,10 +146,21 @@ pub enum AppRequestCommandV0 { impl AppRequestCommandV0 { pub fn is_stream(&self) -> bool { match self { - Self::FilePut | Self::Create | Self::Delete | Self::UnPin | Self::Pin => false, - Self::Fetch(_) | Self::FileGet => true, + Self::Fetch(AppFetchContentV0::Subscribe) | Self::FileGet => true, + Self::FilePut + | Self::Create + | Self::Delete + | Self::UnPin + | Self::Pin + | Self::Fetch(_) => false, } } + pub fn new_read_query() -> Self { + AppRequestCommandV0::Fetch(AppFetchContentV0::ReadQuery) + } + pub fn new_write_query() -> Self { + AppRequestCommandV0::Fetch(AppFetchContentV0::WriteQuery) + } } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -323,6 +346,12 @@ pub enum AppRequestPayload { V0(AppRequestPayloadV0), } +impl AppRequestPayload { + pub fn new_sparql_query(query: String) -> Self { + AppRequestPayload::V0(AppRequestPayloadV0::Query(DocQuery::V0(query))) + } +} + #[derive(Clone, Debug, Serialize, Deserialize)] pub enum DiscretePatch { /// A yrs::Update @@ -402,11 +431,26 @@ pub enum AppResponseV0 { #[serde(with = "serde_bytes")] FileBinary(Vec), FileMeta(FileMetaV0), - QueryResult, // see sparesults + #[serde(with = "serde_bytes")] + QueryResult(Vec), // a serialized [SPARQL Query Results JSON Format](https://www.w3.org/TR/sparql11-results-json/) + #[serde(with = "serde_bytes")] + Graph(Vec), // a serde serialization of a list of triples. can be transformed on the client side to RDF-JS data model, or JSON-LD, or else (Turtle,...) http://rdf.js.org/data-model-spec/ Ok, + True, + False, + Error(String), } #[derive(Clone, Debug, Serialize, Deserialize)] pub enum AppResponse { V0(AppResponseV0), } + +impl AppResponse { + pub fn error(err: String) -> Self { + AppResponse::V0(AppResponseV0::Error(err)) + } + pub fn ok() -> Self { + AppResponse::V0(AppResponseV0::Ok) + } +} diff --git a/ng-sdk-js/Cargo.toml b/ng-sdk-js/Cargo.toml index fc26abf..24f315e 100644 --- a/ng-sdk-js/Cargo.toml +++ b/ng-sdk-js/Cargo.toml @@ -21,18 +21,20 @@ crate-type = ["cdylib"] [dependencies] serde = { version = "1.0", features = ["derive"] } +serde_bare = "0.5.0" serde_bytes = "0.11.7" serde_json = "1.0" async-std = { version = "1.12.0", features = ["attributes","unstable"] } once_cell = "1.17.1" getrandom = { version = "0.1.1", features = ["wasm-bindgen"] } rand = { version = "0.7", features = ["getrandom"] } -wasm-bindgen = "0.2" +wasm-bindgen = { version = "0.2", features = ["serde-serialize"] } ng-repo = { path = "../ng-repo" } ng-net = { path = "../ng-net" } ng-client-ws = { path = "../ng-client-ws" } ng-wallet = { path = "../ng-wallet" } nextgraph = { path = "../nextgraph" } +oxrdf = { git = "https://git.nextgraph.org/NextGraph/oxigraph.git", branch="main", features = ["rdf-star", "oxsdatatypes"] } # [target.'cfg(target_arch = "wasm32")'.dependencies.getrandom] # version = "0.2.7" # features = ["js"] diff --git a/ng-sdk-js/app-node/index.js b/ng-sdk-js/app-node/index.js index 1752f5f..cb60663 100644 --- a/ng-sdk-js/app-node/index.js +++ b/ng-sdk-js/app-node/index.js @@ -19,22 +19,38 @@ let config = { }; ng.init_headless(config).then( async() => { - + let session_id; try { - //let user_id = "ABIojb8XGAGCL4R_-81Kix8vJnSsfpvu8jwi6T-wTQWt"; - //let user_id = "ABA1FBm8ofqCXutaf96pRTWvgXDaCG2JLuRlthzoV4a2"; - let user_id = "AJQ5gCLoXXjalC9diTDCvxxWu5ZQUcYWEE821nhVRMcE"; - //let user_id = await ng.admin_create_user(config); + let user_id = await ng.admin_create_user(config); console.log("user created: ",user_id); - let session = await ng.session_headless_start(user_id); + let other_user_id = "AJQ5gCLoXXjalC9diTDCvxxWu5ZQUcYWEE821nhVRMcE"; + + let session = await ng.session_headless_start(other_user_id); + session_id = session.session_id; console.log(session); + + let sparql_result = await ng.sparql_query(session.session_id, "SELECT * WHERE { ?s ?p ?o }"); + console.log(sparql_result); + + let quads = await ng.sparql_query(session.session_id, "CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }"); + for (const q of quads) { + console.log(q.subject.toString(), q.predicate.toString(), q.object.toString(), q.graph.toString()) + } + + let result = await ng.sparql_update(session.session_id, "INSERT DATA { }"); + console.log(result); - let res = await ng.session_headless_stop(session.session_id, true); + // the 2nd argument `false` means do not `force_close` the dataset. + // it stays in memory even when the session is stopped. (not all the dataset is in memory. just some metadata) + // if you set this to true, the dataset is closed and removed from memory on the server. + // next time you will open a session for this user, the dataset will be loaded again. + let res = await ng.session_headless_stop(session.session_id, false); console.log(res); } catch (e) { console.error(e); + if (session_id) await ng.session_headless_stop(session_id, true); } }) .catch(err => { diff --git a/ng-sdk-js/src/lib.rs b/ng-sdk-js/src/lib.rs index fe91365..81b4427 100644 --- a/ng-sdk-js/src/lib.rs +++ b/ng-sdk-js/src/lib.rs @@ -11,6 +11,8 @@ #![cfg(target_arch = "wasm32")] +mod model; + use std::collections::HashMap; use std::net::IpAddr; use std::str::FromStr; @@ -22,6 +24,8 @@ use serde::{Deserialize, Serialize}; use serde_json::json; // use js_sys::Reflect; use async_std::stream::StreamExt; +use js_sys::Array; +use oxrdf::Triple; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::JsFuture; @@ -53,6 +57,8 @@ use ng_wallet::*; use nextgraph::local_broker::*; +use crate::model::*; + #[wasm_bindgen] pub async fn get_local_bootstrap(location: String, invite: JsValue) -> JsValue { let res = retrieve_local_bootstrap(location, invite.as_string(), false).await; @@ -200,6 +206,68 @@ pub async fn session_headless_stop(session_id: JsValue, force_close: bool) -> Re Ok(()) } +#[cfg(wasmpack_target = "nodejs")] +#[wasm_bindgen] +pub async fn sparql_query(session_id: JsValue, sparql: String) -> Result { + let session_id: u64 = serde_wasm_bindgen::from_value::(session_id) + .map_err(|_| "Invalid session_id".to_string())?; + + let request = AppRequest::V0(AppRequestV0 { + command: AppRequestCommandV0::new_read_query(), + nuri: NuriV0::new_entire_user_site(), + payload: Some(AppRequestPayload::new_sparql_query(sparql)), + session_id, + }); + + let response = nextgraph::local_broker::app_request(request) + .await + .map_err(|e: NgError| e.to_string())?; + + let AppResponse::V0(res) = response; + match res { + AppResponseV0::False => return Ok(JsValue::FALSE), + AppResponseV0::True => return Ok(JsValue::TRUE), + AppResponseV0::Graph(graph) => { + let triples: Vec = serde_bare::from_slice(&graph) + .map_err(|_| "Deserialization error of graph".to_string())?; + + let results = Array::new(); + for triple in triples { + results.push(&JsQuad::from(triple).into()); + } + Ok(results.into()) + } + AppResponseV0::QueryResult(buf) => { + let string = String::from_utf8(buf) + .map_err(|_| "Deserialization error of JSON QueryResult String".to_string())?; + + js_sys::JSON::parse(&string) + } + AppResponseV0::Error(e) => Err(e.to_string().into()), + _ => Err("invalid AppResponse".to_string().into()), + } +} + +#[cfg(wasmpack_target = "nodejs")] +#[wasm_bindgen] +pub async fn sparql_update(session_id: JsValue, sparql: String) -> Result<(), String> { + let session_id: u64 = serde_wasm_bindgen::from_value::(session_id) + .map_err(|_| "Invalid session_id".to_string())?; + + let request = AppRequest::V0(AppRequestV0 { + command: AppRequestCommandV0::new_write_query(), + nuri: NuriV0::new_entire_user_site(), + payload: Some(AppRequestPayload::new_sparql_query(sparql)), + session_id, + }); + + let _ = nextgraph::local_broker::app_request(request) + .await + .map_err(|e: NgError| e.to_string())?; + + Ok(()) +} + #[cfg(wasmpack_target = "nodejs")] #[wasm_bindgen] pub async fn admin_create_user(js_config: JsValue) -> Result { @@ -531,7 +599,6 @@ pub async fn app_request_stream( } } } - //log_info!("END OF LOOP"); Ok(()) } diff --git a/ng-sdk-js/src/model.rs b/ng-sdk-js/src/model.rs new file mode 100644 index 0000000..3aa08a6 --- /dev/null +++ b/ng-sdk-js/src/model.rs @@ -0,0 +1,821 @@ +#![allow(dead_code, clippy::inherent_to_string, clippy::unused_self)] + +use js_sys::{Error, Reflect, UriError}; +use oxrdf::Triple; +use oxrdf::*; +use wasm_bindgen::prelude::*; +use wasm_bindgen::JsValue; + +#[macro_export] +macro_rules! format_err { + ($msg:literal $(,)?) => { + ::wasm_bindgen::JsValue::from(::js_sys::Error::new($msg)) + }; + ($fmt:literal, $($arg:tt)*) => { + ::wasm_bindgen::JsValue::from(::js_sys::Error::new(&format!($fmt, $($arg)*))) + }; +} + +#[allow(clippy::needless_pass_by_value)] +pub fn to_err(e: impl ToString) -> JsValue { + JsValue::from(Error::new(&e.to_string())) +} + +thread_local! { + pub static FROM_JS: FromJsConverter = FromJsConverter::default(); +} + +#[wasm_bindgen(js_name = namedNode)] +pub fn named_node(value: String) -> Result { + NamedNode::new(value) + .map(Into::into) + .map_err(|v| UriError::new(&v.to_string()).into()) +} + +#[wasm_bindgen(js_name = blankNode)] +pub fn blank_node(value: Option) -> Result { + Ok(if let Some(value) = value { + BlankNode::new(value).map_err(to_err)? + } else { + BlankNode::default() + } + .into()) +} + +#[wasm_bindgen] +pub fn literal( + value: Option, + language_or_datatype: &JsValue, +) -> Result { + if language_or_datatype.is_null() || language_or_datatype.is_undefined() { + Ok(Literal::new_simple_literal(value.unwrap_or_default()).into()) + } else if language_or_datatype.is_string() { + Ok(Literal::new_language_tagged_literal( + value.unwrap_or_default(), + language_or_datatype.as_string().unwrap_or_default(), + ) + .map_err(to_err)? + .into()) + } else if let JsTerm::NamedNode(datatype) = FROM_JS.with(|c| c.to_term(language_or_datatype))? { + Ok(Literal::new_typed_literal(value.unwrap_or_default(), datatype).into()) + } else { + Err(format_err!("The literal datatype should be a NamedNode")) + } +} + +#[wasm_bindgen(js_name = defaultGraph)] +pub fn default_graph() -> JsDefaultGraph { + JsDefaultGraph +} + +#[wasm_bindgen(js_name = variable)] +pub fn variable(value: String) -> Result { + Ok(Variable::new(value).map_err(to_err)?.into()) +} + +#[wasm_bindgen(js_name = triple)] +pub fn triple(subject: &JsValue, predicate: &JsValue, object: &JsValue) -> Result { + quad(subject, predicate, object, &JsValue::UNDEFINED) +} + +#[wasm_bindgen(js_name = quad)] +pub fn quad( + subject: &JsValue, + predicate: &JsValue, + object: &JsValue, + graph: &JsValue, +) -> Result { + Ok(FROM_JS + .with(|c| c.to_quad_from_parts(subject, predicate, object, graph))? + .into()) +} + +#[wasm_bindgen(js_name = fromTerm)] +pub fn from_term(original: &JsValue) -> Result { + Ok(if original.is_null() { + JsValue::NULL + } else { + FROM_JS.with(|c| c.to_term(original))?.into() + }) +} + +#[wasm_bindgen(js_name = fromQuad)] +pub fn from_quad(original: &JsValue) -> Result { + Ok(if original.is_null() { + JsValue::NULL + } else { + JsQuad::from(FROM_JS.with(|c| c.to_quad(original))?).into() + }) +} + +#[wasm_bindgen(js_name = NamedNode)] +#[derive(Eq, PartialEq, Debug, Clone, Hash)] +pub struct JsNamedNode { + inner: NamedNode, +} + +#[wasm_bindgen(js_class = NamedNode)] +impl JsNamedNode { + #[wasm_bindgen(getter = termType)] + pub fn term_type(&self) -> String { + "NamedNode".to_owned() + } + + #[wasm_bindgen(getter)] + pub fn value(&self) -> String { + self.inner.as_str().to_owned() + } + + #[wasm_bindgen(js_name = toString)] + pub fn to_string(&self) -> String { + self.inner.to_string() + } + + pub fn equals(&self, other: &JsValue) -> bool { + if let Ok(Some(JsTerm::NamedNode(other))) = + FromJsConverter::default().to_optional_term(other) + { + self == &other + } else { + false + } + } +} + +impl From for JsNamedNode { + fn from(inner: NamedNode) -> Self { + Self { inner } + } +} + +impl From for NamedNode { + fn from(node: JsNamedNode) -> Self { + node.inner + } +} + +impl From for NamedOrBlankNode { + fn from(node: JsNamedNode) -> Self { + node.inner.into() + } +} + +impl From for Subject { + fn from(node: JsNamedNode) -> Self { + node.inner.into() + } +} + +impl From for Term { + fn from(node: JsNamedNode) -> Self { + node.inner.into() + } +} + +impl From for GraphName { + fn from(node: JsNamedNode) -> Self { + node.inner.into() + } +} + +#[wasm_bindgen(js_name = BlankNode)] +#[derive(Eq, PartialEq, Debug, Clone, Hash)] +pub struct JsBlankNode { + inner: BlankNode, +} + +#[wasm_bindgen(js_class = BlankNode)] +impl JsBlankNode { + #[wasm_bindgen(getter = termType)] + pub fn term_type(&self) -> String { + "BlankNode".to_owned() + } + + #[wasm_bindgen(getter)] + pub fn value(&self) -> String { + self.inner.as_str().to_owned() + } + + #[wasm_bindgen(js_name = toString)] + pub fn to_string(&self) -> String { + self.inner.to_string() + } + + pub fn equals(&self, other: &JsValue) -> bool { + if let Ok(Some(JsTerm::BlankNode(other))) = + FromJsConverter::default().to_optional_term(other) + { + self == &other + } else { + false + } + } +} + +impl From for JsBlankNode { + fn from(inner: BlankNode) -> Self { + Self { inner } + } +} + +impl From for BlankNode { + fn from(node: JsBlankNode) -> Self { + node.inner + } +} + +impl From for NamedOrBlankNode { + fn from(node: JsBlankNode) -> Self { + node.inner.into() + } +} + +impl From for Subject { + fn from(node: JsBlankNode) -> Self { + node.inner.into() + } +} + +impl From for Term { + fn from(node: JsBlankNode) -> Self { + node.inner.into() + } +} + +impl From for GraphName { + fn from(node: JsBlankNode) -> Self { + node.inner.into() + } +} + +#[wasm_bindgen(js_name = Literal)] +#[derive(Eq, PartialEq, Debug, Clone, Hash)] +pub struct JsLiteral { + inner: Literal, +} + +#[wasm_bindgen(js_class = Literal)] +impl JsLiteral { + #[wasm_bindgen(getter = termType)] + pub fn term_type(&self) -> String { + "Literal".to_owned() + } + + #[wasm_bindgen(getter)] + pub fn value(&self) -> String { + self.inner.value().to_owned() + } + + #[wasm_bindgen(getter)] + pub fn language(&self) -> String { + self.inner.language().unwrap_or("").to_owned() + } + + #[wasm_bindgen(getter)] + pub fn datatype(&self) -> JsNamedNode { + self.inner.datatype().into_owned().into() + } + + #[wasm_bindgen(js_name = toString)] + pub fn to_string(&self) -> String { + self.inner.to_string() + } + + pub fn equals(&self, other: &JsValue) -> bool { + if let Ok(Some(JsTerm::Literal(other))) = FromJsConverter::default().to_optional_term(other) + { + self == &other + } else { + false + } + } +} + +impl From for JsLiteral { + fn from(inner: Literal) -> Self { + Self { inner } + } +} + +impl From for Literal { + fn from(node: JsLiteral) -> Self { + node.inner + } +} + +impl From for Term { + fn from(node: JsLiteral) -> Self { + node.inner.into() + } +} + +#[wasm_bindgen(js_name = DefaultGraph)] +#[derive(Eq, PartialEq, Debug, Clone, Hash)] +pub struct JsDefaultGraph; + +#[wasm_bindgen(js_class = DefaultGraph)] +impl JsDefaultGraph { + #[wasm_bindgen(getter = termType)] + pub fn term_type(&self) -> String { + "DefaultGraph".to_owned() + } + + #[wasm_bindgen(getter)] + pub fn value(&self) -> String { + String::new() + } + + #[wasm_bindgen(js_name = toString)] + pub fn to_string(&self) -> String { + "DEFAULT".to_owned() + } + + pub fn equals(&self, other: &JsValue) -> bool { + if let Ok(Some(JsTerm::DefaultGraph(other))) = + FromJsConverter::default().to_optional_term(other) + { + self == &other + } else { + false + } + } +} + +#[wasm_bindgen(js_name = Variable)] +#[derive(Eq, PartialEq, Debug, Clone, Hash)] +pub struct JsVariable { + inner: Variable, +} + +#[wasm_bindgen(js_class = Variable)] +impl JsVariable { + #[wasm_bindgen(getter = termType)] + pub fn term_type(&self) -> String { + "Variable".to_owned() + } + + #[wasm_bindgen(getter)] + pub fn value(&self) -> String { + self.inner.as_str().to_owned() + } + + #[wasm_bindgen(js_name = toString)] + pub fn to_string(&self) -> String { + self.inner.to_string() + } + + pub fn equals(&self, other: &JsValue) -> bool { + if let Ok(Some(JsTerm::Variable(other))) = + FromJsConverter::default().to_optional_term(other) + { + self == &other + } else { + false + } + } +} + +impl From for JsVariable { + fn from(inner: Variable) -> Self { + Self { inner } + } +} + +impl From for Variable { + fn from(node: JsVariable) -> Self { + node.inner + } +} + +#[wasm_bindgen(js_name = Quad)] +#[derive(Eq, PartialEq, Debug, Clone, Hash)] +pub struct JsQuad { + inner: Quad, +} + +#[wasm_bindgen(js_class = Quad)] +impl JsQuad { + #[wasm_bindgen(getter = termType)] + pub fn term_type(&self) -> String { + "Quad".to_owned() + } + + #[wasm_bindgen(getter)] + pub fn value(&self) -> String { + String::new() + } + + #[wasm_bindgen(getter = subject)] + pub fn subject(&self) -> JsValue { + JsTerm::from(self.inner.subject.clone()).into() + } + + #[wasm_bindgen(getter = predicate)] + pub fn predicate(&self) -> JsValue { + JsTerm::from(self.inner.predicate.clone()).into() + } + + #[wasm_bindgen(getter = object)] + pub fn object(&self) -> JsValue { + JsTerm::from(self.inner.object.clone()).into() + } + + #[wasm_bindgen(getter = graph)] + pub fn graph(&self) -> JsValue { + JsTerm::from(self.inner.graph_name.clone()).into() + } + + #[wasm_bindgen(js_name = toString)] + pub fn to_string(&self) -> String { + self.inner.to_string() + } + + pub fn equals(&self, other: &JsValue) -> bool { + if let Ok(Some(JsTerm::Quad(other))) = FromJsConverter::default().to_optional_term(other) { + self == &other + } else { + false + } + } +} + +impl From for JsQuad { + fn from(inner: Triple) -> Self { + Self { + inner: inner.in_graph(GraphName::DefaultGraph), + } + } +} + +impl From for JsQuad { + fn from(inner: Quad) -> Self { + Self { inner } + } +} + +impl From for Quad { + fn from(quad: JsQuad) -> Self { + quad.inner + } +} + +impl From for Triple { + fn from(quad: JsQuad) -> Self { + quad.inner.into() + } +} + +#[derive(Eq, PartialEq, Debug, Clone, Hash)] +pub enum JsTerm { + NamedNode(JsNamedNode), + BlankNode(JsBlankNode), + Literal(JsLiteral), + DefaultGraph(JsDefaultGraph), + Variable(JsVariable), + Quad(JsQuad), +} + +impl From for JsValue { + fn from(value: JsTerm) -> Self { + match value { + JsTerm::NamedNode(v) => v.into(), + JsTerm::BlankNode(v) => v.into(), + JsTerm::Literal(v) => v.into(), + JsTerm::DefaultGraph(v) => v.into(), + JsTerm::Variable(v) => v.into(), + JsTerm::Quad(v) => v.into(), + } + } +} + +impl From for JsTerm { + fn from(node: NamedNode) -> Self { + Self::NamedNode(node.into()) + } +} + +impl From for JsTerm { + fn from(node: BlankNode) -> Self { + Self::BlankNode(node.into()) + } +} + +impl From for JsTerm { + fn from(literal: Literal) -> Self { + Self::Literal(literal.into()) + } +} + +impl From for JsTerm { + fn from(node: NamedOrBlankNode) -> Self { + match node { + NamedOrBlankNode::NamedNode(node) => node.into(), + NamedOrBlankNode::BlankNode(node) => node.into(), + } + } +} + +impl From for JsTerm { + fn from(node: Subject) -> Self { + match node { + Subject::NamedNode(node) => node.into(), + Subject::BlankNode(node) => node.into(), + Subject::Triple(node) => node.into(), + } + } +} + +impl From for JsTerm { + fn from(term: Term) -> Self { + match term { + Term::NamedNode(node) => node.into(), + Term::BlankNode(node) => node.into(), + Term::Literal(literal) => literal.into(), + Term::Triple(node) => node.into(), + } + } +} + +impl From for JsTerm { + fn from(name: GraphName) -> Self { + match name { + GraphName::NamedNode(node) => node.into(), + GraphName::BlankNode(node) => node.into(), + GraphName::DefaultGraph => Self::DefaultGraph(JsDefaultGraph), + } + } +} + +impl From for JsTerm { + fn from(variable: Variable) -> Self { + Self::Variable(variable.into()) + } +} + +impl From for JsTerm { + fn from(triple: Triple) -> Self { + Self::Quad(triple.into()) + } +} + +impl From> for JsTerm { + fn from(triple: Box) -> Self { + triple.as_ref().clone().into() + } +} + +impl From for JsTerm { + fn from(quad: Quad) -> Self { + Self::Quad(quad.into()) + } +} + +impl TryFrom for NamedNode { + type Error = JsValue; + + fn try_from(value: JsTerm) -> Result { + match value { + JsTerm::NamedNode(node) => Ok(node.into()), + JsTerm::BlankNode(node) => Err(format_err!( + "The blank node {} is not a named node", + node.inner + )), + JsTerm::Literal(literal) => Err(format_err!( + "The literal {} is not a named node", + literal.inner + )), + JsTerm::DefaultGraph(_) => Err(format_err!("The default graph is not a named node")), + JsTerm::Variable(variable) => Err(format_err!( + "The variable {} is not a named node", + variable.inner + )), + JsTerm::Quad(quad) => Err(format_err!("The quad {} is not a named node", quad.inner)), + } + } +} + +impl TryFrom for NamedOrBlankNode { + type Error = JsValue; + + fn try_from(value: JsTerm) -> Result { + match value { + JsTerm::NamedNode(node) => Ok(node.into()), + JsTerm::BlankNode(node) => Ok(node.into()), + JsTerm::Literal(literal) => Err(format_err!( + "The literal {} is not a possible named or blank node term", + literal.inner + )), + JsTerm::DefaultGraph(_) => Err(format_err!( + "The default graph is not a possible named or blank node term" + )), + JsTerm::Variable(variable) => Err(format_err!( + "The variable {} is not a possible named or blank node term", + variable.inner + )), + JsTerm::Quad(quad) => Err(format_err!( + "The quad {} is not a possible named or blank node term", + quad.inner + )), + } + } +} + +impl TryFrom for Subject { + type Error = JsValue; + + fn try_from(value: JsTerm) -> Result { + match value { + JsTerm::NamedNode(node) => Ok(node.into()), + JsTerm::BlankNode(node) => Ok(node.into()), + JsTerm::Literal(literal) => Err(format_err!( + "The literal {} is not a possible RDF subject", + literal.inner + )), + JsTerm::DefaultGraph(_) => Err(format_err!( + "The default graph is not a possible RDF subject" + )), + JsTerm::Variable(variable) => Err(format_err!( + "The variable {} is not a possible RDF subject", + variable.inner + )), + JsTerm::Quad(quad) => Ok(Triple::from(quad).into()), + } + } +} + +impl TryFrom for Term { + type Error = JsValue; + + fn try_from(value: JsTerm) -> Result { + match value { + JsTerm::NamedNode(node) => Ok(node.into()), + JsTerm::BlankNode(node) => Ok(node.into()), + JsTerm::Literal(literal) => Ok(literal.into()), + JsTerm::DefaultGraph(_) => { + Err(format_err!("The default graph is not a possible RDF term")) + } + JsTerm::Variable(variable) => Err(format_err!( + "The variable {} is not a possible RDF term", + variable.inner + )), + JsTerm::Quad(quad) => Ok(Triple::from(quad).into()), + } + } +} + +impl TryFrom for GraphName { + type Error = JsValue; + + fn try_from(value: JsTerm) -> Result { + match value { + JsTerm::NamedNode(node) => Ok(node.into()), + JsTerm::BlankNode(node) => Ok(node.into()), + JsTerm::Literal(literal) => Err(format_err!( + "The literal {} is not a possible graph name", + literal.inner + )), + JsTerm::DefaultGraph(_) => Ok(Self::DefaultGraph), + JsTerm::Variable(variable) => Err(format_err!( + "The variable {} is not a possible RDF term", + variable.inner + )), + JsTerm::Quad(quad) => Err(format_err!( + "The quad {} is not a possible RDF term", + quad.inner + )), + } + } +} + +pub struct FromJsConverter { + term_type: JsValue, + value: JsValue, + language: JsValue, + datatype: JsValue, + subject: JsValue, + predicate: JsValue, + object: JsValue, + graph: JsValue, +} + +impl Default for FromJsConverter { + fn default() -> Self { + Self { + term_type: JsValue::from_str("termType"), + value: JsValue::from_str("value"), + language: JsValue::from_str("language"), + datatype: JsValue::from_str("datatype"), + subject: JsValue::from_str("subject"), + predicate: JsValue::from_str("predicate"), + object: JsValue::from_str("object"), + graph: JsValue::from_str("graph"), + } + } +} + +impl FromJsConverter { + pub fn to_term(&self, value: &JsValue) -> Result { + let term_type = Reflect::get(value, &self.term_type)?; + if let Some(term_type) = term_type.as_string() { + match term_type.as_str() { + "NamedNode" => Ok(NamedNode::new( + Reflect::get(value, &self.value)? + .as_string() + .ok_or_else(|| format_err!("NamedNode should have a string value"))?, + ) + .map_err(|v| UriError::new(&v.to_string()))? + .into()), + "BlankNode" => Ok(BlankNode::new( + Reflect::get(value, &self.value)? + .as_string() + .ok_or_else(|| format_err!("BlankNode should have a string value"))?, + ) + .map_err(to_err)? + .into()), + "Literal" => { + if let JsTerm::NamedNode(datatype) = + self.to_term(&Reflect::get(value, &self.datatype)?)? + { + let datatype = NamedNode::from(datatype); + let literal_value = Reflect::get(value, &self.value)? + .as_string() + .ok_or_else(|| format_err!("Literal should have a string value"))?; + Ok(match datatype.as_str() { + "http://www.w3.org/2001/XMLSchema#string" => Literal::new_simple_literal(literal_value), + "http://www.w3.org/1999/02/22-rdf-syntax-ns#langString" => Literal::new_language_tagged_literal(literal_value, Reflect::get(value, &self.language)?.as_string().ok_or_else( + || format_err!("Literal with rdf:langString datatype should have a language"), + )?).map_err(to_err)?, + _ => Literal::new_typed_literal(literal_value, datatype) + }.into()) + } else { + Err(format_err!( + "Literal should have a datatype that is a NamedNode" + )) + } + } + "DefaultGraph" => Ok(JsTerm::DefaultGraph(JsDefaultGraph)), + "Variable" => Ok(Variable::new( + Reflect::get(value, &self.value)? + .as_string() + .ok_or_else(|| format_err!("Variable should have a string value"))?, + ) + .map_err(to_err)? + .into()), + "Quad" => Ok(self.to_quad(value)?.into()), + _ => Err(format_err!( + "The termType {term_type} is not supported by Oxigraph" + )), + } + } else if term_type.is_undefined() { + // It's a quad without the proper type + if Reflect::has(value, &self.subject)? + && Reflect::has(value, &self.predicate)? + && Reflect::has(value, &self.object)? + { + Ok(self.to_quad(value)?.into()) + } else { + Err(format_err!( + "RDF term objects should have a termType attribute" + )) + } + } else { + Err(format_err!("The object termType field should be a string")) + } + } + + pub fn to_optional_term(&self, value: &JsValue) -> Result, JsValue> { + if value.is_null() || value.is_undefined() { + Ok(None) + } else { + self.to_term(value).map(Some) + } + } + + pub fn to_quad(&self, value: &JsValue) -> Result { + self.to_quad_from_parts( + &Reflect::get(value, &self.subject)?, + &Reflect::get(value, &self.predicate)?, + &Reflect::get(value, &self.object)?, + &Reflect::get(value, &self.graph)?, + ) + } + + pub fn to_quad_from_parts( + &self, + subject: &JsValue, + predicate: &JsValue, + object: &JsValue, + graph_name: &JsValue, + ) -> Result { + Ok(Quad { + subject: Subject::try_from(self.to_term(subject)?)?, + predicate: NamedNode::try_from(self.to_term(predicate)?)?, + object: Term::try_from(self.to_term(object)?)?, + graph_name: if graph_name.is_undefined() { + GraphName::DefaultGraph + } else { + GraphName::try_from(self.to_term(graph_name)?)? + }, + }) + } +} diff --git a/ng-verifier/src/request_processor.rs b/ng-verifier/src/request_processor.rs index 888fdd7..f3da9d6 100644 --- a/ng-verifier/src/request_processor.rs +++ b/ng-verifier/src/request_processor.rs @@ -14,6 +14,7 @@ use std::sync::Arc; use futures::channel::mpsc; use futures::SinkExt; use futures::StreamExt; +use ng_oxigraph::sparql::{results::*, Query, QueryResults}; use ng_repo::errors::*; use ng_repo::file::{RandomAccessFile, ReadFile}; @@ -152,6 +153,45 @@ impl Verifier { Ok((repo_id, branch, store_repo)) } + pub fn handle_query_results(results: QueryResults) -> Result { + Ok(match results { + QueryResults::Solutions(solutions) => { + let serializer = QueryResultsSerializer::from_format(QueryResultsFormat::Json); + + let mut solutions_writer = serializer + .serialize_solutions_to_write(Vec::new(), solutions.variables().to_vec()) + .map_err(|_| "QueryResult serializer error")?; + for solution in solutions { + solutions_writer + .write(&solution.map_err(|e| e.to_string())?) + .map_err(|_| "QueryResult serializer error")?; + } + AppResponse::V0(AppResponseV0::QueryResult( + solutions_writer + .finish() + .map_err(|_| "QueryResult serializer error")?, + )) + } + QueryResults::Boolean(b) => { + if b { + AppResponse::V0(AppResponseV0::True) + } else { + AppResponse::V0(AppResponseV0::False) + } + } + QueryResults::Graph(quads) => { + let mut results = vec![]; + for quad in quads { + match quad { + Err(e) => return Ok(AppResponse::error(e.to_string())), + Ok(triple) => results.push(triple), + } + } + AppResponse::V0(AppResponseV0::Graph(serde_bare::to_vec(&results).unwrap())) + } + }) + } + pub(crate) async fn process( &mut self, command: &AppRequestCommandV0, @@ -159,6 +199,53 @@ impl Verifier { payload: Option, ) -> Result { match command { + AppRequestCommandV0::Fetch(fetch) => match fetch { + AppFetchContentV0::ReadQuery => { + if let Some(AppRequestPayload::V0(AppRequestPayloadV0::Query(DocQuery::V0( + query, + )))) = payload + { + log_debug!("query={}", query); + let store = self.graph_dataset.as_ref().unwrap(); + let parsed = Query::parse(&query, None); + if parsed.is_err() { + return Ok(AppResponse::error(parsed.unwrap_err().to_string())); + } + let mut parsed = parsed.unwrap(); + parsed.dataset_mut().set_default_graph_as_union(); + + let results = store.query(parsed); + return Ok(match results { + Err(e) => AppResponse::error(e.to_string()), + Ok(qr) => { + let res = Self::handle_query_results(qr); + match res { + Ok(ok) => ok, + Err(s) => AppResponse::error(s), + } + } + }); + } else { + return Err(NgError::InvalidPayload); + } + } + AppFetchContentV0::WriteQuery => { + if let Some(AppRequestPayload::V0(AppRequestPayloadV0::Query(DocQuery::V0( + query, + )))) = payload + { + let store = self.graph_dataset.as_ref().unwrap(); + let res = store.update(&query); + return Ok(match res { + Err(e) => AppResponse::error(e.to_string()), + Ok(_) => AppResponse::ok(), + }); + } else { + return Err(NgError::InvalidPayload); + } + } + _ => unimplemented!(), + }, AppRequestCommandV0::FilePut => match payload { None => return Err(NgError::InvalidPayload), Some(AppRequestPayload::V0(v0)) => match v0 { diff --git a/ng-verifier/src/verifier.rs b/ng-verifier/src/verifier.rs index 61aec7f..37b5504 100644 --- a/ng-verifier/src/verifier.rs +++ b/ng-verifier/src/verifier.rs @@ -78,8 +78,7 @@ use crate::user_storage::UserStorage; pub struct Verifier { pub(crate) config: VerifierConfig, pub connected_broker: BrokerPeerId, - #[allow(dead_code)] - graph_dataset: Option, + pub(crate) graph_dataset: Option, pub(crate) user_storage: Option>>, block_storage: Option>>, last_seq_num: u64,