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",
"qrcode",
"rand 0.7.3",
"regex",
"sbbf-rs-safe",
"serde",
"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)]
pub mod orm;
#[doc(hidden)]
pub mod create_or_open_wallet;

@ -7,146 +7,33 @@
// 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::{
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 crate::local_broker::{doc_create, doc_sparql_construct, doc_sparql_update};
use crate::tests::create_or_open_wallet::create_or_open_wallet;
use ng_net::orm::{
OrmSchemaDataType, OrmSchemaLiteralType, OrmSchemaLiterals, OrmSchemaPredicate, OrmSchemaShape,
OrmShapeType,
};
use ng_net::types::BootstrapContentV0;
use ng_repo::log_info;
use ng_repo::types::PubKey;
use ng_wallet::types::{CreateWalletV0, SensitiveWallet};
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 ng_verifier::orm::sparql_construct_from_orm_shape_type;
use std::collections::HashMap;
// 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);
}
#[async_std::test]
async fn test_create_sparql_from_schema() {
// Setup wallet and document
let (_wallet, session_id) = create_or_open_wallet().await;
let doc_nuri = doc_create(
session_id,
"Graph".to_string(),
"test_orm_query".to_string(),
"store".to_string(),
None,
None,
)
.await
.expect("error creating doc");
fn build_insert_sparql() -> String {
// Data conforms to testShape.shex
// Shape requires: a ex:TestObject + required fields.
r#"
// Insert data with unrelated predicates
let insert_sparql = r#"
PREFIX ex: <http://example.org/>
INSERT DATA {
<urn:test:obj1> a ex:TestObject ;
@ -167,61 +54,487 @@ INSERT DATA {
ex:prop2 2
] ;
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 {
r#"
CONSTRUCT {
?s ?p ?o .
} WHERE {
?s ?p ?o .
#[async_std::test]
async fn test_orm_query_partial_match_missing_required() {
// Setup
let (_wallet, session_id) = create_or_open_wallet().await;
let doc_nuri = doc_create(
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 fn test_wallet_and_sparql_insert() {
let (wallet, session_id) = create_or_open_wallet().await;
let sparql = build_insert_sparql();
async fn test_orm_query_partial_match_missing_optional() {
// Setup
let (_wallet, session_id) = create_or_open_wallet().await;
let doc_nuri = doc_create(
session_id,
"Graph".to_string(),
"test".to_string(),
"test_orm_partial_optional".to_string(),
"store".to_string(),
None,
None,
)
.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;
assert!(
update_result.is_ok(),
"SPARQL update failed: {:?}",
update_result.err()
// Schema with one required and one optional field
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: 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.
let query_result =
doc_sparql_construct(session_id, build_construct_sparql(), Some(doc_nuri.clone())).await;
log_info!("Sparql construct result: {:?}", query_result.unwrap());
// 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: 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
.expect("disconnect user");
session_stop(&wallet.personal_identity())
.unwrap();
// 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
.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,
}
/* == Diff Types == */
#[derive(Clone, Debug, Serialize, Deserialize)]
#[allow(non_camel_case_types)]
pub enum OrmDiffOpType {
@ -42,12 +43,13 @@ pub struct OrmDiffOp {
pub op: OrmDiffOpType,
pub valType: Option<OrmDiffType>,
pub path: String,
pub value: Option<Value>,
pub value: Option<Value>, // TODO: Improve type
}
pub type OrmDiff = Vec<OrmDiffOp>;
type OrmSchema = HashMap<String, OrmSchemaShape>;
/* == ORM Schema == */
pub type OrmSchema = HashMap<String, OrmSchemaShape>;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct OrmSchemaShape {
@ -55,7 +57,7 @@ pub struct OrmSchemaShape {
pub predicates: Vec<OrmSchemaPredicate>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
#[allow(non_camel_case_types)]
pub enum OrmSchemaLiteralType {
number,
@ -63,23 +65,12 @@ pub enum OrmSchemaLiteralType {
boolean,
iri,
literal,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[allow(non_camel_case_types)]
pub enum OrmSchemaPredicateType {
number,
string,
boolean,
iri,
literal,
nested,
eitherOf,
shape,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum OrmLiterals {
pub enum OrmSchemaLiterals {
Bool(bool),
NumArray(Vec<f64>),
StrArray(Vec<String>),
@ -88,26 +79,47 @@ pub enum OrmLiterals {
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct OrmSchemaDataType {
pub valType: OrmSchemaLiteralType,
pub literals: Option<OrmLiterals>,
pub literals: Option<OrmSchemaLiterals>,
pub shape: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct OrmSchemaPredicate {
pub valType: OrmSchemaPredicateType,
pub dataTypes: Vec<OrmSchemaDataType>,
pub iri: String,
pub readablePredicate: String,
pub literalValue: Option<Value>, // Strictly speaking, no objects.
pub nestedShape: Option<String>, // Only by reference.
pub maxCardinality: i64, // -1 for infinity
/// `-1` for infinity
pub maxCardinality: i64,
pub minCardinality: i64,
pub eitherOf: Option<Vec<OrmSchemaEitherOfOption>>, // Shape references or multi type.
pub extra: Option<bool>,
}
// TODO: Will this be serialized correctly?
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum OrmSchemaEitherOfOption {
ShapeRef(String),
DataType(OrmSchemaDataType),
impl Default for OrmSchemaDataType {
fn default() -> Self {
Self {
literals: None,
shape: None,
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,
InternalError,
InvalidInboxPost,
InvalidOrmSchema,
}
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-oxigraph = { path = "../ng-oxigraph", version = "0.4.0-alpha.8-ngalpha" }
once_cell = "1.17.1"
regex = "1.8.4"
[target.'cfg(target_family = "wasm")'.dependencies]
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::SinkExt;
use ng_net::app_protocol::*;
use lazy_static::lazy_static;
pub use ng_net::orm::OrmDiff;
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::{
connection::NoiseFSM,
types::*,
utils::{Receiver, Sender},
};
@ -24,7 +26,9 @@ use ng_oxigraph::oxigraph::sparql::{results::*, Query, QueryResults};
use ng_oxigraph::oxrdf::Term;
use ng_oxigraph::oxrdf::Triple;
use ng_repo::errors::NgError;
use ng_repo::errors::VerifierError;
use ng_repo::log::*;
use regex::Regex;
use crate::types::*;
use crate::verifier::*;
@ -88,7 +92,7 @@ impl Verifier {
let results = oxistore
.query(parsed, None)
.map_err(|e| NgError::OxiGraphError(e.to_string()))?;
match results {
let sols = match results {
QueryResults::Solutions(sols) => {
let mut results = vec![];
for t in sols {
@ -103,12 +107,15 @@ impl Verifier {
Ok(results)
}
_ => 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 frontend_update_orm(
pub(crate) async fn orm_frontend_update(
&mut self,
scope: &NuriV0,
shape_id: String,
@ -150,7 +157,7 @@ impl Verifier {
pub(crate) async fn start_orm(
&mut self,
nuri: &NuriV0,
schema: &OrmShapeType,
shape_type: &OrmShapeType,
session_id: u64,
) -> Result<(Receiver<AppResponse>, CancelFn), NgError> {
let (tx, rx) = mpsc::unbounded::<AppResponse>();
@ -158,12 +165,12 @@ impl Verifier {
self.orm_subscriptions.insert(
nuri.clone(),
HashMap::from([(
schema.shape.clone(),
shape_type.shape.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 || {
//log_debug!("CLOSE_CHANNEL of subscription for branch {}", branch_id);
@ -174,3 +181,231 @@ impl Verifier {
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,
nuri: &NuriV0,
_payload: &Option<AppRequestPayload>,
session_id: u64
session_id: u64,
) -> Result<(Receiver<AppResponse>, CancelFn), NgError> {
match command {
AppRequestCommandV0::OrmStart => {
match _payload {
Some(AppRequestPayload::V0(AppRequestPayloadV0::OrmStart(shape_type))) => {
self.start_orm(nuri, shape_type, session_id).await
},
_ => return Err(NgError::InvalidArgument)
AppRequestCommandV0::OrmStart => match _payload {
Some(AppRequestPayload::V0(AppRequestPayloadV0::OrmStart(shape_type))) => {
self.start_orm(nuri, shape_type, session_id).await
}
_ => return Err(NgError::InvalidArgument),
},
AppRequestCommandV0::Fetch(fetch) => match fetch {
AppFetchContentV0::Subscribe => {
@ -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 graph_name = NuriV0::branch_repo_graph_name(
&branch_id,
@ -196,9 +198,7 @@ impl Verifier {
);
}
}
let query = format!(
"DELETE {{ {deletes} }} INSERT {{ {inserts} }} WHERE {{ {wheres} }}"
);
let query = format!("DELETE {{ {deletes} }} INSERT {{ {inserts} }} WHERE {{ {wheres} }}");
let oxistore = self.graph_dataset.as_ref().unwrap();
@ -212,13 +212,12 @@ impl Verifier {
if inserts.is_empty() && removes.is_empty() {
Ok(())
} else {
self
.prepare_sparql_update(
Vec::from_iter(inserts),
Vec::from_iter(removes),
self.get_peer_id_for_skolem(),
)
.await?;
self.prepare_sparql_update(
Vec::from_iter(inserts),
Vec::from_iter(removes),
self.get_peer_id_for_skolem(),
)
.await?;
Ok(())
}
}
@ -628,24 +627,26 @@ impl Verifier {
destination: String,
store_repo: Option<StoreRepo>,
) -> Result<String, NgError> {
let class = BranchCrdt::from(crdt, class_name)?;
let nuri = if store_repo.is_none() {
NuriV0::new_private_store_target()
} else {
NuriV0::from_store_repo(&store_repo.unwrap())
};
let destination = DocCreateDestination::from(destination)?;
self.doc_create(nuri, DocCreate {
class,
destination,
}).await
self.doc_create(nuri, DocCreate { class, 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);
let store = self.graph_dataset.as_ref().unwrap();
let mut parsed = Query::parse(&sparql, base.as_deref())
@ -658,27 +659,22 @@ impl Verifier {
}
store
.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(
&mut self,
nuri: NuriV0,
doc_create: DocCreate
doc_create: DocCreate,
) -> Result<String, NgError> {
//TODO: deal with doc_create.destination
let user_id = self.user_id().clone();
let user_priv_key = self.user_privkey().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
.new_repo_default(
&user_id,
&user_priv_key,
&store,
doc_create.class,
)
.new_repo_default(&user_id, &user_priv_key, &store, doc_create.class)
.await?;
let header_branch_id = {
@ -687,8 +683,7 @@ impl Verifier {
};
// adding an AddRepo commit to the Store branch of store.
self.send_add_repo_to_store(&repo_id, &store)
.await?;
self.send_add_repo_to_store(&repo_id, &store).await?;
// adding an ldp:contains triple to the store main branch
let overlay_id = store.outer_overlay();
@ -696,7 +691,9 @@ impl Verifier {
let nuri_result = NuriV0::repo_graph_name(&repo_id, &overlay_id);
let store_nuri = NuriV0::from_store_repo(&store);
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
.process_sparql_update(&store_nuri, &query, &None, vec![])
@ -722,26 +719,35 @@ impl Verifier {
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 {
self.config.protected_store_id.unwrap()
} else {
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 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 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
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.
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.
// 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 = 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#>
INSERT DATA {{ <> ng:{a_or_b} <{profile_nuri_string}>.
<> ng:{a_or_b}_inbox <{inbox_nuri_string}>.
<> a vcard:Individual .
<> vcard:fn \"{}\".
{has_email} }}", contact.name);
{has_email} }}",
contact.name
);
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;
if let Err(e) = ret {
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(
from_profile,
from_profile,
from_inbox,
contact.profile.outer_overlay(),
contact.inbox,
@ -814,31 +832,43 @@ impl Verifier {
false,
name,
email,
)?).await?;
)?)
.await?;
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 {
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 {
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
{{ ?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:protected ?profile_id . ?c ng:protected_inbox ?inbox_id }}
FILTER ( bound(?profile_id) {extra_conditions} )
}}");
}}"
);
//log_info!("{sparql}");
let sols = match self.sparql_query(
&NuriV0::new_entire_user_site(),
sparql, None).await?
let sols = match self
.sparql_query(&NuriV0::new_entire_user_site(), sparql, None)
.await?
{
QueryResults::Solutions(sols) => { sols }
_ => return Err(VerifierError::SparqlError(NgError::InvalidResponse.to_string())),
QueryResults::Solutions(sols) => sols,
_ => {
return Err(VerifierError::SparqlError(
NgError::InvalidResponse.to_string(),
))
}
};
let mut res = vec![];
@ -857,8 +887,7 @@ impl Verifier {
}
}
Ok(res)
}
}
pub(crate) async fn process(
&mut self,
@ -867,30 +896,35 @@ impl Verifier {
payload: Option<AppRequestPayload>,
) -> Result<AppResponse, NgError> {
match command {
AppRequestCommandV0::OrmUpdate => {
match payload {
Some(AppRequestPayload::V0(AppRequestPayloadV0::OrmUpdate((diff,shape_id)))) => {
self.frontend_update_orm(&nuri, shape_id, diff).await
},
_ => return Err(NgError::InvalidArgument)
AppRequestCommandV0::OrmUpdate => match payload {
Some(AppRequestPayload::V0(AppRequestPayloadV0::OrmUpdate((diff, shape_id)))) => {
self.orm_frontend_update(&nuri, shape_id, diff).await
}
_ => return Err(NgError::InvalidArgument),
},
AppRequestCommandV0::SocialQueryStart => {
let (from_profile, contacts_string, degree) = if let Some(AppRequestPayload::V0(AppRequestPayloadV0::SocialQueryStart{
from_profile, contacts, degree
})) =
payload
{ (from_profile, contacts, degree) }
else {
return Err(NgError::InvalidPayload);
};
let (from_profile, contacts_string, degree) =
if let Some(AppRequestPayload::V0(AppRequestPayloadV0::SocialQueryStart {
from_profile,
contacts,
degree,
})) = payload
{
(from_profile, contacts, degree)
} else {
return Err(NgError::InvalidPayload);
};
let query_id = nuri.target.repo_id();
// checking that the query hasn't been started yet
match self.sparql_query(
&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?
match self
.sparql_query(
&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) => {
return Err(NgError::SocialQueryAlreadyStarted);
@ -921,32 +955,36 @@ impl Verifier {
//resolve from_profile
let from_profile_id = match from_profile.target {
NuriTargetV0::ProtectedProfile => {
self.config.protected_store_id.unwrap()
}
NuriTargetV0::PublicProfile => {
self.config.public_store_id.unwrap()
},
_ => return Err(NgError::InvalidNuri)
NuriTargetV0::ProtectedProfile => self.config.protected_store_id.unwrap(),
NuriTargetV0::PublicProfile => self.config.public_store_id.unwrap(),
_ => return Err(NgError::InvalidNuri),
};
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()
};
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 mut blocks= Vec::with_capacity(block_ids.len());
let block_ids =
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());
for bid in block_ids.iter() {
blocks.push(store.get(bid)?);
}
// creating the ForwardedSocialQuery in the private store
let forwarder = self.doc_create_with_store_repo(
"Graph".to_string(), "social:query:forwarded".to_string(),
"store".to_string(), None // meaning in private store
).await?;
let forwarder = self
.doc_create_with_store_repo(
"Graph".to_string(),
"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_id = forwarder_nuri.target.repo_id().clone();
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}> .
<> <did:ng:x:ng#social_query_started> \"{}\"^^<http://www.w3.org/2001/XMLSchema#dateTime> . }}",DateTime::now());
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;
if let Err(e) = ret {
log_err!("{sparql_update}");
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 {
match self.social_query_dispatch(
&to_profile_nuri,
&to_inbox_nuri,
&forwarder_nuri,
&forwarder_id,
&from_profiles,
query_id,
&definition_commit_body_ref,
&blocks,
degree
).await {
match self
.social_query_dispatch(
&to_profile_nuri,
&to_inbox_nuri,
&forwarder_nuri,
&forwarder_id,
&from_profiles,
query_id,
&definition_commit_body_ref,
&blocks,
degree,
)
.await
{
Ok(_) => {}
Err(e) => return Ok(AppResponse::error(e.to_string())),
}
@ -1017,32 +1063,34 @@ impl Verifier {
// };
}
AppRequestCommandV0::QrCodeProfile => {
let size = if let Some(AppRequestPayload::V0(AppRequestPayloadV0::QrCodeProfile(size))) =
payload
{
size
} else {
return Err(NgError::InvalidPayload);
};
let size =
if let Some(AppRequestPayload::V0(AppRequestPayloadV0::QrCodeProfile(size))) =
payload
{
size
} else {
return Err(NgError::InvalidPayload);
};
let public = match nuri.target {
NuriTargetV0::PublicProfile => true,
NuriTargetV0::ProtectedProfile => false,
_ => return Err(NgError::InvalidPayload)
_ => return Err(NgError::InvalidPayload),
};
return match self.get_qrcode_for_profile(public, size).await {
Err(e) => Ok(AppResponse::error(e.to_string())),
Ok(qrcode) => Ok(AppResponse::text(qrcode)),
Ok(qrcode) => Ok(AppResponse::text(qrcode)),
};
}
AppRequestCommandV0::QrCodeProfileImport => {
let profile = if let Some(AppRequestPayload::V0(AppRequestPayloadV0::QrCodeProfileImport( text))) =
payload
let profile = if let Some(AppRequestPayload::V0(
AppRequestPayloadV0::QrCodeProfileImport(text),
)) = payload
{
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 {
NgQRCode::ProfileSharingV0(profile) => profile,
_ => return Err(NgError::InvalidPayload)
_ => return Err(NgError::InvalidPayload),
};
profile
} else {
@ -1050,20 +1098,23 @@ impl Verifier {
};
let repo_id = match nuri.target {
NuriTargetV0::Repo(id) => id,
_ => return Err(NgError::InvalidPayload)
_ => return Err(NgError::InvalidPayload),
};
return match self.import_contact_from_qrcode(repo_id, profile).await {
Err(e) => Ok(AppResponse::error(e.to_string())),
Ok(()) => Ok(AppResponse::ok()),
Ok(()) => Ok(AppResponse::ok()),
};
}
AppRequestCommandV0::Header => {
if let Some(AppRequestPayload::V0(AppRequestPayloadV0::Header(doc_header))) =
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()),
Err(e) => Ok(AppResponse::error(e.to_string()))
Err(e) => Ok(AppResponse::error(e.to_string())),
};
} else {
return Err(NgError::InvalidPayload);
@ -1076,7 +1127,7 @@ impl Verifier {
match self.doc_create(nuri, doc_create).await {
Err(NgError::SparqlError(e)) => Ok(AppResponse::error(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 {
Err(NgError::InvalidPayload)
@ -1250,15 +1301,16 @@ impl Verifier {
return Ok(AppResponse::V0(AppResponseV0::Text(vec.join("\n"))));
}
AppFetchContentV0::CurrentHeads => {
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::error(VerifierError::InvalidNuri.to_string()));
}
AppFetchContentV0::History => {
if !nuri.is_valid_for_sparql_update() {
return Err(NgError::InvalidNuri);

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

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

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

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

@ -6,73 +6,104 @@ import type { Schema } from "@nextgraph-monorepo/ng-shex-orm";
* =============================================================================
*/
export const catShapeSchema: Schema = {
"http://example.org/Cat": {
iri: "http://example.org/Cat",
predicates: [
{
valType: "literal",
literalValue: ["http://example.org/Cat"],
maxCardinality: 1,
minCardinality: 1,
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",
},
"http://example.org/Cat": {
iri: "http://example.org/Cat",
predicates: [
{
dataTypes: [
{
valType: "literal",
literals: ["http://example.org/Cat"],
},
],
},
"http://example.org/Cat||http://example.org/address": {
iri: "http://example.org/Cat||http://example.org/address",
predicates: [
{
valType: "string",
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/street",
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://www.w3.org/1999/02/22-rdf-syntax-ns#type",
readablePredicate: "type",
},
{
dataTypes: [
{
valType: "string",
},
],
},
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 = {
"http://example.org/Person": {
iri: "http://example.org/Person",
predicates: [
{
valType: "literal",
literalValue: ["http://example.org/Person"],
maxCardinality: 1,
minCardinality: 1,
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",
},
"http://example.org/Person": {
iri: "http://example.org/Person",
predicates: [
{
dataTypes: [
{
valType: "literal",
literals: ["http://example.org/Person"],
},
],
},
"http://example.org/Person||http://example.org/address": {
iri: "http://example.org/Person||http://example.org/address",
predicates: [
{
valType: "string",
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/street",
readablePredicate: "street",
},
{
valType: "string",
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/houseNumber",
readablePredicate: "houseNumber",
},
maxCardinality: 1,
minCardinality: 1,
iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
readablePredicate: "type",
},
{
dataTypes: [
{
valType: "string",
},
],
},
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/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 = {
"http://example.org/TestObject": {
iri: "http://example.org/TestObject",
predicates: [
{
valType: "literal",
literalValue: ["http://example.org/TestObject"],
maxCardinality: 1,
minCardinality: 1,
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",
},
"http://example.org/TestObject": {
iri: "http://example.org/TestObject",
predicates: [
{
dataTypes: [
{
valType: "literal",
literals: ["http://example.org/TestObject"],
},
],
},
"http://example.org/TestObject||http://example.org/objectValue": {
iri: "http://example.org/TestObject||http://example.org/objectValue",
predicates: [
{
valType: "string",
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/nestedString",
readablePredicate: "nestedString",
},
{
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,
minCardinality: 1,
iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
readablePredicate: "type",
extra: true,
},
{
dataTypes: [
{
valType: "string",
},
],
},
"http://example.org/TestObject||http://example.org/anotherObject": {
iri: "http://example.org/TestObject||http://example.org/anotherObject",
predicates: [
{
valType: "string",
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/prop1",
readablePredicate: "prop1",
},
{
valType: "number",
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/prop2",
readablePredicate: "prop2",
},
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/stringValue",
readablePredicate: "stringValue",
},
{
dataTypes: [
{
valType: "number",
},
],
},
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) {
schema[shape.iri] = shape;
// Find nested, unflattened (i.e. anonymous) schemas in properties.
const nestedSchemaPredicates = shape.predicates.filter(
(pred) =>
pred.valType === "nested" &&
typeof pred.nestedShape === "object"
);
for (const pred of nestedSchemaPredicates) {
const newId = shape.iri + "||" + pred.iri;
// Find nested, unflattened (i.e. anonymous) schemas in predicates' dataTypes.
for (const pred of shape.predicates) {
for (let i = 0; i < pred.dataTypes.length; i++) {
const dt = pred.dataTypes[i];
if (
dt.valType === "shape" &&
typeof dt.shape === "object" &&
dt.shape !== null
) {
// 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
const flattened = flattenSchema([
{
...(pred.nestedShape as Shape),
iri: newId,
},
]);
// Replace the nested schema with its new id.
pred.nestedShape = newId;
// Recurse
const flattened = flattenSchema([
{
...(dt.shape as Shape),
iri: newId,
},
]);
// Replace the nested schema with its new id.
dt.shape = newId;
schema = { ...schema, ...flattened };
schema = { ...schema, ...flattened };
}
}
}
// Flatten / Recurse
}

@ -65,7 +65,7 @@ export const ShexJSchemaTransformerCompact = ShexJTraverser.createTransformer<
EachOf: { return: Shape };
TripleConstraint: { return: Predicate };
NodeConstraint: { return: DataType };
ShapeOr: { return: (DataType | Shape | string)[] };
ShapeOr: { return: DataType[] };
ShapeAnd: { return: never };
ShapeNot: { return: never };
ShapeExternal: { return: never };
@ -143,24 +143,31 @@ export const ShexJSchemaTransformerCompact = ShexJTraverser.createTransformer<
if (typeof transformedChildren.valueExpr === "string") {
// Reference to nested object
return {
valType: "nested",
nestedShape: transformedChildren.valueExpr,
dataTypes: [
{
valType: "shape",
shape: transformedChildren.valueExpr,
},
],
...commonProperties,
} satisfies Predicate;
};
} else if (
transformedChildren.valueExpr &&
(transformedChildren.valueExpr as Shape).predicates
) {
// Nested object
return {
valType: "nested",
nestedShape: transformedChildren.valueExpr as Shape,
dataTypes: [
{
valType: "shape",
shape: transformedChildren.valueExpr as Shape,
},
],
...commonProperties,
} satisfies Predicate;
};
} else if (Array.isArray(transformedChildren.valueExpr)) {
return {
valType: "eitherOf",
eitherOf: transformedChildren.valueExpr,
dataTypes: transformedChildren.valueExpr, // DataType[]
...commonProperties,
};
} else {
@ -168,10 +175,14 @@ export const ShexJSchemaTransformerCompact = ShexJTraverser.createTransformer<
const nodeConstraint =
transformedChildren.valueExpr as DataType;
return {
valType: nodeConstraint.valType,
literalValue: nodeConstraint.literals,
dataTypes: [
{
valType: nodeConstraint.valType,
literals: nodeConstraint.literals,
},
],
...commonProperties,
} satisfies Predicate;
};
}
},
},
@ -192,6 +203,8 @@ export const ShexJSchemaTransformerCompact = ShexJTraverser.createTransformer<
valType: "literal",
literals: nodeConstraint.values.map(
// 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
),
};
@ -210,11 +223,9 @@ export const ShexJSchemaTransformerCompact = ShexJTraverser.createTransformer<
transformer: async (shapeOr, getTransformedChildren) => {
const { shapeExprs } = await getTransformedChildren();
// Either a shape IRI, a nested shape or a node CompactSchemaValue (node constraint).
return (Array.isArray(shapeExprs) ? shapeExprs : [shapeExprs]) as (
| string
| Shape
| DataType
)[];
return (
Array.isArray(shapeExprs) ? shapeExprs : [shapeExprs]
) as DataType[];
},
},

@ -17,27 +17,25 @@ export interface Shape {
}
export type DataType = {
/** The required literal value(s), if type is `literal`. Others are allowed, if `extra` is true. */
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 {
/** Type of property. */
valType: DataType["valType"] | "nested" | "eitherOf";
/** Allowed type of object. If more than one is present, either of them is allowed. */
dataTypes: DataType[];
/** The RDF predicate URI. */
iri: string;
/** The alias of the `predicateUri` when serialized to a JSON object. */
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. */
maxCardinality: number;
/** Minimum required number of values */
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. */
extra?: boolean;
}

Loading…
Cancel
Save