From 9b4b736bf8092eec40f833854979986b8cf81897 Mon Sep 17 00:00:00 2001 From: Danny Browning Date: Wed, 27 Sep 2023 21:37:43 -0600 Subject: [PATCH 1/2] feat: single models Adds support for creating models of single account relations. Utilizes wasi branches in support of fluence deployments. --- Cargo.toml | 11 ++- src/api.rs | 2 +- src/lib.rs | 178 +++++++++++++++++++++++++++++++++++++--- src/model_definition.rs | 9 ++ src/query.rs | 14 ++-- 5 files changed, 191 insertions(+), 23 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 006b551..b2fc27a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,20 +13,23 @@ publish = false [dependencies] anyhow = "1" -ceramic-event = { git = "https://github.com/3box/rust-ceramic", branch = "main" } -json-patch = { version = "1.0.0", features = ["diff"] } +#ceramic-event = { git = "https://github.com/ceramicnetwork/rust-ceramic", branch = "main" } +#ceramic-event = { path = "/Users/dbrowning/code/3box/rust-ceramic/event", default-features = false } +ceramic-event = { git = "https://github.com/ceramicnetwork/rust-ceramic", branch = "feat/wasi" } +json-patch = { version = "1.2.0", features = ["diff"] } reqwest = { version = "0.11.14", features = ["json"], optional = true } schemars = "0.8.12" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -ssi = { version = "0.7", features = ["ed25519"] } -url = { version = "2.2.2", optional = true } +#ssi = { version = "0.7", features = ["ed25519"] } +url = { version = "2.5.0", optional = true } [features] default = ["remote"] remote = ["reqwest", "url"] [dev-dependencies] +rand = "0.8.5" test-log = { version = "0.2", default-features = false, features = ["trace"] } tokio = { version = "1", default-features = false, features = ["macros", "rt"] } tracing = "0.1" diff --git a/src/api.rs b/src/api.rs index a2db02b..7dcb983 100644 --- a/src/api.rs +++ b/src/api.rs @@ -25,7 +25,7 @@ pub struct BlockData { /// Header for block pub header: BlockHeader, /// Data for block - #[serde(skip_serializing_if = "Option::is_none")] + //#[serde(skip_serializing_if = "Option::is_none")] pub data: Option, /// Signature for block #[serde(skip_serializing_if = "Option::is_none")] diff --git a/src/lib.rs b/src/lib.rs index 8e3c233..bc32a29 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,7 +9,7 @@ mod model_definition; mod query; use ceramic_event::{ - Base64String, Cid, DagCborEncoded, EventArgs, Jws, MultiBase36String, Signer, StreamId, + Base64String, Cid, DeterministicInitEvent, EventArgs, Jws, MultiBase36String, Signer, StreamId, StreamIdType, }; use serde::Serialize; @@ -17,6 +17,7 @@ use std::str::FromStr; use crate::api::ModelData; pub use ceramic_event; +pub use json_patch; pub use model_definition::{ GetRootSchema, ModelAccountRelation, ModelDefinition, ModelRelationDefinition, ModelViewDefinition, @@ -146,12 +147,12 @@ impl CeramicHttpClient { pub async fn create_single_instance_request( &self, model_id: &StreamId, - ) -> anyhow::Result> { + ) -> anyhow::Result> { if !model_id.is_model() { anyhow::bail!("StreamId was not a model"); } let args = EventArgs::new_with_parent(&self.signer, model_id); - let commit = args.init()?; + let _commit = args.init()?; let controllers: Vec<_> = args.controllers().map(|c| c.id.clone()).collect(); let model = Base64String::from(model_id.to_vec()?); Ok(api::CreateRequest { @@ -164,7 +165,7 @@ impl CeramicHttpClient { }, linked_block: None, jws: None, - data: Some(commit.encoded), + data: None, cacao_block: None, }, }) @@ -587,7 +588,7 @@ pub mod tests { use crate::model_definition::{GetRootSchema, ModelAccountRelation, ModelDefinition}; use crate::query::{FilterQuery, OperationFilter}; use ceramic_event::{DidDocument, JwkSigner}; - use json_patch::ReplaceOperation; + use json_patch::{AddOperation, ReplaceOperation}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -629,6 +630,11 @@ pub mod tests { cli.create_model(&model).await.unwrap() } + pub async fn create_single_model(cli: &CeramicRemoteHttpClient) -> StreamId { + let model = ModelDefinition::new::("TestBall", ModelAccountRelation::Single).unwrap(); + cli.create_model(&model).await.unwrap() + } + #[tokio::test] async fn should_create_model() { let ceramic = CeramicRemoteHttpClient::new(signer().await, ceramic_url()); @@ -636,12 +642,48 @@ pub mod tests { ceramic.create_model(&model).await.unwrap(); } - // #[tokio::test] - // async fn should_create_single_instance() { - // let ceramic = CeramicRemoteHttpClient::new(did(), &did_private_key(), ceramic_url()); - // let model = create_model(&ceramic).await; - // ceramic.create_single_instance(&model).await.unwrap(); - // } + #[tokio::test] + async fn should_create_single_instance() { + let ceramic = CeramicRemoteHttpClient::new(signer().await, ceramic_url()); + let model = create_single_model(&ceramic).await; + ceramic.create_single_instance(&model).await.unwrap(); + } + + #[tokio::test] + async fn should_create_and_update_single_instance() { + let ceramic = CeramicRemoteHttpClient::new(signer().await, ceramic_url()); + let model = create_single_model(&ceramic).await; + let stream_id = ceramic.create_single_instance(&model).await.unwrap(); + + tokio::time::sleep(Duration::from_secs(1)).await; + + let patch = json_patch::Patch(vec![ + json_patch::PatchOperation::Add(AddOperation { + path: "/creator".to_string(), + value: serde_json::Value::String(ceramic.client().signer().id().id.clone()), + }), + json_patch::PatchOperation::Add(AddOperation { + path: "/radius".to_string(), + value: serde_json::Value::Number(serde_json::Number::from(1)), + }), + json_patch::PatchOperation::Add(AddOperation { + path: "/red".to_string(), + value: serde_json::Value::Number(serde_json::Number::from(2)), + }), + json_patch::PatchOperation::Add(AddOperation { + path: "/green".to_string(), + value: serde_json::Value::Number(serde_json::Number::from(3)), + }), + json_patch::PatchOperation::Add(AddOperation { + path: "/blue".to_string(), + value: serde_json::Value::Number(serde_json::Number::from(4)), + }), + ]); + let post_resp = ceramic.update(&model, &stream_id, patch).await.unwrap(); + assert_eq!(post_resp.stream_id, stream_id); + let post_resp: Ball = serde_json::from_value(post_resp.state.unwrap().content).unwrap(); + assert_eq!(post_resp.red, 2); + } #[tokio::test] async fn should_create_and_update_list() { @@ -795,5 +837,119 @@ pub mod tests { .await .unwrap(); assert_eq!(res.edges.len(), 1); + let node = &res.edges[0].node; + let result: Ball = serde_json::from_value(node.content.clone()).unwrap(); + assert_eq!(result.blue, 5); + } + + #[tokio::test] + async fn should_query_models_after_update() { + let ceramic = CeramicRemoteHttpClient::new(signer().await, ceramic_url()); + let model = create_model(&ceramic).await; + ceramic.index_model(&model).await.unwrap(); + let _instance1 = ceramic + .create_list_instance( + &model, + &Ball { + creator: ceramic.client().signer().id().id.clone(), + radius: 1, + red: 2, + green: 3, + blue: 4, + }, + ) + .await + .unwrap(); + + let instance2 = ceramic + .create_list_instance( + &model, + &Ball { + creator: ceramic.client().signer().id().id.clone(), + radius: 2, + red: 3, + green: 4, + blue: 5, + }, + ) + .await + .unwrap(); + + //give anchor time to complete + tokio::time::sleep(Duration::from_secs(1)).await; + + let replace = Ball { + creator: ceramic.client().signer().id().id.clone(), + radius: 1, + red: 0, + green: 3, + blue: 10, + }; + let post_resp = ceramic.replace(&model, &instance2, &replace).await.unwrap(); + assert_eq!(post_resp.stream_id, instance2); + + //give anchor time to complete + tokio::time::sleep(Duration::from_secs(1)).await; + + let mut where_filter = HashMap::new(); + where_filter.insert("blue".to_string(), OperationFilter::EqualTo(10.into())); + let filter = FilterQuery::Where(where_filter); + let res = ceramic + .query(&model, Some(filter), Pagination::default()) + .await + .unwrap(); + assert_eq!(res.edges.len(), 1); + let node = &res.edges[0].node; + let result: Ball = serde_json::from_value(node.content.clone()).unwrap(); + assert_eq!(result.blue, 10); + } + + #[tokio::test] + async fn should_create_and_repeatedly_update_list() { + let ceramic = CeramicRemoteHttpClient::new(signer().await, ceramic_url()); + let model = create_model(&ceramic).await; + let stream_id = ceramic + .create_list_instance( + &model, + &Ball { + creator: ceramic.client().signer().id().id.clone(), + radius: 1, + red: 2, + green: 3, + blue: 4, + }, + ) + .await + .unwrap(); + + for i in 0..100 { + //give anchor time to complete + tokio::time::sleep(Duration::from_millis(100)).await; + + let replace_value = rand::random::() % 10i32; + let patch = json_patch::Patch(vec![json_patch::PatchOperation::Replace( + ReplaceOperation { + path: "/red".to_string(), + value: serde_json::json!(replace_value), + }, + )]); + let post_resp = ceramic.update(&model, &stream_id, patch).await.unwrap(); + assert_eq!(post_resp.stream_id, stream_id); + let post_resp: Ball = serde_json::from_value(post_resp.state.unwrap().content).unwrap(); + assert_eq!( + post_resp.red, replace_value, + "Failed to return expected value on iteration {}", + i + ); + + let get_resp: Ball = ceramic.get_as(&stream_id).await.unwrap(); + assert_eq!(get_resp.red, replace_value); + assert_eq!(get_resp.blue, 4); + assert_eq!( + get_resp, post_resp, + "Failed to retrieve expected value on iteration {}", + i + ); + } } } diff --git a/src/model_definition.rs b/src/model_definition.rs index 1818528..a4a6b1c 100644 --- a/src/model_definition.rs +++ b/src/model_definition.rs @@ -87,6 +87,15 @@ impl ModelDefinition { ) -> anyhow::Result { let schema = T::root_schema(); let schema = serde_json::to_value(&schema)?; + Self::new_for_value(name, account_relation, schema) + } + + /// Create a new definition from schema that is already json + pub fn new_for_value( + name: &str, + account_relation: ModelAccountRelation, + schema: serde_json::Value, + ) -> anyhow::Result { Ok(Self { version: "1.0", name: name.to_string(), diff --git a/src/query.rs b/src/query.rs index 2fed8a5..211016e 100644 --- a/src/query.rs +++ b/src/query.rs @@ -1,8 +1,8 @@ -use serde::Serialize; +use serde::{Deserialize, Serialize}; use std::collections::HashMap; /// Valid values for operation Filter -#[derive(Clone, Debug, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[serde(untagged)] pub enum NumberFilter { /// I64 Value @@ -40,7 +40,7 @@ impl From for NumberFilter { } /// Valid values for operation Filter -#[derive(Clone, Debug, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[serde(untagged)] pub enum ValueFilter { /// String value @@ -86,7 +86,7 @@ impl From for ValueFilter { } /// Valid values for operation Filter -#[derive(Clone, Debug, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[serde(untagged)] pub enum EqualValueFilter { /// Boolean value @@ -138,7 +138,7 @@ impl From for EqualValueFilter { } /// Operation Filter -#[derive(Clone, Debug, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub enum OperationFilter { /// Filter by null or not null @@ -162,7 +162,7 @@ pub enum OperationFilter { } /// Combination query -#[derive(Clone, Debug, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct CombinationQuery(Vec); impl CombinationQuery { @@ -189,7 +189,7 @@ macro_rules! or { } /// Filter Query -#[derive(Clone, Debug, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub enum FilterQuery { /// Filter by where #[serde(rename = "where")] From 65bcee69bc20240966caffd538799d2543932c57 Mon Sep 17 00:00:00 2001 From: Danny Browning Date: Fri, 16 Feb 2024 10:02:34 -0700 Subject: [PATCH 2/2] fix: review comments, api fix --- Cargo.toml | 3 --- src/api.rs | 5 +++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b2fc27a..0e1579e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,15 +13,12 @@ publish = false [dependencies] anyhow = "1" -#ceramic-event = { git = "https://github.com/ceramicnetwork/rust-ceramic", branch = "main" } -#ceramic-event = { path = "/Users/dbrowning/code/3box/rust-ceramic/event", default-features = false } ceramic-event = { git = "https://github.com/ceramicnetwork/rust-ceramic", branch = "feat/wasi" } json-patch = { version = "1.2.0", features = ["diff"] } reqwest = { version = "0.11.14", features = ["json"], optional = true } schemars = "0.8.12" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -#ssi = { version = "0.7", features = ["ed25519"] } url = { version = "2.5.0", optional = true } [features] diff --git a/src/api.rs b/src/api.rs index 7dcb983..d58486d 100644 --- a/src/api.rs +++ b/src/api.rs @@ -25,7 +25,7 @@ pub struct BlockData { /// Header for block pub header: BlockHeader, /// Data for block - //#[serde(skip_serializing_if = "Option::is_none")] + #[serde(skip_serializing_if = "Option::is_none")] pub data: Option, /// Signature for block #[serde(skip_serializing_if = "Option::is_none")] @@ -313,7 +313,8 @@ pub struct PageInfo { /// Whether previous page exists pub has_previous_page: bool, /// Cursor for next page - pub end_cursor: Base64UrlString, + #[serde(default)] + pub end_cursor: Option, /// Cursor for previous page pub start_cursor: Base64UrlString, }