diff --git a/Cargo.lock b/Cargo.lock index 1c64715..fb4654d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -572,18 +572,18 @@ checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" [[package]] name = "const_format" -version = "0.2.33" +version = "0.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50c655d81ff1114fb0dcdea9225ea9f0cc712a6f8d189378e82bdf62a473a64b" +checksum = "126f97965c8ad46d6d9163268ff28432e8f6a1196a55578867832e3049df63dd" dependencies = [ "const_format_proc_macros", ] [[package]] name = "const_format_proc_macros" -version = "0.2.33" +version = "0.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eff1a44b93f47b1bac19a27932f5c591e43d1ba357ee4f61526c8a25603f0eb1" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" dependencies = [ "proc-macro2", "quote", @@ -2547,6 +2547,7 @@ version = "0.1.0" dependencies = [ "anyhow", "chrono", + "const_format", "md-5", "neo4rs", "prost", diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index 350b8d6..cedad6d 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" [dependencies] anyhow = "1.0.93" chrono = "0.4.38" +const_format = "0.2.34" md-5 = "0.10.6" neo4rs = "0.8.0" prost = "0.13.3" diff --git a/sdk/src/ids/system_ids.rs b/sdk/src/ids/system_ids.rs index 300faad..f87a9ce 100644 --- a/sdk/src/ids/system_ids.rs +++ b/sdk/src/ids/system_ids.rs @@ -297,9 +297,13 @@ pub const VOTE_CAST: &str = "PfgzdxPYwDUTBCzkXCT9ga"; // Proposal pub const PROPOSAL_TYPE: &str = "9No6qfEutiKg1WLeXDv73x"; -pub const MEMBERSHIP_PROPOSAL_TYPE: &str = "6dJ23LRTHRdwqoWhtivRrM"; -pub const EDITORSHIP_PROPOSAL_TYPE: &str = "7W7SE2UTj5YTsQvqSmCfLN"; -pub const SUBSPACE_PROPOSAL_TYPE: &str = "DcEZrRpmAuwxfw7C5G7gjC"; +pub const ADD_MEMBER_PROPOSAL: &str = "6dJ23LRTHRdwqoWhtivRrM"; +pub const REMOVE_MEMBER_PROPOSAL: &str = "8dJ23LRTHRdwqoWhtivRrM"; +pub const ADD_EDITOR_PROPOSAL: &str = "7W7SE2UTj5YTsQvqSmCfLN"; +pub const REMOVE_EDITOR_PROPOSAL: &str = "9W7SE2UTj5YTsQvqSmCfLN"; +pub const ADD_SUBSPACE_PROPOSAL: &str = "DcEZrRpmAuwxfw7C5G7gjC"; +pub const REMOVE_SUBSPACE_PROPOSAL: &str = "FcEZrRpmAuwxfw7C5G7gjC"; +pub const EDIT_PROPOSAL: &str = "GcEZrRpmAuwxfw7C5G7gjC"; /// MEMBERSHIP_PROPOSAL_TYPE > PROPOSED_ACCOUNT > GEO_ACCOUNT /// EDITORSHIP_PROPOSAL_TYPE > PROPOSED_ACCOUNT > GEO_ACCOUNT @@ -310,3 +314,6 @@ pub const PROPOSED_SUBSPACE: &str = "5ZVrZv7S3Mk2ATV9LAZAha"; /// INDEXED_SPACE > PROPOSALS > PROPOSAL pub const PROPOSALS: &str = "3gmeTonVCB6B11p3YF8mj5"; + +/// PROPOSAL > CREATOR > ACCOUNT +pub const PROPOSAL_CREATOR: &str = "213"; \ No newline at end of file diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index 47bdf41..49d2c8e 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -6,6 +6,7 @@ pub mod models; pub mod pb; pub mod mapping; pub mod relation; +pub mod neo4j_utils; pub use ids::network_ids; pub use ids::system_ids; diff --git a/sdk/src/mapping.rs b/sdk/src/mapping.rs deleted file mode 100644 index b92d43c..0000000 --- a/sdk/src/mapping.rs +++ /dev/null @@ -1,480 +0,0 @@ -use std::collections::HashMap; - -use serde::{ser::SerializeMap, Deserialize, Serialize}; -use serde_with::with_prefix; - -#[derive(Debug, Deserialize, PartialEq)] -pub struct Relation { - // pub id: String, - pub relation_type: String, - pub from: String, - pub to: String, - #[serde(flatten)] - pub attributes: Attributes, -} - -impl TryFrom for Relation -where - T: for<'a> serde::Deserialize<'a>, -{ - type Error = neo4rs::DeError; - - fn try_from(value: neo4rs::Relation) -> Result { - let attributes = value.to()?; - Ok(Self { - relation_type: value.typ().to_string(), - attributes, - from: value.start_node_id().to_string(), - to: value.end_node_id().to_string(), - }) - } -} - -impl Relation { - pub fn new( - id: &str, - space_id: &str, - from: &str, - to: &str, - relation_type: &str, - data: T, - ) -> Self { - Self { - // id: id.to_string(), - from: from.to_string(), - to: to.to_string(), - relation_type: relation_type.to_string(), - attributes: Attributes { - id: id.to_string(), - space_id: space_id.to_string(), - attributes: data, - }, - } - } - - pub fn id(&self) -> &str { - &self.attributes.id - } - - pub fn space_id(&self) -> &str { - &self.attributes.space_id - } - - pub fn attributes(&self) -> &T { - &self.attributes.attributes - } - - pub fn attributes_mut(&mut self) -> &mut T { - &mut self.attributes.attributes - } -} - -impl Relation> { - pub fn with_attribute(mut self, key: String, value: T) -> Self - where - T: Into, - { - self.attributes_mut().insert(key, value.into()); - self - } -} - -/// GRC20 Node -#[derive(Debug, Deserialize, PartialEq)] -pub struct Node { - #[serde(rename = "labels", deserialize_with = "deserialize_labels")] - pub types: Vec, - #[serde(flatten)] - pub attributes: Attributes, -} - -impl TryFrom for Node -where - T: for<'a> serde::Deserialize<'a>, -{ - type Error = neo4rs::DeError; - - fn try_from(value: neo4rs::Node) -> Result { - let labels = value.labels().iter().map(|l| l.to_string()).collect(); - let attributes = value.to()?; - Ok(Self { - types: labels, - attributes, - }) - } -} - -fn deserialize_labels<'de, D>(deserializer: D) -> Result, D::Error> -where - D: serde::Deserializer<'de>, -{ - let labels: neo4rs::Labels = serde::Deserialize::deserialize(deserializer)?; - Ok(labels.0) -} - -impl Node { - pub fn new(id: &str, space_id: &str, data: T) -> Self { - Self { - types: Vec::new(), - attributes: Attributes { - id: id.to_string(), - space_id: space_id.to_string(), - attributes: data, - }, - } - } - - pub fn id(&self) -> &str { - &self.attributes.id - } - - pub fn space_id(&self) -> &str { - &self.attributes.space_id - } - - pub fn attributes(&self) -> &T { - &self.attributes.attributes - } - - pub fn attributes_mut(&mut self) -> &mut T { - &mut self.attributes.attributes - } - - pub fn with_type(mut self, type_id: &str) -> Self { - self.types.push(type_id.to_string()); - self - } -} - -impl Node> { - pub fn with_attribute(mut self, attribute_id: String, value: T) -> Self - where - T: Into, - { - self.attributes_mut().insert(attribute_id, value.into()); - self - } -} - -impl Node { - pub fn name(&self) -> Option { - self.attributes() - .get("name") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()) - } - - pub fn name_or_id(&self) -> String { - self.name().unwrap_or_else(|| self.id().to_string()) - } -} - -pub type DefaultAttributes = HashMap; - -#[derive(Debug, Deserialize, PartialEq)] -pub struct Named { - pub name: Option, -} - -impl Node { - pub fn name_or_id(&self) -> String { - self.name().unwrap_or_else(|| self.id().to_string()) - } - - pub fn name(&self) -> Option { - self.attributes().name.clone() - } -} - -/// Neo4j node representing a GRC20 entity of type `T`. -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] -pub struct Attributes { - pub id: String, - pub space_id: String, - // pub space_id: String, - #[serde(flatten)] - pub attributes: T, -} - -#[derive(Clone, Debug, PartialEq)] -pub struct Triple { - pub value: String, - pub r#type: ValueType, - pub options: Options, -} - -impl Serialize for Triple { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - let mut map = serializer.serialize_map(None)?; - map.serialize_entry("", &self.value)?; - map.serialize_entry(".type", &self.r#type)?; - if let Some(ref format) = self.options.format { - map.serialize_entry(".options.format", format)?; - } - if let Some(ref unit) = self.options.unit { - map.serialize_entry(".options.unit", unit)?; - } - if let Some(ref language) = self.options.language { - map.serialize_entry(".options.language", language)?; - } - if let Some(ref space) = self.options.space { - map.serialize_entry(".options.space", space)?; - } - map.end() - } -} - -impl<'de> Deserialize<'de> for Triple { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - #[derive(Deserialize)] - struct TripleHelper { - #[serde(rename = "")] - value: String, - #[serde(rename = ".type")] - r#type: ValueType, - #[serde(rename = ".options.format")] - format: Option, - #[serde(rename = ".options.unit")] - unit: Option, - #[serde(rename = ".options.language")] - language: Option, - #[serde(rename = ".options.space")] - space: Option, - } - - let helper = TripleHelper::deserialize(deserializer)?; - Ok(Triple { - value: helper.value, - r#type: helper.r#type, - options: Options { - format: helper.format, - unit: helper.unit, - language: helper.language, - space: helper.space, - }, - }) - } -} - -#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)] -pub struct Options { - pub format: Option, - pub unit: Option, - pub language: Option, - pub space: Option, -} - -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub enum ValueType { - Text, - Number, - Checkbox, - Url, - Time, - Point, -} - -#[cfg(test)] -mod tests { - use std::collections::HashMap; - - use super::*; - - #[test] - pub fn test_serialize_triple() { - with_prefix!(foo_prefix "foo"); - #[derive(Debug, Deserialize, Serialize, PartialEq)] - struct Foo { - #[serde(flatten, with = "foo_prefix")] - foo: Triple, - } - - let value = Foo { - foo: Triple { - value: "Hello, World!".to_string(), - r#type: ValueType::Text, - options: Options { - format: Some("text".to_string()), - unit: Some("unit".to_string()), - ..Default::default() - }, - }, - }; - - let serialized = serde_json::to_value(&value).expect("Failed to serialize Value"); - - assert_eq!( - serialized, - serde_json::json!({ - "foo": "Hello, World!", - "foo.type": "TEXT", - "foo.options.format": "text", - "foo.options.unit": "unit", - }) - ) - } - - #[test] - pub fn test_serialize_triple_multiple_fields() { - with_prefix!(foo_prefix "foo"); - with_prefix!(bar_prefix "bar"); - #[derive(Debug, Deserialize, Serialize, PartialEq)] - struct Foo { - #[serde(flatten, with = "foo_prefix")] - foo: Triple, - - #[serde(flatten, with = "bar_prefix")] - bar: Triple, - - other_field: String, - } - - let value = Foo { - foo: Triple { - value: "Hello, World!".to_string(), - r#type: ValueType::Text, - options: Options { - format: Some("text".to_string()), - ..Default::default() - }, - }, - bar: Triple { - value: "123".to_string(), - r#type: ValueType::Number, - options: Options { - unit: Some("int".to_string()), - ..Default::default() - }, - }, - other_field: "other".to_string(), - }; - - let serialized = serde_json::to_value(&value).expect("Failed to serialize Value"); - - assert_eq!( - serialized, - serde_json::json!({ - "foo": "Hello, World!", - "foo.type": "TEXT", - "foo.options.format": "text", - "bar": "123", - "bar.type": "NUMBER", - "bar.options.unit": "int", - "other_field": "other", - }) - ) - } - - #[test] - pub fn test_serialize_triple_hashmap() { - with_prefix!(foo_prefix "foo"); - with_prefix!(bar_prefix "bar"); - #[derive(Debug, Deserialize, Serialize, PartialEq)] - struct Foo { - #[serde(flatten)] - fields: HashMap, - } - - let value = Foo { - fields: HashMap::from([ - ("foo".to_string(), Triple { - value: "Hello, World!".to_string(), - r#type: ValueType::Text, - options: Options { - format: Some("text".to_string()), - ..Default::default() - }, - }), - ("bar".to_string(), Triple { - value: "123".to_string(), - r#type: ValueType::Number, - options: Options { - unit: Some("int".to_string()), - ..Default::default() - }, - }) - ]) - }; - - let serialized = serde_json::to_value(&value).expect("Failed to serialize Value"); - - assert_eq!( - serialized, - serde_json::json!({ - "foo": "Hello, World!", - "foo.type": "TEXT", - "foo.options.format": "text", - "bar": "123", - "bar.type": "NUMBER", - "bar.options.unit": "int", - }) - ) - } - - - #[test] - pub fn test_node_conversion() { - let node = neo4rs::Node::new(neo4rs::BoltNode { - id: neo4rs::BoltInteger { value: 425 }, - labels: neo4rs::BoltList { - value: vec![neo4rs::BoltType::String(neo4rs::BoltString { - value: "9u4zseS3EDXG9ZvwR9RmqU".to_string(), - })], - }, - properties: neo4rs::BoltMap { - value: HashMap::from([ - ( - neo4rs::BoltString { - value: "space_id".to_string(), - }, - neo4rs::BoltType::String(neo4rs::BoltString { - value: "NBDtpHimvrkmVu7vVBXX7b".to_string(), - }), - ), - ( - neo4rs::BoltString { - value: "GG8Z4cSkjv8CywbkLqVU5M".to_string(), - }, - neo4rs::BoltType::String(neo4rs::BoltString { - value: "Person Posts Page Template".to_string(), - }), - ), - ( - neo4rs::BoltString { - value: "id".to_string(), - }, - neo4rs::BoltType::String(neo4rs::BoltString { - value: "98wgvodwzidmVA4ryVzGX6".to_string(), - }), - ), - ]), - }, - }); - - let node: Node> = node - .try_into() - .expect("Failed to convert neo4rs::Node to Node>"); - - assert_eq!( - node, - Node { - types: vec!["9u4zseS3EDXG9ZvwR9RmqU".to_string()], - attributes: Attributes { - id: "98wgvodwzidmVA4ryVzGX6".to_string(), - space_id: "NBDtpHimvrkmVu7vVBXX7b".to_string(), - attributes: HashMap::from([( - "GG8Z4cSkjv8CywbkLqVU5M".to_string(), - serde_json::Value::String("Person Posts Page Template".to_string()) - ),]) - } - } - ) - } -} diff --git a/sdk/src/mapping/attributes.rs b/sdk/src/mapping/attributes.rs new file mode 100644 index 0000000..4f91a76 --- /dev/null +++ b/sdk/src/mapping/attributes.rs @@ -0,0 +1,175 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +pub struct Attributes { + pub id: String, + pub space_id: String, + #[serde(flatten)] + pub attributes: T, +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use super::*; + use crate::mapping::triple::{Options, Triple, Triples, ValueType}; + use serde_with::with_prefix; + + #[test] + fn test_attributes_struct() { + with_prefix!(foo_prefix "foo"); + #[derive(Debug, Deserialize, Serialize, PartialEq)] + struct Foo { + #[serde(flatten, with = "foo_prefix")] + foo: Triple, + } + + let attributes = Attributes { + id: "id".to_string(), + space_id: "space_id".to_string(), + attributes: Foo { + foo: Triple { + value: "Hello, World!".to_string(), + r#type: ValueType::Text, + options: Options { + format: Some("text".to_string()), + unit: Some("unit".to_string()), + ..Default::default() + }, + }, + }, + }; + + let serialized = serde_json::to_value(&attributes).unwrap(); + + assert_eq!( + serialized, + serde_json::json!({ + "id": "id", + "space_id": "space_id", + "foo": "Hello, World!", + "foo.type": "TEXT", + "foo.options.format": "text", + "foo.options.unit": "unit", + }) + ); + + let deserialized: Attributes = serde_json::from_value(serialized).unwrap(); + + assert_eq!(attributes, deserialized); + } + + #[test] + fn test_attributes_multiple_fields() { + with_prefix!(foo_prefix "foo"); + with_prefix!(bar_prefix "bar"); + #[derive(Debug, Deserialize, Serialize, PartialEq)] + struct Foo { + #[serde(flatten, with = "foo_prefix")] + foo: Triple, + + #[serde(flatten, with = "bar_prefix")] + bar: Triple, + + other_field: String, + } + + let attributes = Attributes { + id: "id".to_string(), + space_id: "space_id".to_string(), + attributes: Foo { + foo: Triple { + value: "Hello, World!".to_string(), + r#type: ValueType::Text, + options: Options { + format: Some("text".to_string()), + ..Default::default() + }, + }, + bar: Triple { + value: "123".to_string(), + r#type: ValueType::Number, + options: Options { + unit: Some("int".to_string()), + ..Default::default() + }, + }, + other_field: "other".to_string(), + } + }; + + let serialized = serde_json::to_value(&attributes).unwrap(); + + assert_eq!( + serialized, + serde_json::json!({ + "id": "id", + "space_id": "space_id", + "foo": "Hello, World!", + "foo.type": "TEXT", + "foo.options.format": "text", + "bar": "123", + "bar.type": "NUMBER", + "bar.options.unit": "int", + "other_field": "other", + }) + ); + + let deserialized: Attributes = serde_json::from_value(serialized).unwrap(); + + assert_eq!(attributes, deserialized); + } + + #[test] + fn test_attribtes_triples() { + let attributes = Attributes { + id: "id".to_string(), + space_id: "space_id".to_string(), + attributes: Triples(HashMap::from([ + ( + "foo".to_string(), + Triple { + value: "Hello, World!".to_string(), + r#type: ValueType::Text, + options: Options { + format: Some("text".to_string()), + ..Default::default() + }, + }, + ), + ( + "bar".to_string(), + Triple { + value: "123".to_string(), + r#type: ValueType::Number, + options: Options { + unit: Some("int".to_string()), + ..Default::default() + }, + }, + ), + ])) + }; + + let serialized = serde_json::to_value(&attributes).expect("Failed to serialize Value"); + + assert_eq!( + serialized, + serde_json::json!({ + "id": "id", + "space_id": "space_id", + "foo": "Hello, World!", + "foo.type": "TEXT", + "foo.options.format": "text", + "bar": "123", + "bar.type": "NUMBER", + "bar.options.unit": "int", + }) + ); + + let deserialized: Attributes = serde_json::from_value(serialized).expect("Failed to deserialize Value"); + + assert_eq!(deserialized, attributes); + } +} \ No newline at end of file diff --git a/sdk/src/mapping/entity.rs b/sdk/src/mapping/entity.rs new file mode 100644 index 0000000..4946bdc --- /dev/null +++ b/sdk/src/mapping/entity.rs @@ -0,0 +1,376 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +use crate::{graph_uri::{self, GraphUri}, mapping, models::BlockMetadata, neo4j_utils::serde_value_to_bolt, pb, system_ids}; + +use super::{attributes::Attributes, query::Query}; + +/// GRC20 Node +#[derive(Debug, Deserialize, PartialEq)] +pub struct Entity { + #[serde(rename = "labels")] + pub types: Vec, + #[serde(flatten)] + pub attributes: Attributes, +} + +impl Entity { + /// Creates a new entity with the given ID, space ID, and data + pub fn new(id: &str, space_id: &str, data: T) -> Self { + Self { + types: Vec::new(), + attributes: Attributes { + id: id.to_string(), + space_id: space_id.to_string(), + attributes: data, + }, + } + } + + pub fn id(&self) -> &str { + &self.attributes.id + } + + pub fn space_id(&self) -> &str { + &self.attributes.space_id + } + + pub fn attributes(&self) -> &T { + &self.attributes.attributes + } + + pub fn attributes_mut(&mut self) -> &mut T { + &mut self.attributes.attributes + } + + pub fn with_type(mut self, type_id: &str) -> Self { + self.types.push(type_id.to_string()); + self + } + + /// Returns a query to find a node by its ID + pub fn find_by_id_query(id: &str) -> Query { + const QUERY: &str = const_format::formatcp!( + "MATCH (n) WHERE n.id = $id RETURN n", + ); + + Query::new(QUERY).param("id", id) + } + + pub fn set_triple( + block: &BlockMetadata, + space_id: &str, + entity_id: &str, + attribute_id: &str, + value: &pb::grc20::Value, + ) -> Result, SetTripleError> { + match (attribute_id, value.r#type(), value.value.as_str()) { + // Setting the type of the entity + (system_ids::TYPES, pb::grc20::ValueType::Url, value) => { + const SET_TYPE_QUERY: &str = const_format::formatcp!( + r#" + MERGE (n {{ id: $id, space_id: $space_id }}) + ON CREATE SET n += {{ + `{CREATED_AT}`: datetime($created_at), + `{CREATED_AT_BLOCK}`: $created_at_block + }} + SET n += {{ + `{UPDATED_AT}`: datetime($updated_at), + `{UPDATED_AT_BLOCK}`: $updated_at_block + }} + SET n:$($labels) + "#, + CREATED_AT = system_ids::CREATED_AT_TIMESTAMP, + CREATED_AT_BLOCK = system_ids::CREATED_AT_BLOCK, + UPDATED_AT = system_ids::UPDATED_AT_TIMESTAMP, + UPDATED_AT_BLOCK = system_ids::UPDATED_AT_BLOCK, + ); + + let uri = GraphUri::from_uri(&value)?; + + Ok(Query::new(SET_TYPE_QUERY) + .param("id", entity_id) + .param("space_id", space_id) + .param("created_at", block.timestamp.to_rfc3339()) + .param("created_at_block", block.block_number.to_string()) + .param("updated_at", block.timestamp.to_rfc3339()) + .param("updated_at_block", block.block_number.to_string()) + .param("labels", uri.id)) + } + + // Setting the FROM_ENTITY or TO_ENTITY relation + (system_ids::RELATION_FROM_ATTRIBUTE | system_ids::RELATION_TO_ATTRIBUTE, pb::grc20::ValueType::Url, value) => { + let query = format!( + r#" + MATCH (n {{ id: $other, space_id: $space_id }}) + MERGE (r {{ id: $id, space_id: $space_id }}) + MERGE (r) -[:`{attribute_id}`]-> (n) + ON CREATE SET r += {{ + `{CREATED_AT}`: datetime($created_at), + `{CREATED_AT_BLOCK}`: $created_at_block + }} + SET r += {{ + `{UPDATED_AT}`: datetime($updated_at), + `{UPDATED_AT_BLOCK}`: $updated_at_block + }} + "#, + attribute_id = attribute_id, + CREATED_AT = system_ids::CREATED_AT_TIMESTAMP, + CREATED_AT_BLOCK = system_ids::CREATED_AT_BLOCK, + UPDATED_AT = system_ids::UPDATED_AT_TIMESTAMP, + UPDATED_AT_BLOCK = system_ids::UPDATED_AT_BLOCK, + ); + + let uri = GraphUri::from_uri(&value)?; + + Ok(Query::new(&query) + .param("id", entity_id) + .param("other", uri.id) + .param("space_id", space_id) + .param("created_at", block.timestamp.to_rfc3339()) + .param("created_at_block", block.block_number.to_string()) + .param("updated_at", block.timestamp.to_rfc3339()) + .param("updated_at_block", block.block_number.to_string())) + + } + + (attribute_id, _, value) => { + let entity = Entity::::new( + entity_id, + space_id, + mapping::Triples(HashMap::from([ + ( + attribute_id.to_string(), + mapping::Triple { + value: value.to_string(), + r#type: mapping::ValueType::Text, + options: Default::default(), + }, + ), + ])), + ); + + Ok(entity.upsert_query(block)?) + } + } + } + + pub fn delete_triple( + block: &BlockMetadata, + space_id: &str, + triple: pb::grc20::Triple, + ) -> Query<()> { + let query = format!( + r#" + MATCH (n {{ id: $id, space_id: $space_id }}) + REMOVE n.`{attribute_label}` + SET n += {{ + `{UPDATED_AT}`: datetime($updated_at), + `{UPDATED_AT_BLOCK}`: $updated_at_block + }} + "#, + attribute_label = triple.attribute, + UPDATED_AT = system_ids::UPDATED_AT_TIMESTAMP, + UPDATED_AT_BLOCK = system_ids::UPDATED_AT_BLOCK, + ); + + Query::new(&query) + .param("id", triple.entity) + .param("space_id", space_id) + .param("created_at", block.timestamp.to_rfc3339()) + .param("created_at_block", block.block_number.to_string()) + .param("updated_at", block.timestamp.to_rfc3339()) + .param("updated_at_block", block.block_number.to_string()) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum SetTripleError { + #[error("Invalid graph URI: {0}")] + InvalidGraphUri(#[from] graph_uri::InvalidGraphUri), + #[error("Serde JSON error: {0}")] + SerdeJson(#[from] serde_json::Error), +} + +impl Entity +where + T: Serialize, +{ + /// Returns a query to upsert the current entity + pub fn upsert_query(&self, block: &BlockMetadata) -> Result, serde_json::Error> { + const QUERY: &str = const_format::formatcp!( + r#" + MERGE (n {{id: $id, space_id: $space_id}}) + ON CREATE SET n += {{ + `{CREATED_AT}`: datetime($created_at), + `{CREATED_AT_BLOCK}`: $created_at_block + }} + SET n:$($labels) + SET n += {{ + `{UPDATED_AT}`: datetime($updated_at), + `{UPDATED_AT_BLOCK}`: $updated_at_block + }} + SET n += $data + "#, + CREATED_AT = system_ids::CREATED_AT_TIMESTAMP, + CREATED_AT_BLOCK = system_ids::CREATED_AT_BLOCK, + UPDATED_AT = system_ids::UPDATED_AT_TIMESTAMP, + UPDATED_AT_BLOCK = system_ids::UPDATED_AT_BLOCK, + ); + + let bolt_data = match serde_value_to_bolt(serde_json::to_value(self.attributes())?) { + neo4rs::BoltType::Map(map) => neo4rs::BoltType::Map(map), + _ => neo4rs::BoltType::Map(Default::default()), + }; + + let query = Query::new(QUERY) + .param("id", self.id()) + .param("space_id", self.space_id()) + .param("created_at", block.timestamp.to_rfc3339()) + .param("created_at_block", block.block_number.to_string()) + .param("updated_at", block.timestamp.to_rfc3339()) + .param("updated_at_block", block.block_number.to_string()) + .param("labels", self.types.clone()) + .param("data", bolt_data); + + Ok(query) + } +} + +impl TryFrom for Entity +where + T: for<'a> serde::Deserialize<'a>, +{ + type Error = neo4rs::DeError; + + fn try_from(value: neo4rs::Node) -> Result { + let labels = value.labels().iter().map(|l| l.to_string()).collect(); + let attributes = value.to()?; + Ok(Self { + types: labels, + attributes, + }) + } +} + +impl Entity> { + pub fn with_attribute(mut self, attribute_id: String, value: T) -> Self + where + T: Into, + { + self.attributes_mut().insert(attribute_id, value.into()); + self + } +} + +impl Entity { + pub fn name(&self) -> Option { + self.attributes() + .get("name") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) + } + + pub fn name_or_id(&self) -> String { + self.name().unwrap_or_else(|| self.id().to_string()) + } +} + +pub type DefaultAttributes = HashMap; + +#[derive(Debug, Deserialize, PartialEq)] +pub struct Named { + pub name: Option, +} + +impl Entity { + pub fn name_or_id(&self) -> String { + self.name().unwrap_or_else(|| self.id().to_string()) + } + + pub fn name(&self) -> Option { + self.attributes().name.clone() + } +} + +#[cfg(test)] +mod tests { + use crate::mapping::triple::{Triple, Triples, ValueType}; + + use super::*; + use std::collections::HashMap; + + #[test] + pub fn test_node_conversion() { + let node = neo4rs::Node::new(neo4rs::BoltNode { + id: neo4rs::BoltInteger { value: 425 }, + labels: neo4rs::BoltList { + value: vec![neo4rs::BoltType::String(neo4rs::BoltString { + value: "9u4zseS3EDXG9ZvwR9RmqU".to_string(), + })], + }, + properties: neo4rs::BoltMap { + value: HashMap::from([ + ( + neo4rs::BoltString { + value: "space_id".to_string(), + }, + neo4rs::BoltType::String(neo4rs::BoltString { + value: "NBDtpHimvrkmVu7vVBXX7b".to_string(), + }), + ), + ( + neo4rs::BoltString { + value: "GG8Z4cSkjv8CywbkLqVU5M".to_string(), + }, + neo4rs::BoltType::String(neo4rs::BoltString { + value: "Person Posts Page Template".to_string(), + }), + ), + ( + neo4rs::BoltString { + value: "GG8Z4cSkjv8CywbkLqVU5M.type".to_string(), + }, + neo4rs::BoltType::String(neo4rs::BoltString { + value: "TEXT".to_string(), + }), + ), + ( + neo4rs::BoltString { + value: "id".to_string(), + }, + neo4rs::BoltType::String(neo4rs::BoltString { + value: "98wgvodwzidmVA4ryVzGX6".to_string(), + }), + ), + ]), + }, + }); + + let node: Entity = node + .try_into() + .expect("Failed to convert neo4rs::Node to Node"); + + assert_eq!( + node, + Entity { + types: vec!["9u4zseS3EDXG9ZvwR9RmqU".to_string()], + attributes: Attributes { + id: "98wgvodwzidmVA4ryVzGX6".to_string(), + space_id: "NBDtpHimvrkmVu7vVBXX7b".to_string(), + attributes: Triples(HashMap::from([ + ( + "GG8Z4cSkjv8CywbkLqVU5M".to_string(), + Triple { + value: "Person Posts Page Template".to_string(), + r#type: ValueType::Text, + options: Default::default(), + }, + ), + ])) + } + } + ) + } +} diff --git a/sdk/src/mapping/mod.rs b/sdk/src/mapping/mod.rs new file mode 100644 index 0000000..b2c6861 --- /dev/null +++ b/sdk/src/mapping/mod.rs @@ -0,0 +1,11 @@ +pub mod attributes; +pub mod entity; +pub mod relation; +pub mod triple; +pub mod query; + +pub use attributes::Attributes; +pub use entity::{Entity, Named}; +pub use query::Query; +pub use relation::Relation; +pub use triple::{Triple, Triples, ValueType, Options}; \ No newline at end of file diff --git a/sdk/src/mapping/query.rs b/sdk/src/mapping/query.rs new file mode 100644 index 0000000..27a3b03 --- /dev/null +++ b/sdk/src/mapping/query.rs @@ -0,0 +1,20 @@ +/// Wrapper around neo4rs::Query to allow for type-safe queries. +/// `T` is the type of the result of the query. +pub struct Query { + pub query: neo4rs::Query, + _phantom: std::marker::PhantomData, +} + +impl Query { + pub fn new(query: &str) -> Self { + Self { + query: neo4rs::query(query), + _phantom: std::marker::PhantomData, + } + } + + pub fn param>(mut self, key: &str, value: U) -> Self { + self.query = self.query.param(key, value); + self + } +} \ No newline at end of file diff --git a/sdk/src/mapping/relation.rs b/sdk/src/mapping/relation.rs new file mode 100644 index 0000000..c848983 --- /dev/null +++ b/sdk/src/mapping/relation.rs @@ -0,0 +1,134 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +use crate::{models::BlockMetadata, neo4j_utils::serde_value_to_bolt, system_ids}; + +use super::{attributes::Attributes, query::Query}; + +#[derive(Debug, Deserialize, PartialEq)] +pub struct Relation { + pub id: String, + pub types: Vec, + pub from: String, + pub to: String, + #[serde(flatten)] + pub attributes: Attributes, +} + +impl Relation { + pub fn new( + id: &str, + space_id: &str, + from: &str, + to: &str, + data: T, + ) -> Self { + Self { + id: id.to_string(), + from: from.to_string(), + to: to.to_string(), + types: vec![system_ids::RELATION_TYPE.to_string()], + attributes: Attributes { + id: id.to_string(), + space_id: space_id.to_string(), + attributes: data, + }, + } + } + + pub fn id(&self) -> &str { + &self.attributes.id + } + + pub fn space_id(&self) -> &str { + &self.attributes.space_id + } + + pub fn attributes(&self) -> &T { + &self.attributes.attributes + } + + pub fn attributes_mut(&mut self) -> &mut T { + &mut self.attributes.attributes + } + + pub fn with_type(mut self, type_id: &str) -> Self { + self.types.push(type_id.to_string()); + self + } + + /// Returns a query to delete the current relation + pub fn delete_query(id: &str) -> Query<()> { + const QUERY: &str = const_format::formatcp!( + r#" + MATCH (r {{id: $id}}) + DETACH DELETE r + "#, + ); + + Query::new(QUERY).param("id", id) + } +} + +impl Relation +where + T: Serialize, +{ + /// Returns a query to upsert the current relation + pub fn upsert_query(&self, block: &BlockMetadata) -> Result, serde_json::Error> { + const QUERY: &str = const_format::formatcp!( + r#" + MATCH (from {{id: $from_id}}) + MATCH (to {{id: $to_id}}) + MERGE (from)<-[:`{FROM_ENTITY}`]-(r {{id: $id, space_id: $space_id}})-[:`{TO_ENTITY}`]->(to) + ON CREATE SET r += {{ + `{CREATED_AT}`: datetime($created_at), + `{CREATED_AT_BLOCK}`: $created_at_block + }} + SET r:$($labels) + SET r += {{ + `{UPDATED_AT}`: datetime($updated_at), + `{UPDATED_AT_BLOCK}`: $updated_at_block + }} + SET r += $data + "#, + FROM_ENTITY = system_ids::RELATION_FROM_ATTRIBUTE, + TO_ENTITY = system_ids::RELATION_TO_ATTRIBUTE, + CREATED_AT = system_ids::CREATED_AT_TIMESTAMP, + CREATED_AT_BLOCK = system_ids::CREATED_AT_BLOCK, + UPDATED_AT = system_ids::UPDATED_AT_TIMESTAMP, + UPDATED_AT_BLOCK = system_ids::UPDATED_AT_BLOCK, + ); + + let bolt_data = match serde_value_to_bolt(serde_json::to_value(self.attributes())?) { + neo4rs::BoltType::Map(map) => neo4rs::BoltType::Map(map), + _ => neo4rs::BoltType::Map(Default::default()), + }; + + let query = Query::new(QUERY) + .param("id", self.id()) + .param("space_id", self.space_id()) + .param("from_id", self.from.clone()) + .param("to_id", self.to.clone()) + .param("space_id", self.space_id()) + .param("created_at", block.timestamp.to_rfc3339()) + .param("created_at_block", block.block_number.to_string()) + .param("updated_at", block.timestamp.to_rfc3339()) + .param("updated_at_block", block.block_number.to_string()) + .param("labels", self.types.clone()) + .param("data", bolt_data); + + Ok(query) + } +} + +impl Relation> { + pub fn with_attribute(mut self, key: String, value: T) -> Self + where + T: Into, + { + self.attributes_mut().insert(key, value.into()); + self + } +} diff --git a/sdk/src/mapping/triple.rs b/sdk/src/mapping/triple.rs new file mode 100644 index 0000000..159308a --- /dev/null +++ b/sdk/src/mapping/triple.rs @@ -0,0 +1,324 @@ +use std::collections::HashMap; + +use serde::{ser::SerializeMap, Deserialize, Serialize}; + +use crate::pb; + +#[derive(Clone, Debug, PartialEq)] +pub struct Triples(pub(crate) HashMap); + +impl Serialize for Triples { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut map = serializer.serialize_map(None)?; + for (key, value) in &self.0 { + map.serialize_entry(key, &value.value)?; + map.serialize_entry(&format!("{}.type", key), &value.r#type)?; + if let Some(ref format) = value.options.format { + map.serialize_entry(&format!("{}.options.format", key), format)?; + } + if let Some(ref unit) = value.options.unit { + map.serialize_entry(&format!("{}.options.unit", key), unit)?; + } + if let Some(ref language) = value.options.language { + map.serialize_entry(&format!("{}.options.language", key), language)?; + } + } + map.end() + } +} + +impl<'de> Deserialize<'de> for Triples { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct TriplesVisitor; + + impl<'de> serde::de::Visitor<'de> for TriplesVisitor { + type Value = Triples; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a map representing triples") + } + + fn visit_map(self, mut map: M) -> Result + where + M: serde::de::MapAccess<'de>, + { + let mut triples = HashMap::new(); + + while let Some(key) = map.next_key::()? { + match key.split('.').collect::>()[..] { + [key] => { + let value = map.next_value::()?; + triples.entry(key.to_string()).or_insert(Triple::default()).value = value; + } + [key, "type"] => { + let value = map.next_value::()?; + triples.entry(key.to_string()).or_insert(Triple::default()).r#type = value; + } + [key, "options", "format"] => { + let value = map.next_value::()?; + triples.entry(key.to_string()).or_insert(Triple::default()).options.format = Some(value); + } + [key, "options", "unit"] => { + let value = map.next_value::()?; + triples.entry(key.to_string()).or_insert(Triple::default()).options.unit = Some(value); + } + [key, "options", "language"] => { + let value = map.next_value::()?; + triples.entry(key.to_string()).or_insert(Triple::default()).options.language = Some(value); + } + _ => return Err(serde::de::Error::custom(format!("Invalid key: {}", key))), + } + } + + Ok(Triples(triples)) + } + } + + deserializer.deserialize_map(TriplesVisitor) + } +} + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct Triple { + pub value: String, + pub r#type: ValueType, + pub options: Options, +} + +impl Serialize for Triple { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut map = serializer.serialize_map(None)?; + map.serialize_entry("", &self.value)?; + map.serialize_entry(".type", &self.r#type)?; + if let Some(ref format) = self.options.format { + map.serialize_entry(".options.format", format)?; + } + if let Some(ref unit) = self.options.unit { + map.serialize_entry(".options.unit", unit)?; + } + if let Some(ref language) = self.options.language { + map.serialize_entry(".options.language", language)?; + } + map.end() + } +} + +impl<'de> Deserialize<'de> for Triple { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + struct TripleHelper { + #[serde(rename = "")] + value: String, + #[serde(rename = ".type")] + r#type: ValueType, + #[serde(rename = ".options.format")] + format: Option, + #[serde(rename = ".options.unit")] + unit: Option, + #[serde(rename = ".options.language")] + language: Option, + } + + let helper = TripleHelper::deserialize(deserializer)?; + Ok(Triple { + value: helper.value, + r#type: helper.r#type, + options: Options { + format: helper.format, + unit: helper.unit, + language: helper.language, + }, + }) + } +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)] +pub struct Options { + pub format: Option, + pub unit: Option, + pub language: Option, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum ValueType { + #[default] + Text, + Number, + Checkbox, + Url, + Time, + Point, +} + +impl From for Option { + fn from(value: pb::grc20::ValueType) -> Self { + match value { + pb::grc20::ValueType::Text => Some(ValueType::Text), + pb::grc20::ValueType::Number => Some(ValueType::Number), + pb::grc20::ValueType::Checkbox => Some(ValueType::Checkbox), + pb::grc20::ValueType::Url => Some(ValueType::Url), + pb::grc20::ValueType::Time => Some(ValueType::Time), + pb::grc20::ValueType::Point => Some(ValueType::Point), + pb::grc20::ValueType::Unknown => None, + } + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + use serde_with::with_prefix; + use super::*; + + #[test] + pub fn test_serialize_triple() { + with_prefix!(foo_prefix "foo"); + #[derive(Debug, Deserialize, Serialize, PartialEq)] + struct Foo { + #[serde(flatten, with = "foo_prefix")] + foo: Triple, + } + + let value = Foo { + foo: Triple { + value: "Hello, World!".to_string(), + r#type: ValueType::Text, + options: Options { + format: Some("text".to_string()), + unit: Some("unit".to_string()), + ..Default::default() + }, + }, + }; + + let serialized = serde_json::to_value(&value).expect("Failed to serialize Value"); + + assert_eq!( + serialized, + serde_json::json!({ + "foo": "Hello, World!", + "foo.type": "TEXT", + "foo.options.format": "text", + "foo.options.unit": "unit", + }) + ); + + let deserialized: Foo = serde_json::from_value(serialized).expect("Failed to deserialize Value"); + + assert_eq!(deserialized, value); + } + + #[test] + pub fn test_serialize_triple_multiple_fields() { + with_prefix!(foo_prefix "foo"); + with_prefix!(bar_prefix "bar"); + #[derive(Debug, Deserialize, Serialize, PartialEq)] + struct Foo { + #[serde(flatten, with = "foo_prefix")] + foo: Triple, + + #[serde(flatten, with = "bar_prefix")] + bar: Triple, + + other_field: String, + } + + let value = Foo { + foo: Triple { + value: "Hello, World!".to_string(), + r#type: ValueType::Text, + options: Options { + format: Some("text".to_string()), + ..Default::default() + }, + }, + bar: Triple { + value: "123".to_string(), + r#type: ValueType::Number, + options: Options { + unit: Some("int".to_string()), + ..Default::default() + }, + }, + other_field: "other".to_string(), + }; + + let serialized = serde_json::to_value(&value).expect("Failed to serialize Value"); + + assert_eq!( + serialized, + serde_json::json!({ + "foo": "Hello, World!", + "foo.type": "TEXT", + "foo.options.format": "text", + "bar": "123", + "bar.type": "NUMBER", + "bar.options.unit": "int", + "other_field": "other", + }) + ); + + let deserialized: Foo = serde_json::from_value(serialized).expect("Failed to deserialize Value"); + + assert_eq!(deserialized, value); + } + + #[test] + fn test_deserialize_triples() { + let triples = Triples(HashMap::from([ + ( + "foo".to_string(), + Triple { + value: "Hello, World!".to_string(), + r#type: ValueType::Text, + options: Options { + format: Some("text".to_string()), + ..Default::default() + }, + }, + ), + ( + "bar".to_string(), + Triple { + value: "123".to_string(), + r#type: ValueType::Number, + options: Options { + unit: Some("int".to_string()), + ..Default::default() + }, + }, + ), + ])); + + let serialized = serde_json::to_value(&triples).expect("Failed to serialize Value"); + + assert_eq!( + serialized, + serde_json::json!({ + "foo": "Hello, World!", + "foo.type": "TEXT", + "foo.options.format": "text", + "bar": "123", + "bar.type": "NUMBER", + "bar.options.unit": "int", + }) + ); + + let deserialized: Triples = serde_json::from_value(serialized).expect("Failed to deserialize Value"); + + assert_eq!(deserialized, triples); + } +} \ No newline at end of file diff --git a/sdk/src/models.rs b/sdk/src/models.rs deleted file mode 100644 index fbb9ce7..0000000 --- a/sdk/src/models.rs +++ /dev/null @@ -1,281 +0,0 @@ -//! This module contains models reserved for use by the KG Indexer. - -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; -use web3_utils::checksum_address; - -use crate::{ - ids, mapping::Node, pb::{self, grc20}, system_ids -}; - -pub struct BlockMetadata { - pub cursor: String, - pub block_number: u64, - pub timestamp: DateTime, - pub request_id: String, -} - -#[derive(Clone, Deserialize, Serialize)] -pub struct GeoAccount { - pub id: String, - pub address: String, -} - -impl GeoAccount { - pub fn new(address: String) -> Self { - let checksummed_address = checksum_address(&address, None); - Self { - id: ids::create_id_from_unique_string(&checksummed_address), - address: checksummed_address, - } - } - - pub fn id_from_address(address: &str) -> String { - ids::create_id_from_unique_string(&checksum_address(address, None)) - } -} - -#[derive(Clone, Default, Deserialize, Serialize)] -pub enum SpaceType { - #[default] - Public, - Personal, -} - -#[derive(Clone, Default, Deserialize, Serialize)] -#[serde(rename = "306598522df542f69ad72921c33ad84b", tag = "$type")] -pub struct Space { - // pub id: String, - pub network: String, - // #[serde(rename = "`65da3fab6e1c48b7921a6a3260119b48`")] - pub r#type: SpaceType, - /// The address of the space's DAO contract. - pub dao_contract_address: String, - /// The address of the space plugin contract. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub space_plugin_address: Option, - /// The address of the voting plugin contract. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub voting_plugin_address: Option, - /// The address of the member access plugin contract. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub member_access_plugin: Option, - /// The address of the personal space admin plugin contract. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub personal_space_admin_plugin: Option, -} - -/// Space editor relation. -#[derive(Deserialize, Serialize)] -pub struct SpaceEditor; - -/// Space member relation. -#[derive(Deserialize, Serialize)] -pub struct SpaceMember; - -/// Parent space relation (for subspaces). -#[derive(Deserialize, Serialize)] -pub struct ParentSpace; - -pub struct EditProposal { - pub name: String, - pub proposal_id: String, - pub space: String, - pub space_address: String, - pub creator: String, - pub ops: Vec, -} - -#[derive(Deserialize, Serialize)] -#[serde(tag = "$type")] -pub struct Cursor { - pub cursor: String, - pub block_number: u64, -} - -#[derive(Deserialize, Serialize)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub enum VoteType { - Accept, - Reject, -} - -impl TryFrom for VoteType { - type Error = String; - - fn try_from(vote: u64) -> Result { - match vote { - 2 => Ok(Self::Accept), - 3 => Ok(Self::Reject), - _ => Err(format!("Invalid vote type: {}", vote)), - } - } -} - -#[derive(Deserialize, Serialize)] -pub struct VoteCast { - pub id: String, - pub vote_type: VoteType, -} - -#[derive(Clone, Deserialize, Serialize)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub enum ProposalType { - AddEdit, - ImportSpace, - AddSubspace, - RemoveSubspace, - AddEditor, - RemoveEditor, - AddMember, - RemoveMember, -} - -impl TryFrom for ProposalType { - type Error = String; - - fn try_from(action_type: pb::ipfs::ActionType) -> Result { - match action_type { - pb::ipfs::ActionType::AddMember => Ok(Self::AddMember), - pb::ipfs::ActionType::RemoveMember => Ok(Self::RemoveMember), - pb::ipfs::ActionType::AddEditor => Ok(Self::AddEditor), - pb::ipfs::ActionType::RemoveEditor => Ok(Self::RemoveEditor), - pb::ipfs::ActionType::AddSubspace => Ok(Self::AddSubspace), - pb::ipfs::ActionType::RemoveSubspace => Ok(Self::RemoveSubspace), - pb::ipfs::ActionType::AddEdit => Ok(Self::AddEdit), - pb::ipfs::ActionType::ImportSpace => Ok(Self::ImportSpace), - _ => Err(format!("Invalid action type: {:?}", action_type)), - } - } -} - -#[derive(Clone, Deserialize, Serialize)] -pub enum ProposalStatus { - Proposed, - Accepted, - Rejected, - Canceled, - Executed, -} - -#[derive(Clone, Deserialize, Serialize)] -pub struct Proposal { - pub id: String, - pub onchain_proposal_id: String, - pub proposal_type: ProposalType, - pub status: ProposalStatus, - pub plugin_address: String, - pub start_time: DateTime, - pub end_time: DateTime, -} - -#[derive(Deserialize, Serialize)] -pub struct Proposals; - -pub trait AsProposal { - fn as_proposal(&self) -> &Proposal; - - fn type_id(&self) -> &'static str; -} - -#[derive(Deserialize, Serialize)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub enum MembershipProposalType { - AddMember, - RemoveMember, -} - -impl TryFrom for MembershipProposalType { - type Error = String; - - fn try_from(action_type: pb::ipfs::ActionType) -> Result { - match action_type { - pb::ipfs::ActionType::AddMember => Ok(Self::AddMember), - pb::ipfs::ActionType::RemoveMember => Ok(Self::RemoveMember), - _ => Err(format!("Invalid action type: {:?}", action_type)), - } - } -} - -#[derive(Deserialize, Serialize)] -pub struct MembershipProposal { - #[serde(flatten)] - pub proposal: Proposal, - pub proposal_type: MembershipProposalType, -} - -impl AsProposal for MembershipProposal { - fn as_proposal(&self) -> &Proposal { - &self.proposal - } - - fn type_id(&self) -> &'static str { - system_ids::MEMBERSHIP_PROPOSAL_TYPE - } -} - -#[derive(Deserialize, Serialize)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub enum EditorshipProposalType { - AddEditor, - RemoveEditor, -} - -#[derive(Deserialize, Serialize)] -pub struct EditorshipProposal { - #[serde(flatten)] - pub proposal: Proposal, - pub proposal_type: MembershipProposalType, -} - -impl AsProposal for EditorshipProposal { - fn as_proposal(&self) -> &Proposal { - &self.proposal - } - - fn type_id(&self) -> &'static str { - system_ids::EDITORSHIP_PROPOSAL_TYPE - } -} - -#[derive(Deserialize, Serialize)] -pub struct ProposedAccount; - -#[derive(Deserialize, Serialize)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub enum SubspaceProposalType { - AddSubspace, - RemoveSubspace, -} - -impl TryFrom for SubspaceProposalType { - type Error = String; - - fn try_from(action_type: pb::ipfs::ActionType) -> Result { - match action_type { - pb::ipfs::ActionType::AddSubspace => Ok(Self::AddSubspace), - pb::ipfs::ActionType::RemoveSubspace => Ok(Self::RemoveSubspace), - _ => Err(format!("Invalid action type: {:?}", action_type)), - } - } -} - -#[derive(Deserialize, Serialize)] -pub struct SubspaceProposal { - #[serde(flatten)] - pub proposal: Proposal, - pub proposal_type: SubspaceProposalType, -} - -impl AsProposal for SubspaceProposal { - fn as_proposal(&self) -> &Proposal { - &self.proposal - } - - fn type_id(&self) -> &'static str { - system_ids::SUBSPACE_PROPOSAL_TYPE - } -} - -#[derive(Deserialize, Serialize)] -pub struct ProposedSubspace; diff --git a/sdk/src/models/account.rs b/sdk/src/models/account.rs new file mode 100644 index 0000000..f307e1d --- /dev/null +++ b/sdk/src/models/account.rs @@ -0,0 +1,28 @@ +use serde::{Deserialize, Serialize}; +use web3_utils::checksum_address; + +use crate::{ids, mapping::Entity, system_ids}; + + +#[derive(Clone, Deserialize, Serialize, PartialEq)] +pub struct GeoAccount { + pub address: String, +} + +impl GeoAccount { + pub fn new(address: String) -> Entity { + let checksummed_address = checksum_address(&address, None); + Entity::new( + &ids::create_id_from_unique_string(&checksummed_address), + system_ids::INDEXER_SPACE_ID, + Self { + address: checksummed_address, + } + ) + .with_type(system_ids::GEO_ACCOUNT) + } + + pub fn new_id(address: &str) -> String { + ids::create_id_from_unique_string(&checksum_address(address, None)) + } +} \ No newline at end of file diff --git a/sdk/src/models/block.rs b/sdk/src/models/block.rs new file mode 100644 index 0000000..af25619 --- /dev/null +++ b/sdk/src/models/block.rs @@ -0,0 +1,17 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize)] +#[serde(tag = "$type")] +pub struct Cursor { + pub cursor: String, + pub block_number: u64, +} + +#[derive(Default)] +pub struct BlockMetadata { + pub cursor: String, + pub block_number: u64, + pub timestamp: DateTime, + pub request_id: String, +} \ No newline at end of file diff --git a/sdk/src/models/editor.rs b/sdk/src/models/editor.rs new file mode 100644 index 0000000..2b3d07a --- /dev/null +++ b/sdk/src/models/editor.rs @@ -0,0 +1,43 @@ +use serde::{Deserialize, Serialize}; + +use crate::{ids, mapping::{Query, Relation}, system_ids}; + +/// Space editor relation. +#[derive(Deserialize, Serialize)] +pub struct SpaceEditor; + +impl SpaceEditor { + pub fn new( + editor_id: &str, + space_id: &str, + ) -> Relation { + Relation::new( + &ids::create_geo_id(), + system_ids::INDEXER_SPACE_ID, + editor_id, + space_id, + Self, + ) + .with_type(system_ids::EDITOR_RELATION) + } + + /// Returns a query to delete a relation between an editor and a space. + pub fn remove_query( + editor_id: &str, + space_id: &str, + ) -> Query<()> { + const QUERY: &str = const_format::formatcp!( + r#" + MATCH ({{id: $from}})<-[:`{FROM_ENTITY}`]-(r:`{EDITOR_RELATION}`)-[:`{TO_ENTITY}`]->({{id: $to}}) + DETACH DELETE r + "#, + FROM_ENTITY = system_ids::RELATION_FROM_ATTRIBUTE, + TO_ENTITY = system_ids::RELATION_TO_ATTRIBUTE, + EDITOR_RELATION = system_ids::EDITOR_RELATION, + ); + + Query::new(QUERY) + .param("from", editor_id) + .param("to", space_id) + } +} \ No newline at end of file diff --git a/sdk/src/models/member.rs b/sdk/src/models/member.rs new file mode 100644 index 0000000..5797af7 --- /dev/null +++ b/sdk/src/models/member.rs @@ -0,0 +1,43 @@ +use serde::{Deserialize, Serialize}; + +use crate::{ids, mapping::{Query, Relation}, system_ids}; + +/// Space editor relation. +#[derive(Deserialize, Serialize)] +pub struct SpaceMember; + +impl SpaceMember { + pub fn new( + member_id: &str, + space_id: &str, + ) -> Relation { + Relation::new( + &ids::create_geo_id(), + system_ids::INDEXER_SPACE_ID, + member_id, + space_id, + Self, + ) + .with_type(system_ids::MEMBER_RELATION) + } + + /// Returns a query to delete a relation between an member and a space. + pub fn remove_query( + member_id: &str, + space_id: &str, + ) -> Query<()> { + const QUERY: &str = const_format::formatcp!( + r#" + MATCH ({{id: $from}})<-[:`{FROM_ENTITY}`]-(r:`{MEMBER_RELATION}`)-[:`{TO_ENTITY}`]->({{id: $to}}) + DETACH DELETE r + "#, + FROM_ENTITY = system_ids::RELATION_FROM_ATTRIBUTE, + TO_ENTITY = system_ids::RELATION_TO_ATTRIBUTE, + MEMBER_RELATION = system_ids::MEMBER_RELATION, + ); + + Query::new(QUERY) + .param("from", member_id) + .param("to", space_id) + } +} \ No newline at end of file diff --git a/sdk/src/models/mod.rs b/sdk/src/models/mod.rs new file mode 100644 index 0000000..22e06b6 --- /dev/null +++ b/sdk/src/models/mod.rs @@ -0,0 +1,15 @@ +pub mod account; +pub mod block; +pub mod editor; +pub mod member; +pub mod proposal; +pub mod space; +pub mod vote; + +pub use account::GeoAccount; +pub use block::{BlockMetadata, Cursor}; +pub use editor::SpaceEditor; +pub use member::SpaceMember; +pub use proposal::{Creator, EditProposal, Proposal, Proposals, AddEditorProposal, AddMemberProposal, AddSubspaceProposal, RemoveEditorProposal, RemoveMemberProposal, RemoveSubspaceProposal}; +pub use space::{Space, SpaceBuilder, SpaceType}; +pub use vote::{VoteCast, VoteType}; diff --git a/sdk/src/models/proposal.rs b/sdk/src/models/proposal.rs new file mode 100644 index 0000000..b92cc40 --- /dev/null +++ b/sdk/src/models/proposal.rs @@ -0,0 +1,312 @@ +use std::fmt::Display; + +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +use crate::{ids, mapping::{Entity, Query, Relation}, pb::{self, grc20}, system_ids}; + +use super::BlockMetadata; + +#[derive(Clone, Deserialize, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum ProposalType { + AddEdit, + ImportSpace, + AddSubspace, + RemoveSubspace, + AddEditor, + RemoveEditor, + AddMember, + RemoveMember, +} + +impl TryFrom for ProposalType { + type Error = String; + + fn try_from(action_type: pb::ipfs::ActionType) -> Result { + match action_type { + pb::ipfs::ActionType::AddMember => Ok(Self::AddMember), + pb::ipfs::ActionType::RemoveMember => Ok(Self::RemoveMember), + pb::ipfs::ActionType::AddEditor => Ok(Self::AddEditor), + pb::ipfs::ActionType::RemoveEditor => Ok(Self::RemoveEditor), + pb::ipfs::ActionType::AddSubspace => Ok(Self::AddSubspace), + pb::ipfs::ActionType::RemoveSubspace => Ok(Self::RemoveSubspace), + pb::ipfs::ActionType::AddEdit => Ok(Self::AddEdit), + pb::ipfs::ActionType::ImportSpace => Ok(Self::ImportSpace), + _ => Err(format!("Invalid action type: {:?}", action_type)), + } + } +} + +#[derive(Clone, Deserialize, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum ProposalStatus { + Proposed, + Accepted, + Rejected, + Canceled, + Executed, +} + +impl Display for ProposalStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ProposalStatus::Proposed => write!(f, "PROPOSED"), + ProposalStatus::Accepted => write!(f, "ACCEPTED"), + ProposalStatus::Rejected => write!(f, "REJECTED"), + ProposalStatus::Canceled => write!(f, "CANCELED"), + ProposalStatus::Executed => write!(f, "EXECUTED"), + } + } +} + +/// Common fields for all proposals +#[derive(Clone, Deserialize, Serialize)] +pub struct Proposal { + pub onchain_proposal_id: String, + pub status: ProposalStatus, + pub plugin_address: String, + pub start_time: String, + pub end_time: String, +} + +impl Proposal { + pub fn new_id(proposal_id: &str) -> String { + ids::create_id_from_unique_string(proposal_id) + } + + pub fn find_by_id_and_address( + proposal_id: &str, + plugin_address: &str, + ) -> Query { + const QUERY: &str = const_format::formatcp!( + r#" + MATCH (n:`{PROPOSAL_TYPE}` {{onchain_proposal_id: $proposal_id, plugin_address: $plugin_address}}) + RETURN n + "#, + PROPOSAL_TYPE = system_ids::PROPOSAL_TYPE, + ); + + Query::new(QUERY) + .param("proposal_id", proposal_id) + .param("plugin_address", plugin_address) + } + + /// Returns a query to set the status of a proposal given its ID + pub fn set_status_query(block: &BlockMetadata, proposal_id: &str, status: ProposalStatus) -> Query<()> { + const QUERY: &str = const_format::formatcp!( + r#" + MATCH (n:`{PROPOSAL_TYPE}` {{onchain_proposal_id: $proposal_id}}) + SET n.status = $status + SET n += {{ + `{UPDATED_AT}`: datetime($updated_at), + `{UPDATED_AT_BLOCK}`: $updated_at_block + }} + "#, + PROPOSAL_TYPE = system_ids::PROPOSAL_TYPE, + UPDATED_AT = system_ids::UPDATED_AT_TIMESTAMP, + UPDATED_AT_BLOCK = system_ids::UPDATED_AT_BLOCK, + ); + + Query::new(QUERY) + .param("proposal_id", proposal_id) + .param("status", status.to_string()) + .param("updated_at", block.timestamp.to_rfc3339()) + .param("updated_at_block", block.block_number.to_string()) + } +} + +// Relation for Space > PROPOSALS > Proposal +#[derive(Deserialize, Serialize)] +pub struct Proposals; + +impl Proposals { + pub fn new( + space_id: &str, + proposal_id: &str, + ) -> Relation { + Relation::new( + &ids::create_id_from_unique_string(&format!("{space_id}-{proposal_id}")), + system_ids::INDEXER_SPACE_ID, + space_id, + proposal_id, + Proposals {} + ) + .with_type(system_ids::PROPOSALS) + } +} + +// Proposal > CREATOR > Account +#[derive(Deserialize, Serialize)] +pub struct Creator; + +impl Creator { + pub fn new( + proposal_id: &str, + account_id: &str, + ) -> Relation { + Relation::new( + &ids::create_id_from_unique_string(&format!("{proposal_id}-{account_id}")), + system_ids::INDEXER_SPACE_ID, + proposal_id, + account_id, + Creator {} + ) + .with_type(system_ids::PROPOSAL_CREATOR) + } +} + +pub struct EditProposal { + pub name: String, + pub proposal_id: String, + pub space: String, + pub space_address: String, + pub creator: String, + pub ops: Vec, +} + +#[derive(Deserialize, Serialize)] +pub struct AddMemberProposal { + #[serde(flatten)] + pub proposal: Proposal, +} + +impl AddMemberProposal { + pub fn new(proposal: Proposal) -> Entity { + Entity::new( + &Proposal::new_id(&proposal.onchain_proposal_id), + system_ids::INDEXER_SPACE_ID, + Self {proposal} + ) + .with_type(system_ids::PROPOSAL_TYPE) + .with_type(system_ids::ADD_MEMBER_PROPOSAL) + } +} + +#[derive(Deserialize, Serialize)] +pub struct RemoveMemberProposal { + #[serde(flatten)] + pub proposal: Proposal, +} + +impl RemoveMemberProposal { + pub fn new(proposal: Proposal) -> Entity { + Entity::new( + &Proposal::new_id(&proposal.onchain_proposal_id), + system_ids::INDEXER_SPACE_ID, + Self {proposal} + ) + .with_type(system_ids::PROPOSAL_TYPE) + .with_type(system_ids::REMOVE_MEMBER_PROPOSAL) + } +} + +#[derive(Deserialize, Serialize)] +pub struct AddEditorProposal { + #[serde(flatten)] + pub proposal: Proposal, +} + +impl AddEditorProposal { + pub fn new(proposal: Proposal) -> Entity { + Entity::new( + &Proposal::new_id(&proposal.onchain_proposal_id), + system_ids::INDEXER_SPACE_ID, + Self {proposal} + ) + .with_type(system_ids::PROPOSAL_TYPE) + .with_type(system_ids::ADD_EDITOR_PROPOSAL) + } +} + +#[derive(Deserialize, Serialize)] +pub struct RemoveEditorProposal { + #[serde(flatten)] + pub proposal: Proposal, +} + +impl RemoveEditorProposal { + pub fn new(proposal: Proposal) -> Entity { + Entity::new( + &Proposal::new_id(&proposal.onchain_proposal_id), + system_ids::INDEXER_SPACE_ID, + Self {proposal} + ) + .with_type(system_ids::PROPOSAL_TYPE) + .with_type(system_ids::REMOVE_EDITOR_PROPOSAL) + } +} + +#[derive(Deserialize, Serialize)] +pub struct ProposedAccount; + +impl ProposedAccount { + pub fn new( + proposal_id: &str, + account_id: &str, + ) -> Relation { + Relation::new( + &ids::create_id_from_unique_string(&format!("{}-{}", proposal_id, account_id)), + system_ids::INDEXER_SPACE_ID, + proposal_id, + account_id, + Self {}, + ) + .with_type(system_ids::PROPOSED_ACCOUNT) + } +} + +#[derive(Deserialize, Serialize)] +pub struct AddSubspaceProposal { + #[serde(flatten)] + pub proposal: Proposal, +} + +impl AddSubspaceProposal { + pub fn new(proposal: Proposal) -> Entity { + Entity::new( + &Proposal::new_id(&proposal.onchain_proposal_id), + system_ids::INDEXER_SPACE_ID, + Self {proposal} + ) + .with_type(system_ids::PROPOSAL_TYPE) + .with_type(system_ids::ADD_SUBSPACE_PROPOSAL) + } +} + +#[derive(Deserialize, Serialize)] +pub struct RemoveSubspaceProposal { + #[serde(flatten)] + pub proposal: Proposal, +} + +impl RemoveSubspaceProposal { + pub fn new(proposal: Proposal) -> Entity { + Entity::new( + &Proposal::new_id(&proposal.onchain_proposal_id), + system_ids::INDEXER_SPACE_ID, + Self {proposal} + ) + .with_type(system_ids::PROPOSAL_TYPE) + .with_type(system_ids::REMOVE_SUBSPACE_PROPOSAL) + } +} + +#[derive(Deserialize, Serialize)] +pub struct ProposedSubspace; + +impl ProposedSubspace { + pub fn new( + subspace_proposal_id: &str, + subspace_id: &str, + ) -> Relation { + Relation::new( + &ids::create_id_from_unique_string(&format!("{}-{}", subspace_proposal_id, subspace_id)), + system_ids::INDEXER_SPACE_ID, + subspace_proposal_id, + subspace_id, + Self {}, + ) + .with_type(system_ids::PROPOSED_SUBSPACE) + } +} \ No newline at end of file diff --git a/sdk/src/models/space.rs b/sdk/src/models/space.rs new file mode 100644 index 0000000..93c34a5 --- /dev/null +++ b/sdk/src/models/space.rs @@ -0,0 +1,204 @@ +use serde::{Deserialize, Serialize}; +use web3_utils::checksum_address; + +use crate::{ + ids, + mapping::{query::Query, Entity, Relation}, + network_ids, system_ids, +}; + +#[derive(Clone, Deserialize, Serialize)] +pub struct Space { + pub network: String, + pub r#type: SpaceType, + /// The address of the space's DAO contract. + pub dao_contract_address: String, + /// The address of the space plugin contract. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub space_plugin_address: Option, + /// The address of the voting plugin contract. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub voting_plugin_address: Option, + /// The address of the member access plugin contract. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub member_access_plugin: Option, + /// The address of the personal space admin plugin contract. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub personal_space_admin_plugin: Option, +} + +impl Space { + pub fn new_id(network: &str, address: &str) -> String { + ids::create_id_from_unique_string(&format!("{network}:{}", checksum_address(address, None))) + } + + pub fn builder(id: &str, dao_contract_address: &str) -> SpaceBuilder { + SpaceBuilder::new(id, dao_contract_address) + } + + pub fn new(id: &str, space: Space) -> Entity { + Entity::new(id, system_ids::INDEXER_SPACE_ID, space).with_type(system_ids::INDEXED_SPACE) + } + + /// Returns a query to find a space by its DAO contract address. + pub fn find_by_dao_address_query(dao_contract_address: &str) -> Query { + const QUERY: &str = const_format::formatcp!( + "MATCH (n:`{INDEXED_SPACE}` {{dao_contract_address: $dao_contract_address}}) RETURN n", + INDEXED_SPACE = system_ids::INDEXED_SPACE, + ); + + Query::new(QUERY).param("dao_contract_address", dao_contract_address) + } + + /// Returns a query to find a space by its space plugin address. + pub fn find_by_space_plugin_address(space_plugin_address: &str) -> Query { + const QUERY: &str = const_format::formatcp!( + "MATCH (n:`{INDEXED_SPACE}` {{space_plugin_address: $space_plugin_address}}) RETURN n", + INDEXED_SPACE = system_ids::INDEXED_SPACE, + ); + + Query::new(QUERY).param("space_plugin_address", checksum_address(space_plugin_address, None)) + } + + /// Returns a query to find a space by its voting plugin address. + pub fn find_by_voting_plugin_address(voting_plugin_address: &str) -> Query { + const QUERY: &str = const_format::formatcp!( + "MATCH (n:`{INDEXED_SPACE}` {{voting_plugin_address: $voting_plugin_address}}) RETURN n", + INDEXED_SPACE = system_ids::INDEXED_SPACE, + ); + + Query::new(QUERY).param("voting_plugin_address", checksum_address(voting_plugin_address, None)) + } + + /// Returns a query to find a space by its member access plugin address. + pub fn find_by_member_access_plugin(member_access_plugin: &str) -> Query { + const QUERY: &str = const_format::formatcp!( + "MATCH (n:`{INDEXED_SPACE}` {{member_access_plugin: $member_access_plugin}}) RETURN n", + INDEXED_SPACE = system_ids::INDEXED_SPACE, + ); + + Query::new(QUERY).param("member_access_plugin", checksum_address(member_access_plugin, None)) + } + + /// Returns a query to find a space by its personal space admin plugin address. + pub fn find_by_personal_plugin_address(personal_space_admin_plugin: &str) -> Query { + const QUERY: &str = const_format::formatcp!( + "MATCH (n:`{INDEXED_SPACE}` {{personal_space_admin_plugin: $personal_space_admin_plugin}}) RETURN n", + INDEXED_SPACE = system_ids::INDEXED_SPACE, + ); + + Query::new(QUERY).param("personal_space_admin_plugin", checksum_address(personal_space_admin_plugin, None)) + } + + /// Returns a query to find all spaces. + pub fn find_all() -> Query { + const QUERY: &str = const_format::formatcp!( + "MATCH (n:`{INDEXED_SPACE}`) RETURN n", + INDEXED_SPACE = system_ids::INDEXED_SPACE, + ); + + Query::new(QUERY) + } +} + +#[derive(Clone, Default, Deserialize, Serialize)] +pub enum SpaceType { + #[default] + Public, + Personal, +} + +pub struct SpaceBuilder { + id: String, + network: String, + r#type: SpaceType, + dao_contract_address: String, + space_plugin_address: Option, + voting_plugin_address: Option, + member_access_plugin: Option, + personal_space_admin_plugin: Option, +} + +impl SpaceBuilder { + pub fn new(id: &str, dao_contract_address: &str) -> Self { + Self { + id: id.to_string(), + network: network_ids::GEO.to_string(), + r#type: SpaceType::Public, + dao_contract_address: checksum_address(dao_contract_address, None), + space_plugin_address: None, + voting_plugin_address: None, + member_access_plugin: None, + personal_space_admin_plugin: None, + } + } + + pub fn network(mut self, network: String) -> Self { + self.network = network; + self + } + + pub fn r#type(mut self, r#type: SpaceType) -> Self { + self.r#type = r#type; + self + } + + pub fn dao_contract_address(mut self, dao_contract_address: &str) -> Self { + self.dao_contract_address = checksum_address(dao_contract_address, None); + self + } + + pub fn space_plugin_address(mut self, space_plugin_address: &str) -> Self { + self.space_plugin_address = Some(checksum_address(space_plugin_address, None)); + self + } + + pub fn voting_plugin_address(mut self, voting_plugin_address: &str) -> Self { + self.voting_plugin_address = Some(checksum_address(voting_plugin_address, None)); + self + } + + pub fn member_access_plugin(mut self, member_access_plugin: &str) -> Self { + self.member_access_plugin = Some(checksum_address(member_access_plugin, None)); + self + } + + pub fn personal_space_admin_plugin(mut self, personal_space_admin_plugin: &str) -> Self { + self.personal_space_admin_plugin = Some(checksum_address(personal_space_admin_plugin, None)); + self + } + + pub fn build(self) -> Entity { + Entity::new( + &self.id, + system_ids::INDEXER_SPACE_ID, + Space { + network: self.network, + r#type: self.r#type, + dao_contract_address: self.dao_contract_address, + space_plugin_address: self.space_plugin_address, + voting_plugin_address: self.voting_plugin_address, + member_access_plugin: self.member_access_plugin, + personal_space_admin_plugin: self.personal_space_admin_plugin, + }, + ) + .with_type(system_ids::INDEXED_SPACE) + } +} + +/// Parent space relation (for subspaces). +#[derive(Deserialize, Serialize)] +pub struct ParentSpace; + +impl ParentSpace { + pub fn new(space_id: &str, parent_space_id: &str) -> Relation { + Relation::new( + &ids::create_geo_id(), + system_ids::INDEXER_SPACE_ID, + space_id, + parent_space_id, + Self, + ) + .with_type(system_ids::PARENT_SPACE) + } +} diff --git a/sdk/src/models/vote.rs b/sdk/src/models/vote.rs new file mode 100644 index 0000000..4b79316 --- /dev/null +++ b/sdk/src/models/vote.rs @@ -0,0 +1,62 @@ +//! This module contains models reserved for use by the KG Indexer. + +use serde::{Deserialize, Serialize}; + +use crate::{ + ids, mapping::{Relation}, system_ids +}; + + +#[derive(Deserialize, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum VoteType { + Accept, + Reject, +} + +impl TryFrom for VoteType { + type Error = String; + + fn try_from(vote: u64) -> Result { + match vote { + 2 => Ok(Self::Accept), + 3 => Ok(Self::Reject), + _ => Err(format!("Invalid vote type: {}", vote)), + } + } +} + +/// A vote cast by a user on a proposal. +/// +/// `Person > VOTE_CAST > Proposal` +#[derive(Deserialize, Serialize)] +pub struct VoteCast { + pub vote_type: VoteType, +} + +impl VoteCast { + pub fn new_id( + account_id: &str, + proposal_id: &str, + + ) -> String { + ids::create_id_from_unique_string(&format!("{account_id}-{proposal_id}")) + } + + /// Creates a new vote cast with the given vote type. + pub fn new( + account_id: &str, + proposal_id: &str, + vote_type: VoteType + ) -> Relation { + Relation::new( + &Self::new_id(account_id, proposal_id), + system_ids::INDEXER_SPACE_ID, + account_id, + proposal_id, + Self { + vote_type, + }, + ) + } +} \ No newline at end of file diff --git a/sink/src/neo4j_utils.rs b/sdk/src/neo4j_utils.rs similarity index 100% rename from sink/src/neo4j_utils.rs rename to sdk/src/neo4j_utils.rs diff --git a/sink/src/bootstrap/constants.rs b/sink/src/bootstrap/constants.rs index 81ee9d1..ca4f4b4 100644 --- a/sink/src/bootstrap/constants.rs +++ b/sink/src/bootstrap/constants.rs @@ -2,12 +2,11 @@ pub const ROOT_SPACE_CREATED_AT: u32 = 1670280473; pub const ROOT_SPACE_CREATED_AT_BLOCK: u32 = 620; pub const ROOT_SPACE_CREATED_BY_ID: &str = "0x66703c058795B9Cb215fbcc7c6b07aee7D216F24"; -// pub const SPACE_ID: &str = "NBDtpHimvrkmVu7vVBXX7b"; -pub const ROOT_SPACE_ID: &str = "NBDtpHimvrkmVu7vVBXX7b"; -pub const DAO_ADDRESS: &str = "0x9e2342C55080f2fCb6163c739a88c4F2915163C4"; -pub const SPACE_ADDRESS: &str = "0x7a260AC2D569994AA22a259B19763c9F681Ff84c"; -pub const MAIN_VOTING_ADDRESS: &str = "0x379408c230817DC7aA36033BEDC05DCBAcE7DF50"; -pub const MEMBER_ACCESS_ADDRESS: &str = "0xd09225EAe465f562719B9cA07da2E8ab286DBB36"; +pub const ROOT_SPACE_ID: &str = "BJqiLPcSgfF8FRxkFr76Uy"; +pub const ROOT_SPACE_DAO_ADDRESS: &str = "0xB3191d353c4e409Add754112544296449B18c1Af"; +pub const ROOT_SPACE_PLUGIN_ADDRESS: &str = "0x2a2d20e5262b27e6383da774E942dED3e4Bf5FaF"; +pub const ROOT_SPACE_MAIN_VOTING_ADDRESS: &str = "0x9445A38102792654D92F1ba76Ee26a52Aa1E466e"; +pub const ROOT_SPACE_MEMBER_ACCESS_ADDRESS: &str = "0xfd6FEd74F611539E6e0F199bB6a3248d79ca832E"; // export const INITIAL_BLOCK = { // blockNumber: ROOT_SPACE_CREATED_AT_BLOCK, diff --git a/sink/src/events/edit_published.rs b/sink/src/events/edit_published.rs index 4a16ab8..bcfdaab 100644 --- a/sink/src/events/edit_published.rs +++ b/sink/src/events/edit_published.rs @@ -1,8 +1,9 @@ use futures::{stream, StreamExt, TryStreamExt}; use ipfs::deserialize; use sdk::{ - mapping::Node, models::{self, EditProposal}, pb::{self, geo, grc20} + models::{self, EditProposal, Space}, network_ids, pb::{self, geo, grc20} }; +use web3_utils::checksum_address; use super::{handler::HandlerError, EventHandler}; @@ -25,19 +26,23 @@ impl EventHandler { .flatten() .collect::>(); + // let space_id = Space::new_id(network_ids::GEO, address) + + // TODO: Create "synthetic" proposals for newly created spaces and // personal spaces stream::iter(proposals) .map(Ok) // Need to wrap the proposal in a Result to use try_for_each - .try_for_each(|proposal| async { + .try_for_each(|proposal| async move { tracing::info!( - "Block #{} ({}): Creating edit proposal {}", + "Block #{} ({}): Processing ops for proposal {}", block.block_number, block.timestamp, proposal.proposal_id ); - self.kg.process_edit(proposal).await + + self.kg.process_ops(block, &proposal.space, proposal.ops).await }) .await .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; // TODO: Convert anyhow::Error to HandlerError properly @@ -51,9 +56,12 @@ impl EventHandler { ) -> Result, HandlerError> { let space = if let Some(space) = self .kg - .get_space_by_space_plugin_address(&edit.plugin_address) + .find_node(Space::find_by_space_plugin_address(&edit.plugin_address)) .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))? + .map_err(|e| HandlerError::Other(format!( + "Error querying space with plugin address {} {e:?}", + checksum_address(&edit.plugin_address, None) + ).into()))? { space } else { diff --git a/sink/src/events/editor_added.rs b/sink/src/events/editor_added.rs index 56791e5..b802569 100644 --- a/sink/src/events/editor_added.rs +++ b/sink/src/events/editor_added.rs @@ -1,5 +1,5 @@ use futures::join; -use sdk::{models, pb::geo}; +use sdk::{models::{self, SpaceEditor, Space}, pb::geo}; use web3_utils::checksum_address; use super::{handler::HandlerError, EventHandler}; @@ -12,18 +12,29 @@ impl EventHandler { ) -> Result<(), HandlerError> { match join!( self.kg - .get_space_by_voting_plugin_address(&editor_added.main_voting_plugin_address), + .find_node(Space::find_by_voting_plugin_address(&editor_added.main_voting_plugin_address)), self.kg - .get_space_by_personal_plugin_address(&editor_added.main_voting_plugin_address) + .find_node(Space::find_by_personal_plugin_address(&editor_added.main_voting_plugin_address)) ) { // Space found (Ok(Some(space)), Ok(_)) | (Ok(None), Ok(Some(space))) => { let editor = models::GeoAccount::new(editor_added.editor_address.clone()); + // Add geo account self.kg - .add_editor(&space.id(), &editor, &models::SpaceEditor, block) + .upsert_entity(block, &editor) .await .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; + + // Add space editor relation + self.kg + .upsert_relation(block, &SpaceEditor::new( + editor.id(), + space.id(), + )) + .await + .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; + } // Space not found (Ok(None), Ok(None)) => { diff --git a/sink/src/events/editor_removed.rs b/sink/src/events/editor_removed.rs index 8cbaece..2ac8d19 100644 --- a/sink/src/events/editor_removed.rs +++ b/sink/src/events/editor_removed.rs @@ -1,4 +1,4 @@ -use sdk::{models, pb::geo}; +use sdk::{models::{self, GeoAccount, Space, SpaceEditor}, pb::geo}; use super::{handler::HandlerError, EventHandler}; @@ -10,16 +10,17 @@ impl EventHandler { ) -> Result<(), HandlerError> { let space = self .kg - .get_space_by_dao_address(&editor_removed.dao_address) + .find_node(Space::find_by_dao_address_query(&editor_removed.dao_address)) .await .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; if let Some(space) = space { self.kg - .remove_editor( - &models::GeoAccount::id_from_address(&editor_removed.editor_address), - &space.id(), - block, + .run( + SpaceEditor::remove_query( + &GeoAccount::new_id(&editor_removed.editor_address), + space.id(), + ), ) .await .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; diff --git a/sink/src/events/handler.rs b/sink/src/events/handler.rs index f6859fe..674afae 100644 --- a/sink/src/events/handler.rs +++ b/sink/src/events/handler.rs @@ -72,9 +72,13 @@ impl substreams_utils::Sink for EventHandler { block.timestamp, value.spaces_created.len() ); - let created_space_ids = self - .handle_spaces_created(&value.spaces_created, &value.edits_published, &block) + let created_space_ids = stream::iter(&value.spaces_created) + .then(|event| async { self.handle_space_created(event, &block).await }) + .try_collect::>() .await?; + // let created_space_ids = self + // .handle_spaces_created(&value.spaces_created, &value.edits_published, &block) + // .await?; // Handle personal space creation tracing::info!( @@ -129,60 +133,178 @@ impl substreams_utils::Sink for EventHandler { .await?; // Handle members removed + tracing::info!( + "Block #{} ({}): Processing {} members removed events", + block.block_number, + block.timestamp, + value.members_removed.len() + ); stream::iter(&value.members_removed) .map(Ok) .try_for_each(|event| async { self.handle_member_removed(event, &block).await }) .await?; // Handle editors added + tracing::info!( + "Block #{} ({}): Processing {} editors added events", + block.block_number, + block.timestamp, + value.editors_added.len() + ); stream::iter(&value.editors_added) .map(Ok) .try_for_each(|event| async { self.handle_editor_added(event, &block).await }) .await?; // Handle editors removed + tracing::info!( + "Block #{} ({}): Processing {} editors removed events", + block.block_number, + block.timestamp, + value.editors_removed.len() + ); stream::iter(&value.editors_removed) .map(Ok) .try_for_each(|event| async { self.handle_editor_removed(event, &block).await }) .await?; - // Handle subspaces creation + // Handle subspaces added + tracing::info!( + "Block #{} ({}): Processing {} subspaces added events", + block.block_number, + block.timestamp, + value.subspaces_added.len() + ); stream::iter(&value.subspaces_added) .map(Ok) .try_for_each(|event| async { self.handle_subspace_added(event, &block).await }) .await?; // Handle subspace removal + tracing::info!( + "Block #{} ({}): Processing {} subspaces removed events", + block.block_number, + block.timestamp, + value.subspaces_removed.len() + ); stream::iter(&value.subspaces_removed) .map(Ok) .try_for_each(|event| async { self.handle_subspace_removed(event, &block).await }) .await?; - // Handle proposal creation - // stream::iter(&value.proposals_created) + // Handle AddMemberProposalCreated events + tracing::info!( + "Block #{} ({}): Processing {} add member proposal created events", + block.block_number, + block.timestamp, + value.proposed_added_members.len() + ); + stream::iter(&value.proposed_added_members) + .map(Ok) + .try_for_each(|event| async { self.handle_add_member_proposal_created(event, &block).await }) + .await?; + + // Handle RemoveMemberProposalCreated events + tracing::info!( + "Block #{} ({}): Processing {} remove member proposal created events", + block.block_number, + block.timestamp, + value.proposed_removed_members.len() + ); + stream::iter(&value.proposed_removed_members) + .map(Ok) + .try_for_each(|event| async { self.handle_remove_member_proposal_created(event, &block).await }) + .await?; + + // Handle AddEditorProposalCreated events + tracing::info!( + "Block #{} ({}): Processing {} add editor proposal created events", + block.block_number, + block.timestamp, + value.proposed_added_editors.len() + ); + stream::iter(&value.proposed_added_editors) + .map(Ok) + .try_for_each(|event| async { self.handle_add_editor_proposal_created(event, &block).await }) + .await?; + + // Handle RemoveEditorProposalCreated events + tracing::info!( + "Block #{} ({}): Processing {} remove editor proposal created events", + block.block_number, + block.timestamp, + value.proposed_removed_editors.len() + ); + stream::iter(&value.proposed_removed_editors) + .map(Ok) + .try_for_each(|event| async { self.handle_remove_editor_proposal_created(event, &block).await }) + .await?; + + // Handle AddSubspaceProposalCreated events + tracing::info!( + "Block #{} ({}): Processing {} add subspace proposal created events", + block.block_number, + block.timestamp, + value.proposed_added_subspaces.len() + ); + stream::iter(&value.proposed_added_subspaces) + .map(Ok) + .try_for_each(|event| async { self.handle_add_subspace_proposal_created(event, &block).await }) + .await?; + + // Handle RemoveSubspaceProposalCreated events + tracing::info!( + "Block #{} ({}): Processing {} remove subspace proposal created events", + block.block_number, + block.timestamp, + value.proposed_removed_subspaces.len() + ); + stream::iter(&value.proposed_removed_subspaces) + .map(Ok) + .try_for_each(|event| async { self.handle_remove_subspace_proposal_created(event, &block).await }) + .await?; + + // Handle PublishEditProposalCreated events + // tracing::info!( + // "Block #{} ({}): Processing {} publish edit proposal created events", + // block.block_number, + // block.timestamp, + // value.proposed_published_edits.len() + // ); + // stream::iter(&value.proposed_published_edits) // .map(Ok) - // .try_for_each(|event| async { self.handle_proposal_created(event, &block).await }) + // .try_for_each(|event| async { self.handle_publish_edit_proposal_created(event, &block).await }) // .await?; - // TODO: Handle AddMemberProposalCreated events - // TODO: Handle RemoveMemberProposalCreated events - // TODO: Handle AddEditorProposalCreated events - // TODO: Handle RemoveEditorProposalCreated events - // TODO: Handle AddSubspaceProposalCreated events - // TODO: Handle RemoveSubspaceProposalCreated events - // TODO: Handle PublishEditProposalCreated events - // Handle vote cast + tracing::info!( + "Block #{} ({}): Processing {} vote cast events", + block.block_number, + block.timestamp, + value.votes_cast.len() + ); stream::iter(&value.votes_cast) .map(Ok) .try_for_each(|event| async { self.handle_vote_cast(event, &block).await }) .await?; - // Handle proposal processing + // Handle edits published + tracing::info!( + "Block #{} ({}): Processing {} edits published events", + block.block_number, + block.timestamp, + value.edits_published.len() + ); self.handle_edits_published(&value.edits_published, &created_space_ids, &block) .await?; // Handle executed proposal + tracing::info!( + "Block #{} ({}): Processing {} executed proposal events", + block.block_number, + block.timestamp, + value.executed_proposals.len() + ); stream::iter(&value.executed_proposals) .map(Ok) .try_for_each(|event| async { self.handle_proposal_executed(event, &block).await }) diff --git a/sink/src/events/initial_editors_added.rs b/sink/src/events/initial_editors_added.rs index 2efb279..bc64fbd 100644 --- a/sink/src/events/initial_editors_added.rs +++ b/sink/src/events/initial_editors_added.rs @@ -1,5 +1,5 @@ use futures::{stream, StreamExt, TryStreamExt}; -use sdk::{models, pb::geo}; +use sdk::{models::{self, GeoAccount, Space, SpaceEditor}, pb::geo}; use super::{handler::HandlerError, EventHandler}; @@ -11,7 +11,8 @@ impl EventHandler { ) -> Result<(), HandlerError> { let space = self .kg - .get_space_by_voting_plugin_address(&initial_editor_added.plugin_address) + // .get_space_by_voting_plugin_address(&initial_editor_added.plugin_address) + .find_node(Space::find_by_voting_plugin_address(&initial_editor_added.plugin_address)) .await .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; @@ -19,9 +20,20 @@ impl EventHandler { stream::iter(&initial_editor_added.addresses) .map(Result::<_, HandlerError>::Ok) .try_for_each(|editor| async move { - let editor = models::GeoAccount::new(editor.clone()); + let editor = GeoAccount::new(editor.clone()); + + // Add geo account self.kg - .add_editor(&space.id(), &editor, &models::SpaceEditor, block) + .upsert_entity(block, &editor) + .await + .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; // TODO: Convert anyhow::Error to HandlerError properly + + // Add space editor relation + self.kg + .upsert_relation(block, &SpaceEditor::new( + editor.id(), + space.id(), + )) .await .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; // TODO: Convert anyhow::Error to HandlerError properly diff --git a/sink/src/events/member_added.rs b/sink/src/events/member_added.rs index 518a58c..dad887f 100644 --- a/sink/src/events/member_added.rs +++ b/sink/src/events/member_added.rs @@ -1,5 +1,5 @@ use futures::join; -use sdk::{models, pb::geo}; +use sdk::{models::{self, BlockMetadata, GeoAccount, Space, SpaceMember}, pb::geo}; use super::{handler::HandlerError, EventHandler}; @@ -7,20 +7,30 @@ impl EventHandler { pub async fn handle_member_added( &self, member_added: &geo::MemberAdded, - block: &models::BlockMetadata, + block: &BlockMetadata, ) -> Result<(), HandlerError> { match join!( self.kg - .get_space_by_voting_plugin_address(&member_added.main_voting_plugin_address), + .find_node(Space::find_by_voting_plugin_address(&member_added.main_voting_plugin_address)), self.kg - .get_space_by_personal_plugin_address(&member_added.main_voting_plugin_address) + .find_node(Space::find_by_personal_plugin_address(&member_added.main_voting_plugin_address)) ) { // Space found (Ok(Some(space)), Ok(_)) | (Ok(None), Ok(Some(space))) => { - let member = models::GeoAccount::new(member_added.member_address.clone()); + let member = GeoAccount::new(member_added.member_address.clone()); + // Add geo account self.kg - .add_member(&space.id(), &member, &models::SpaceMember, block) + .upsert_entity(block, &member) + .await + .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; + + // Add space member relation + self.kg + .upsert_relation(block, &SpaceMember::new( + member.id(), + space.id(), + )) .await .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; } diff --git a/sink/src/events/member_removed.rs b/sink/src/events/member_removed.rs index 078051b..da2241b 100644 --- a/sink/src/events/member_removed.rs +++ b/sink/src/events/member_removed.rs @@ -1,4 +1,4 @@ -use sdk::{models, pb::geo}; +use sdk::{models::{self, SpaceMember}, pb::geo}; use super::{handler::HandlerError, EventHandler}; @@ -10,16 +10,17 @@ impl EventHandler { ) -> Result<(), HandlerError> { let space = self .kg - .get_space_by_dao_address(&member_removed.dao_address) + .find_node(models::Space::find_by_dao_address_query(&member_removed.dao_address)) .await .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; if let Some(space) = space { self.kg - .remove_member( - &models::GeoAccount::id_from_address(&member_removed.member_address), - &space.id(), - block, + .run( + SpaceMember::remove_query( + &models::GeoAccount::new_id(&member_removed.member_address), + space.id(), + ), ) .await .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; diff --git a/sink/src/events/mod.rs b/sink/src/events/mod.rs index 1b5bd70..8b94d0d 100644 --- a/sink/src/events/mod.rs +++ b/sink/src/events/mod.rs @@ -5,7 +5,7 @@ mod editor_removed; mod initial_editors_added; mod member_added; mod member_removed; -// mod proposal_created; +mod proposal_created; mod proposal_executed; mod edit_published; mod space_created; diff --git a/sink/src/events/proposal_created.rs b/sink/src/events/proposal_created.rs index e757125..fd51a2b 100644 --- a/sink/src/events/proposal_created.rs +++ b/sink/src/events/proposal_created.rs @@ -1,344 +1,243 @@ -use futures::join; -use ipfs::deserialize; use sdk::{ - ids, - models::{self, EditorshipProposal, GeoAccount, MembershipProposal, Proposal}, - pb::{self, geo}, - system_ids::{self, INDEXER_SPACE_ID}, + models, network_ids, pb::geo }; -use web3_utils::checksum_address; - -use crate::kg::mapping::{Node, Relation}; use super::{handler::HandlerError, EventHandler}; impl EventHandler { - pub async fn handle_proposal_created( + pub async fn handle_add_member_proposal_created( &self, - proposal_created: &geo::ProposalCreated, + add_member_proposal: &geo::AddMemberProposalCreated, block: &models::BlockMetadata, ) -> Result<(), HandlerError> { - match join!( - self.kg - .get_space_by_voting_plugin_address(&proposal_created.plugin_address), - self.kg - .get_space_by_member_access_plugin(&proposal_created.plugin_address) - ) { - // Space found - (Ok(Some(space)), Ok(_)) | (Ok(None), Ok(Some(space))) => { - let bytes = self - .ipfs - .get_bytes(&proposal_created.metadata_uri.replace("ipfs://", ""), true) - .await?; - - let metadata = deserialize::(&bytes)?; - - match metadata.r#type() { - pb::ipfs::ActionType::AddEdit => { - // tracing::warn!( - // "Block #{} ({}): Edit proposal not supported", - // block.block_number, - // block.timestamp - // ); - // TODO: Implement edit proposal - // Ok(()) - } - pb::ipfs::ActionType::AddSubspace | pb::ipfs::ActionType::RemoveSubspace => { - let subspace_proposal = deserialize::(&bytes)?; - - self.kg - .upsert_node( - block, - Node::new( - &subspace_proposal.id, - INDEXER_SPACE_ID, - models::SubspaceProposal { - proposal: Proposal { - id: subspace_proposal.id.clone(), - onchain_proposal_id: proposal_created - .proposal_id - .clone(), - proposal_type: metadata.r#type().try_into().map_err( - |e: String| HandlerError::Other(e.into()), - )?, - status: models::ProposalStatus::Proposed, - plugin_address: checksum_address( - &proposal_created.plugin_address, - None, - ), - start_time: proposal_created - .start_time - .parse() - .map_err(|e| { - HandlerError::Other(format!("{e:?}").into()) - })?, - end_time: proposal_created.end_time.parse().map_err( - |e| HandlerError::Other(format!("{e:?}").into()), - )?, - }, - proposal_type: subspace_proposal - .r#type() - .try_into() - .map_err(|e: String| HandlerError::Other(e.into()))?, - }, - ) - .with_type(system_ids::PROPOSAL_TYPE) - .with_type(system_ids::SUBSPACE_PROPOSAL_TYPE), - ) - .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; - - // Try to get the subspace - let subspace = if let Some(subspace) = self - .kg - .get_space_by_dao_address(&subspace_proposal.subspace) - .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))? - { - subspace - } else { - tracing::warn!( - "Block #{} ({}): Failed to get space for subspace DAO address = {}", - block.block_number, - block.timestamp, - checksum_address(&subspace_proposal.subspace, None) - ); - return Ok(()); - }; + // Create proposal + self.kg.upsert_entity( + block, + &models::AddMemberProposal::new(models::Proposal { + onchain_proposal_id: add_member_proposal.proposal_id.clone(), + status: sdk::models::proposal::ProposalStatus::Proposed, + plugin_address: add_member_proposal.plugin_address.clone(), + start_time: add_member_proposal.start_time.clone(), + end_time: add_member_proposal.end_time.clone(), + }), + ).await?; + + // Create Space > PROPOSALS > Proposal relation + self.kg.upsert_relation( + block, + &models::Proposals::new( + &models::Space::new_id(network_ids::GEO, &add_member_proposal.dao_address), + &models::Proposal::new_id(&add_member_proposal.proposal_id) + ), + ).await?; + + // Create Proposal > CREATOR > Account relation + self.kg.upsert_relation( + block, + &models::Creator::new( + &models::Proposal::new_id(&add_member_proposal.proposal_id), + &add_member_proposal.creator, + ), + ).await?; - // Create relation between the proposal and the subspace - self.kg - .upsert_relation( - block, - Relation::new( - &ids::create_geo_id(), - INDEXER_SPACE_ID, - &subspace_proposal.id, - &subspace.id, - system_ids::PROPOSED_SUBSPACE, - models::ProposedSubspace, - ), - ) - .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; - - // Create relation between the proposal and the space - self.kg - .upsert_relation( - block, - Relation::new( - &ids::create_geo_id(), - INDEXER_SPACE_ID, - &space.id, - &subspace_proposal.id, - system_ids::PROPOSALS, - models::Proposals, - ), - ) - .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; + Ok(()) + } - tracing::info!( - "Block #{} ({}): Added subspace proposal {} for space {}", - block.block_number, - block.timestamp, - subspace_proposal.id, - space.id - ); - } - pb::ipfs::ActionType::AddEditor | pb::ipfs::ActionType::RemoveEditor => { - let editor_proposal = deserialize::(&bytes)?; + pub async fn handle_remove_member_proposal_created( + &self, + remove_member_proposal: &geo::RemoveMemberProposalCreated, + block: &models::BlockMetadata, + ) -> Result<(), HandlerError> { + // Create proposal + self.kg.upsert_entity( + block, + &models::RemoveMemberProposal::new(models::Proposal { + onchain_proposal_id: remove_member_proposal.proposal_id.clone(), + status: sdk::models::proposal::ProposalStatus::Proposed, + plugin_address: remove_member_proposal.plugin_address.clone(), + start_time: remove_member_proposal.start_time.clone(), + end_time: remove_member_proposal.end_time.clone(), + }), + ).await?; + + // Create Space > PROPOSALS > Proposal relation + self.kg.upsert_relation( + block, + &models::Proposals::new( + &models::Space::new_id(network_ids::GEO, &remove_member_proposal.dao_address), + &models::Proposal::new_id(&remove_member_proposal.proposal_id) + ), + ).await?; + + // Create Proposal > CREATOR > Account relation + self.kg.upsert_relation( + block, + &models::Creator::new( + &models::Proposal::new_id(&remove_member_proposal.proposal_id), + &remove_member_proposal.creator, + ), + ).await?; - self.kg - .upsert_node( - block, - Node::new( - &editor_proposal.id, - INDEXER_SPACE_ID, - EditorshipProposal { - proposal: Proposal { - id: editor_proposal.id.clone(), - onchain_proposal_id: proposal_created - .proposal_id - .clone(), - proposal_type: metadata.r#type().try_into().map_err( - |e: String| HandlerError::Other(e.into()), - )?, - status: models::ProposalStatus::Proposed, - plugin_address: checksum_address( - &proposal_created.plugin_address, - None, - ), - start_time: proposal_created - .start_time - .parse() - .map_err(|e| { - HandlerError::Other(format!("{e:?}").into()) - })?, - end_time: proposal_created.end_time.parse().map_err( - |e| HandlerError::Other(format!("{e:?}").into()), - )?, - }, - proposal_type: editor_proposal - .r#type() - .try_into() - .map_err(|e: String| HandlerError::Other(e.into()))?, - }, - ) - .with_type(system_ids::PROPOSAL_TYPE) - .with_type(system_ids::EDITORSHIP_PROPOSAL_TYPE), - ) - .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; + Ok(()) + } - // Create relation between the proposal and the editor - self.kg - .upsert_relation( - block, - Relation::new( - INDEXER_SPACE_ID, - &ids::create_geo_id(), - &editor_proposal.id, - &GeoAccount::id_from_address(&editor_proposal.user), - system_ids::PROPOSED_ACCOUNT, - models::ProposedAccount, - ), - ) - .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; + pub async fn handle_add_editor_proposal_created( + &self, + add_editor_proposal: &geo::AddEditorProposalCreated, + block: &models::BlockMetadata, + ) -> Result<(), HandlerError> { + // Create proposal + self.kg.upsert_entity( + block, + &models::AddEditorProposal::new(models::Proposal { + onchain_proposal_id: add_editor_proposal.proposal_id.clone(), + status: sdk::models::proposal::ProposalStatus::Proposed, + plugin_address: add_editor_proposal.plugin_address.clone(), + start_time: add_editor_proposal.start_time.clone(), + end_time: add_editor_proposal.end_time.clone(), + }), + ).await?; + + // Create Space > PROPOSALS > Proposal relation + self.kg.upsert_relation( + block, + &models::Proposals::new( + &models::Space::new_id(network_ids::GEO, &add_editor_proposal.dao_address), + &models::Proposal::new_id(&add_editor_proposal.proposal_id) + ), + ).await?; + + // Create Proposal > CREATOR > Account relation + self.kg.upsert_relation( + block, + &models::Creator::new( + &models::Proposal::new_id(&add_editor_proposal.proposal_id), + &add_editor_proposal.creator, + ), + ).await?; - // Create relation between the space and the proposal - self.kg - .upsert_relation( - block, - Relation::new( - INDEXER_SPACE_ID, - &ids::create_geo_id(), - &space.id, - &editor_proposal.id, - system_ids::PROPOSALS, - models::Proposals, - ), - ) - .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; + Ok(()) + } - tracing::info!( - "Block #{} ({}): Added editorship proposal {} for space {}", - block.block_number, - block.timestamp, - editor_proposal.id, - space.id - ); - } - pb::ipfs::ActionType::AddMember | pb::ipfs::ActionType::RemoveMember => { - let member_proposal = deserialize::(&bytes)?; + pub async fn handle_remove_editor_proposal_created( + &self, + remove_editor_proposal: &geo::RemoveEditorProposalCreated, + block: &models::BlockMetadata, + ) -> Result<(), HandlerError> { + // Create proposal + self.kg.upsert_entity( + block, + &models::RemoveEditorProposal::new(models::Proposal { + onchain_proposal_id: remove_editor_proposal.proposal_id.clone(), + status: sdk::models::proposal::ProposalStatus::Proposed, + plugin_address: remove_editor_proposal.plugin_address.clone(), + start_time: remove_editor_proposal.start_time.clone(), + end_time: remove_editor_proposal.end_time.clone(), + }), + ).await?; + + // Create Space > PROPOSALS > Proposal relation + self.kg.upsert_relation( + block, + &models::Proposals::new( + &models::Space::new_id(network_ids::GEO, &remove_editor_proposal.dao_address), + &models::Proposal::new_id(&remove_editor_proposal.proposal_id) + ), + ).await?; + + // Create Proposal > CREATOR > Account relation + self.kg.upsert_relation( + block, + &models::Creator::new( + &models::Proposal::new_id(&remove_editor_proposal.proposal_id), + &remove_editor_proposal.creator, + ), + ).await?; - self.kg - .upsert_node( - block, - Node::new( - &member_proposal.id, - INDEXER_SPACE_ID, - MembershipProposal { - proposal: Proposal { - id: member_proposal.id.clone(), - onchain_proposal_id: proposal_created - .proposal_id - .clone(), - proposal_type: metadata.r#type().try_into().map_err( - |e: String| HandlerError::Other(e.into()), - )?, - status: models::ProposalStatus::Proposed, - plugin_address: checksum_address( - &proposal_created.plugin_address, - None, - ), - start_time: proposal_created - .start_time - .parse() - .map_err(|e| { - HandlerError::Other(format!("{e:?}").into()) - })?, - end_time: proposal_created.end_time.parse().map_err( - |e| HandlerError::Other(format!("{e:?}").into()), - )?, - }, - proposal_type: member_proposal - .r#type() - .try_into() - .map_err(|e: String| HandlerError::Other(e.into()))?, - }, - ) - .with_type(system_ids::PROPOSAL_TYPE) - .with_type(system_ids::MEMBERSHIP_PROPOSAL_TYPE), - ) - .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; + Ok(()) + } - // Create relation between the proposal and the member - self.kg - .upsert_relation( - block, - Relation::new( - INDEXER_SPACE_ID, - &ids::create_geo_id(), - &member_proposal.id, - &GeoAccount::id_from_address(&member_proposal.user), - system_ids::PROPOSED_ACCOUNT, - models::ProposedAccount, - ), - ) - .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; + pub async fn handle_add_subspace_proposal_created( + &self, + add_subspace_proposal: &geo::AddSubspaceProposalCreated, + block: &models::BlockMetadata, + ) -> Result<(), HandlerError> { + // Create proposal + self.kg.upsert_entity( + block, + &models::AddSubspaceProposal::new(models::Proposal { + onchain_proposal_id: add_subspace_proposal.proposal_id.clone(), + status: sdk::models::proposal::ProposalStatus::Proposed, + plugin_address: add_subspace_proposal.plugin_address.clone(), + start_time: add_subspace_proposal.start_time.clone(), + end_time: add_subspace_proposal.end_time.clone(), + }), + ).await?; + + // Create Space > PROPOSALS > Proposal relation + self.kg.upsert_relation( + block, + &models::Proposals::new( + &models::Space::new_id(network_ids::GEO, &add_subspace_proposal.dao_address), + &models::Proposal::new_id(&add_subspace_proposal.proposal_id) + ), + ).await?; + + // Create Proposal > CREATOR > Account relation + self.kg.upsert_relation( + block, + &models::Creator::new( + &models::Proposal::new_id(&add_subspace_proposal.proposal_id), + &add_subspace_proposal.creator, + ), + ).await?; - // Create relation between the space and the proposal - self.kg - .upsert_relation( - block, - Relation::new( - INDEXER_SPACE_ID, - &ids::create_geo_id(), - &space.id, - &member_proposal.id, - system_ids::PROPOSALS, - models::Proposals, - ), - ) - .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; + Ok(()) + } - tracing::info!( - "Block #{} ({}): Added membership proposal {} for space {}", - block.block_number, - block.timestamp, - member_proposal.id, - space.id - ); - } - pb::ipfs::ActionType::Empty => (), - action_type => { - return Err(HandlerError::Other( - format!("Invalid proposal action type {action_type:?}").into(), - )) - } - } - } - // Space not found - (Ok(None), Ok(None)) => { - tracing::warn!( - "Block #{} ({}): Matching space in Proposal not found for plugin address = {}", - block.block_number, - block.timestamp, - checksum_address(&proposal_created.plugin_address, None) - ); - } - // Errors - (Err(e), _) | (_, Err(e)) => { - return Err(HandlerError::Other(format!("{e:?}").into())); - } - }; + pub async fn handle_remove_subspace_proposal_created( + &self, + remove_subspace_proposal: &geo::RemoveSubspaceProposalCreated, + block: &models::BlockMetadata, + ) -> Result<(), HandlerError> { + // Create proposal + self.kg.upsert_entity( + block, + &models::RemoveSubspaceProposal::new(models::Proposal { + onchain_proposal_id: remove_subspace_proposal.proposal_id.clone(), + status: sdk::models::proposal::ProposalStatus::Proposed, + plugin_address: remove_subspace_proposal.plugin_address.clone(), + start_time: remove_subspace_proposal.start_time.clone(), + end_time: remove_subspace_proposal.end_time.clone(), + }), + ).await?; + + // Create Space > PROPOSALS > Proposal relation + self.kg.upsert_relation( + block, + &models::Proposals::new( + &models::Space::new_id(network_ids::GEO, &remove_subspace_proposal.dao_address), + &models::Proposal::new_id(&remove_subspace_proposal.proposal_id) + ), + ).await?; + + // Create Proposal > CREATOR > Account relation + self.kg.upsert_relation( + block, + &models::Creator::new( + &models::Proposal::new_id(&remove_subspace_proposal.proposal_id), + &remove_subspace_proposal.creator, + ), + ).await?; Ok(()) } + + pub async fn handle_publish_edit_proposal_created( + &self, + publish_edit_proposal: &geo::PublishEditProposalCreated, + block: &models::BlockMetadata, + ) -> Result<(), HandlerError> { + todo!() + } } diff --git a/sink/src/events/proposal_executed.rs b/sink/src/events/proposal_executed.rs index 1690147..4a2ad54 100644 --- a/sink/src/events/proposal_executed.rs +++ b/sink/src/events/proposal_executed.rs @@ -8,40 +8,13 @@ impl EventHandler { proposal_executed: &geo::ProposalExecuted, block: &models::BlockMetadata, ) -> Result<(), HandlerError> { - let proposal = self - .kg - .get_proposal_by_id_and_address( - &proposal_executed.proposal_id, - &proposal_executed.plugin_address, + Ok(self.kg.run( + models::Proposal::set_status_query( + block, + &proposal_executed.proposal_id, + models::proposal::ProposalStatus::Executed, ) - .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; - - if let Some(mut proposal) = proposal { - proposal.attributes_mut().status = models::ProposalStatus::Executed; - self.kg - .upsert_node( - block, - proposal, - ) - .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; - - tracing::info!( - "Block #{} ({}): Proposal {} executed", - block.block_number, - block.timestamp, - proposal_executed.proposal_id - ); - } else { - tracing::warn!( - "Block #{} ({}): Proposal {} not found", - block.block_number, - block.timestamp, - proposal_executed.proposal_id - ); - }; - - Ok(()) + ) + .await?) } } diff --git a/sink/src/events/space_created.rs b/sink/src/events/space_created.rs index bab0423..a0fe525 100644 --- a/sink/src/events/space_created.rs +++ b/sink/src/events/space_created.rs @@ -2,106 +2,91 @@ use std::collections::HashMap; use futures::{stream, StreamExt, TryStreamExt}; use sdk::{ - ids, - mapping::Node, models::{self, GeoAccount, Space, SpaceType}, network_ids, pb::{geo, grc20}, - system_ids, }; use web3_utils::checksum_address; use super::{handler::HandlerError, EventHandler}; impl EventHandler { - /// Handles `GeoSpaceCreated` events. `ProposalProcessed` events are used to determine - /// the space's ID in cases where the space is imported. - /// - /// The method returns the IDs of the spaces that were successfully created. - pub async fn handle_spaces_created( + /// Handles `GeoSpaceCreated` events. + pub async fn handle_space_created( &self, - spaces_created: &[geo::GeoSpaceCreated], - edits_published: &[geo::EditPublished], + space_created: &geo::GeoSpaceCreated, + // edits_published: &[geo::EditPublished], block: &models::BlockMetadata, - ) -> Result, HandlerError> { + ) -> Result { // Match the space creation events with their corresponding initial proposal (if any) - let initial_proposals = spaces_created - .iter() - .filter_map(|event| { - edits_published - .iter() - .find(|proposal| { - checksum_address(&proposal.plugin_address, None) - == checksum_address(&event.space_address, None) - }) - .map(|proposal| (event.space_address.clone(), proposal)) - }) - .collect::>(); + // let initial_proposals = spaces_created + // .iter() + // .filter_map(|event| { + // edits_published + // .iter() + // .find(|proposal| { + // checksum_address(&proposal.plugin_address, None) + // == checksum_address(&event.space_address, None) + // }) + // .map(|proposal| (event.space_address.clone(), proposal)) + // }) + // .collect::>(); + + // tracing::info!() // For spaces with an initial proposal, get the space ID from the import (if available) - let space_ids = stream::iter(initial_proposals) - .filter_map(|(space_address, proposal_processed)| async move { - let ipfs_hash = proposal_processed.content_uri.replace("ipfs://", ""); - self.ipfs - .get::(&ipfs_hash, true) - .await - .ok() - .map(|import| { - ( - space_address, - ids::create_space_id( - &import.previous_network, - &import.previous_contract_address, - ), - ) - }) - }) - .collect::>() - .await; + // let space_ids = stream::iter(initial_proposals) + // .filter_map(|(space_address, proposal_processed)| async move { + // let ipfs_hash = proposal_processed.content_uri.replace("ipfs://", ""); + // self.ipfs + // .get::(&ipfs_hash, true) + // .await + // .ok() + // .map(|import| { + // ( + // space_address, + // Space::new_id( + // &import.previous_network, + // &import.previous_contract_address, + // ), + // ) + // }) + // }) + // .collect::>() + // .await; + let space_id = Space::new_id(network_ids::GEO, &space_created.dao_address); + + tracing::info!( + "Block #{} ({}): Creating space {}", + block.block_number, + block.timestamp, + space_id + ); + + self.kg.upsert_entity( + block, + &Space::builder(&space_id, &space_created.dao_address) + .network(network_ids::GEO.to_string()) + .space_plugin_address(&space_created.space_address) + .build() + ).await?; // Create the spaces - let created_ids: Vec<_> = stream::iter(spaces_created) - .then(|event| async { - let space_id = space_ids - .get(&event.space_address) - .cloned() - .unwrap_or(ids::create_space_id(network_ids::GEO, &event.dao_address)); - - tracing::info!( - "Block #{} ({}): Creating space {}", - block.block_number, - block.timestamp, - space_id - ); - - self.kg - .upsert_node( - block, - Node::new( - &space_id, - system_ids::INDEXER_SPACE_ID, - Space { - network: network_ids::GEO.to_string(), - dao_contract_address: checksum_address(&event.dao_address, None), - space_plugin_address: Some(checksum_address( - &event.space_address, - None, - )), - r#type: SpaceType::Public, - ..Default::default() - }, - ) - .with_type(system_ids::INDEXED_SPACE), - ) - .await?; + // let created_ids: Vec<_> = stream::iter(spaces_created) + // .then(|event| async { + // let space_id = space_ids + // .get(&event.space_address) + // .cloned() + // .unwrap_or(Space::new_id(network_ids::GEO, &event.dao_address)); - anyhow::Ok(space_id) - }) - .try_collect() - .await - .map_err(|err| HandlerError::Other(format!("{err:?}").into()))?; - Ok(created_ids) + // anyhow::Ok(space_id) + // }) + // .try_collect() + // .await + // .map_err(|err| HandlerError::Other(format!("{err:?}").into()))?; + + Ok(space_id) } pub async fn handle_personal_space_created( @@ -111,36 +96,28 @@ impl EventHandler { ) -> Result<(), HandlerError> { let space = self .kg - .get_space_by_dao_address(&personal_space_created.dao_address) + .find_node(Space::find_by_dao_address_query(&personal_space_created.dao_address)) .await .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; // TODO: Convert anyhow::Error to HandlerError properly if let Some(space) = &space { - self.kg - .upsert_node( - block, - Node::new( - space.id(), - system_ids::INDEXER_SPACE_ID, - Space { - r#type: SpaceType::Personal, - personal_space_admin_plugin: Some(checksum_address( - &personal_space_created.personal_admin_address, - None, - )), - ..space.attributes().clone() - }, + self.kg.upsert_entity( + block, + &Space::builder(space.id(), &space.attributes().dao_contract_address) + .r#type(SpaceType::Personal) + .personal_space_admin_plugin( + &personal_space_created.personal_admin_address, ) - .with_type(system_ids::INDEXED_SPACE), - ) - .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; // TODO: Convert anyhow::Error to HandlerError properly + .build() + ) + .await + .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; // TODO: Convert anyhow::Error to HandlerError properly - // // Add initial editors to the personal space + // Add initial editors to the personal space let editor = GeoAccount::new(personal_space_created.initial_editor.clone()); self.kg - .add_editor(space.id(), &editor, &models::SpaceEditor, block) + .upsert_entity(block, &editor) .await .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; // TODO: Convert anyhow::Error to HandlerError properly @@ -149,14 +126,14 @@ impl EventHandler { block.block_number, block.timestamp, space.id(), - editor.id, + editor.id(), ); } else { tracing::warn!( "Block #{} ({}): Could not create personal admin space plugin for unknown space with dao_address = {}", block.block_number, block.timestamp, - personal_space_created.dao_address + checksum_address(&personal_space_created.dao_address, None) ); } @@ -170,7 +147,7 @@ impl EventHandler { ) -> Result<(), HandlerError> { let space = self .kg - .get_space_by_dao_address(&governance_plugin_created.dao_address) + .find_node(Space::find_by_dao_address_query(&governance_plugin_created.dao_address)) .await .map_err(|e| HandlerError::Other(format!("Error fetching space with dao address = {}: {e:?}", checksum_address(&governance_plugin_created.dao_address, None)).into()))?; // TODO: Convert anyhow::Error to HandlerError properly @@ -182,28 +159,12 @@ impl EventHandler { space.id() ); - self.kg - .upsert_node( - block, - Node::new( - space.id(), - system_ids::INDEXER_SPACE_ID, - Space { - voting_plugin_address: Some(checksum_address( - &governance_plugin_created.main_voting_address, - None, - )), - member_access_plugin: Some(checksum_address( - &governance_plugin_created.member_access_address, - None, - )), - ..space.attributes().clone() - }, - ) - .with_type(system_ids::INDEXED_SPACE), - ) - .await - .map_err(|e| HandlerError::Other(format!("Error updating space: {e:?}").into()))?; // TODO: Convert anyhow::Error to HandlerError properly + self.kg.upsert_entity(block, &Space::builder(space.id(), &space.attributes().dao_contract_address) + .voting_plugin_address(&governance_plugin_created.main_voting_address) + .member_access_plugin(&governance_plugin_created.member_access_address) + .build() + ).await + .map_err(|e| HandlerError::Other(format!("Error updating space: {e:?}").into()))?; // TODO: Convert anyhow::Error to HandlerError properly } else { tracing::warn!( "Block #{} ({}): Could not create governance plugin for unknown space with dao_address = {}", diff --git a/sink/src/events/subspace_added.rs b/sink/src/events/subspace_added.rs index 9d3b9cf..a811953 100644 --- a/sink/src/events/subspace_added.rs +++ b/sink/src/events/subspace_added.rs @@ -1,5 +1,6 @@ use futures::join; -use sdk::{models, pb::geo}; +use sdk::{models::{self, space::ParentSpace}, pb::geo}; +use web3_utils::checksum_address; use super::{handler::HandlerError, EventHandler}; @@ -11,22 +12,25 @@ impl EventHandler { ) -> Result<(), HandlerError> { match join!( self.kg - .get_space_by_space_plugin_address(&subspace_added.plugin_address), - self.kg.get_space_by_dao_address(&subspace_added.subspace) + .find_node(models::Space::find_by_space_plugin_address(&subspace_added.plugin_address)), + self.kg + .find_node(models::Space::find_by_dao_address_query(&subspace_added.subspace)) ) { (Ok(Some(parent_space)), Ok(Some(subspace))) => { self.kg - .add_subspace(block, &parent_space.id(), &subspace.id()) + .upsert_relation(block, &ParentSpace::new( + subspace.id(), + parent_space.id(), + )) .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; - // TODO: Convert anyhow::Error to HandlerError properly + .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; // TODO: Convert anyhow::Error to HandlerError properly } (Ok(None), Ok(_)) => { tracing::warn!( "Block #{} ({}): Could not create subspace: parent space with plugin_address = {} not found", block.block_number, block.timestamp, - subspace_added.plugin_address + checksum_address(&subspace_added.plugin_address, None) ); } (Ok(Some(_)), Ok(None)) => { @@ -34,7 +38,7 @@ impl EventHandler { "Block #{} ({}): Could not create subspace: space with dao_address = {} not found", block.block_number, block.timestamp, - subspace_added.plugin_address + checksum_address(&subspace_added.plugin_address, None) ); } (Err(e), _) | (_, Err(e)) => { diff --git a/sink/src/events/subspace_removed.rs b/sink/src/events/subspace_removed.rs index ba3c9f0..f0b708c 100644 --- a/sink/src/events/subspace_removed.rs +++ b/sink/src/events/subspace_removed.rs @@ -10,7 +10,7 @@ impl EventHandler { ) -> Result<(), HandlerError> { let space = self .kg - .get_space_by_space_plugin_address(&subspace_removed.plugin_address) + .find_node(models::Space::find_by_space_plugin_address(&subspace_removed.plugin_address)) .await .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; // TODO: Convert anyhow::Error to HandlerError properly diff --git a/sink/src/events/vote_cast.rs b/sink/src/events/vote_cast.rs index 90033f9..78584e7 100644 --- a/sink/src/events/vote_cast.rs +++ b/sink/src/events/vote_cast.rs @@ -1,9 +1,6 @@ use futures::join; use sdk::{ - ids, models, - pb::geo, - system_ids::{self, INDEXER_SPACE_ID}, - mapping::Relation, + ids, mapping::{Entity, Relation}, models::{self, Space}, pb::geo, system_ids::{self, INDEXER_SPACE_ID} }; use web3_utils::checksum_address; @@ -17,40 +14,27 @@ impl EventHandler { ) -> Result<(), HandlerError> { match join!( self.kg - .get_space_by_voting_plugin_address(&vote.plugin_address), + .find_node(Space::find_by_voting_plugin_address(&vote.plugin_address)), self.kg - .get_space_by_member_access_plugin(&vote.plugin_address) + .find_node(Space::find_by_member_access_plugin(&vote.plugin_address)) ) { // Space found (Ok(Some(space)), Ok(_)) | (Ok(None), Ok(Some(space))) => { - let proposal = self.kg - .find_node::(neo4rs::query(&format!( - "MATCH (p:`{PROPOSAL_TYPE}` {{onchain_proposal_id: $onchain_proposal_id}})<-[:`{PROPOSALS}`]-(:`{INDEXED_SPACE}` {{id: $space_id}}) RETURN p", - PROPOSAL_TYPE = system_ids::PROPOSAL_TYPE, - PROPOSALS = system_ids::PROPOSALS, - INDEXED_SPACE = system_ids::INDEXED_SPACE, - )) - .param("onchain_proposal_id", vote.onchain_proposal_id.clone()) - .param("space_id", space.id())) - .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; + let maybe_proposal = self.kg + .find_node(models::Proposal::find_by_id_and_address(&vote.onchain_proposal_id, &vote.plugin_address)) + .await?; let account = self .kg - .find_node::( - neo4rs::query(&format!( - "MATCH (a:`{ACCOUNT}` {{address: $address}}) RETURN a", - ACCOUNT = system_ids::GEO_ACCOUNT, + .find_node( + Entity::::find_by_id_query( + &models::GeoAccount::new_id(&vote.voter), )) - .param("address", checksum_address(&vote.voter, None)), - ) - .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; + .await?; - match (proposal, account) { + match (maybe_proposal, account) { (Some(proposal), Some(account)) => { let vote_cast = models::VoteCast { - id: ids::create_geo_id(), vote_type: vote .vote_option .try_into() @@ -60,17 +44,15 @@ impl EventHandler { self.kg .upsert_relation( block, - Relation::new( + &Relation::new( + &ids::create_geo_id(), INDEXER_SPACE_ID, - &vote_cast.id.clone(), account.id(), proposal.id(), - system_ids::VOTE_CAST, vote_cast, ), ) - .await - .map_err(|e| HandlerError::Other(format!("{e:?}").into()))?; + .await?; } // Proposal or account not found (Some(_), None) => { @@ -95,7 +77,7 @@ impl EventHandler { "Block #{} ({}): Matching space in Proposal not found for plugin address = {}", block.block_number, block.timestamp, - vote.plugin_address, + checksum_address(&vote.plugin_address, None), ); } // Errors diff --git a/sink/src/kg/client.rs b/sink/src/kg/client.rs index 0a149a2..1cd329e 100644 --- a/sink/src/kg/client.rs +++ b/sink/src/kg/client.rs @@ -1,18 +1,11 @@ use futures::{stream, StreamExt, TryStreamExt}; use serde::Deserialize; -use crate::{ - bootstrap::{self, constants::ROOT_SPACE_ID}, - neo4j_utils::serde_value_to_bolt, - ops::{conversions, op::Op}, -}; -use web3_utils::checksum_address; +use crate::bootstrap::{self, constants}; +// use web3_utils::checksum_address; use sdk::{ - ids::{self, id}, - models::{self, EditProposal, Proposal, Space}, - system_ids, - mapping::{Node, Relation}, + mapping::{self, Entity, Named, Relation}, models::{self, BlockMetadata, EditProposal}, pb, system_ids }; #[derive(Clone)] @@ -28,6 +21,8 @@ pub enum DatabaseError { DeserializationError(#[from] neo4rs::DeError), #[error("Serialization Error: {0}")] SerializationError(#[from] serde_json::Error), + #[error("SetTripleError: {0}")] + SetTripleError(#[from] mapping::entity::SetTripleError), } impl Client { @@ -37,19 +32,30 @@ impl Client { } /// Bootstrap the database with the initial data - pub async fn bootstrap(&self, rollup: bool) -> anyhow::Result<()> { - let bootstrap_ops = if rollup { - conversions::batch_ops(bootstrap::bootstrap()) - } else { - bootstrap::bootstrap().map(Op::from).collect() - }; - - stream::iter(bootstrap_ops) - .map(Ok) // Convert to Result to be able to use try_for_each - .try_for_each(|op| async move { op.apply_op(self, ROOT_SPACE_ID).await }) - .await?; - - Ok(()) + pub async fn bootstrap(&self, rollup: bool) -> Result<(), DatabaseError> { + // let bootstrap_ops = if rollup { + // conversions::batch_ops(bootstrap::bootstrap()) + // } else { + // bootstrap::bootstrap().map(Op::from).collect() + // }; + + // stream::iter(bootstrap_ops) + // .map(Ok) // Convert to Result to be able to use try_for_each + // .try_for_each(|op| async move { op.apply_op(self, ROOT_SPACE_ID).await }) + // .await?; + self.upsert_entity( + &BlockMetadata::default(), + &models::Space::builder( + constants::ROOT_SPACE_ID, + constants::ROOT_SPACE_DAO_ADDRESS, + ) + .space_plugin_address(constants::ROOT_SPACE_PLUGIN_ADDRESS) + .voting_plugin_address(constants::ROOT_SPACE_MAIN_VOTING_ADDRESS) + .member_access_plugin(constants::ROOT_SPACE_MEMBER_ACCESS_ADDRESS) + .build() + ).await?; + + Ok(self.process_ops(&BlockMetadata::default(), constants::ROOT_SPACE_ID, bootstrap::bootstrap()).await?) } /// Reset the database by deleting all nodes and relations and re-bootstrapping it @@ -65,479 +71,50 @@ impl Client { Ok(()) } - pub async fn add_space( - &self, - block: &models::BlockMetadata, - space: Node, - ) -> Result<(), DatabaseError> { - self.upsert_node( - block, - space, - ) - .await - } - - pub async fn get_space_by_dao_address( - &self, - dao_address: &str, - ) -> Result>, DatabaseError> { - let query = neo4rs::query(&format!( - "MATCH (n:`{INDEXED_SPACE}` {{dao_contract_address: $dao_contract_address}}) RETURN n", - INDEXED_SPACE = system_ids::INDEXED_SPACE, - )) - .param("dao_contract_address", checksum_address(dao_address, None)); - - self.find_node::(query) - .await - } - - pub async fn get_space_by_space_plugin_address( - &self, - plugin_address: &str, - ) -> Result>, DatabaseError> { - let query = neo4rs::query(&format!( - "MATCH (n:`{INDEXED_SPACE}` {{space_plugin_address: $space_plugin_address}}) RETURN n", - INDEXED_SPACE = system_ids::INDEXED_SPACE, - )) - .param( - "space_plugin_address", - checksum_address(plugin_address, None), - ); - - self - .find_node::(query) - .await - } - - pub async fn get_space_by_voting_plugin_address( - &self, - voting_plugin_address: &str, - ) -> Result>, DatabaseError> { - let query = neo4rs::query(&format!( - "MATCH (n:`{INDEXED_SPACE}` {{voting_plugin_address: $voting_plugin_address}}) RETURN n", - INDEXED_SPACE = system_ids::INDEXED_SPACE, - )) - .param( - "voting_plugin_address", - checksum_address(voting_plugin_address, None), - ); - - self - .find_node::(query) - .await - } - - pub async fn get_space_by_member_access_plugin( - &self, - member_access_plugin: &str, - ) -> Result>, DatabaseError> { - let query = neo4rs::query(&format!( - "MATCH (n:`{INDEXED_SPACE}` {{member_access_plugin: $member_access_plugin}}) RETURN n", - INDEXED_SPACE = system_ids::INDEXED_SPACE, - )) - .param( - "member_access_plugin", - checksum_address(member_access_plugin, None), - ); - - self - .find_node::(query) - .await - } - - pub async fn get_space_by_personal_plugin_address( - &self, - personal_space_admin_plugin: &str, - ) -> Result>, DatabaseError> { - let query = neo4rs::query(&format!( - "MATCH (n:`{INDEXED_SPACE}` {{personal_space_admin_plugin: $personal_space_admin_plugin}}) RETURN n", - INDEXED_SPACE = system_ids::INDEXED_SPACE, - )) - .param( - "personal_space_admin_plugin", - checksum_address(personal_space_admin_plugin, None), - ); - - self - .find_node::(query) - .await - } - - pub async fn get_proposal_by_id_and_address( - &self, - proposal_id: &str, - plugin_address: &str, - ) -> Result>, DatabaseError> { - let query = neo4rs::query(&format!( - "MATCH (n:`{PROPOSAL_TYPE}` {{onchain_proposal_id: $proposal_id, plugin_address: $plugin_address}}) RETURN n", - PROPOSAL_TYPE = system_ids::PROPOSAL_TYPE, - )) - .param("proposal_id", proposal_id) - .param("plugin_address", plugin_address); - - self - .find_node::(query) - .await - } - - pub async fn add_subspace( - &self, - block: &models::BlockMetadata, - space_id: &str, - subspace_id: &str, - ) -> Result<(), DatabaseError> { - self.upsert_relation( - block, - Relation::new( - &ids::create_geo_id(), - system_ids::INDEXER_SPACE_ID, - subspace_id, - space_id, - system_ids::PARENT_SPACE, - models::ParentSpace, - ), - ) - .await - } - - /// Add an editor to a space - pub async fn add_editor( - &self, - space_id: &str, - account: &models::GeoAccount, - editor_relation: &models::SpaceEditor, - block: &models::BlockMetadata, - ) -> anyhow::Result<()> { - self.upsert_node( - block, - Node::new(&account.id, system_ids::INDEXER_SPACE_ID, account.clone()) - .with_type(system_ids::GEO_ACCOUNT), - ) - .await?; - - self.upsert_relation( - block, - Relation::new( - &ids::create_geo_id(), - system_ids::INDEXER_SPACE_ID, - &account.id, - space_id, - system_ids::EDITOR_RELATION, - editor_relation, - ), - ) - .await?; - - // Add the editor as a member of the space - self.upsert_relation( - block, - Relation::new( - &ids::create_geo_id(), - system_ids::INDEXER_SPACE_ID, - &account.id, - space_id, - system_ids::MEMBER_RELATION, - models::SpaceMember, - ), - ) - .await?; - - tracing::info!( - "Block #{} ({}): Added editor {} to space {}", - block.block_number, - block.timestamp, - account.id, - space_id - ); - - Ok(()) - } - - pub async fn remove_editor( - &self, - editor_id: &str, - space_id: &str, - block: &models::BlockMetadata, - ) -> anyhow::Result<()> { - const REMOVE_EDITOR_QUERY: &str = const_format::formatcp!( - r#" - MATCH (e:`{GEO_ACCOUNT}` {{id: $editor_id}}) -[r:`{EDITOR_RELATION}`]-> (s:`{INDEXED_SPACE}` {{id: $space_id}}) - DELETE r - SET e.`{UPDATED_AT}` = datetime($updated_at) - SET e.`{UPDATED_AT_BLOCK}` = $updated_at_block - "#, - GEO_ACCOUNT = system_ids::GEO_ACCOUNT, - EDITOR_RELATION = system_ids::EDITOR_RELATION, - INDEXED_SPACE = system_ids::INDEXED_SPACE, - UPDATED_AT = system_ids::UPDATED_AT_TIMESTAMP, - UPDATED_AT_BLOCK = system_ids::UPDATED_AT_BLOCK, - ); - - let query = neo4rs::query(REMOVE_EDITOR_QUERY) - .param("editor_id", editor_id) - .param("space_id", space_id) - .param("updated_at", block.timestamp.to_rfc3339()) - .param("updated_at_block", block.block_number.to_string()); - - self.neo4j.run(query).await?; - - tracing::info!( - "Block #{} ({}): Removed editor {} from space {}", - block.block_number, - block.timestamp, - editor_id, - space_id - ); - - Ok(()) - } - - pub async fn add_member( - &self, - space_id: &str, - account: &models::GeoAccount, - member_relation: &models::SpaceMember, - block: &models::BlockMetadata, - ) -> anyhow::Result<()> { - self.upsert_node( - block, - Node::new(&account.id, system_ids::INDEXER_SPACE_ID, account.clone()) - .with_type(system_ids::GEO_ACCOUNT), - ) - .await?; - - self.upsert_relation( - block, - Relation::new( - &ids::create_geo_id(), - system_ids::INDEXER_SPACE_ID, - &account.id, - space_id, - system_ids::MEMBER_RELATION, - member_relation, - ), - ) - .await?; - - tracing::info!( - "Block #{} ({}): Added member {} to space {}", - block.block_number, - block.timestamp, - account.id, - space_id - ); - - Ok(()) - } - - /// Remove a member from a space - pub async fn remove_member( + pub async fn upsert_relation( &self, - member_id: &str, - space_id: &str, block: &models::BlockMetadata, + relation: &Relation, ) -> Result<(), DatabaseError> { - const REMOVE_MEMBER_QUERY: &str = const_format::formatcp!( - r#" - MATCH (m:`{GEO_ACCOUNT}` {{id: $member_id}}) -[r:`{MEMBER_RELATION}`]-> (s:`{INDEXED_SPACE}` {{id: $space_id}}) - DELETE r - SET m.`{UPDATED_AT}` = datetime($updated_at) - SET m.`{UPDATED_AT_BLOCK}` = $updated_at_block - "#, - GEO_ACCOUNT = system_ids::GEO_ACCOUNT, - MEMBER_RELATION = system_ids::MEMBER_RELATION, - INDEXED_SPACE = system_ids::INDEXED_SPACE, - UPDATED_AT = system_ids::UPDATED_AT_TIMESTAMP, - UPDATED_AT_BLOCK = system_ids::UPDATED_AT_BLOCK, - ); - - let query = neo4rs::query(REMOVE_MEMBER_QUERY) - .param("member_id", member_id) - .param("space_id", space_id) - .param("updated_at", block.timestamp.to_rfc3339()) - .param("updated_at_block", block.block_number.to_string()); - - self.neo4j.run(query).await?; - - tracing::info!( - "Block #{} ({}): Removed member {} from space {}", - block.block_number, - block.timestamp, - member_id, - space_id - ); + self.run(relation.upsert_query(block)?).await?; Ok(()) } - // pub async fn add_vote_cast( - // &self, - // block: &models::BlockMetadata, - // space_id: &str, - // account_id: &str, - // vote: &models::Vote, - // vote_cast: &models::VoteCast, - // ) -> anyhow::Result<()> { - // // self.upsert_relation( - // // INDEXER_SPACE_ID, - // // block, - // // Relation::new( - // // &ids::create_geo_id(), - // // account_id, - // // &vote.id, - // // system_ids::VOTE_CAST_RELATION, - // // vote_cast, - // // ), - // // ).await?; - // // todo!() - - // Ok(()) - // } - - // pub async fn add_proposal( - // &self, - // block: &models::BlockMetadata, - // space_id: &str, - // proposal: &T, - // space_proposal_relation: &models::SpaceProposalRelation, - // ) -> anyhow::Result<()> { - // self.upsert_node( - // system_ids::INDEXER_SPACE_ID, - // block, - // Node::new(proposal.as_proposal().id.clone(), proposal) - // .with_type(system_ids::PROPOSAL_TYPE) - // .with_type(proposal.type_id()), - // ).await?; - - // self.upsert_relation( - // system_ids::INDEXER_SPACE_ID, - // block, - // Relation::new( - // &ids::create_geo_id(), - // &proposal.as_proposal().id, - // space_id, - // system_ids::PROPOSAL_SPACE_RELATION, - // space_proposal_relation, - // ), - // ).await?; - - // Ok(()) - // } - - pub async fn upsert_relation( + pub async fn upsert_entity( &self, block: &models::BlockMetadata, - relation: Relation, + entity: &Entity, ) -> Result<(), DatabaseError> { - let query_string = format!( - r#" - MERGE (from {{id: $from_id}}) -[r:`{relation_type}` {{id: $id}}]-> (to {{id: $to_id}}) - ON CREATE SET r += {{ - `{CREATED_AT}`: datetime($created_at), - `{CREATED_AT_BLOCK}`: $created_at_block - }} - SET r += {{ - `{UPDATED_AT}`: datetime($updated_at), - `{UPDATED_AT_BLOCK}`: $updated_at_block - }} - SET r += $data - "#, - relation_type = relation.relation_type, - CREATED_AT = system_ids::CREATED_AT_TIMESTAMP, - CREATED_AT_BLOCK = system_ids::CREATED_AT_BLOCK, - UPDATED_AT = system_ids::UPDATED_AT_TIMESTAMP, - UPDATED_AT_BLOCK = system_ids::UPDATED_AT_BLOCK, - ); - - let bolt_data = match serde_value_to_bolt(serde_json::to_value(relation.attributes())?) { - neo4rs::BoltType::Map(map) => neo4rs::BoltType::Map(map), - _ => neo4rs::BoltType::Map(Default::default()), - }; - - let query = neo4rs::query(&query_string) - .param("id", relation.id()) - .param("from_id", relation.from.clone()) - .param("to_id", relation.to.clone()) - .param("space_id", relation.space_id()) - .param("created_at", block.timestamp.to_rfc3339()) - .param("created_at_block", block.block_number.to_string()) - .param("updated_at", block.timestamp.to_rfc3339()) - .param("updated_at_block", block.block_number.to_string()) - .param("data", bolt_data); - - self.neo4j.run(query).await?; + self.run(entity.upsert_query(block)?).await?; Ok(()) } - pub async fn upsert_node( - &self, - block: &models::BlockMetadata, - node: Node, - ) -> Result<(), DatabaseError> { - const UPSERT_NODE_QUERY: &str = const_format::formatcp!( - r#" - MERGE (n {{id: $id}}) - ON CREATE SET n += {{ - `{CREATED_AT}`: datetime($created_at), - `{CREATED_AT_BLOCK}`: $created_at_block - }} - SET n:$($labels) - SET n += {{ - `{UPDATED_AT}`: datetime($updated_at), - `{UPDATED_AT_BLOCK}`: $updated_at_block - }} - SET n += $data - "#, - // SPACE = system_ids::SPACE, - CREATED_AT = system_ids::CREATED_AT_TIMESTAMP, - CREATED_AT_BLOCK = system_ids::CREATED_AT_BLOCK, - UPDATED_AT = system_ids::UPDATED_AT_TIMESTAMP, - UPDATED_AT_BLOCK = system_ids::UPDATED_AT_BLOCK, - ); - - let id = node.id().to_string(); - - let bolt_data = match serde_value_to_bolt(serde_json::to_value(node.attributes)?) { - neo4rs::BoltType::Map(map) => neo4rs::BoltType::Map(map), - _ => neo4rs::BoltType::Map(Default::default()), - }; - - let query = neo4rs::query(UPSERT_NODE_QUERY) - .param("id", id) - // .param("space_id", node.space_id()) - .param("created_at", block.timestamp.to_rfc3339()) - .param("created_at_block", block.block_number.to_string()) - .param("updated_at", block.timestamp.to_rfc3339()) - .param("updated_at_block", block.block_number.to_string()) - .param("labels", node.types) - .param("data", bolt_data); - - self.neo4j.run(query).await?; - + pub async fn run(&self, query: mapping::Query<()>) -> Result<(), DatabaseError> { + self.neo4j.run(query.query).await?; Ok(()) } pub async fn find_node_by_id Deserialize<'a> + Send>( &self, id: &str, - ) -> Result>, DatabaseError> { - let query = neo4rs::query("MATCH (n { id: $id }) RETURN n").param("id", id); - self.find_node::(query).await + ) -> Result>, DatabaseError> { + let query = Entity::::find_by_id_query(id); + self.find_node(query).await } pub async fn find_node Deserialize<'a> + Send>( &self, - query: neo4rs::Query, - ) -> Result>, DatabaseError> { + query: mapping::Query, + ) -> Result>, DatabaseError> { self.neo4j - .execute(query) + .execute(query.query) .await? .next() .await? .map(|row| { - // tracing::info!("Row: {:?}", row); - Ok::<_, DatabaseError>(Node::::try_from(row.to::()?)?) + Ok::<_, DatabaseError>(Entity::::try_from(row.to::()?)?) }) .transpose() } @@ -545,13 +122,13 @@ impl Client { pub async fn find_nodes Deserialize<'a> + Send>( &self, query: neo4rs::Query, - ) -> anyhow::Result>, DatabaseError> { + ) -> anyhow::Result>, DatabaseError> { self.neo4j .execute(query) .await? .into_stream_as::() .map_err(DatabaseError::from) - .and_then(|neo4j_node| async move { Ok(Node::::try_from(neo4j_node)?) }) + .and_then(|neo4j_node| async move { Ok(Entity::::try_from(neo4j_node)?) }) .try_collect::>() .await } @@ -559,139 +136,62 @@ impl Client { pub async fn find_node_from_relation Deserialize<'a> + Send>( &self, relation_id: &str, - ) -> Result>, DatabaseError> { + ) -> Result>, DatabaseError> { let query = - neo4rs::query("MATCH (n) -[r {id: $id}]-> () RETURN n").param("id", relation_id); + mapping::Query::new("MATCH (n) -[r {id: $id}]-> () RETURN n").param("id", relation_id); self.find_node::(query).await } pub async fn find_node_to_relation Deserialize<'a> + Send>( &self, relation_id: &str, - ) -> Result>, DatabaseError> { + ) -> Result>, DatabaseError> { let query = - neo4rs::query("MATCH () -[r {id: $id}]-> (n) RETURN n").param("id", relation_id); + mapping::Query::new("MATCH () -[r {id: $id}]-> (n) RETURN n").param("id", relation_id); self.find_node::(query).await } - pub async fn find_relation_by_id Deserialize<'a> + Send>( - &self, - id: &str, - ) -> Result>, DatabaseError> { - let query = neo4rs::query("MATCH () -[r]-> () WHERE r.id = $id RETURN r").param("id", id); - self.find_relation::(query).await - } - - pub async fn find_relation_from_node Deserialize<'a> + Send>( - &self, - node_id: &str, - ) -> Result>, DatabaseError> { - let query = neo4rs::query("MATCH (n {id: $id}) -[r]-> () RETURN r").param("id", node_id); - self.find_relations::(query).await - } - - pub async fn find_relation Deserialize<'a> + Send>( - &self, - query: neo4rs::Query, - ) -> Result>, DatabaseError> { - self.neo4j - .execute(query) - .await? - .next() - .await? - .map(|row| { - Ok::<_, DatabaseError>(Relation::::try_from(row.to::()?)?) - }) - .transpose() - } - - pub async fn attribute_nodes Deserialize<'a> + Send>( - &self, - id: &str, - ) -> Result>, DatabaseError> { - let query = neo4rs::query(&format!( - r#" - MATCH ({{id: $id}}) -[:`{attr_attr}`]-> (a:`{attr_type}`) - WHERE a.id IS NOT NULL AND a.`{name_attr}` IS NOT NULL - RETURN a - "#, - attr_attr = system_ids::ATTRIBUTES, - attr_type = system_ids::ATTRIBUTE, - name_attr = system_ids::NAME, - )) - .param("id", id); - self.find_nodes::(query).await - } - - pub async fn value_type_node Deserialize<'a> + Send>( - &self, - id: &str, - ) -> Result>, DatabaseError> { - let query = neo4rs::query(&format!( - r#" - MATCH (a {{id: $id}}) -[:`{value_type_attr}`]-> (t:`{type_type}`) - WHERE t.id IS NOT NULL AND t.`{name_attr}` IS NOT NULL - RETURN t - "#, - value_type_attr = system_ids::VALUE_TYPE, - type_type = system_ids::SCHEMA_TYPE, - name_attr = system_ids::NAME, - )) - .param("id", id); - - self.find_node::(query).await - } - - pub async fn find_relations Deserialize<'a> + Send>( - &self, - query: neo4rs::Query, - ) -> anyhow::Result>, DatabaseError> { - self.neo4j - .execute(query) - .await? - .into_stream_as::() - .map_err(DatabaseError::from) - .and_then(|neo4j_rel| async move { Ok(Relation::::try_from(neo4j_rel)?) }) - .try_collect::>() - .await - } - - pub async fn get_name(&self, entity_id: &str) -> anyhow::Result> { - #[derive(Debug, Deserialize)] - struct Named { - #[serde(default)] - name: Option, - } - - let query = neo4rs::query("MATCH (n { id: $id }) RETURN n").param("id", entity_id); - - match self - .find_node::(query) - .await? - .map(|node| node.attributes.attributes) - { - Some(Named { - name: Some(name), .. - }) => Ok(Some(name)), - None | Some(Named { name: None, .. }) => Ok(None), - } - } - pub async fn find_types Deserialize<'a> + Send>( &self, - ) -> Result>, DatabaseError> { + ) -> Result>, DatabaseError> { let query = neo4rs::query(&format!("MATCH (t:`{}`) RETURN t", system_ids::SCHEMA_TYPE)); self.find_nodes::(query).await } - pub async fn process_edit(&self, edit: EditProposal) -> anyhow::Result<()> { - let space_id = edit.space.as_str(); - let rolled_up_ops = conversions::batch_ops(edit.ops); - - stream::iter(rolled_up_ops) - .map(Ok) // Convert to Result to be able to use try_for_each - .try_for_each(|op| async move { op.apply_op(self, space_id).await }) - .await?; + pub async fn process_ops(&self, block: &models::BlockMetadata, space_id: &str, ops: impl IntoIterator) -> Result<(), DatabaseError> { + for op in ops { + match (op.r#type(), op.triple) { + (pb::grc20::OpType::SetTriple, Some(pb::grc20::Triple { entity, attribute, value: Some(value) })) => { + tracing::info!( + "SetTriple: {}, {}, {:?}", + entity, + attribute, + value, + ); + + self.run(Entity::<()>::set_triple( + block, + space_id, + &entity, + &attribute, + &value + )?).await? + } + (pb::grc20::OpType::DeleteTriple, Some(triple)) => { + tracing::info!( + "DeleteTriple: {}, {}, {:?}", + triple.entity, + triple.attribute, + triple.value, + ); + + self.run(Entity::<()>::delete_triple(block, space_id, triple)).await? + } + (typ, maybe_triple) => { + tracing::warn!("Unhandled case: {:?} {:?}", typ, maybe_triple); + } + } + } Ok(()) } diff --git a/sink/src/lib.rs b/sink/src/lib.rs index 5fc5189..a3ae2da 100644 --- a/sink/src/lib.rs +++ b/sink/src/lib.rs @@ -1,5 +1,4 @@ pub mod bootstrap; pub mod events; pub mod kg; -pub mod neo4j_utils; -pub mod ops; +// pub mod ops; diff --git a/sink/src/ops/batch_set_triple.rs b/sink/src/ops/batch_set_triple.rs deleted file mode 100644 index 059d26b..0000000 --- a/sink/src/ops/batch_set_triple.rs +++ /dev/null @@ -1,7 +0,0 @@ -use crate::ops::Value; - -pub struct BatchSetTriples { - pub entity_id: String, - pub type_id: String, - pub values: Vec, -} diff --git a/sink/src/ops/conversions.rs b/sink/src/ops/conversions.rs deleted file mode 100644 index b165a06..0000000 --- a/sink/src/ops/conversions.rs +++ /dev/null @@ -1,127 +0,0 @@ -use std::{collections::HashMap, iter}; - -use super::{ - create_relation::CreateRelationBuilder, - delete_triple::DeleteTriple, - op::{self, Op}, - set_triple::SetTriple, - Value, -}; -use sdk::{graph_uri::GraphUri, pb::grc20, system_ids}; - -impl From for Op { - fn from(op: grc20::Op) -> Self { - match (op.r#type(), op.triple) { - (grc20::OpType::SetTriple, Some(triple)) => Op::new(SetTriple { - entity_id: triple.entity, - attribute_id: triple.attribute, - value: triple.value.map(Value::from).unwrap_or(Value::Null), - }), - (grc20::OpType::DeleteTriple, Some(triple)) => Op::new(DeleteTriple { - entity_id: triple.entity, - attribute_id: triple.attribute, - }), - (grc20::OpType::None, _) | (_, None) => op::Op::null(), - } - } -} - -impl From<&grc20::Op> for Op { - fn from(op: &grc20::Op) -> Self { - match (op.r#type(), &op.triple) { - (grc20::OpType::SetTriple, Some(triple)) => Op::new(SetTriple { - entity_id: triple.entity.clone(), - attribute_id: triple.attribute.clone(), - value: triple.value.clone().map(Value::from).unwrap_or(Value::Null), - }), - (grc20::OpType::DeleteTriple, Some(triple)) => Op::new(DeleteTriple { - entity_id: triple.entity.clone(), - attribute_id: triple.attribute.clone(), - }), - (grc20::OpType::None, _) | (_, None) => op::Op::null(), - } - } -} - -type EntityOps = HashMap, Option)>; - -pub fn group_ops(ops: Vec) -> EntityOps { - let mut entity_ops: EntityOps = HashMap::new(); - - for op in ops { - match (op.r#type(), &op.triple) { - ( - grc20::OpType::SetTriple, - Some(grc20::Triple { - entity, - attribute, - value: Some(grc20::Value { r#type, value }), - }), - ) if attribute == system_ids::TYPES && *r#type == grc20::ValueType::Url as i32 => { - // If triple sets the type, set the type of the entity op batch - let entry = entity_ops.entry(entity.clone()).or_insert(( - Vec::new(), - Some( - GraphUri::from_uri(value) - .expect("URI should be validated by match pattern guard") - .id, - ), - )); - - entry.1 = Some( - GraphUri::from_uri(value) - .expect("URI should be validated by match pattern guard") - .id, - ); - entry.0.push(op); - } - (_, Some(triple)) => { - // If tiple sets or deletes an attribute, add it to the entity op batch - entity_ops - .entry(triple.entity.clone()) - .or_insert((Vec::new(), None)) - .0 - .push(op); - } - _ => { - // If triple is invalid, add it to the entity op batch - entity_ops - .entry("".to_string()) - .or_insert((Vec::new(), None)) - .0 - .push(op) - } - } - } - - entity_ops -} - -pub fn batch_ops(ops: impl IntoIterator) -> Vec { - let entity_ops = group_ops(ops.into_iter().collect()); - - entity_ops - .into_iter() - .flat_map(|(entity_id, (ops, r#type))| match r#type.as_deref() { - // If the entity has type RELATION_TYPE, build a CreateRelation batch - Some(system_ids::RELATION_TYPE) => { - // tracing::info!("Found relation: {}", entity_id); - - let (batch, remaining) = CreateRelationBuilder::new(entity_id).from_ops(&ops); - match batch.build() { - // If the batch is successfully built, return the batch and the remaining ops - Ok(batch) => iter::once(Op::new(batch)) - .chain(remaining.into_iter().map(Op::from)) - .collect::>(), - // If the batch fails to build, log the error and return the ops as is - Err(err) => { - tracing::error!("Failed to build relation batch: {:?}! Ignoring", err); - // ops.into_iter().map(Op::from).collect::>() - vec![] - } - } - } - _ => ops.into_iter().map(Op::from).collect::>(), - }) - .collect() -} diff --git a/sink/src/ops/create_relation.rs b/sink/src/ops/create_relation.rs deleted file mode 100644 index ed77f79..0000000 --- a/sink/src/ops/create_relation.rs +++ /dev/null @@ -1,255 +0,0 @@ -use sdk::{graph_uri::GraphUri, pb::grc20, system_ids}; - -use super::KgOp; - -pub struct CreateRelation { - /// ID of the relation entity - pub entity_id: String, - /// ID of the "from" entity - pub from_entity_id: String, - /// ID of the "to" entity - pub to_entity_id: String, - /// ID of the relation type entity - pub relation_type_id: String, - /// Index of the relation - pub index: String, -} - -impl KgOp for CreateRelation { - async fn apply_op(&self, kg: &crate::kg::Client, space_id: &str) -> anyhow::Result<()> { - let relation_name = kg - .get_name(&self.relation_type_id) - .await? - .unwrap_or(self.relation_type_id.to_string()); - - tracing::info!( - "CreateRelation {}: {} {} -> {}", - self.entity_id, - if relation_name == self.relation_type_id { - self.relation_type_id.to_string() - } else { - format!("{} ({})", relation_name, self.relation_type_id) - }, - self.from_entity_id, - self.to_entity_id, - ); - - match self.relation_type_id.as_str() { - system_ids::TYPES => { - let type_label = match kg.get_name(&self.to_entity_id).await? { - Some(name) if name.replace(" ", "").is_empty() => self.to_entity_id.clone(), - Some(name) => name, - None => self.to_entity_id.clone(), - }; - - tracing::info!( - "SetType {}: {}", - self.from_entity_id, - if type_label == self.to_entity_id { - self.to_entity_id.to_string() - } else { - format!("{} ({})", type_label, self.to_entity_id) - }, - ); - - kg.neo4j - .run( - neo4rs::query(&format!( - r#" - MERGE (n {{ id: $id, space_id: $space_id }}) - ON CREATE - SET n :`{type_id}` - ON MATCH - SET n :`{type_id}` - "#, - type_id = self.to_entity_id - )) - .param("id", self.from_entity_id.clone()) - .param("space_id", space_id), - ) - .await?; - } - _ => { - kg.neo4j - .run( - neo4rs::query(&format!( - r#" - MERGE (from {{id: $from_id, space_id: $space_id}}) - MERGE (to {{id: $to_id, space_id: $space_id}}) - MERGE (from)-[:`{relation_type_id}` {{id: $relation_id, `{index_id}`: $index, space_id: $space_id}}]->(to) - "#, - relation_type_id = self.relation_type_id, - index_id = system_ids::RELATION_INDEX - )) - .param("from_id", self.from_entity_id.clone()) - .param("to_id", self.to_entity_id.clone()) - .param("relation_id", self.entity_id.clone()) - .param("index", self.index.clone()) - .param("relation_type_id", self.relation_type_id.clone()) - .param("space_id", space_id) - ) - .await?; - } - } - - Ok(()) - } -} - -pub struct CreateRelationBuilder { - entity_id: String, - from_entity_id: Option, - to_entity_id: Option, - relation_type_id: Option, - index: Option, -} - -impl CreateRelationBuilder { - pub fn new(entity_id: String) -> Self { - CreateRelationBuilder { - entity_id, - from_entity_id: None, - to_entity_id: None, - relation_type_id: None, - index: None, - } - } - - /// Extracts the from, to, and relation type entities from the ops and returns the remaining ops - pub fn from_ops(mut self, ops: &[grc20::Op]) -> (Self, Vec<&grc20::Op>) { - let remaining = ops - .iter() - .filter(|op| match (op.r#type(), &op.triple) { - // Ignore the TYPES relation of the entity - ( - grc20::OpType::SetTriple, - Some(grc20::Triple { - attribute, - value: Some(grc20::Value { r#type, .. }), - .. - }), - ) if attribute == system_ids::TYPES && *r#type == grc20::ValueType::Url as i32 => { - false - } - - // Set the FROM_ENTITY attribute - ( - grc20::OpType::SetTriple, - Some(grc20::Triple { - attribute, - value: Some(grc20::Value { r#type, value }), - .. - }), - ) if attribute == system_ids::RELATION_FROM_ATTRIBUTE - && *r#type == grc20::ValueType::Url as i32 - && GraphUri::is_valid(value) => - { - self.from_entity_id = - Some(GraphUri::from_uri(value).expect("Uri should be valid").id); - false - } - - // Set the TO_ENTITY attribute - ( - grc20::OpType::SetTriple, - Some(grc20::Triple { - attribute, - value: Some(grc20::Value { r#type, value }), - .. - }), - ) if attribute == system_ids::RELATION_TO_ATTRIBUTE - && *r#type == grc20::ValueType::Url as i32 - && GraphUri::is_valid(value) => - { - self.to_entity_id = - Some(GraphUri::from_uri(value).expect("Uri should be valid").id); - false - } - - // Set the RELATION_TYPE attribute - ( - grc20::OpType::SetTriple, - Some(grc20::Triple { - attribute, - value: Some(grc20::Value { r#type, value }), - .. - }), - ) if attribute == system_ids::RELATION_TYPE_ATTRIBUTE - && *r#type == grc20::ValueType::Url as i32 - && GraphUri::is_valid(value) => - { - self.relation_type_id = - Some(GraphUri::from_uri(value).expect("Uri should be valid").id); - false - } - - // Set the INDEX attribute - ( - grc20::OpType::SetTriple, - Some(grc20::Triple { - attribute, - value: Some(grc20::Value { r#type, value }), - .. - }), - ) if attribute == system_ids::RELATION_INDEX - && *r#type == grc20::ValueType::Text as i32 => - { - self.index = Some(value.clone()); - false - } - - _ => true, - }) - .collect(); - - (self, remaining) - } - - pub fn build(self) -> anyhow::Result { - Ok(CreateRelation { - from_entity_id: match self.from_entity_id { - Some(id) if id.is_empty() => { - return Err(anyhow::anyhow!( - "{}: Invalid from entity id: `{id}`", - self.entity_id - )) - } - Some(id) => id, - None => return Err(anyhow::anyhow!("{}: Missing from entity", self.entity_id)), - }, - to_entity_id: match self.to_entity_id { - Some(id) if id.is_empty() => { - return Err(anyhow::anyhow!( - "{}: Invalid to entity id: `{id}`", - self.entity_id - )) - } - Some(id) => id, - None => return Err(anyhow::anyhow!("{}: Missing to entity", self.entity_id)), - }, - relation_type_id: match self.relation_type_id { - Some(id) if id.is_empty() => { - return Err(anyhow::anyhow!( - "{}: Invalid relation type id: `{id}`", - self.entity_id - )) - } - Some(id) => id, - None => return Err(anyhow::anyhow!("{}: Missing relation type", self.entity_id)), - }, - // relation_type_id: match self.relation_type_id { - // Some(id) if id.is_empty() => { - // tracing::warn!("{}: Invalid relation type id: `{id}`! Using default _UNKNOWN", self.entity_id); - // "_UNKNOWN".to_string() - // }, - // Some(id) => id, - // None => { - // tracing::warn!("{}: Missing relation type! Using default _UNKNOWN", self.entity_id); - // "_UNKNOWN".to_string() - // }, - // }, - index: self.index.unwrap_or_else(|| "a0".to_string()), - entity_id: self.entity_id, - }) - } -} diff --git a/sink/src/ops/delete_triple.rs b/sink/src/ops/delete_triple.rs deleted file mode 100644 index 1e6de26..0000000 --- a/sink/src/ops/delete_triple.rs +++ /dev/null @@ -1,52 +0,0 @@ -use sdk::mapping::DefaultAttributes; -use crate::ops::KgOp; - -pub struct DeleteTriple { - pub entity_id: String, - pub attribute_id: String, -} - -impl KgOp for DeleteTriple { - async fn apply_op(&self, kg: &crate::kg::client::Client, space_id: &str) -> anyhow::Result<()> { - let entity_name = kg - .find_node_by_id::(&self.entity_id) - .await? - .and_then(|entity| entity.name()) - .unwrap_or(self.entity_id.to_string()); - - let attribute_name = kg - .get_name(&self.attribute_id) - .await? - .unwrap_or(self.attribute_id.to_string()); - - tracing::info!( - "DeleteTriple: {}, {}", - if entity_name == self.entity_id { - self.entity_id.to_string() - } else { - format!("{} ({})", entity_name, self.entity_id) - }, - if attribute_name == self.attribute_id { - self.attribute_id.to_string() - } else { - format!("{} ({})", attribute_name, self.attribute_id) - }, - ); - - kg.neo4j - .run( - neo4rs::query(&format!( - r#" - MATCH (n {{ id: $id, space_id: $space_id }}) - REMOVE n.`{attribute_label}` - "#, - attribute_label = self.attribute_id, - )) - .param("id", self.entity_id.clone()) - .param("space_id", space_id), - ) - .await?; - - Ok(()) - } -} diff --git a/sink/src/ops/mod.rs b/sink/src/ops/mod.rs deleted file mode 100644 index a87e5e7..0000000 --- a/sink/src/ops/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub mod batch_set_triple; -pub mod conversions; -pub mod create_relation; -pub mod delete_triple; -pub mod op; -pub mod set_triple; - -pub use op::{KgOp, Value}; diff --git a/sink/src/ops/op.rs b/sink/src/ops/op.rs deleted file mode 100644 index 7c6b83c..0000000 --- a/sink/src/ops/op.rs +++ /dev/null @@ -1,121 +0,0 @@ -use std::fmt::Display; - -use crate::kg::client::Client; -use futures::future::BoxFuture; -use sdk::pb::grc20; - -#[derive(Clone, Debug)] -pub enum Value { - Null, - Text(String), - Number(String), - Entity(String), - Uri(String), - Checkbox(bool), - Time(String), // TODO: Change to proper type - GeoLocation(String), // TODO: Change to proper type -} - -impl Display for Value { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Value::Null => write!(f, "null"), - Value::Text(value) => write!(f, "{}", value), - Value::Number(value) => write!(f, "{}", value), - Value::Entity(value) => write!(f, "{}", value), - Value::Uri(value) => write!(f, "{}", value), - Value::Checkbox(value) => write!(f, "{}", value), - Value::Time(value) => write!(f, "{}", value), - Value::GeoLocation(value) => write!(f, "{}", value), - } - } -} - -impl From for Value { - fn from(value: grc20::Value) -> Self { - match value.r#type() { - grc20::ValueType::Unknown => Value::Null, - grc20::ValueType::Text => Value::Text(value.value), - grc20::ValueType::Number => Value::Number(value.value), - grc20::ValueType::Checkbox => Value::Checkbox(value.value.parse().unwrap_or(false)), - grc20::ValueType::Url => Value::Uri(value.value), - grc20::ValueType::Time => Value::Time(value.value), - grc20::ValueType::Point => Value::GeoLocation(value.value), - } - } -} - -impl From<&grc20::Value> for Value { - fn from(value: &grc20::Value) -> Self { - Value::from(value.clone()) - } -} - -impl From for neo4rs::BoltType { - fn from(value: Value) -> Self { - match value { - Value::Null => neo4rs::BoltType::Null(neo4rs::BoltNull), - Value::Text(value) => neo4rs::BoltType::String(value.into()), - Value::Number(value) => neo4rs::BoltType::String(value.into()), - Value::Entity(value) => neo4rs::BoltType::String(value.into()), - Value::Uri(value) => neo4rs::BoltType::String(value.into()), - Value::Checkbox(value) => neo4rs::BoltType::Boolean(neo4rs::BoltBoolean::new(value)), - Value::Time(value) => neo4rs::BoltType::String(value.into()), - Value::GeoLocation(value) => neo4rs::BoltType::String(value.into()), - } - } -} - -pub struct Op(Box); - -impl Op { - pub fn new(op: T) -> Self { - Op(Box::new(op)) - } - - pub fn null() -> Self { - Op(Box::new(NullOp)) - } - - pub fn apply_op<'a>( - &'a self, - kg: &'a Client, - space_id: &'a str, - ) -> BoxFuture<'a, anyhow::Result<()>> { - self.0.apply_op(kg, space_id) - } -} - -pub trait KgOp: Send { - fn apply_op( - &self, - kg: &Client, - space_id: &str, - ) -> impl std::future::Future> + Send; -} - -pub trait KgOpDyn: Send { - fn apply_op<'a>( - &'a self, - kg: &'a Client, - space_id: &'a str, - ) -> BoxFuture<'a, anyhow::Result<()>>; -} - -impl KgOpDyn for T { - fn apply_op<'a>( - &'a self, - kg: &'a Client, - space_id: &'a str, - ) -> BoxFuture<'a, anyhow::Result<()>> { - Box::pin(self.apply_op(kg, space_id)) - } -} - -pub struct NullOp; - -impl KgOp for NullOp { - async fn apply_op(&self, _kg: &Client, _space_id: &str) -> anyhow::Result<()> { - Ok(()) - } -} diff --git a/sink/src/ops/set_triple.rs b/sink/src/ops/set_triple.rs deleted file mode 100644 index 2264579..0000000 --- a/sink/src/ops/set_triple.rs +++ /dev/null @@ -1,166 +0,0 @@ -use sdk::{mapping::DefaultAttributes, system_ids}; - -use crate::ops::{KgOp, Value}; - -pub struct SetTriple { - pub entity_id: String, - pub attribute_id: String, - pub value: Value, -} - -impl KgOp for SetTriple { - async fn apply_op(&self, kg: &crate::kg::client::Client, space_id: &str) -> anyhow::Result<()> { - let entity_name = kg - .get_name(&self.entity_id) - .await? - .unwrap_or(self.entity_id.to_string()); - - let attribute_name = kg - .get_name(&self.attribute_id) - .await? - .unwrap_or(self.attribute_id.to_string()); - - tracing::info!( - "SetTriple: {}, {}, {}", - if entity_name == self.entity_id { - self.entity_id.to_string() - } else { - format!("{} ({})", entity_name, self.entity_id) - }, - if attribute_name == self.attribute_id { - self.attribute_id.to_string() - } else { - format!("{} ({})", attribute_name, self.attribute_id) - }, - self.value, - ); - - match (self.attribute_id.as_str(), &self.value) { - (system_ids::TYPES, Value::Entity(value)) => { - if kg - .find_relation_by_id::(&self.entity_id) - .await? - .is_some() - { - // let entity = Entity::from_entity(kg.clone(), relation); - // kg.neo4j.run( - // neo4rs::query(&format!( - // r#" - // MATCH (n) -[{{id: $relation_id}}]-> (m) - // CREATE (n) -[:{relation_label} {{id: $relation_id, relation_type_id: $relation_type_id}}]-> (m) - // "#, - // relation_label = RelationLabel::new(value), - // )) - // .param("relation_id", self.entity_id.clone()) - // .param("relation_type_id", system_ids::TYPES), - // ).await?; - tracing::warn!( - "Unhandled case: Setting type on existing relation {entity_name}" - ); - } else { - kg.neo4j - .run( - neo4rs::query(&format!( - r#" - MERGE (t {{ id: $value, space_id: $space_id }}) - MERGE (n {{ id: $id, space_id: $space_id }}) - ON CREATE - SET n :`{value}` - ON MATCH - SET n :`{value}` - "#, - // MERGE (n) -[:TYPE {{id: $attribute_id}}]-> (t) - // "#, - )) - .param("id", self.entity_id.clone()) - .param("value", self.value.clone()) - .param("space_id", space_id), - ) - .await?; - } - } - // (system_ids::NAME, Value::Text(value)) => { - // if let Some(_) = kg.find_relation_by_id::(&self.entity_id).await? { - // tracing::warn!("Unhandled case: Setting name on relation {entity_name}"); - // } else { - // kg.set_name(&self.entity_id, &value).await?; - // } - // } - (attribute_id, Value::Entity(value)) => { - if ![ - system_ids::RELATION_FROM_ATTRIBUTE, - system_ids::RELATION_TO_ATTRIBUTE, - system_ids::RELATION_INDEX, - system_ids::RELATION_TYPE_ATTRIBUTE, - ] - .contains(&attribute_id) - { - panic!("Unhandled case: Setting entity value on attribute {attribute_name}({attribute_id}) of entity {entity_name}({})", self.entity_id); - } - - if kg - .find_relation_by_id::(&self.entity_id) - .await? - .is_some() - { - tracing::warn!("Unhandled case: Relation {attribute_name} defined on relation {entity_name}"); - } else { - kg.neo4j - .run( - neo4rs::query(&format!( - r#" - MERGE (n {{ id: $id }}) - MERGE (m {{ id: $value }}) - MERGE (n) -[:`{attribute_id}` {{space_id: $space_id}}]-> (m) - "#, - )) - .param("id", self.entity_id.clone()) - .param("value", value.clone()) - .param("space_id", space_id), - ) - .await?; - } - } - (attribute_id, value) => { - if kg - .find_relation_by_id::(&self.entity_id) - .await? - .is_some() - { - kg.neo4j - .run( - neo4rs::query(&format!( - r#" - MATCH () -[r {{id: $relation_id, space_id: $space_id}}]-> () - SET r.`{attribute_id}` = $value - "#, - )) - .param("relation_id", self.entity_id.clone()) - .param("value", value.clone()) - .param("space_id", space_id), - ) - .await?; - } else { - kg.neo4j - .run( - neo4rs::query(&format!( - r#" - MERGE (n {{ id: $id, space_id: $space_id }}) - ON CREATE - SET n.`{attribute_id}` = $value - ON MATCH - SET n.`{attribute_id}` = $value - "#, - )) - .param("id", self.entity_id.clone()) - .param("value", value.clone()) - .param("space_id", space_id), - ) - .await?; - } - } - }; - - Ok(()) - } -}