orm: improve schema, add schema to sparql + tests

feat/orm
Laurin Weger 3 weeks ago
parent b53abbd46c
commit 0b37d103c2
No known key found for this signature in database
GPG Key ID: 9B372BB0B792770F
  1. 1
      Cargo.lock
  2. 135
      nextgraph/src/tests/create_or_open_wallet.rs
  3. 0
      nextgraph/src/tests/get_or_create_wallet.rs
  4. 3
      nextgraph/src/tests/mod.rs
  5. 649
      nextgraph/src/tests/orm.rs
  6. 68
      ng-net/src/orm.rs
  7. 1
      ng-repo/src/errors.rs
  8. 1
      ng-verifier/Cargo.toml
  9. 251
      ng-verifier/src/orm.rs
  10. 320
      ng-verifier/src/request_processor.rs
  11. 119
      ng-verifier/src/verifier.rs
  12. 4
      sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/sparql/buildConstruct.ts
  13. 2
      sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/sparql/buildSparqlConstructFromShape.ts
  14. 26
      sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/sparql/testShape.schema.ts
  15. 165
      sdk/ng-sdk-js/examples/multi-framework-signals/src/shapes/ldo/catShape.schema.ts
  16. 147
      sdk/ng-sdk-js/examples/multi-framework-signals/src/shapes/ldo/personShape.schema.ts
  17. 301
      sdk/ng-sdk-js/examples/multi-framework-signals/src/shapes/ldo/testShape.schema.ts
  18. 49
      sdk/ng-sdk-js/ng-shex-orm/src/schema-converter/converter.ts
  19. 45
      sdk/ng-sdk-js/ng-shex-orm/src/schema-converter/transformers/ShexJSchemaTransformer.ts
  20. 16
      sdk/ng-sdk-js/ng-shex-orm/src/types.ts

1
Cargo.lock generated

@ -2424,6 +2424,7 @@ dependencies = [
"once_cell", "once_cell",
"qrcode", "qrcode",
"rand 0.7.3", "rand 0.7.3",
"regex",
"sbbf-rs-safe", "sbbf-rs-safe",
"serde", "serde",
"serde_bare", "serde_bare",

@ -0,0 +1,135 @@
// Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
// All rights reserved.
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
// 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::fs::{self, create_dir_all, File};
use std::io::{Read, Write};
use std::path::PathBuf;
use crate::local_broker::{
init_local_broker, session_start, wallet_create_v0, wallet_get_file, wallet_import,
wallet_open_with_mnemonic_words, wallet_read_file, LocalBrokerConfig, SessionConfig,
};
use ng_net::types::BootstrapContentV0;
use ng_repo::types::PubKey;
use ng_wallet::types::{CreateWalletV0, SensitiveWallet};
static WALLET_PIN: [u8; 4] = [2, 3, 2, 3];
// Persistent test assets (wallet base path + stored credentials)
fn test_base_path() -> PathBuf {
// Use the crate manifest dir so tests find files regardless of the
// process current working directory when `cargo test` runs.
let mut base = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
base.push("src");
base.push("tests");
base
}
fn build_wallet_and_creds_paths() -> (PathBuf, PathBuf) {
let mut base = test_base_path();
base.push(".ng");
create_dir_all(&base).expect("create test base path");
(base.join("test_wallet.ngw"), base.join("wallet_creds.txt"))
}
pub async fn create_or_open_wallet() -> (SensitiveWallet, u64) {
let base_path = test_base_path();
fs::create_dir_all(&base_path).expect("create base path");
init_local_broker(Box::new(move || {
LocalBrokerConfig::BasePath(base_path.clone())
}))
.await;
let wallet;
let session_id: u64;
let (wallet_path, creds_path) = build_wallet_and_creds_paths();
// Don't load from file due to a bug which makes reloading wallets fail.
if wallet_path.exists() && false {
// Read the wallet file from the known test base path (not the process cwd)
let wallet_file = fs::read(&wallet_path).expect("read wallet file");
// load stored wallet_name + mnemonic
let mut s = String::new();
File::open(creds_path)
.expect("open creds")
.read_to_string(&mut s)
.expect("read creds");
let mut lines = s.lines();
let mnemonic_line = lines.next().expect("missing mnemonic").to_string();
let mnemonic_words: Vec<String> = mnemonic_line
.split_whitespace()
.map(|s| s.to_string())
.collect();
let read_wallet = wallet_read_file(wallet_file).await.unwrap();
wallet =
wallet_open_with_mnemonic_words(&read_wallet, &mnemonic_words, WALLET_PIN).unwrap();
let _client = wallet_import(read_wallet.clone(), wallet.clone(), true)
.await
.unwrap();
let session = session_start(SessionConfig::new_in_memory(
&wallet.personal_identity(),
&read_wallet.name(),
))
.await
.unwrap();
session_id = session.session_id;
} else {
// first run: create wallet
// Load a real security image from the crate so tests don't depend on cwd.
// Try a few known candidate locations inside the crate.
let manifest_dir = test_base_path();
let security_img =
fs::read(manifest_dir.join("security-image.png")).expect("read sec image file");
let peer_id_of_server_broker = PubKey::nil();
let result = wallet_create_v0(CreateWalletV0 {
security_img,
security_txt: "know yourself".to_string(),
pin: WALLET_PIN,
pazzle_length: 9,
send_bootstrap: false,
send_wallet: false,
result_with_wallet_file: false,
local_save: false,
core_bootstrap: BootstrapContentV0::new_localhost(peer_id_of_server_broker),
core_registration: None,
additional_bootstrap: None,
pdf: false,
device_name: "test".to_string(),
})
.await
.expect("wallet_create_v0");
// Save wallet to file.
let wallet_bin = wallet_get_file(&result.wallet_name).await.unwrap();
let mut creds_file = File::create(creds_path).expect("create creds file");
let mut wallet_file = File::create(wallet_path).expect("create wallet file");
// Use the mnemonic_str already provided (list of words) to avoid mistakes
let mnemonic_words: Vec<String> = result.mnemonic_str.clone();
writeln!(creds_file, "{}", mnemonic_words.join(" ")).expect("write mnemonic to creds file");
creds_file.flush().expect("flush creds file");
wallet_file
.write_all(&wallet_bin)
.expect("write wallet file");
wallet = wallet_open_with_mnemonic_words(&result.wallet, &mnemonic_words, WALLET_PIN)
.expect("open wallet");
session_id = result.session_id;
}
return (wallet, session_id);
}

@ -9,3 +9,6 @@
#[doc(hidden)] #[doc(hidden)]
pub mod orm; pub mod orm;
#[doc(hidden)]
pub mod create_or_open_wallet;

@ -7,146 +7,33 @@
// 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 std::fs::{self, create_dir_all, File}; use crate::local_broker::{doc_create, doc_sparql_construct, doc_sparql_update};
use std::io::{Read, Write}; use crate::tests::create_or_open_wallet::create_or_open_wallet;
use std::path::PathBuf; use ng_net::orm::{
OrmSchemaDataType, OrmSchemaLiteralType, OrmSchemaLiterals, OrmSchemaPredicate, OrmSchemaShape,
use crate::local_broker::{ OrmShapeType,
doc_create, doc_sparql_construct, doc_sparql_update, init_local_broker, session_start,
session_stop, user_disconnect, wallet_close, wallet_create_v0, wallet_get_file, wallet_import,
wallet_open_with_mnemonic_words, wallet_read_file, LocalBrokerConfig, SessionConfig,
}; };
use ng_net::types::BootstrapContentV0;
use ng_repo::log_info; use ng_repo::log_info;
use ng_repo::types::PubKey; use ng_verifier::orm::sparql_construct_from_orm_shape_type;
use ng_wallet::types::{CreateWalletV0, SensitiveWallet}; use std::collections::HashMap;
use once_cell::sync::OnceCell;
static WALLET_PIN: [u8; 4] = [2, 3, 2, 3];
// Persistent test assets (wallet base path + stored credentials)
fn test_base_path() -> PathBuf {
// Use the crate manifest dir so tests find files regardless of the
// process current working directory when `cargo test` runs.
let mut base = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
base.push("src");
base.push("tests");
base
}
fn build_wallet_and_creds_paths() -> (PathBuf, PathBuf) {
let mut base = test_base_path();
base.push(".ng");
create_dir_all(&base).expect("create test base path");
(base.join("test_wallet.ngw"), base.join("wallet_creds.txt"))
}
static INIT: OnceCell<()> = OnceCell::new();
async fn init_broker() {
if INIT.get().is_none() {
let base = test_base_path();
fs::create_dir_all(&base).expect("create base path");
init_local_broker(Box::new(move || LocalBrokerConfig::BasePath(base.clone()))).await;
let _ = INIT.set(());
}
}
async fn create_or_open_wallet() -> (SensitiveWallet, u64) {
init_broker().await;
let wallet;
let session_id: u64;
let (wallet_path, creds_path) = build_wallet_and_creds_paths();
// Don't load from file due to a bug which makes reloading wallets fail.
if wallet_path.exists() && false {
// Read the wallet file from the known test base path (not the process cwd)
let wallet_file = fs::read(&wallet_path).expect("read wallet file");
// load stored wallet_name + mnemonic
let mut s = String::new();
File::open(creds_path)
.expect("open creds")
.read_to_string(&mut s)
.expect("read creds");
let mut lines = s.lines();
let mnemonic_line = lines.next().expect("missing mnemonic").to_string();
let mnemonic_words: Vec<String> = mnemonic_line
.split_whitespace()
.map(|s| s.to_string())
.collect();
let read_wallet = wallet_read_file(wallet_file).await.unwrap();
wallet =
wallet_open_with_mnemonic_words(&read_wallet, &mnemonic_words, WALLET_PIN).unwrap();
let _client = wallet_import(read_wallet.clone(), wallet.clone(), true)
.await
.unwrap();
let session = session_start(SessionConfig::new_in_memory(
&wallet.personal_identity(),
&read_wallet.name(),
))
.await
.unwrap();
session_id = session.session_id;
} else {
// first run: create wallet
// Load a real security image from the crate so tests don't depend on cwd.
// Try a few known candidate locations inside the crate.
let manifest_dir = test_base_path();
let security_img =
fs::read(manifest_dir.join("security-image.png")).expect("read sec image file");
let peer_id_of_server_broker = PubKey::nil();
let result = wallet_create_v0(CreateWalletV0 {
security_img,
security_txt: "know yourself".to_string(),
pin: WALLET_PIN,
pazzle_length: 9,
send_bootstrap: false,
send_wallet: false,
result_with_wallet_file: false,
local_save: false,
core_bootstrap: BootstrapContentV0::new_localhost(peer_id_of_server_broker),
core_registration: None,
additional_bootstrap: None,
pdf: false,
device_name: "test".to_string(),
})
.await
.expect("wallet_create_v0");
// Save wallet to file.
let wallet_bin = wallet_get_file(&result.wallet_name).await.unwrap();
let mut creds_file = File::create(creds_path).expect("create creds file");
let mut wallet_file = File::create(wallet_path).expect("create wallet file");
// Use the mnemonic_str already provided (list of words) to avoid mistakes #[async_std::test]
let mnemonic_words: Vec<String> = result.mnemonic_str.clone(); async fn test_create_sparql_from_schema() {
writeln!(creds_file, "{}", mnemonic_words.join(" ")).expect("write mnemonic to creds file"); // Setup wallet and document
creds_file.flush().expect("flush creds file"); let (_wallet, session_id) = create_or_open_wallet().await;
let doc_nuri = doc_create(
wallet_file session_id,
.write_all(&wallet_bin) "Graph".to_string(),
.expect("write wallet file"); "test_orm_query".to_string(),
"store".to_string(),
wallet = wallet_open_with_mnemonic_words(&result.wallet, &mnemonic_words, WALLET_PIN) None,
.expect("open wallet"); None,
session_id = result.session_id; )
} .await
.expect("error creating doc");
return (wallet, session_id);
}
fn build_insert_sparql() -> String { // Insert data with unrelated predicates
// Data conforms to testShape.shex let insert_sparql = r#"
// Shape requires: a ex:TestObject + required fields.
r#"
PREFIX ex: <http://example.org/> PREFIX ex: <http://example.org/>
INSERT DATA { INSERT DATA {
<urn:test:obj1> a ex:TestObject ; <urn:test:obj1> a ex:TestObject ;
@ -167,61 +54,487 @@ INSERT DATA {
ex:prop2 2 ex:prop2 2
] ; ] ;
ex:numOrStr "either" ; ex:numOrStr "either" ;
ex:lit1Or2 "lit1" . ex:lit1Or2 "lit1" ;
ex:unrelated "some value" ;
ex:anotherUnrelated 4242 .
} }
"# "#
.trim() .to_string();
.to_string()
doc_sparql_update(session_id, insert_sparql, Some(doc_nuri.clone()))
.await
.expect("SPARQL update failed");
// Define the ORM schema
let mut schema = HashMap::new();
schema.insert(
"http://example.org/TestObject||http://example.org/anotherObject".to_string(),
OrmSchemaShape {
iri: "http://example.org/TestObject||http://example.org/anotherObject".to_string(),
predicates: vec![
OrmSchemaPredicate {
dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::string,
literals: None,
shape: None,
}],
iri: "http://example.org/prop1".to_string(),
readablePredicate: "prop1".to_string(),
maxCardinality: 1,
minCardinality: 1,
extra: None,
},
OrmSchemaPredicate {
dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::number,
literals: None,
shape: None,
}],
iri: "http://example.org/prop2".to_string(),
readablePredicate: "prop2".to_string(),
maxCardinality: 1,
minCardinality: 1,
extra: None,
},
],
},
);
schema.insert(
"http://example.org/TestObject".to_string(),
OrmSchemaShape {
iri: "http://example.org/TestObject".to_string(),
predicates: vec![
OrmSchemaPredicate {
dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::literal,
literals: Some(OrmSchemaLiterals::StrArray(vec![
"http://example.org/TestObject".to_string(),
])),
shape: None,
}],
iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".to_string(),
readablePredicate: "type".to_string(),
maxCardinality: 1,
minCardinality: 1,
extra: Some(true),
},
OrmSchemaPredicate {
dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::string,
literals: None,
shape: None,
}],
iri: "http://example.org/stringValue".to_string(),
readablePredicate: "stringValue".to_string(),
maxCardinality: 1,
minCardinality: 1,
extra: None,
},
OrmSchemaPredicate {
dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::number,
literals: None,
shape: None,
}],
iri: "http://example.org/numValue".to_string(),
readablePredicate: "numValue".to_string(),
maxCardinality: 1,
minCardinality: 1,
extra: None,
},
OrmSchemaPredicate {
dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::boolean,
literals: None,
shape: None,
}],
iri: "http://example.org/boolValue".to_string(),
readablePredicate: "boolValue".to_string(),
maxCardinality: 1,
minCardinality: 1,
extra: None,
},
OrmSchemaPredicate {
dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::number,
literals: None,
shape: None,
}],
iri: "http://example.org/arrayValue".to_string(),
readablePredicate: "arrayValue".to_string(),
maxCardinality: -1,
minCardinality: 0,
extra: None,
},
OrmSchemaPredicate {
dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::shape,
literals: None,
shape: Some(
"http://example.org/TestObject||http://example.org/objectValue"
.to_string(),
),
}],
iri: "http://example.org/objectValue".to_string(),
readablePredicate: "objectValue".to_string(),
maxCardinality: 1,
minCardinality: 1,
extra: None,
},
OrmSchemaPredicate {
dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::shape,
literals: None,
shape: Some(
"http://example.org/TestObject||http://example.org/anotherObject"
.to_string(),
),
}],
iri: "http://example.org/anotherObject".to_string(),
readablePredicate: "anotherObject".to_string(),
maxCardinality: -1,
minCardinality: 0,
extra: None,
},
OrmSchemaPredicate {
dataTypes: vec![
OrmSchemaDataType {
valType: OrmSchemaLiteralType::string,
literals: None,
shape: None,
},
OrmSchemaDataType {
valType: OrmSchemaLiteralType::number,
literals: None,
shape: None,
},
],
iri: "http://example.org/numOrStr".to_string(),
readablePredicate: "numOrStr".to_string(),
maxCardinality: 1,
minCardinality: 1,
extra: None,
},
OrmSchemaPredicate {
dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::literal,
literals: Some(OrmSchemaLiterals::StrArray(vec![
"lit1".to_string(),
"lit2".to_string(),
])),
shape: None,
}],
iri: "http://example.org/lit1Or2".to_string(),
readablePredicate: "lit1Or2".to_string(),
maxCardinality: 1,
minCardinality: 1,
extra: None,
},
],
},
);
schema.insert(
"http://example.org/TestObject||http://example.org/objectValue".to_string(),
OrmSchemaShape {
iri: "http://example.org/TestObject||http://example.org/objectValue".to_string(),
predicates: vec![
OrmSchemaPredicate {
dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::string,
literals: None,
shape: None,
}],
iri: "http://example.org/nestedString".to_string(),
readablePredicate: "nestedString".to_string(),
maxCardinality: 1,
minCardinality: 1,
extra: None,
},
OrmSchemaPredicate {
dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::number,
literals: None,
shape: None,
}],
iri: "http://example.org/nestedNum".to_string(),
readablePredicate: "nestedNum".to_string(),
maxCardinality: 1,
minCardinality: 1,
extra: None,
},
OrmSchemaPredicate {
dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::number,
literals: None,
shape: None,
}],
iri: "http://example.org/nestedArray".to_string(),
readablePredicate: "nestedArray".to_string(),
maxCardinality: -1,
minCardinality: 0,
extra: None,
},
],
},
);
let shape_type = OrmShapeType {
schema,
shape: "http://example.org/TestObject".to_string(),
};
// Generate and execute the CONSTRUCT query
let query = sparql_construct_from_orm_shape_type(&shape_type, Some(1)).unwrap();
let triples = doc_sparql_construct(session_id, query, Some(doc_nuri.clone()))
.await
.expect("SPARQL construct failed");
// Assert the results
let predicates: Vec<String> = triples
.iter()
.map(|t| t.predicate.as_str().to_string())
.collect();
// Expected predicates based on the schema
let expected_predicates = vec![
"http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
"http://example.org/stringValue",
"http://example.org/numValue",
"http://example.org/boolValue",
"http://example.org/arrayValue",
"http://example.org/objectValue",
"http://example.org/anotherObject",
"http://example.org/numOrStr",
"http://example.org/lit1Or2",
"http://example.org/nestedString",
"http://example.org/nestedNum",
"http://example.org/nestedArray",
"http://example.org/prop1",
"http://example.org/prop2",
];
for p in expected_predicates {
assert!(
predicates.contains(&p.to_string()),
"Missing predicate: {}",
p
);
}
// Unrelated predicates should not be in the result
assert!(
!predicates.contains(&"http://example.org/unrelated".to_string()),
"Found unrelated predicate"
);
assert!(
!predicates.contains(&"http://example.org/anotherUnrelated".to_string()),
"Found another unrelated predicate"
);
} }
fn build_construct_sparql() -> String { #[async_std::test]
r#" async fn test_orm_query_partial_match_missing_required() {
CONSTRUCT { // Setup
?s ?p ?o . let (_wallet, session_id) = create_or_open_wallet().await;
} WHERE { let doc_nuri = doc_create(
?s ?p ?o . session_id,
"Graph".to_string(),
"test_orm_partial_required".to_string(),
"store".to_string(),
None,
None,
)
.await
.unwrap();
// Insert data missing a required field (`prop2`)
let insert_sparql = r#"
PREFIX ex: <http://example.org/>
INSERT DATA {
<urn:test:obj1> a ex:TestObject ;
ex:prop1 "one" .
} }
"# "#
.to_string() .to_string();
doc_sparql_update(session_id, insert_sparql, Some(doc_nuri.clone()))
.await
.unwrap();
// Schema with two required fields
let mut schema = HashMap::new();
schema.insert(
"http://example.org/TestObject".to_string(),
OrmSchemaShape {
iri: "http://example.org/TestObject".to_string(),
predicates: vec![
OrmSchemaPredicate {
iri: "http://example.org/prop1".to_string(),
minCardinality: 1,
..Default::default()
},
OrmSchemaPredicate {
iri: "http://example.org/prop2".to_string(),
minCardinality: 1,
..Default::default()
},
],
},
);
let shape_type = OrmShapeType {
schema,
shape: "http://example.org/TestObject".to_string(),
};
// Generate and run query
let query = sparql_construct_from_orm_shape_type(&shape_type, Some(1)).unwrap();
let triples = doc_sparql_construct(session_id, query, Some(doc_nuri.clone()))
.await
.unwrap();
// Assert: No triples should be returned as the object is incomplete.
assert!(triples.is_empty());
} }
#[async_std::test] #[async_std::test]
async fn test_wallet_and_sparql_insert() { async fn test_orm_query_partial_match_missing_optional() {
let (wallet, session_id) = create_or_open_wallet().await; // Setup
let (_wallet, session_id) = create_or_open_wallet().await;
let sparql = build_insert_sparql();
let doc_nuri = doc_create( let doc_nuri = doc_create(
session_id, session_id,
"Graph".to_string(), "Graph".to_string(),
"test".to_string(), "test_orm_partial_optional".to_string(),
"store".to_string(), "store".to_string(),
None, None,
None, None,
) )
.await .await
.expect("error creating doc"); .unwrap();
log_info!("session_id: {:?} doc nuri: {:?}", session_id, doc_nuri); // Insert data missing an optional field (`prop2`)
let insert_sparql = r#"
PREFIX ex: <http://example.org/>
INSERT DATA {
<urn:test:obj1> a ex:TestObject ;
ex:prop1 "one" .
}
"#
.to_string();
doc_sparql_update(session_id, insert_sparql, Some(doc_nuri.clone()))
.await
.unwrap();
let update_result = doc_sparql_update(session_id, sparql.clone(), Some(doc_nuri.clone())).await; // Schema with one required and one optional field
assert!( let mut schema = HashMap::new();
update_result.is_ok(), schema.insert(
"SPARQL update failed: {:?}", "http://example.org/TestObject".to_string(),
update_result.err() OrmSchemaShape {
iri: "http://example.org/TestObject".to_string(),
predicates: vec![
OrmSchemaPredicate {
iri: "http://example.org/prop1".to_string(),
minCardinality: 1,
..Default::default()
},
OrmSchemaPredicate {
iri: "http://example.org/prop2".to_string(),
minCardinality: 0, // Optional
..Default::default()
},
],
},
); );
log_info!("Sparql update result: {:?}", update_result.unwrap()); let shape_type = OrmShapeType {
schema,
shape: "http://example.org/TestObject".to_string(),
};
// Query the data. // Generate and run query
let query_result = let query = sparql_construct_from_orm_shape_type(&shape_type, Some(1)).unwrap();
doc_sparql_construct(session_id, build_construct_sparql(), Some(doc_nuri.clone())).await; let triples = doc_sparql_construct(session_id, query, Some(doc_nuri.clone()))
log_info!("Sparql construct result: {:?}", query_result.unwrap()); .await
.unwrap();
// Assert: One triple for prop1 should be returned.
assert_eq!(triples.len(), 1);
assert_eq!(triples[0].predicate.as_str(), "http://example.org/prop1");
}
#[async_std::test]
async fn test_orm_query_cyclic_schema() {
// Setup
let (_wallet, session_id) = create_or_open_wallet().await;
let doc_nuri = doc_create(
session_id,
"Graph".to_string(),
"test_orm_cyclic".to_string(),
"store".to_string(),
None,
None,
)
.await
.unwrap();
user_disconnect(&wallet.personal_identity()) // Insert cyclic data (two people who know each other)
let insert_sparql = r#"
PREFIX ex: <http://example.org/>
INSERT DATA {
<urn:p1> a ex:Person ; ex:name "Alice" ; ex:knows <urn:p2> .
<urn:p2> a ex:Person ; ex:name "Bob" ; ex:knows <urn:p1> .
}
"#
.to_string();
doc_sparql_update(session_id, insert_sparql, Some(doc_nuri.clone()))
.await .await
.expect("disconnect user"); .unwrap();
session_stop(&wallet.personal_identity())
// Cyclic schema: Person has a `knows` predicate pointing to another Person
let mut schema = HashMap::new();
schema.insert(
"http://example.org/Person".to_string(),
OrmSchemaShape {
iri: "http://example.org/Person".to_string(),
predicates: vec![
OrmSchemaPredicate {
iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".to_string(),
minCardinality: 1,
..Default::default()
},
OrmSchemaPredicate {
iri: "http://example.org/name".to_string(),
minCardinality: 1,
..Default::default()
},
OrmSchemaPredicate {
iri: "http://example.org/knows".to_string(),
minCardinality: 0,
maxCardinality: -1,
dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::shape,
shape: Some("http://example.org/Person".to_string()),
literals: None,
}],
..Default::default()
},
],
},
);
let shape_type = OrmShapeType {
schema,
shape: "http://example.org/Person".to_string(),
};
// Generate and run query. This must not infinite loop.
let query = sparql_construct_from_orm_shape_type(&shape_type, Some(1)).unwrap();
log_info!("cyclic query result:\n{}", query);
let triples = doc_sparql_construct(session_id, query, Some(doc_nuri.clone()))
.await .await
.expect("close session"); .unwrap();
wallet_close(&wallet.name()).await.expect("close wallet"); // Assert: All 6 triples (3 per person) should be returned.
log_info!("Triples:\n{:?}", triples);
assert_eq!(triples.len(), 24);
} }

@ -23,6 +23,7 @@ pub struct OrmShapeType {
pub shape: String, pub shape: String,
} }
/* == Diff Types == */
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
#[allow(non_camel_case_types)] #[allow(non_camel_case_types)]
pub enum OrmDiffOpType { pub enum OrmDiffOpType {
@ -42,12 +43,13 @@ pub struct OrmDiffOp {
pub op: OrmDiffOpType, pub op: OrmDiffOpType,
pub valType: Option<OrmDiffType>, pub valType: Option<OrmDiffType>,
pub path: String, pub path: String,
pub value: Option<Value>, pub value: Option<Value>, // TODO: Improve type
} }
pub type OrmDiff = Vec<OrmDiffOp>; pub type OrmDiff = Vec<OrmDiffOp>;
type OrmSchema = HashMap<String, OrmSchemaShape>; /* == ORM Schema == */
pub type OrmSchema = HashMap<String, OrmSchemaShape>;
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct OrmSchemaShape { pub struct OrmSchemaShape {
@ -55,7 +57,7 @@ pub struct OrmSchemaShape {
pub predicates: Vec<OrmSchemaPredicate>, pub predicates: Vec<OrmSchemaPredicate>,
} }
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
#[allow(non_camel_case_types)] #[allow(non_camel_case_types)]
pub enum OrmSchemaLiteralType { pub enum OrmSchemaLiteralType {
number, number,
@ -63,23 +65,12 @@ pub enum OrmSchemaLiteralType {
boolean, boolean,
iri, iri,
literal, literal,
} shape,
#[derive(Clone, Debug, Serialize, Deserialize)]
#[allow(non_camel_case_types)]
pub enum OrmSchemaPredicateType {
number,
string,
boolean,
iri,
literal,
nested,
eitherOf,
} }
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(untagged)] #[serde(untagged)]
pub enum OrmLiterals { pub enum OrmSchemaLiterals {
Bool(bool), Bool(bool),
NumArray(Vec<f64>), NumArray(Vec<f64>),
StrArray(Vec<String>), StrArray(Vec<String>),
@ -88,26 +79,47 @@ pub enum OrmLiterals {
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct OrmSchemaDataType { pub struct OrmSchemaDataType {
pub valType: OrmSchemaLiteralType, pub valType: OrmSchemaLiteralType,
pub literals: Option<OrmLiterals>, pub literals: Option<OrmSchemaLiterals>,
pub shape: Option<String>,
} }
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct OrmSchemaPredicate { pub struct OrmSchemaPredicate {
pub valType: OrmSchemaPredicateType, pub dataTypes: Vec<OrmSchemaDataType>,
pub iri: String, pub iri: String,
pub readablePredicate: String, pub readablePredicate: String,
pub literalValue: Option<Value>, // Strictly speaking, no objects. /// `-1` for infinity
pub nestedShape: Option<String>, // Only by reference. pub maxCardinality: i64,
pub maxCardinality: i64, // -1 for infinity
pub minCardinality: i64, pub minCardinality: i64,
pub eitherOf: Option<Vec<OrmSchemaEitherOfOption>>, // Shape references or multi type.
pub extra: Option<bool>, pub extra: Option<bool>,
} }
// TODO: Will this be serialized correctly? impl Default for OrmSchemaDataType {
#[derive(Clone, Debug, Serialize, Deserialize)] fn default() -> Self {
#[serde(untagged)] Self {
pub enum OrmSchemaEitherOfOption { literals: None,
ShapeRef(String), shape: None,
DataType(OrmSchemaDataType), valType: OrmSchemaLiteralType::string,
}
}
}
impl Default for OrmSchemaPredicate {
fn default() -> Self {
Self {
dataTypes: Vec::new(),
iri: String::new(),
readablePredicate: String::new(),
maxCardinality: -1,
minCardinality: 0,
extra: None,
}
}
}
/** == Internal data types == */
#[derive(Clone, Debug)]
pub struct OrmShapeTypeRef {
ref_count: u64,
shape_type: OrmShapeType,
} }

@ -397,6 +397,7 @@ pub enum VerifierError {
ContactAlreadyExists, ContactAlreadyExists,
InternalError, InternalError,
InvalidInboxPost, InvalidInboxPost,
InvalidOrmSchema,
} }
impl Error for VerifierError {} impl Error for VerifierError {}

@ -39,6 +39,7 @@ ng-repo = { path = "../ng-repo", version = "0.1.2" }
ng-net = { path = "../ng-net", version = "0.1.2" } ng-net = { path = "../ng-net", version = "0.1.2" }
ng-oxigraph = { path = "../ng-oxigraph", version = "0.4.0-alpha.8-ngalpha" } ng-oxigraph = { path = "../ng-oxigraph", version = "0.4.0-alpha.8-ngalpha" }
once_cell = "1.17.1" once_cell = "1.17.1"
regex = "1.8.4"
[target.'cfg(target_family = "wasm")'.dependencies] [target.'cfg(target_family = "wasm")'.dependencies]
ng-oxigraph = { path = "../ng-oxigraph", version = "0.4.0-alpha.8-ngalpha", features = [ ng-oxigraph = { path = "../ng-oxigraph", version = "0.4.0-alpha.8-ngalpha", features = [

@ -12,11 +12,13 @@ use std::collections::HashMap;
use futures::channel::mpsc; use futures::channel::mpsc;
use futures::SinkExt; use futures::SinkExt;
use ng_net::app_protocol::*; use lazy_static::lazy_static;
pub use ng_net::orm::OrmDiff; pub use ng_net::orm::OrmDiff;
pub use ng_net::orm::OrmShapeType; pub use ng_net::orm::OrmShapeType;
use ng_net::orm::{OrmSchemaDataType, OrmSchemaShape};
use ng_net::orm::{OrmSchemaLiteralType, OrmSchemaLiterals};
use ng_net::{app_protocol::*, orm::OrmSchema};
use ng_net::{ use ng_net::{
connection::NoiseFSM,
types::*, types::*,
utils::{Receiver, Sender}, utils::{Receiver, Sender},
}; };
@ -24,7 +26,9 @@ use ng_oxigraph::oxigraph::sparql::{results::*, Query, QueryResults};
use ng_oxigraph::oxrdf::Term; use ng_oxigraph::oxrdf::Term;
use ng_oxigraph::oxrdf::Triple; use ng_oxigraph::oxrdf::Triple;
use ng_repo::errors::NgError; use ng_repo::errors::NgError;
use ng_repo::errors::VerifierError;
use ng_repo::log::*; use ng_repo::log::*;
use regex::Regex;
use crate::types::*; use crate::types::*;
use crate::verifier::*; use crate::verifier::*;
@ -88,7 +92,7 @@ impl Verifier {
let results = oxistore let results = oxistore
.query(parsed, None) .query(parsed, None)
.map_err(|e| NgError::OxiGraphError(e.to_string()))?; .map_err(|e| NgError::OxiGraphError(e.to_string()))?;
match results { let sols = match results {
QueryResults::Solutions(sols) => { QueryResults::Solutions(sols) => {
let mut results = vec![]; let mut results = vec![];
for t in sols { for t in sols {
@ -103,12 +107,15 @@ impl Verifier {
Ok(results) Ok(results)
} }
_ => return Err(NgError::InvalidResponse), _ => return Err(NgError::InvalidResponse),
} };
sols
} }
fn create_orm_from_triples(&mut self, scope: &NuriV0, shape_type: &OrmShapeType) {}
pub(crate) async fn orm_update(&mut self, scope: &NuriV0, patch: GraphQuadsPatch) {} pub(crate) async fn orm_update(&mut self, scope: &NuriV0, patch: GraphQuadsPatch) {}
pub(crate) async fn frontend_update_orm( pub(crate) async fn orm_frontend_update(
&mut self, &mut self,
scope: &NuriV0, scope: &NuriV0,
shape_id: String, shape_id: String,
@ -150,7 +157,7 @@ impl Verifier {
pub(crate) async fn start_orm( pub(crate) async fn start_orm(
&mut self, &mut self,
nuri: &NuriV0, nuri: &NuriV0,
schema: &OrmShapeType, shape_type: &OrmShapeType,
session_id: u64, session_id: u64,
) -> Result<(Receiver<AppResponse>, CancelFn), NgError> { ) -> Result<(Receiver<AppResponse>, CancelFn), NgError> {
let (tx, rx) = mpsc::unbounded::<AppResponse>(); let (tx, rx) = mpsc::unbounded::<AppResponse>();
@ -158,12 +165,12 @@ impl Verifier {
self.orm_subscriptions.insert( self.orm_subscriptions.insert(
nuri.clone(), nuri.clone(),
HashMap::from([( HashMap::from([(
schema.shape.clone(), shape_type.shape.clone(),
HashMap::from([(session_id, tx.clone())]), HashMap::from([(session_id, tx.clone())]),
)]), )]),
); );
//self.push_orm_response().await; //self.push_orm_response().await; (only for requester, not all sessions)
let close = Box::new(move || { let close = Box::new(move || {
//log_debug!("CLOSE_CHANNEL of subscription for branch {}", branch_id); //log_debug!("CLOSE_CHANNEL of subscription for branch {}", branch_id);
@ -174,3 +181,231 @@ impl Verifier {
Ok((rx, close)) Ok((rx, close))
} }
} }
fn is_iri(s: &str) -> bool {
lazy_static! {
static ref IRI_REGEX: Regex = Regex::new(r"^[A-Za-z][A-Za-z0-9+\.\-]{1,12}:").unwrap();
}
IRI_REGEX.is_match(s)
}
fn literal_to_sparql_str(var: OrmSchemaDataType) -> Vec<String> {
match var.literals {
None => [].to_vec(),
Some(literals) => match literals {
OrmSchemaLiterals::Bool(val) => {
if val == true {
["true".to_string()].to_vec()
} else {
["false".to_string()].to_vec()
}
}
OrmSchemaLiterals::NumArray(numbers) => {
numbers.iter().map(|num| num.to_string()).collect()
}
OrmSchemaLiterals::StrArray(stings) => stings
.iter()
.map(|str| {
// We assume that strings can be IRIs (currently no support for typed literals).
if is_iri(str) {
format!("<{}>", escape_iri(str))
} else {
format!("\"{}\"", escape_literal(str))
}
})
.collect(),
},
}
}
pub fn sparql_construct_from_orm_shape_type(
shape_type: &OrmShapeType,
max_recursion: Option<u8>,
) -> Result<String, NgError> {
// Use a counter to generate unique variable names.
let mut var_counter = 0;
fn get_new_var_name(counter: &mut i32) -> String {
let name = format!("v{}", counter);
*counter += 1;
name
}
// Collect all statements to be added to the construct and where bodies.
let mut construct_statements = Vec::new();
let mut where_statements = Vec::new();
// Keep track of visited shapes while recursing to prevent infinite loops.
// TODO: Update type
let mut visited_shapes: HashMap<String, u8> = HashMap::new();
// Recursive function to call for (nested) shapes.
fn process_shape(
schema: &OrmSchema,
shape: &OrmSchemaShape,
subject_var_name: &str,
construct_statements: &mut Vec<String>,
where_statements: &mut Vec<String>,
var_counter: &mut i32,
visited_shapes: &mut HashMap<String, u8>,
max_recursion: u8,
) {
// Prevent infinite recursion on cyclic schemas.
// Keep track of the number of shape occurrences and return if it's larger than max_recursion.
// For the last recursion, we could use by-reference queries but that could be for the future.
let current_self_recursion_depth = visited_shapes.get(&shape.iri).unwrap_or(&0);
if *current_self_recursion_depth > max_recursion {
return;
} else {
visited_shapes.insert(shape.iri.clone(), current_self_recursion_depth + 1);
}
// Add statements for each predicate.
for predicate in &shape.predicates {
let mut union_branches = Vec::new();
let mut allowed_literals = Vec::new();
// Predicate constraints might have more than one acceptable data type. Traverse each.
// It is assumed that constant literals, nested shapes and regular types are not mixed.
for datatype in &predicate.dataTypes {
if datatype.valType == OrmSchemaLiteralType::literal {
// Collect allowed literals and as strings
// (already in SPARQL-format, e.g. `"a astring"`, `<http:ex.co/>`, `true`, or `42`).
allowed_literals.extend(literal_to_sparql_str(datatype.clone()));
} else if datatype.valType == OrmSchemaLiteralType::shape {
if let Some(shape_id) = &datatype.shape {
if let Some(nested_shape) = schema.get(shape_id) {
// For the current acceptable shape, add CONSTRUCT, WHERE, and recurse.
// Each shape option gets its own var.
let obj_var_name = get_new_var_name(var_counter);
construct_statements.push(format!(
" ?{} <{}> ?{}",
subject_var_name, predicate.iri, obj_var_name
));
// Those are later added to a UNION, if there is more than one shape.
union_branches.push(format!(
" ?{} <{}> ?{}",
subject_var_name, predicate.iri, obj_var_name
));
// Recurse to add statements for nested object.
process_shape(
schema,
nested_shape,
&obj_var_name,
construct_statements,
where_statements,
var_counter,
visited_shapes,
max_recursion,
);
}
}
}
}
// The where statement which might be wrapped in OPTIONAL.
let where_body: String;
if !allowed_literals.is_empty()
&& !predicate.extra.unwrap_or(false)
&& predicate.minCardinality > 0
{
// If we have literal requirements and they are not optional ("extra"),
// Add CONSTRUCT, WHERE, and FILTER.
let pred_var_name = get_new_var_name(var_counter);
construct_statements.push(format!(
" ?{} <{}> ?{}",
subject_var_name, predicate.iri, pred_var_name
));
where_body = format!(
" ?{s} <{p}> ?{o} . \n FILTER (?{o} IN ({lits}))",
s = subject_var_name,
p = predicate.iri,
o = pred_var_name,
lits = allowed_literals.join(", ")
);
} else if !union_branches.is_empty() {
// We have nested shape(s) which were already added to CONSTRUCT above.
// Join them with UNION.
where_body = union_branches
.into_iter()
.map(|b| format!("{{\n{}\n}}", b))
.collect::<Vec<_>>()
.join(" UNION ");
} else {
// Regular predicate data type. Just add basic CONSTRUCT and WHERE statements.
let pred_var_name = get_new_var_name(var_counter);
construct_statements.push(format!(
" ?{} <{}> ?{}",
subject_var_name, predicate.iri, pred_var_name
));
where_body = format!(
" ?{} <{}> ?{}",
subject_var_name, predicate.iri, pred_var_name
);
}
// Wrap in optional, if necessary.
if predicate.minCardinality < 1 {
where_statements.push(format!(" OPTIONAL {{\n{}\n }}", where_body));
} else {
where_statements.push(where_body);
};
}
visited_shapes.remove(&shape.iri);
}
let root_shape = shape_type
.schema
.get(&shape_type.shape)
.ok_or(VerifierError::InvalidOrmSchema)?;
// Root subject variable name
let root_var_name = get_new_var_name(&mut var_counter);
process_shape(
&shape_type.schema,
root_shape,
&root_var_name,
&mut construct_statements,
&mut where_statements,
&mut var_counter,
&mut visited_shapes,
max_recursion.unwrap_or(1),
);
// Create query from statements.
let construct_body = construct_statements.join(" .\n");
let where_body = where_statements.join(" .\n");
Ok(format!(
"CONSTRUCT {{\n{}\n}}\nWHERE {{\n{}\n}}",
construct_body, where_body
))
}
// Escape an IRI fragment if needed (very conservative, only wrap with <...>). Assumes input already a full IRI.
fn escape_iri(iri: &str) -> String {
format!("<{}>", iri)
}
// SPARQL literal escape: backslash, quotes, newlines, tabs.
fn escape_literal(lit: &str) -> String {
let mut out = String::with_capacity(lit.len() + 4);
for c in lit.chars() {
match c {
'\\' => out.push_str("\\\\"),
'\"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
_ => out.push(c),
}
}
return out;
}

@ -49,16 +49,14 @@ impl Verifier {
command: &AppRequestCommandV0, command: &AppRequestCommandV0,
nuri: &NuriV0, nuri: &NuriV0,
_payload: &Option<AppRequestPayload>, _payload: &Option<AppRequestPayload>,
session_id: u64 session_id: u64,
) -> Result<(Receiver<AppResponse>, CancelFn), NgError> { ) -> Result<(Receiver<AppResponse>, CancelFn), NgError> {
match command { match command {
AppRequestCommandV0::OrmStart => { AppRequestCommandV0::OrmStart => match _payload {
match _payload { Some(AppRequestPayload::V0(AppRequestPayloadV0::OrmStart(shape_type))) => {
Some(AppRequestPayload::V0(AppRequestPayloadV0::OrmStart(shape_type))) => { self.start_orm(nuri, shape_type, session_id).await
self.start_orm(nuri, shape_type, session_id).await
},
_ => return Err(NgError::InvalidArgument)
} }
_ => return Err(NgError::InvalidArgument),
}, },
AppRequestCommandV0::Fetch(fetch) => match fetch { AppRequestCommandV0::Fetch(fetch) => match fetch {
AppFetchContentV0::Subscribe => { AppFetchContentV0::Subscribe => {
@ -162,8 +160,12 @@ impl Verifier {
} }
} }
pub(crate) async fn update_header(&mut self, target: &NuriTargetV0, title: Option<String>, about: Option<String>) -> Result<(), VerifierError> { pub(crate) async fn update_header(
&mut self,
target: &NuriTargetV0,
title: Option<String>,
about: Option<String>,
) -> Result<(), VerifierError> {
let (repo_id, branch_id, store_repo) = self.resolve_header_branch(target)?; let (repo_id, branch_id, store_repo) = self.resolve_header_branch(target)?;
let graph_name = NuriV0::branch_repo_graph_name( let graph_name = NuriV0::branch_repo_graph_name(
&branch_id, &branch_id,
@ -196,9 +198,7 @@ impl Verifier {
); );
} }
} }
let query = format!( let query = format!("DELETE {{ {deletes} }} INSERT {{ {inserts} }} WHERE {{ {wheres} }}");
"DELETE {{ {deletes} }} INSERT {{ {inserts} }} WHERE {{ {wheres} }}"
);
let oxistore = self.graph_dataset.as_ref().unwrap(); let oxistore = self.graph_dataset.as_ref().unwrap();
@ -212,13 +212,12 @@ impl Verifier {
if inserts.is_empty() && removes.is_empty() { if inserts.is_empty() && removes.is_empty() {
Ok(()) Ok(())
} else { } else {
self self.prepare_sparql_update(
.prepare_sparql_update( Vec::from_iter(inserts),
Vec::from_iter(inserts), Vec::from_iter(removes),
Vec::from_iter(removes), self.get_peer_id_for_skolem(),
self.get_peer_id_for_skolem(), )
) .await?;
.await?;
Ok(()) Ok(())
} }
} }
@ -628,24 +627,26 @@ impl Verifier {
destination: String, destination: String,
store_repo: Option<StoreRepo>, store_repo: Option<StoreRepo>,
) -> Result<String, NgError> { ) -> Result<String, NgError> {
let class = BranchCrdt::from(crdt, class_name)?; let class = BranchCrdt::from(crdt, class_name)?;
let nuri = if store_repo.is_none() { let nuri = if store_repo.is_none() {
NuriV0::new_private_store_target() NuriV0::new_private_store_target()
} else { } else {
NuriV0::from_store_repo(&store_repo.unwrap()) NuriV0::from_store_repo(&store_repo.unwrap())
}; };
let destination = DocCreateDestination::from(destination)?; let destination = DocCreateDestination::from(destination)?;
self.doc_create(nuri, DocCreate { self.doc_create(nuri, DocCreate { class, destination })
class, .await
destination,
}).await
} }
pub(crate) async fn sparql_query(&self, nuri: &NuriV0, sparql: String, base: Option<String>) -> Result<QueryResults, VerifierError> { pub(crate) async fn sparql_query(
&self,
nuri: &NuriV0,
sparql: String,
base: Option<String>,
) -> Result<QueryResults, VerifierError> {
//log_debug!("query={}", query); //log_debug!("query={}", query);
let store = self.graph_dataset.as_ref().unwrap(); let store = self.graph_dataset.as_ref().unwrap();
let mut parsed = Query::parse(&sparql, base.as_deref()) let mut parsed = Query::parse(&sparql, base.as_deref())
@ -658,27 +659,22 @@ impl Verifier {
} }
store store
.query(parsed, self.resolve_target_for_sparql(&nuri.target, false)?) .query(parsed, self.resolve_target_for_sparql(&nuri.target, false)?)
.map_err(|e| VerifierError::SparqlError(e.to_string())) .map_err(|e| VerifierError::SparqlError(e.to_string()))
} }
pub(crate) async fn doc_create( pub(crate) async fn doc_create(
&mut self, &mut self,
nuri: NuriV0, nuri: NuriV0,
doc_create: DocCreate doc_create: DocCreate,
) -> Result<String, NgError> { ) -> Result<String, NgError> {
//TODO: deal with doc_create.destination //TODO: deal with doc_create.destination
let user_id = self.user_id().clone(); let user_id = self.user_id().clone();
let user_priv_key = self.user_privkey().clone(); let user_priv_key = self.user_privkey().clone();
let primary_class = doc_create.class.class().clone(); let primary_class = doc_create.class.class().clone();
let (_,_,store) = self.resolve_target(&nuri.target)?; let (_, _, store) = self.resolve_target(&nuri.target)?;
let repo_id = self let repo_id = self
.new_repo_default( .new_repo_default(&user_id, &user_priv_key, &store, doc_create.class)
&user_id,
&user_priv_key,
&store,
doc_create.class,
)
.await?; .await?;
let header_branch_id = { let header_branch_id = {
@ -687,8 +683,7 @@ impl Verifier {
}; };
// adding an AddRepo commit to the Store branch of store. // adding an AddRepo commit to the Store branch of store.
self.send_add_repo_to_store(&repo_id, &store) self.send_add_repo_to_store(&repo_id, &store).await?;
.await?;
// adding an ldp:contains triple to the store main branch // adding an ldp:contains triple to the store main branch
let overlay_id = store.outer_overlay(); let overlay_id = store.outer_overlay();
@ -696,7 +691,9 @@ impl Verifier {
let nuri_result = NuriV0::repo_graph_name(&repo_id, &overlay_id); let nuri_result = NuriV0::repo_graph_name(&repo_id, &overlay_id);
let store_nuri = NuriV0::from_store_repo(&store); let store_nuri = NuriV0::from_store_repo(&store);
let store_nuri_string = NuriV0::repo_id(store.repo_id()); let store_nuri_string = NuriV0::repo_id(store.repo_id());
let query = format!("INSERT DATA {{ <{store_nuri_string}> <http://www.w3.org/ns/ldp#contains> <{nuri}>. }}"); let query = format!(
"INSERT DATA {{ <{store_nuri_string}> <http://www.w3.org/ns/ldp#contains> <{nuri}>. }}"
);
let ret = self let ret = self
.process_sparql_update(&store_nuri, &query, &None, vec![]) .process_sparql_update(&store_nuri, &query, &None, vec![])
@ -722,26 +719,35 @@ impl Verifier {
Ok(nuri_result) Ok(nuri_result)
} }
fn get_profile_for_inbox_post(&self, public: bool) -> Result<(StoreRepo, PrivKey),NgError> { fn get_profile_for_inbox_post(&self, public: bool) -> Result<(StoreRepo, PrivKey), NgError> {
let from_profile_id = if !public { let from_profile_id = if !public {
self.config.protected_store_id.unwrap() self.config.protected_store_id.unwrap()
} else { } else {
self.config.public_store_id.unwrap() self.config.public_store_id.unwrap()
}; };
let repo = self.repos.get(&from_profile_id).ok_or(NgError::RepoNotFound)?; let repo = self
.repos
.get(&from_profile_id)
.ok_or(NgError::RepoNotFound)?;
let inbox = repo.inbox.to_owned().ok_or(NgError::InboxNotFound)?; let inbox = repo.inbox.to_owned().ok_or(NgError::InboxNotFound)?;
let store_repo = repo.store.get_store_repo(); let store_repo = repo.store.get_store_repo();
Ok( (store_repo.clone(), inbox.clone()) ) Ok((store_repo.clone(), inbox.clone()))
} }
async fn import_contact_from_qrcode(&mut self, repo_id: RepoId, contact: NgQRCodeProfileSharingV0) -> Result<(), VerifierError> { async fn import_contact_from_qrcode(
&mut self,
repo_id: RepoId,
contact: NgQRCodeProfileSharingV0,
) -> Result<(), VerifierError> {
let inbox_nuri_string: String = NuriV0::inbox(&contact.inbox); let inbox_nuri_string: String = NuriV0::inbox(&contact.inbox);
let profile_nuri_string: String = NuriV0::from_store_repo_string(&contact.profile); let profile_nuri_string: String = NuriV0::from_store_repo_string(&contact.profile);
let a_or_b = if contact.profile.is_public() { "site" } else { "protected" }; let a_or_b = if contact.profile.is_public() {
"site"
} else {
"protected"
};
// checking if this contact has already been added // checking if this contact has already been added
match self.sparql_query( match self.sparql_query(
@ -755,7 +761,8 @@ impl Verifier {
} }
// getting the privkey of the inbox and ovelray because we will need it here below to send responses. // getting the privkey of the inbox and ovelray because we will need it here below to send responses.
let (from_profile, from_inbox) = self.get_profile_for_inbox_post(contact.profile.is_public())?; let (from_profile, from_inbox) =
self.get_profile_for_inbox_post(contact.profile.is_public())?;
// get the name and optional email address of the profile we will respond with. // get the name and optional email address of the profile we will respond with.
// if we don't have a name, we fail // if we don't have a name, we fail
@ -787,26 +794,37 @@ impl Verifier {
let contact_doc_nuri_string = NuriV0::repo_id(&repo_id); let contact_doc_nuri_string = NuriV0::repo_id(&repo_id);
let contact_doc_nuri = NuriV0::new_repo_target_from_id(&repo_id); let contact_doc_nuri = NuriV0::new_repo_target_from_id(&repo_id);
let has_email = contact.email.map_or("".to_string(), |email| format!("<> vcard:hasEmail \"{email}\".")); let has_email = contact.email.map_or("".to_string(), |email| {
format!("<> vcard:hasEmail \"{email}\".")
});
let sparql_update = format!(" PREFIX ng: <did:ng:x:ng#> let sparql_update = format!(
" PREFIX ng: <did:ng:x:ng#>
PREFIX vcard: <http://www.w3.org/2006/vcard/ns#> PREFIX vcard: <http://www.w3.org/2006/vcard/ns#>
INSERT DATA {{ <> ng:{a_or_b} <{profile_nuri_string}>. INSERT DATA {{ <> ng:{a_or_b} <{profile_nuri_string}>.
<> ng:{a_or_b}_inbox <{inbox_nuri_string}>. <> ng:{a_or_b}_inbox <{inbox_nuri_string}>.
<> a vcard:Individual . <> a vcard:Individual .
<> vcard:fn \"{}\". <> vcard:fn \"{}\".
{has_email} }}", contact.name); {has_email} }}",
contact.name
);
let ret = self let ret = self
.process_sparql_update(&contact_doc_nuri, &sparql_update, &Some(contact_doc_nuri_string), vec![]) .process_sparql_update(
&contact_doc_nuri,
&sparql_update,
&Some(contact_doc_nuri_string),
vec![],
)
.await; .await;
if let Err(e) = ret { if let Err(e) = ret {
return Err(VerifierError::SparqlError(e)); return Err(VerifierError::SparqlError(e));
} }
self.update_header(&contact_doc_nuri.target, Some(contact.name), None).await?; self.update_header(&contact_doc_nuri.target, Some(contact.name), None)
.await?;
self.post_to_inbox(InboxPost::new_contact_details( self.post_to_inbox(InboxPost::new_contact_details(
from_profile, from_profile,
from_inbox, from_inbox,
contact.profile.outer_overlay(), contact.profile.outer_overlay(),
contact.inbox, contact.inbox,
@ -814,31 +832,43 @@ impl Verifier {
false, false,
name, name,
email, email,
)?).await?; )?)
.await?;
Ok(()) Ok(())
} }
pub(crate) async fn search_for_contacts(&self, excluding_profile_id_nuri: Option<String>) -> Result<Vec<(String,String)>, VerifierError> { pub(crate) async fn search_for_contacts(
&self,
excluding_profile_id_nuri: Option<String>,
) -> Result<Vec<(String, String)>, VerifierError> {
let extra_conditions = if let Some(s) = excluding_profile_id_nuri { let extra_conditions = if let Some(s) = excluding_profile_id_nuri {
format!("&& NOT EXISTS {{ ?c ng:site <{s}> }} && NOT EXISTS {{ ?c ng:protected <{s}> }}") format!(
"&& NOT EXISTS {{ ?c ng:site <{s}> }} && NOT EXISTS {{ ?c ng:protected <{s}> }}"
)
} else { } else {
String::new() String::new()
}; };
let sparql = format!("PREFIX ng: <did:ng:x:ng#> let sparql = format!(
"PREFIX ng: <did:ng:x:ng#>
SELECT ?profile_id ?inbox_id WHERE SELECT ?profile_id ?inbox_id WHERE
{{ ?c a <http://www.w3.org/2006/vcard/ns#Individual> . {{ ?c a <http://www.w3.org/2006/vcard/ns#Individual> .
OPTIONAL {{ ?c ng:site ?profile_id . ?c ng:site_inbox ?inbox_id }} OPTIONAL {{ ?c ng:site ?profile_id . ?c ng:site_inbox ?inbox_id }}
OPTIONAL {{ ?c ng:protected ?profile_id . ?c ng:protected_inbox ?inbox_id }} OPTIONAL {{ ?c ng:protected ?profile_id . ?c ng:protected_inbox ?inbox_id }}
FILTER ( bound(?profile_id) {extra_conditions} ) FILTER ( bound(?profile_id) {extra_conditions} )
}}"); }}"
);
//log_info!("{sparql}"); //log_info!("{sparql}");
let sols = match self.sparql_query( let sols = match self
&NuriV0::new_entire_user_site(), .sparql_query(&NuriV0::new_entire_user_site(), sparql, None)
sparql, None).await? .await?
{ {
QueryResults::Solutions(sols) => { sols } QueryResults::Solutions(sols) => sols,
_ => return Err(VerifierError::SparqlError(NgError::InvalidResponse.to_string())), _ => {
return Err(VerifierError::SparqlError(
NgError::InvalidResponse.to_string(),
))
}
}; };
let mut res = vec![]; let mut res = vec![];
@ -857,8 +887,7 @@ impl Verifier {
} }
} }
Ok(res) Ok(res)
}
}
pub(crate) async fn process( pub(crate) async fn process(
&mut self, &mut self,
@ -867,30 +896,35 @@ impl Verifier {
payload: Option<AppRequestPayload>, payload: Option<AppRequestPayload>,
) -> Result<AppResponse, NgError> { ) -> Result<AppResponse, NgError> {
match command { match command {
AppRequestCommandV0::OrmUpdate => { AppRequestCommandV0::OrmUpdate => match payload {
match payload { Some(AppRequestPayload::V0(AppRequestPayloadV0::OrmUpdate((diff, shape_id)))) => {
Some(AppRequestPayload::V0(AppRequestPayloadV0::OrmUpdate((diff,shape_id)))) => { self.orm_frontend_update(&nuri, shape_id, diff).await
self.frontend_update_orm(&nuri, shape_id, diff).await
},
_ => return Err(NgError::InvalidArgument)
} }
_ => return Err(NgError::InvalidArgument),
}, },
AppRequestCommandV0::SocialQueryStart => { AppRequestCommandV0::SocialQueryStart => {
let (from_profile, contacts_string, degree) = if let Some(AppRequestPayload::V0(AppRequestPayloadV0::SocialQueryStart{ let (from_profile, contacts_string, degree) =
from_profile, contacts, degree if let Some(AppRequestPayload::V0(AppRequestPayloadV0::SocialQueryStart {
})) = from_profile,
payload contacts,
{ (from_profile, contacts, degree) } degree,
else { })) = payload
return Err(NgError::InvalidPayload); {
}; (from_profile, contacts, degree)
} else {
return Err(NgError::InvalidPayload);
};
let query_id = nuri.target.repo_id(); let query_id = nuri.target.repo_id();
// checking that the query hasn't been started yet // checking that the query hasn't been started yet
match self.sparql_query( match self
&NuriV0::new_repo_target_from_id(query_id), .sparql_query(
format!("ASK {{ <> <did:ng:x:ng#social_query_forwarder> ?forwarder }}"), Some(NuriV0::repo_id(query_id))).await? &NuriV0::new_repo_target_from_id(query_id),
format!("ASK {{ <> <did:ng:x:ng#social_query_forwarder> ?forwarder }}"),
Some(NuriV0::repo_id(query_id)),
)
.await?
{ {
QueryResults::Boolean(true) => { QueryResults::Boolean(true) => {
return Err(NgError::SocialQueryAlreadyStarted); return Err(NgError::SocialQueryAlreadyStarted);
@ -921,32 +955,36 @@ impl Verifier {
//resolve from_profile //resolve from_profile
let from_profile_id = match from_profile.target { let from_profile_id = match from_profile.target {
NuriTargetV0::ProtectedProfile => { NuriTargetV0::ProtectedProfile => self.config.protected_store_id.unwrap(),
self.config.protected_store_id.unwrap() NuriTargetV0::PublicProfile => self.config.public_store_id.unwrap(),
} _ => return Err(NgError::InvalidNuri),
NuriTargetV0::PublicProfile => {
self.config.public_store_id.unwrap()
},
_ => return Err(NgError::InvalidNuri)
}; };
let store = { let store = {
let repo = self.repos.get(&from_profile_id).ok_or(NgError::RepoNotFound)?; let repo = self
.repos
.get(&from_profile_id)
.ok_or(NgError::RepoNotFound)?;
repo.store.clone() repo.store.clone()
}; };
let definition_commit_body_ref = nuri.get_first_commit_ref()?; let definition_commit_body_ref = nuri.get_first_commit_ref()?;
let block_ids = Commit::collect_block_ids(definition_commit_body_ref.clone(), &store, true)?; let block_ids =
let mut blocks= Vec::with_capacity(block_ids.len()); Commit::collect_block_ids(definition_commit_body_ref.clone(), &store, true)?;
let mut blocks = Vec::with_capacity(block_ids.len());
//log_info!("blocks nbr {}",block_ids.len()); //log_info!("blocks nbr {}",block_ids.len());
for bid in block_ids.iter() { for bid in block_ids.iter() {
blocks.push(store.get(bid)?); blocks.push(store.get(bid)?);
} }
// creating the ForwardedSocialQuery in the private store // creating the ForwardedSocialQuery in the private store
let forwarder = self.doc_create_with_store_repo( let forwarder = self
"Graph".to_string(), "social:query:forwarded".to_string(), .doc_create_with_store_repo(
"store".to_string(), None // meaning in private store "Graph".to_string(),
).await?; "social:query:forwarded".to_string(),
"store".to_string(),
None, // meaning in private store
)
.await?;
let forwarder_nuri = NuriV0::new_from_repo_graph(&forwarder)?; let forwarder_nuri = NuriV0::new_from_repo_graph(&forwarder)?;
let forwarder_id = forwarder_nuri.target.repo_id().clone(); let forwarder_id = forwarder_nuri.target.repo_id().clone();
let forwarder_nuri_string = NuriV0::repo_id(&forwarder_id); let forwarder_nuri_string = NuriV0::repo_id(&forwarder_id);
@ -965,28 +1003,36 @@ impl Verifier {
let sparql_update = format!("INSERT DATA {{ <> <did:ng:x:ng#social_query_id> <{social_query_doc_nuri_string}> . let sparql_update = format!("INSERT DATA {{ <> <did:ng:x:ng#social_query_id> <{social_query_doc_nuri_string}> .
<> <did:ng:x:ng#social_query_started> \"{}\"^^<http://www.w3.org/2001/XMLSchema#dateTime> . }}",DateTime::now()); <> <did:ng:x:ng#social_query_started> \"{}\"^^<http://www.w3.org/2001/XMLSchema#dateTime> . }}",DateTime::now());
let ret = self let ret = self
.process_sparql_update(&forwarder_nuri, &sparql_update, &Some(forwarder_nuri_string), vec![]) .process_sparql_update(
&forwarder_nuri,
&sparql_update,
&Some(forwarder_nuri_string),
vec![],
)
.await; .await;
if let Err(e) = ret { if let Err(e) = ret {
log_err!("{sparql_update}"); log_err!("{sparql_update}");
return Err(NgError::SparqlError(e)); return Err(NgError::SparqlError(e));
} }
let from_profiles: ((StoreRepo, PrivKey), (StoreRepo, PrivKey)) = self.get_2_profiles()?; let from_profiles: ((StoreRepo, PrivKey), (StoreRepo, PrivKey)) =
self.get_2_profiles()?;
for (to_profile_nuri, to_inbox_nuri) in contacts { for (to_profile_nuri, to_inbox_nuri) in contacts {
match self
match self.social_query_dispatch( .social_query_dispatch(
&to_profile_nuri, &to_profile_nuri,
&to_inbox_nuri, &to_inbox_nuri,
&forwarder_nuri, &forwarder_nuri,
&forwarder_id, &forwarder_id,
&from_profiles, &from_profiles,
query_id, query_id,
&definition_commit_body_ref, &definition_commit_body_ref,
&blocks, &blocks,
degree degree,
).await { )
.await
{
Ok(_) => {} Ok(_) => {}
Err(e) => return Ok(AppResponse::error(e.to_string())), Err(e) => return Ok(AppResponse::error(e.to_string())),
} }
@ -1017,32 +1063,34 @@ impl Verifier {
// }; // };
} }
AppRequestCommandV0::QrCodeProfile => { AppRequestCommandV0::QrCodeProfile => {
let size = if let Some(AppRequestPayload::V0(AppRequestPayloadV0::QrCodeProfile(size))) = let size =
payload if let Some(AppRequestPayload::V0(AppRequestPayloadV0::QrCodeProfile(size))) =
{ payload
size {
} else { size
return Err(NgError::InvalidPayload); } else {
}; return Err(NgError::InvalidPayload);
};
let public = match nuri.target { let public = match nuri.target {
NuriTargetV0::PublicProfile => true, NuriTargetV0::PublicProfile => true,
NuriTargetV0::ProtectedProfile => false, NuriTargetV0::ProtectedProfile => false,
_ => return Err(NgError::InvalidPayload) _ => return Err(NgError::InvalidPayload),
}; };
return match self.get_qrcode_for_profile(public, size).await { return match self.get_qrcode_for_profile(public, size).await {
Err(e) => Ok(AppResponse::error(e.to_string())), Err(e) => Ok(AppResponse::error(e.to_string())),
Ok(qrcode) => Ok(AppResponse::text(qrcode)), Ok(qrcode) => Ok(AppResponse::text(qrcode)),
}; };
} }
AppRequestCommandV0::QrCodeProfileImport => { AppRequestCommandV0::QrCodeProfileImport => {
let profile = if let Some(AppRequestPayload::V0(AppRequestPayloadV0::QrCodeProfileImport( text))) = let profile = if let Some(AppRequestPayload::V0(
payload AppRequestPayloadV0::QrCodeProfileImport(text),
)) = payload
{ {
let ser = base64_url::decode(&text).map_err(|_| NgError::SerializationError)?; let ser = base64_url::decode(&text).map_err(|_| NgError::SerializationError)?;
let code:NgQRCode = serde_bare::from_slice(&ser)?; let code: NgQRCode = serde_bare::from_slice(&ser)?;
let profile = match code { let profile = match code {
NgQRCode::ProfileSharingV0(profile) => profile, NgQRCode::ProfileSharingV0(profile) => profile,
_ => return Err(NgError::InvalidPayload) _ => return Err(NgError::InvalidPayload),
}; };
profile profile
} else { } else {
@ -1050,20 +1098,23 @@ impl Verifier {
}; };
let repo_id = match nuri.target { let repo_id = match nuri.target {
NuriTargetV0::Repo(id) => id, NuriTargetV0::Repo(id) => id,
_ => return Err(NgError::InvalidPayload) _ => return Err(NgError::InvalidPayload),
}; };
return match self.import_contact_from_qrcode(repo_id, profile).await { return match self.import_contact_from_qrcode(repo_id, profile).await {
Err(e) => Ok(AppResponse::error(e.to_string())), Err(e) => Ok(AppResponse::error(e.to_string())),
Ok(()) => Ok(AppResponse::ok()), Ok(()) => Ok(AppResponse::ok()),
}; };
} }
AppRequestCommandV0::Header => { AppRequestCommandV0::Header => {
if let Some(AppRequestPayload::V0(AppRequestPayloadV0::Header(doc_header))) = if let Some(AppRequestPayload::V0(AppRequestPayloadV0::Header(doc_header))) =
payload payload
{ {
return match self.update_header(&nuri.target, doc_header.title, doc_header.about).await { return match self
.update_header(&nuri.target, doc_header.title, doc_header.about)
.await
{
Ok(_) => Ok(AppResponse::ok()), Ok(_) => Ok(AppResponse::ok()),
Err(e) => Ok(AppResponse::error(e.to_string())) Err(e) => Ok(AppResponse::error(e.to_string())),
}; };
} else { } else {
return Err(NgError::InvalidPayload); return Err(NgError::InvalidPayload);
@ -1076,7 +1127,7 @@ impl Verifier {
match self.doc_create(nuri, doc_create).await { match self.doc_create(nuri, doc_create).await {
Err(NgError::SparqlError(e)) => Ok(AppResponse::error(e)), Err(NgError::SparqlError(e)) => Ok(AppResponse::error(e)),
Err(e) => Err(e), Err(e) => Err(e),
Ok(nuri_result) => Ok(AppResponse::V0(AppResponseV0::Nuri(nuri_result))) Ok(nuri_result) => Ok(AppResponse::V0(AppResponseV0::Nuri(nuri_result))),
} }
} else { } else {
Err(NgError::InvalidPayload) Err(NgError::InvalidPayload)
@ -1250,15 +1301,16 @@ impl Verifier {
return Ok(AppResponse::V0(AppResponseV0::Text(vec.join("\n")))); return Ok(AppResponse::V0(AppResponseV0::Text(vec.join("\n"))));
} }
AppFetchContentV0::CurrentHeads => { AppFetchContentV0::CurrentHeads => {
if nuri.target.is_repo_id() { if nuri.target.is_repo_id() {
if let Ok(s) = self.get_main_branch_current_heads_nuri(nuri.target.repo_id()) { if let Ok(s) =
self.get_main_branch_current_heads_nuri(nuri.target.repo_id())
{
return Ok(AppResponse::V0(AppResponseV0::Text(s))); return Ok(AppResponse::V0(AppResponseV0::Text(s)));
} }
} }
return Ok(AppResponse::error(VerifierError::InvalidNuri.to_string())); return Ok(AppResponse::error(VerifierError::InvalidNuri.to_string()));
} }
AppFetchContentV0::History => { AppFetchContentV0::History => {
if !nuri.is_valid_for_sparql_update() { if !nuri.is_valid_for_sparql_update() {
return Err(NgError::InvalidNuri); return Err(NgError::InvalidNuri);

@ -25,6 +25,7 @@ use async_std::stream::StreamExt;
use async_std::sync::{Mutex, RwLockReadGuard}; use async_std::sync::{Mutex, RwLockReadGuard};
use futures::channel::mpsc; use futures::channel::mpsc;
use futures::SinkExt; use futures::SinkExt;
use ng_net::orm::OrmShapeTypeRef;
use ng_oxigraph::oxigraph::sparql::Query; use ng_oxigraph::oxigraph::sparql::Query;
use ng_oxigraph::oxigraph::sparql::QueryResults; use ng_oxigraph::oxigraph::sparql::QueryResults;
use ng_oxigraph::oxrdf::Term; use ng_oxigraph::oxrdf::Term;
@ -111,7 +112,9 @@ pub struct Verifier {
in_memory_outbox: Vec<EventOutboxStorage>, in_memory_outbox: Vec<EventOutboxStorage>,
uploads: BTreeMap<u32, RandomAccessFile>, uploads: BTreeMap<u32, RandomAccessFile>,
branch_subscriptions: HashMap<BranchId, Sender<AppResponse>>, branch_subscriptions: HashMap<BranchId, Sender<AppResponse>>,
pub(crate) orm_subscriptions: HashMap<NuriV0, HashMap<String, HashMap<u64, Sender<AppResponse>>>>, pub(crate) orm_subscriptions:
HashMap<NuriV0, HashMap<String, HashMap<u64, Sender<AppResponse>>>>,
pub(crate) orm_shape_types: HashMap<String, OrmShapeTypeRef>,
pub(crate) temporary_repo_certificates: HashMap<RepoId, ObjectRef>, pub(crate) temporary_repo_certificates: HashMap<RepoId, ObjectRef>,
} }
@ -518,6 +521,7 @@ impl Verifier {
uploads: BTreeMap::new(), uploads: BTreeMap::new(),
branch_subscriptions: HashMap::new(), branch_subscriptions: HashMap::new(),
orm_subscriptions: HashMap::new(), orm_subscriptions: HashMap::new(),
orm_shape_types: HashMap::new(),
temporary_repo_certificates: HashMap::new(), temporary_repo_certificates: HashMap::new(),
} }
} }
@ -1288,9 +1292,9 @@ impl Verifier {
// registering inbox for protected and public store. (FIXME: this should be done instead in the 1st connection during wallet creation) // registering inbox for protected and public store. (FIXME: this should be done instead in the 1st connection during wallet creation)
let remote = self.connected_broker.connected_or_err()?; let remote = self.connected_broker.connected_or_err()?;
let mut done = false; let mut done = false;
for (_,store) in self.stores.iter() { for (_, store) in self.stores.iter() {
if store.id() == self.protected_store_id() || store.id() == self.public_store_id() { if store.id() == self.protected_store_id() || store.id() == self.public_store_id() {
let repo = self.get_repo( store.id(), &store.get_store_repo())?; let repo = self.get_repo(store.id(), &store.get_store_repo())?;
let inbox = repo.inbox.to_owned().unwrap(); let inbox = repo.inbox.to_owned().unwrap();
// sending InboxRegister // sending InboxRegister
let msg = InboxRegister::new(inbox, store.outer_overlay())?; let msg = InboxRegister::new(inbox, store.outer_overlay())?;
@ -1568,8 +1572,11 @@ impl Verifier {
Ok(()) Ok(())
} }
pub async fn get_qrcode_for_profile(&self, public: bool, size: u32) -> Result<String, VerifierError> { pub async fn get_qrcode_for_profile(
&self,
public: bool,
size: u32,
) -> Result<String, VerifierError> {
let profile_id = if public { let profile_id = if public {
self.public_store_id() self.public_store_id()
} else { } else {
@ -1577,21 +1584,31 @@ impl Verifier {
}; };
let repo = self.repos.get(&profile_id).ok_or(NgError::RepoNotFound)?; let repo = self.repos.get(&profile_id).ok_or(NgError::RepoNotFound)?;
let inbox = repo.inbox.to_owned().ok_or(NgError::InboxNotFound)?.to_pub(); let inbox = repo
.inbox
.to_owned()
.ok_or(NgError::InboxNotFound)?
.to_pub();
let profile = repo.store.get_store_repo().clone(); let profile = repo.store.get_store_repo().clone();
let sparql = format!(" let sparql = format!(
"
PREFIX vcard: <http://www.w3.org/2006/vcard/ns#> PREFIX vcard: <http://www.w3.org/2006/vcard/ns#>
SELECT ?name ?email WHERE SELECT ?name ?email WHERE
{{ <> vcard:fn ?name . {{ <> vcard:fn ?name .
<> vcard:hasEmail ?email . <> vcard:hasEmail ?email .
}}"); }}"
);
//log_info!("{sparql}"); //log_info!("{sparql}");
let (name, email) = match self.sparql_query( let (name, email) = match self
&NuriV0::new_repo_target_from_id(profile_id), .sparql_query(
sparql, Some(NuriV0::repo_id(profile_id))).await? &NuriV0::new_repo_target_from_id(profile_id),
sparql,
Some(NuriV0::repo_id(profile_id)),
)
.await?
{ {
QueryResults::Solutions(mut sols) => { QueryResults::Solutions(mut sols) => {
match sols.next() { match sols.next() {
None => { None => {
//log_info!("name or email not found"); //log_info!("name or email not found");
@ -1613,39 +1630,39 @@ impl Verifier {
}; };
(name, email) (name, email)
} }
} }
}
_ => {
return Err(VerifierError::SparqlError(
NgError::InvalidResponse.to_string(),
))
} }
_ => return Err(VerifierError::SparqlError(NgError::InvalidResponse.to_string())),
}; };
if name.is_none() { if name.is_none() {
return Err(VerifierError::InvalidProfile); return Err(VerifierError::InvalidProfile);
} }
let profile_sharing = NgQRCode::ProfileSharingV0(NgQRCodeProfileSharingV0 { let profile_sharing = NgQRCode::ProfileSharingV0(NgQRCodeProfileSharingV0 {
inbox, inbox,
profile, profile,
name: name.unwrap(), name: name.unwrap(),
email email,
}); });
let ser = serde_bare::to_vec(&profile_sharing)?; let ser = serde_bare::to_vec(&profile_sharing)?;
let encoded = base64_url::encode(&ser); let encoded = base64_url::encode(&ser);
log_info!("qrcode= {encoded}"); log_info!("qrcode= {encoded}");
match QrCode::with_error_correction_level(encoded.as_bytes(), qrcode::EcLevel::M) { match QrCode::with_error_correction_level(encoded.as_bytes(), qrcode::EcLevel::M) {
Ok(qr) => { Ok(qr) => Ok(qr
Ok(qr .render()
.render() .max_dimensions(size, size)
.max_dimensions(size, size) .dark_color(svg::Color("#000000"))
.dark_color(svg::Color("#000000")) .light_color(svg::Color("#ffffff"))
.light_color(svg::Color("#ffffff")) .build()),
.build()
)
}
Err(e) => Err(VerifierError::QrCode(e.to_string())), Err(e) => Err(VerifierError::QrCode(e.to_string())),
} }
} }
pub async fn inbox(&mut self, msg: &InboxMsg, from_queue: bool) { pub async fn inbox(&mut self, msg: &InboxMsg, from_queue: bool) {
//log_info!("RECEIVED INBOX MSG {:?}", msg); //log_info!("RECEIVED INBOX MSG {:?}", msg);
match self.inboxes.get(&msg.body.to_inbox) { match self.inboxes.get(&msg.body.to_inbox) {
@ -1660,27 +1677,31 @@ impl Verifier {
if let Err(e) = res { if let Err(e) = res {
log_err!("Error during process_inbox {e}"); log_err!("Error during process_inbox {e}");
} }
}, }
Err(e) => { Err(e) => {
log_err!("cannot unseal inbox msg {e}"); log_err!("cannot unseal inbox msg {e}");
} }
} }
} }
}, }
None => {} None => {}
} }
}, }
None => {} None => {}
} }
if from_queue && self.connected_broker.is_some(){ if from_queue && self.connected_broker.is_some() {
log_info!("try to pop one more inbox msg"); log_info!("try to pop one more inbox msg");
// try to pop inbox msg // try to pop inbox msg
let connected_broker = self.connected_broker.clone(); let connected_broker = self.connected_broker.clone();
let broker = BROKER.read().await; let broker = BROKER.read().await;
let user = self.user_id().clone(); let user = self.user_id().clone();
let _ = broker let _ = broker
.send_client_event(&Some(user), &connected_broker.into(), ClientEvent::InboxPopRequest) .send_client_event(
&Some(user),
&connected_broker.into(),
ClientEvent::InboxPopRequest,
)
.await; .await;
} }
} }
@ -1794,12 +1815,15 @@ impl Verifier {
} }
} }
pub(crate) fn get_main_branch_current_heads_nuri(&self, repo_id: &RepoId) -> Result<String, VerifierError> { pub(crate) fn get_main_branch_current_heads_nuri(
&self,
repo_id: &RepoId,
) -> Result<String, VerifierError> {
if let Some(repo) = self.repos.get(repo_id) { if let Some(repo) = self.repos.get(repo_id) {
if let Some(info) = repo.main_branch() { if let Some(info) = repo.main_branch() {
let mut res = NuriV0::repo_id(repo_id); let mut res = NuriV0::repo_id(repo_id);
for head in info.current_heads.iter() { for head in info.current_heads.iter() {
res = [res,NuriV0::commit_ref(head)].join(":"); res = [res, NuriV0::commit_ref(head)].join(":");
} }
return Ok(res); return Ok(res);
} }
@ -1886,14 +1910,22 @@ impl Verifier {
let storage = match self.repos.get_mut(&inbox_cap.repo_id) { let storage = match self.repos.get_mut(&inbox_cap.repo_id) {
Some(repo) => { Some(repo) => {
repo.inbox = Some(inbox_cap.priv_key.clone()); repo.inbox = Some(inbox_cap.priv_key.clone());
log_info!("INBOX for {} : {}", inbox_cap.repo_id.to_string(), inbox_cap.priv_key.to_pub().to_string()); log_info!(
"INBOX for {} : {}",
inbox_cap.repo_id.to_string(),
inbox_cap.priv_key.to_pub().to_string()
);
self.inboxes.insert(inbox_cap.priv_key.to_pub(), repo.id); self.inboxes.insert(inbox_cap.priv_key.to_pub(), repo.id);
self.user_storage_if_persistent() self.user_storage_if_persistent()
} }
None => self.user_storage(), None => self.user_storage(),
}; };
if let Some(user_storage) = storage { if let Some(user_storage) = storage {
user_storage.update_inbox_cap(&inbox_cap.repo_id, &inbox_cap.overlay, &inbox_cap.priv_key)?; user_storage.update_inbox_cap(
&inbox_cap.repo_id,
&inbox_cap.overlay,
&inbox_cap.priv_key,
)?;
} }
Ok(()) Ok(())
@ -2782,6 +2814,7 @@ impl Verifier {
uploads: BTreeMap::new(), uploads: BTreeMap::new(),
branch_subscriptions: HashMap::new(), branch_subscriptions: HashMap::new(),
orm_subscriptions: HashMap::new(), orm_subscriptions: HashMap::new(),
orm_shape_types: HashMap::new(),
temporary_repo_certificates: HashMap::new(), temporary_repo_certificates: HashMap::new(),
}; };
// this is important as it will load the last seq from storage // this is important as it will load the last seq from storage
@ -2839,8 +2872,14 @@ impl Verifier {
//self.populate_topics(&repo); //self.populate_topics(&repo);
let _ = self.add_doc(&repo.id, &repo.store.overlay_id); let _ = self.add_doc(&repo.id, &repo.store.overlay_id);
if repo.inbox.is_some() { if repo.inbox.is_some() {
log_info!("INBOX for {} : {}", repo.id.to_string(), repo.inbox.as_ref().unwrap().to_pub().to_string()); log_info!(
_ = self.inboxes.insert(repo.inbox.as_ref().unwrap().to_pub(), repo.id); "INBOX for {} : {}",
repo.id.to_string(),
repo.inbox.as_ref().unwrap().to_pub().to_string()
);
_ = self
.inboxes
.insert(repo.inbox.as_ref().unwrap().to_pub(), repo.id);
} }
let repo_ref = self.repos.entry(repo.id).or_insert(repo); let repo_ref = self.repos.entry(repo.id).or_insert(repo);
repo_ref repo_ref

@ -82,7 +82,7 @@ function addConstructPattern(
(pred.minCardinality ?? 0) === 0 && (pred.minCardinality ?? 0) === 0 &&
(options?.includeOptionalForMinZero ?? true); (options?.includeOptionalForMinZero ?? true);
if (pred.valType === "nested" && pred.nestedShape) { if (pred.dataTypes === "nested" && pred.nestedShape) {
template.push(triple); template.push(triple);
const nestedBody: string[] = [triple]; const nestedBody: string[] = [triple];
const nestedPreds = pred.nestedShape.predicates; const nestedPreds = pred.nestedShape.predicates;
@ -108,7 +108,7 @@ function addConstructPattern(
template.push(triple); template.push(triple);
const blockLines: string[] = [triple]; const blockLines: string[] = [triple];
if (pred.valType === "literal" && pred.literalValue !== undefined) { if (pred.dataTypes === "literal" && pred.literalValue !== undefined) {
if (Array.isArray(pred.literalValue)) { if (Array.isArray(pred.literalValue)) {
valuesBlocks.push(valuesBlock(objVar, pred.literalValue as any[])); valuesBlocks.push(valuesBlock(objVar, pred.literalValue as any[]));
} else { } else {

@ -35,7 +35,7 @@ export const buildConstructQuery = ({
for (const pred of predicates) { for (const pred of predicates) {
const subjectVarName = getVarNameFor(shapeId); const subjectVarName = getVarNameFor(shapeId);
if (pred.valType === "nested") { if (pred.dataTypes === "nested") {
if (typeof pred.nestedShape !== "string") if (typeof pred.nestedShape !== "string")
throw new Error("Nested shapes must be by reference"); throw new Error("Nested shapes must be by reference");

@ -10,7 +10,7 @@ export const testShapeSchema: Schema = {
iri: "http://example.org/TestObject", iri: "http://example.org/TestObject",
predicates: [ predicates: [
{ {
valType: "literal", dataTypes: "literal",
literalValue: ["TestObject"], literalValue: ["TestObject"],
maxCardinality: 1, maxCardinality: 1,
minCardinality: 1, minCardinality: 1,
@ -19,35 +19,35 @@ export const testShapeSchema: Schema = {
extra: true, extra: true,
}, },
{ {
valType: "string", dataTypes: "string",
maxCardinality: 1, maxCardinality: 1,
minCardinality: 1, minCardinality: 1,
iri: "http://example.org/stringValue", iri: "http://example.org/stringValue",
readablePredicate: "stringValue", readablePredicate: "stringValue",
}, },
{ {
valType: "number", dataTypes: "number",
maxCardinality: 1, maxCardinality: 1,
minCardinality: 1, minCardinality: 1,
iri: "http://example.org/numValue", iri: "http://example.org/numValue",
readablePredicate: "numValue", readablePredicate: "numValue",
}, },
{ {
valType: "boolean", dataTypes: "boolean",
maxCardinality: 1, maxCardinality: 1,
minCardinality: 1, minCardinality: 1,
iri: "http://example.org/boolValue", iri: "http://example.org/boolValue",
readablePredicate: "boolValue", readablePredicate: "boolValue",
}, },
{ {
valType: "number", dataTypes: "number",
maxCardinality: -1, maxCardinality: -1,
minCardinality: 0, minCardinality: 0,
iri: "http://example.org/arrayValue", iri: "http://example.org/arrayValue",
readablePredicate: "arrayValue", readablePredicate: "arrayValue",
}, },
{ {
valType: "nested", dataTypes: "nested",
nestedShape: nestedShape:
"http://example.org/TestObject||http://example.org/objectValue", "http://example.org/TestObject||http://example.org/objectValue",
maxCardinality: 1, maxCardinality: 1,
@ -56,7 +56,7 @@ export const testShapeSchema: Schema = {
readablePredicate: "objectValue", readablePredicate: "objectValue",
}, },
{ {
valType: "nested", dataTypes: "nested",
nestedShape: nestedShape:
"http://example.org/TestObject||http://example.org/anotherObject", "http://example.org/TestObject||http://example.org/anotherObject",
maxCardinality: -1, maxCardinality: -1,
@ -65,7 +65,7 @@ export const testShapeSchema: Schema = {
readablePredicate: "anotherObject", readablePredicate: "anotherObject",
}, },
{ {
valType: "eitherOf", dataTypes: "eitherOf",
eitherOf: [ eitherOf: [
{ {
valType: "string", valType: "string",
@ -85,21 +85,21 @@ export const testShapeSchema: Schema = {
iri: "http://example.org/TestObject||http://example.org/objectValue", iri: "http://example.org/TestObject||http://example.org/objectValue",
predicates: [ predicates: [
{ {
valType: "string", dataTypes: "string",
maxCardinality: 1, maxCardinality: 1,
minCardinality: 1, minCardinality: 1,
iri: "http://example.org/nestedString", iri: "http://example.org/nestedString",
readablePredicate: "nestedString", readablePredicate: "nestedString",
}, },
{ {
valType: "number", dataTypes: "number",
maxCardinality: 1, maxCardinality: 1,
minCardinality: 1, minCardinality: 1,
iri: "http://example.org/nestedNum", iri: "http://example.org/nestedNum",
readablePredicate: "nestedNum", readablePredicate: "nestedNum",
}, },
{ {
valType: "number", dataTypes: "number",
maxCardinality: -1, maxCardinality: -1,
minCardinality: 0, minCardinality: 0,
iri: "http://example.org/nestedArray", iri: "http://example.org/nestedArray",
@ -111,14 +111,14 @@ export const testShapeSchema: Schema = {
iri: "http://example.org/TestObject||http://example.org/anotherObject", iri: "http://example.org/TestObject||http://example.org/anotherObject",
predicates: [ predicates: [
{ {
valType: "string", dataTypes: "string",
maxCardinality: 1, maxCardinality: 1,
minCardinality: 1, minCardinality: 1,
iri: "http://example.org/prop1", iri: "http://example.org/prop1",
readablePredicate: "prop1", readablePredicate: "prop1",
}, },
{ {
valType: "number", dataTypes: "number",
maxCardinality: 1, maxCardinality: 1,
minCardinality: 1, minCardinality: 1,
iri: "http://example.org/prop2", iri: "http://example.org/prop2",

@ -6,73 +6,104 @@ import type { Schema } from "@nextgraph-monorepo/ng-shex-orm";
* ============================================================================= * =============================================================================
*/ */
export const catShapeSchema: Schema = { export const catShapeSchema: Schema = {
"http://example.org/Cat": { "http://example.org/Cat": {
iri: "http://example.org/Cat", iri: "http://example.org/Cat",
predicates: [ predicates: [
{ {
valType: "literal", dataTypes: [
literalValue: ["http://example.org/Cat"], {
maxCardinality: 1, valType: "literal",
minCardinality: 1, literals: ["http://example.org/Cat"],
iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type", },
readablePredicate: "type",
},
{
valType: "string",
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/name",
readablePredicate: "name",
},
{
valType: "number",
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/age",
readablePredicate: "age",
},
{
valType: "number",
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/numberOfHomes",
readablePredicate: "numberOfHomes",
},
{
valType: "nested",
nestedShape:
"http://example.org/Cat||http://example.org/address",
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/address",
readablePredicate: "address",
},
], ],
}, maxCardinality: 1,
"http://example.org/Cat||http://example.org/address": { minCardinality: 1,
iri: "http://example.org/Cat||http://example.org/address", iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
predicates: [ readablePredicate: "type",
{ },
valType: "string", {
maxCardinality: 1, dataTypes: [
minCardinality: 1, {
iri: "http://example.org/street", valType: "string",
readablePredicate: "street", },
},
{
valType: "string",
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/houseNumber",
readablePredicate: "houseNumber",
},
{
valType: "number",
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/floor",
readablePredicate: "floor",
},
], ],
}, maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/name",
readablePredicate: "name",
},
{
dataTypes: [
{
valType: "number",
},
],
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/age",
readablePredicate: "age",
},
{
dataTypes: [
{
valType: "number",
},
],
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/numberOfHomes",
readablePredicate: "numberOfHomes",
},
{
dataTypes: [
{
valType: "shape",
shape: "http://example.org/Cat||http://example.org/address",
},
],
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/address",
readablePredicate: "address",
},
],
},
"http://example.org/Cat||http://example.org/address": {
iri: "http://example.org/Cat||http://example.org/address",
predicates: [
{
dataTypes: [
{
valType: "string",
},
],
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/street",
readablePredicate: "street",
},
{
dataTypes: [
{
valType: "string",
},
],
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/houseNumber",
readablePredicate: "houseNumber",
},
{
dataTypes: [
{
valType: "number",
},
],
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/floor",
readablePredicate: "floor",
},
],
},
}; };

@ -6,66 +6,93 @@ import type { Schema } from "@nextgraph-monorepo/ng-shex-orm";
* ============================================================================= * =============================================================================
*/ */
export const personShapeSchema: Schema = { export const personShapeSchema: Schema = {
"http://example.org/Person": { "http://example.org/Person": {
iri: "http://example.org/Person", iri: "http://example.org/Person",
predicates: [ predicates: [
{ {
valType: "literal", dataTypes: [
literalValue: ["http://example.org/Person"], {
maxCardinality: 1, valType: "literal",
minCardinality: 1, literals: ["http://example.org/Person"],
iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type", },
readablePredicate: "type",
},
{
valType: "string",
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/name",
readablePredicate: "name",
},
{
valType: "nested",
nestedShape:
"http://example.org/Person||http://example.org/address",
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/address",
readablePredicate: "address",
},
{
valType: "boolean",
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/hasChildren",
readablePredicate: "hasChildren",
},
{
valType: "number",
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/numberOfHouses",
readablePredicate: "numberOfHouses",
},
], ],
}, maxCardinality: 1,
"http://example.org/Person||http://example.org/address": { minCardinality: 1,
iri: "http://example.org/Person||http://example.org/address", iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
predicates: [ readablePredicate: "type",
{ },
valType: "string", {
maxCardinality: 1, dataTypes: [
minCardinality: 1, {
iri: "http://example.org/street", valType: "string",
readablePredicate: "street", },
},
{
valType: "string",
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/houseNumber",
readablePredicate: "houseNumber",
},
], ],
}, maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/name",
readablePredicate: "name",
},
{
dataTypes: [
{
valType: "shape",
shape: "http://example.org/Person||http://example.org/address",
},
],
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/address",
readablePredicate: "address",
},
{
dataTypes: [
{
valType: "boolean",
},
],
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/hasChildren",
readablePredicate: "hasChildren",
},
{
dataTypes: [
{
valType: "number",
},
],
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/numberOfHouses",
readablePredicate: "numberOfHouses",
},
],
},
"http://example.org/Person||http://example.org/address": {
iri: "http://example.org/Person||http://example.org/address",
predicates: [
{
dataTypes: [
{
valType: "string",
},
],
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/street",
readablePredicate: "street",
},
{
dataTypes: [
{
valType: "string",
},
],
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/houseNumber",
readablePredicate: "houseNumber",
},
],
},
}; };

@ -6,132 +6,183 @@ import type { Schema } from "@nextgraph-monorepo/ng-shex-orm";
* ============================================================================= * =============================================================================
*/ */
export const testShapeSchema: Schema = { export const testShapeSchema: Schema = {
"http://example.org/TestObject": { "http://example.org/TestObject": {
iri: "http://example.org/TestObject", iri: "http://example.org/TestObject",
predicates: [ predicates: [
{ {
valType: "literal", dataTypes: [
literalValue: ["http://example.org/TestObject"], {
maxCardinality: 1, valType: "literal",
minCardinality: 1, literals: ["http://example.org/TestObject"],
iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type", },
readablePredicate: "type",
extra: true,
},
{
valType: "string",
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/stringValue",
readablePredicate: "stringValue",
},
{
valType: "number",
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/numValue",
readablePredicate: "numValue",
},
{
valType: "boolean",
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/boolValue",
readablePredicate: "boolValue",
},
{
valType: "number",
maxCardinality: -1,
minCardinality: 0,
iri: "http://example.org/arrayValue",
readablePredicate: "arrayValue",
},
{
valType: "nested",
nestedShape:
"http://example.org/TestObject||http://example.org/objectValue",
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/objectValue",
readablePredicate: "objectValue",
},
{
valType: "nested",
nestedShape:
"http://example.org/TestObject||http://example.org/anotherObject",
maxCardinality: -1,
minCardinality: 0,
iri: "http://example.org/anotherObject",
readablePredicate: "anotherObject",
},
{
valType: "eitherOf",
eitherOf: [
{
valType: "string",
},
{
valType: "number",
},
],
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/numOrStr",
readablePredicate: "numOrStr",
},
{
valType: "literal",
literalValue: ["lit1", "lit2"],
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/lit1Or2",
readablePredicate: "lit1Or2",
},
], ],
}, maxCardinality: 1,
"http://example.org/TestObject||http://example.org/objectValue": { minCardinality: 1,
iri: "http://example.org/TestObject||http://example.org/objectValue", iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
predicates: [ readablePredicate: "type",
{ extra: true,
valType: "string", },
maxCardinality: 1, {
minCardinality: 1, dataTypes: [
iri: "http://example.org/nestedString", {
readablePredicate: "nestedString", valType: "string",
}, },
{
valType: "number",
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/nestedNum",
readablePredicate: "nestedNum",
},
{
valType: "number",
maxCardinality: -1,
minCardinality: 0,
iri: "http://example.org/nestedArray",
readablePredicate: "nestedArray",
},
], ],
}, maxCardinality: 1,
"http://example.org/TestObject||http://example.org/anotherObject": { minCardinality: 1,
iri: "http://example.org/TestObject||http://example.org/anotherObject", iri: "http://example.org/stringValue",
predicates: [ readablePredicate: "stringValue",
{ },
valType: "string", {
maxCardinality: 1, dataTypes: [
minCardinality: 1, {
iri: "http://example.org/prop1", valType: "number",
readablePredicate: "prop1", },
},
{
valType: "number",
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/prop2",
readablePredicate: "prop2",
},
], ],
}, maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/numValue",
readablePredicate: "numValue",
},
{
dataTypes: [
{
valType: "boolean",
},
],
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/boolValue",
readablePredicate: "boolValue",
},
{
dataTypes: [
{
valType: "number",
},
],
maxCardinality: -1,
minCardinality: 0,
iri: "http://example.org/arrayValue",
readablePredicate: "arrayValue",
},
{
dataTypes: [
{
valType: "shape",
shape:
"http://example.org/TestObject||http://example.org/objectValue",
},
],
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/objectValue",
readablePredicate: "objectValue",
},
{
dataTypes: [
{
valType: "shape",
shape:
"http://example.org/TestObject||http://example.org/anotherObject",
},
],
maxCardinality: -1,
minCardinality: 0,
iri: "http://example.org/anotherObject",
readablePredicate: "anotherObject",
},
{
dataTypes: [
{
valType: "string",
},
{
valType: "number",
},
],
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/numOrStr",
readablePredicate: "numOrStr",
},
{
dataTypes: [
{
valType: "literal",
literals: ["lit1", "lit2"],
},
],
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/lit1Or2",
readablePredicate: "lit1Or2",
},
],
},
"http://example.org/TestObject||http://example.org/objectValue": {
iri: "http://example.org/TestObject||http://example.org/objectValue",
predicates: [
{
dataTypes: [
{
valType: "string",
},
],
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/nestedString",
readablePredicate: "nestedString",
},
{
dataTypes: [
{
valType: "number",
},
],
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/nestedNum",
readablePredicate: "nestedNum",
},
{
dataTypes: [
{
valType: "number",
},
],
maxCardinality: -1,
minCardinality: 0,
iri: "http://example.org/nestedArray",
readablePredicate: "nestedArray",
},
],
},
"http://example.org/TestObject||http://example.org/anotherObject": {
iri: "http://example.org/TestObject||http://example.org/anotherObject",
predicates: [
{
dataTypes: [
{
valType: "string",
},
],
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/prop1",
readablePredicate: "prop1",
},
{
dataTypes: [
{
valType: "number",
},
],
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/prop2",
readablePredicate: "prop2",
},
],
},
}; };

@ -77,27 +77,38 @@ function flattenSchema(shapes: Shape[]): ShapeSchema {
for (const shape of shapes) { for (const shape of shapes) {
schema[shape.iri] = shape; schema[shape.iri] = shape;
// Find nested, unflattened (i.e. anonymous) schemas in properties. // Find nested, unflattened (i.e. anonymous) schemas in predicates' dataTypes.
const nestedSchemaPredicates = shape.predicates.filter( for (const pred of shape.predicates) {
(pred) => for (let i = 0; i < pred.dataTypes.length; i++) {
pred.valType === "nested" && const dt = pred.dataTypes[i];
typeof pred.nestedShape === "object" if (
); dt.valType === "shape" &&
typeof dt.shape === "object" &&
for (const pred of nestedSchemaPredicates) { dt.shape !== null
const newId = shape.iri + "||" + pred.iri; ) {
// create a deterministic id for the nested shape; include index if multiple shape entries exist
const shapeCount = pred.dataTypes.filter(
(d) => d.valType === "shape"
).length;
const newId =
shape.iri +
"||" +
pred.iri +
(shapeCount > 1 ? `||${i}` : "");
// Recurse // Recurse
const flattened = flattenSchema([ const flattened = flattenSchema([
{ {
...(pred.nestedShape as Shape), ...(dt.shape as Shape),
iri: newId, iri: newId,
}, },
]); ]);
// Replace the nested schema with its new id. // Replace the nested schema with its new id.
pred.nestedShape = newId; dt.shape = newId;
schema = { ...schema, ...flattened }; schema = { ...schema, ...flattened };
}
}
} }
// Flatten / Recurse // Flatten / Recurse
} }

@ -65,7 +65,7 @@ export const ShexJSchemaTransformerCompact = ShexJTraverser.createTransformer<
EachOf: { return: Shape }; EachOf: { return: Shape };
TripleConstraint: { return: Predicate }; TripleConstraint: { return: Predicate };
NodeConstraint: { return: DataType }; NodeConstraint: { return: DataType };
ShapeOr: { return: (DataType | Shape | string)[] }; ShapeOr: { return: DataType[] };
ShapeAnd: { return: never }; ShapeAnd: { return: never };
ShapeNot: { return: never }; ShapeNot: { return: never };
ShapeExternal: { return: never }; ShapeExternal: { return: never };
@ -143,24 +143,31 @@ export const ShexJSchemaTransformerCompact = ShexJTraverser.createTransformer<
if (typeof transformedChildren.valueExpr === "string") { if (typeof transformedChildren.valueExpr === "string") {
// Reference to nested object // Reference to nested object
return { return {
valType: "nested", dataTypes: [
nestedShape: transformedChildren.valueExpr, {
valType: "shape",
shape: transformedChildren.valueExpr,
},
],
...commonProperties, ...commonProperties,
} satisfies Predicate; };
} else if ( } else if (
transformedChildren.valueExpr && transformedChildren.valueExpr &&
(transformedChildren.valueExpr as Shape).predicates (transformedChildren.valueExpr as Shape).predicates
) { ) {
// Nested object // Nested object
return { return {
valType: "nested", dataTypes: [
nestedShape: transformedChildren.valueExpr as Shape, {
valType: "shape",
shape: transformedChildren.valueExpr as Shape,
},
],
...commonProperties, ...commonProperties,
} satisfies Predicate; };
} else if (Array.isArray(transformedChildren.valueExpr)) { } else if (Array.isArray(transformedChildren.valueExpr)) {
return { return {
valType: "eitherOf", dataTypes: transformedChildren.valueExpr, // DataType[]
eitherOf: transformedChildren.valueExpr,
...commonProperties, ...commonProperties,
}; };
} else { } else {
@ -168,10 +175,14 @@ export const ShexJSchemaTransformerCompact = ShexJTraverser.createTransformer<
const nodeConstraint = const nodeConstraint =
transformedChildren.valueExpr as DataType; transformedChildren.valueExpr as DataType;
return { return {
valType: nodeConstraint.valType, dataTypes: [
literalValue: nodeConstraint.literals, {
valType: nodeConstraint.valType,
literals: nodeConstraint.literals,
},
],
...commonProperties, ...commonProperties,
} satisfies Predicate; };
} }
}, },
}, },
@ -192,6 +203,8 @@ export const ShexJSchemaTransformerCompact = ShexJTraverser.createTransformer<
valType: "literal", valType: "literal",
literals: nodeConstraint.values.map( literals: nodeConstraint.values.map(
// TODO: We do not convert them to number or boolean or lang tag. // TODO: We do not convert them to number or boolean or lang tag.
// And we don't have an annotation of the literal's type.
// @ts-expect-error
(valueRecord) => valueRecord.value || valueRecord.id (valueRecord) => valueRecord.value || valueRecord.id
), ),
}; };
@ -210,11 +223,9 @@ export const ShexJSchemaTransformerCompact = ShexJTraverser.createTransformer<
transformer: async (shapeOr, getTransformedChildren) => { transformer: async (shapeOr, getTransformedChildren) => {
const { shapeExprs } = await getTransformedChildren(); const { shapeExprs } = await getTransformedChildren();
// Either a shape IRI, a nested shape or a node CompactSchemaValue (node constraint). // Either a shape IRI, a nested shape or a node CompactSchemaValue (node constraint).
return (Array.isArray(shapeExprs) ? shapeExprs : [shapeExprs]) as ( return (
| string Array.isArray(shapeExprs) ? shapeExprs : [shapeExprs]
| Shape ) as DataType[];
| DataType
)[];
}, },
}, },

@ -17,27 +17,25 @@ export interface Shape {
} }
export type DataType = { export type DataType = {
/** The required literal value(s), if type is `literal`. Others are allowed, if `extra` is true. */
literals?: number[] | string[] | boolean; literals?: number[] | string[] | boolean;
valType: "number" | "string" | "boolean" | "iri" | "literal"; /** If `valType` is `"shape"`, the nested shape or its reference. Use reference for serialization. */
shape?: string | Shape;
/** The type of object value for a triple constraint. */
valType: "number" | "string" | "boolean" | "iri" | "literal" | "shape";
}; };
export interface Predicate { export interface Predicate {
/** Type of property. */ /** Allowed type of object. If more than one is present, either of them is allowed. */
valType: DataType["valType"] | "nested" | "eitherOf"; dataTypes: DataType[];
/** The RDF predicate URI. */ /** The RDF predicate URI. */
iri: string; iri: string;
/** The alias of the `predicateUri` when serialized to a JSON object. */ /** The alias of the `predicateUri` when serialized to a JSON object. */
readablePredicate: string; readablePredicate: string;
/** The required literal value(s), if type is `literal`. Others are allowed, if `extra` is true. */
literalValue?: number | string | boolean | number[] | string[]; // TODO: We could live without this and use eitherOf instead...
/** If type is `nested`, the shape or its IRI. */
nestedShape?: string | Shape; // TODO: Only allow Shape while parsing from traverser. We flatten afterwards.
/** Maximum allowed number of values. `-1` means infinite. */ /** Maximum allowed number of values. `-1` means infinite. */
maxCardinality: number; maxCardinality: number;
/** Minimum required number of values */ /** Minimum required number of values */
minCardinality: number; minCardinality: number;
/** If type is `eitherOf`, specifies multiple allowed types (CompactSchemaValue, shapes, or shape IRI). */
eitherOf?: (DataType | Shape | string)[]; // TODO: Shape is going to be by reference.
/** If other (additional) values are permitted. Useful for literals. */ /** If other (additional) values are permitted. Useful for literals. */
extra?: boolean; extra?: boolean;
} }

Loading…
Cancel
Save