From 366500fd3c2cfac8af9db4a9c5b63fa33bb04ac3 Mon Sep 17 00:00:00 2001 From: Yun Date: Wed, 1 Nov 2023 13:08:32 -0700 Subject: [PATCH] Torii metadata query format (#1133) --- crates/torii/graphql/src/constants.rs | 5 + crates/torii/graphql/src/mapping.rs | 18 ++- crates/torii/graphql/src/object/metadata.rs | 23 --- .../graphql/src/object/metadata/content.rs | 29 ++++ .../torii/graphql/src/object/metadata/mod.rs | 149 ++++++++++++++++++ .../graphql/src/object/metadata/social.rs | 29 ++++ crates/torii/graphql/src/object/mod.rs | 4 - crates/torii/graphql/src/schema.rs | 4 + 8 files changed, 233 insertions(+), 28 deletions(-) delete mode 100644 crates/torii/graphql/src/object/metadata.rs create mode 100644 crates/torii/graphql/src/object/metadata/content.rs create mode 100644 crates/torii/graphql/src/object/metadata/mod.rs create mode 100644 crates/torii/graphql/src/object/metadata/social.rs diff --git a/crates/torii/graphql/src/constants.rs b/crates/torii/graphql/src/constants.rs index ecb3160e70..8bbfb7bbf7 100644 --- a/crates/torii/graphql/src/constants.rs +++ b/crates/torii/graphql/src/constants.rs @@ -10,6 +10,7 @@ pub const METADATA_TABLE: &str = "metadata"; pub const ID_COLUMN: &str = "id"; pub const EVENT_ID_COLUMN: &str = "event_id"; pub const ENTITY_ID_COLUMN: &str = "entity_id"; +pub const JSON_COLUMN: &str = "json"; pub const INTERNAL_ENTITY_ID_KEY: &str = "$entity_id$"; @@ -17,6 +18,8 @@ pub const INTERNAL_ENTITY_ID_KEY: &str = "$entity_id$"; pub const ENTITY_TYPE_NAME: &str = "World__Entity"; pub const MODEL_TYPE_NAME: &str = "World__Model"; pub const EVENT_TYPE_NAME: &str = "World__Event"; +pub const SOCIAL_TYPE_NAME: &str = "World__Social"; +pub const CONTENT_TYPE_NAME: &str = "World__Content"; pub const METADATA_TYPE_NAME: &str = "World__Metadata"; pub const TRANSACTION_TYPE_NAME: &str = "World__Transaction"; pub const QUERY_TYPE_NAME: &str = "World__Query"; @@ -26,5 +29,7 @@ pub const SUBSCRIPTION_TYPE_NAME: &str = "World__Subscription"; pub const ENTITY_NAMES: (&str, &str) = ("entity", "entities"); pub const MODEL_NAMES: (&str, &str) = ("model", "models"); pub const EVENT_NAMES: (&str, &str) = ("event", "events"); +pub const SOCIAL_NAMES: (&str, &str) = ("social", "socials"); +pub const CONTENT_NAMES: (&str, &str) = ("content", "contents"); pub const METADATA_NAMES: (&str, &str) = ("metadata", "metadatas"); pub const TRANSACTION_NAMES: (&str, &str) = ("transaction", "transactions"); diff --git a/crates/torii/graphql/src/mapping.rs b/crates/torii/graphql/src/mapping.rs index c56cdcff19..b378146787 100644 --- a/crates/torii/graphql/src/mapping.rs +++ b/crates/torii/graphql/src/mapping.rs @@ -4,6 +4,7 @@ use async_graphql::Name; use dojo_types::primitive::Primitive; use lazy_static::lazy_static; +use crate::constants::{CONTENT_TYPE_NAME, SOCIAL_TYPE_NAME}; use crate::types::{GraphqlType, TypeData, TypeMapping}; lazy_static! { @@ -90,10 +91,25 @@ lazy_static! { TypeData::Simple(TypeRef::named(GraphqlType::Cursor.to_string())), ), ]); + pub static ref SOCIAL_TYPE_MAPPING: TypeMapping = IndexMap::from([ + (Name::new("name"), TypeData::Simple(TypeRef::named(TypeRef::STRING))), + (Name::new("url"), TypeData::Simple(TypeRef::named(TypeRef::STRING))), + ]); + pub static ref CONTENT_TYPE_MAPPING: TypeMapping = IndexMap::from([ + (Name::new("name"), TypeData::Simple(TypeRef::named(TypeRef::STRING))), + (Name::new("description"), TypeData::Simple(TypeRef::named(TypeRef::STRING))), + (Name::new("website"), TypeData::Simple(TypeRef::named(TypeRef::STRING))), + (Name::new("icon_uri"), TypeData::Simple(TypeRef::named(TypeRef::STRING))), + (Name::new("cover_uri"), TypeData::Simple(TypeRef::named(TypeRef::STRING))), + (Name::new("socials"), TypeData::Simple(TypeRef::named_list(SOCIAL_TYPE_NAME))) + ]); pub static ref METADATA_TYPE_MAPPING: TypeMapping = IndexMap::from([ (Name::new("id"), TypeData::Simple(TypeRef::named(TypeRef::ID))), (Name::new("uri"), TypeData::Simple(TypeRef::named(TypeRef::STRING))), - (Name::new("json"), TypeData::Simple(TypeRef::named(TypeRef::STRING))), + ( + Name::new("content"), + TypeData::Nested((TypeRef::named(CONTENT_TYPE_NAME), IndexMap::new())) + ), (Name::new("icon_img"), TypeData::Simple(TypeRef::named(TypeRef::STRING))), (Name::new("cover_img"), TypeData::Simple(TypeRef::named(TypeRef::STRING))), ( diff --git a/crates/torii/graphql/src/object/metadata.rs b/crates/torii/graphql/src/object/metadata.rs deleted file mode 100644 index 0e47ed3957..0000000000 --- a/crates/torii/graphql/src/object/metadata.rs +++ /dev/null @@ -1,23 +0,0 @@ -use super::{ObjectTrait, TypeMapping}; -use crate::constants::{METADATA_NAMES, METADATA_TABLE, METADATA_TYPE_NAME}; -use crate::mapping::METADATA_TYPE_MAPPING; - -pub struct MetadataObject; - -impl ObjectTrait for MetadataObject { - fn name(&self) -> (&str, &str) { - METADATA_NAMES - } - - fn type_name(&self) -> &str { - METADATA_TYPE_NAME - } - - fn type_mapping(&self) -> &TypeMapping { - &METADATA_TYPE_MAPPING - } - - fn table_name(&self) -> Option<&str> { - Some(METADATA_TABLE) - } -} diff --git a/crates/torii/graphql/src/object/metadata/content.rs b/crates/torii/graphql/src/object/metadata/content.rs new file mode 100644 index 0000000000..8c5de128cb --- /dev/null +++ b/crates/torii/graphql/src/object/metadata/content.rs @@ -0,0 +1,29 @@ +use async_graphql::dynamic::Field; + +use super::{ObjectTrait, TypeMapping}; +use crate::constants::{CONTENT_NAMES, CONTENT_TYPE_NAME}; +use crate::mapping::CONTENT_TYPE_MAPPING; + +pub struct ContentObject; + +impl ObjectTrait for ContentObject { + fn name(&self) -> (&str, &str) { + CONTENT_NAMES + } + + fn type_name(&self) -> &str { + CONTENT_TYPE_NAME + } + + fn type_mapping(&self) -> &TypeMapping { + &CONTENT_TYPE_MAPPING + } + + fn resolve_one(&self) -> Option { + None + } + + fn resolve_many(&self) -> Option { + None + } +} diff --git a/crates/torii/graphql/src/object/metadata/mod.rs b/crates/torii/graphql/src/object/metadata/mod.rs new file mode 100644 index 0000000000..73c560ce4d --- /dev/null +++ b/crates/torii/graphql/src/object/metadata/mod.rs @@ -0,0 +1,149 @@ +use async_graphql::dynamic::{Field, FieldFuture, TypeRef}; +use async_graphql::{Name, Value}; +use sqlx::sqlite::SqliteRow; +use sqlx::{Pool, Row, Sqlite}; + +use super::connection::{connection_arguments, cursor, parse_connection_arguments}; +use super::ObjectTrait; +use crate::constants::{ + ID_COLUMN, JSON_COLUMN, METADATA_NAMES, METADATA_TABLE, METADATA_TYPE_NAME, +}; +use crate::mapping::METADATA_TYPE_MAPPING; +use crate::query::data::{count_rows, fetch_multiple_rows}; +use crate::query::value_mapping_from_row; +use crate::types::{TypeMapping, ValueMapping}; + +pub mod content; +pub mod social; + +pub struct MetadataObject; + +impl ObjectTrait for MetadataObject { + fn name(&self) -> (&str, &str) { + METADATA_NAMES + } + + fn type_name(&self) -> &str { + METADATA_TYPE_NAME + } + + fn type_mapping(&self) -> &TypeMapping { + &METADATA_TYPE_MAPPING + } + + fn table_name(&self) -> Option<&str> { + Some(METADATA_TABLE) + } + + fn resolve_one(&self) -> Option { + None + } + + fn resolve_many(&self) -> Option { + let type_mapping = self.type_mapping().clone(); + let table_name = self.table_name().unwrap().to_string(); + + let mut field = Field::new( + self.name().1, + TypeRef::named(format!("{}Connection", self.type_name())), + move |ctx| { + let type_mapping = type_mapping.clone(); + let table_name = table_name.to_string(); + + FieldFuture::new(async move { + let mut conn = ctx.data::>()?.acquire().await?; + let connection = parse_connection_arguments(&ctx)?; + let total_count = count_rows(&mut conn, &table_name, &None, &None).await?; + let data = fetch_multiple_rows( + &mut conn, + &table_name, + ID_COLUMN, + &None, + &None, + &None, + &connection, + ) + .await?; + + // convert json field to value_mapping expected by content object + let results = metadata_connection_output(&data, &type_mapping, total_count)?; + + Ok(Some(Value::Object(results))) + }) + }, + ); + + field = connection_arguments(field); + + Some(field) + } +} + +// NOTE: need to generalize `connection_output` or maybe preprocess to support both predefined +// objects AND dynamic model objects +fn metadata_connection_output( + data: &[SqliteRow], + types: &TypeMapping, + total_count: i64, +) -> sqlx::Result { + let edges = data + .iter() + .map(|row| { + let order = row.try_get::(ID_COLUMN)?; + let cursor = cursor::encode(&order, &order); + let mut value_mapping = value_mapping_from_row(row, types, false)?; + + let json_str = row.try_get::(JSON_COLUMN)?; + let serde_value: serde_json::Value = + serde_json::from_str(&json_str).map_err(|e| sqlx::Error::Decode(e.into()))?; + + let content = ValueMapping::from([ + extract_str_mapping("name", &serde_value), + extract_str_mapping("description", &serde_value), + extract_str_mapping("website", &serde_value), + extract_str_mapping("icon_uri", &serde_value), + extract_str_mapping("cover_uri", &serde_value), + extract_socials_mapping("socials", &serde_value), + ]); + + value_mapping.insert(Name::new("content"), Value::Object(content)); + + let mut edge = ValueMapping::new(); + edge.insert(Name::new("node"), Value::Object(value_mapping)); + edge.insert(Name::new("cursor"), Value::String(cursor)); + + Ok(Value::Object(edge)) + }) + .collect::>>(); + + Ok(ValueMapping::from([ + (Name::new("total_count"), Value::from(total_count)), + (Name::new("edges"), Value::List(edges?)), + ])) +} + +fn extract_str_mapping(name: &str, serde_value: &serde_json::Value) -> (Name, Value) { + if let Some(serde_json::Value::String(str)) = serde_value.get(name) { + return (Name::new(name), Value::String(str.to_owned())); + } + + (Name::new(name), Value::Null) +} + +fn extract_socials_mapping(name: &str, serde_value: &serde_json::Value) -> (Name, Value) { + if let Some(serde_json::Value::Object(obj)) = serde_value.get(name) { + let list = obj + .iter() + .map(|(social_name, social_url)| { + Value::Object(ValueMapping::from([ + (Name::new("name"), Value::String(social_name.to_string())), + (Name::new("url"), Value::String(social_url.as_str().unwrap().to_string())), + ])) + }) + .collect::>(); + + return (Name::new(name), Value::List(list)); + } + + (Name::new(name), Value::List(vec![])) +} diff --git a/crates/torii/graphql/src/object/metadata/social.rs b/crates/torii/graphql/src/object/metadata/social.rs new file mode 100644 index 0000000000..d936564a25 --- /dev/null +++ b/crates/torii/graphql/src/object/metadata/social.rs @@ -0,0 +1,29 @@ +use async_graphql::dynamic::Field; + +use super::{ObjectTrait, TypeMapping}; +use crate::constants::{SOCIAL_NAMES, SOCIAL_TYPE_NAME}; +use crate::mapping::SOCIAL_TYPE_MAPPING; + +pub struct SocialObject; + +impl ObjectTrait for SocialObject { + fn name(&self) -> (&str, &str) { + SOCIAL_NAMES + } + + fn type_name(&self) -> &str { + SOCIAL_TYPE_NAME + } + + fn type_mapping(&self) -> &TypeMapping { + &SOCIAL_TYPE_MAPPING + } + + fn resolve_one(&self) -> Option { + None + } + + fn resolve_many(&self) -> Option { + None + } +} diff --git a/crates/torii/graphql/src/object/mod.rs b/crates/torii/graphql/src/object/mod.rs index 60167a2ecc..34e2700caa 100644 --- a/crates/torii/graphql/src/object/mod.rs +++ b/crates/torii/graphql/src/object/mod.rs @@ -145,10 +145,6 @@ pub trait ObjectTrait: Send + Sync { let mut object = Object::new(self.type_name()); for (field_name, type_data) in self.type_mapping().clone() { - if type_data.is_nested() { - continue; - } - let field = Field::new(field_name.to_string(), type_data.type_ref(), move |ctx| { let field_name = field_name.clone(); diff --git a/crates/torii/graphql/src/schema.rs b/crates/torii/graphql/src/schema.rs index e3a912abbb..8f28ad089a 100644 --- a/crates/torii/graphql/src/schema.rs +++ b/crates/torii/graphql/src/schema.rs @@ -12,6 +12,8 @@ use super::object::model_data::ModelDataObject; use super::object::ObjectTrait; use super::types::ScalarType; use crate::constants::{QUERY_TYPE_NAME, SUBSCRIPTION_TYPE_NAME}; +use crate::object::metadata::content::ContentObject; +use crate::object::metadata::social::SocialObject; use crate::object::metadata::MetadataObject; use crate::object::model::ModelObject; use crate::object::transaction::TransactionObject; @@ -108,6 +110,8 @@ async fn build_objects(pool: &SqlitePool) -> Result<(Vec>, let mut objects: Vec> = vec![ Box::new(EntityObject), Box::new(EventObject), + Box::new(SocialObject), + Box::new(ContentObject), Box::new(MetadataObject), Box::new(ModelObject), Box::new(PageInfoObject),