diff --git a/Cargo.lock b/Cargo.lock index 2311bed3e4..3fad329f90 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7932,6 +7932,8 @@ dependencies = [ "sqlx", "starknet", "starknet-crypto 0.6.0", + "strum 0.25.0", + "strum_macros 0.25.2", "tokio", "tokio-stream", "tokio-util", diff --git a/crates/torii/graphql/Cargo.toml b/crates/torii/graphql/Cargo.toml index 5842bf0917..ec5a87adfd 100644 --- a/crates/torii/graphql/Cargo.toml +++ b/crates/torii/graphql/Cargo.toml @@ -19,11 +19,14 @@ indexmap = "1.9.3" scarb-ui.workspace = true serde.workspace = true serde_json.workspace = true +strum.workspace = true +strum_macros.workspace = true sqlx = { version = "0.6.2", features = [ "chrono", "macros", "offline", "runtime-actix-rustls", "sqlite", "uuid" ] } tokio-stream = "0.1.11" tokio-util = "0.7.7" tokio.workspace = true torii-core = { path = "../core" } +dojo-types = { path = "../../dojo-types" } tracing.workspace = true url.workspace = true warp.workspace = true @@ -31,7 +34,6 @@ warp.workspace = true [dev-dependencies] camino.workspace = true dojo-test-utils = { path = "../../dojo-test-utils" } -dojo-types = { path = "../../dojo-types" } dojo-world = { path = "../../dojo-world" } starknet-crypto.workspace = true starknet.workspace = true diff --git a/crates/torii/graphql/src/object/connection/edge.rs b/crates/torii/graphql/src/object/connection/edge.rs index 2eb8b727f6..0fb8681e45 100644 --- a/crates/torii/graphql/src/object/connection/edge.rs +++ b/crates/torii/graphql/src/object/connection/edge.rs @@ -1,9 +1,8 @@ use async_graphql::dynamic::TypeRef; use async_graphql::Name; -use indexmap::IndexMap; -use crate::object::{ObjectTrait, TypeMapping}; -use crate::types::ScalarType; +use crate::object::ObjectTrait; +use crate::types::{GraphqlType, TypeData, TypeMapping}; pub struct EdgeObject { pub name: String, @@ -13,9 +12,12 @@ pub struct EdgeObject { impl EdgeObject { pub fn new(name: String, type_name: String) -> Self { - let type_mapping = IndexMap::from([ - (Name::new("node"), TypeRef::named(type_name.clone())), - (Name::new("cursor"), TypeRef::named_nn(ScalarType::Cursor.to_string())), + let type_mapping = TypeMapping::from([ + (Name::new("node"), TypeData::Simple(TypeRef::named(type_name.clone()))), + ( + Name::new("cursor"), + TypeData::Simple(TypeRef::named(GraphqlType::Cursor.to_string())), + ), ]); Self { diff --git a/crates/torii/graphql/src/object/connection/mod.rs b/crates/torii/graphql/src/object/connection/mod.rs index fa21205977..37f01ee601 100644 --- a/crates/torii/graphql/src/object/connection/mod.rs +++ b/crates/torii/graphql/src/object/connection/mod.rs @@ -2,11 +2,10 @@ use async_graphql::dynamic::{Field, InputValue, ResolverContext, TypeRef}; use async_graphql::{Error, Name, Value}; use base64::engine::general_purpose; use base64::Engine as _; -use indexmap::IndexMap; use serde_json::Number; -use super::{ObjectTrait, TypeMapping, ValueMapping}; -use crate::types::ScalarType; +use super::ObjectTrait; +use crate::types::{GraphqlType, TypeData, TypeMapping, ValueMapping}; use crate::utils::extract_value::extract; pub mod edge; @@ -28,9 +27,12 @@ pub struct ConnectionObject { impl ConnectionObject { pub fn new(name: String, type_name: String) -> Self { - let type_mapping = IndexMap::from([ - (Name::new("edges"), TypeRef::named_list(format!("{}Edge", type_name))), - (Name::new("totalCount"), TypeRef::named_nn(TypeRef::INT)), + let type_mapping = TypeMapping::from([ + ( + Name::new("edges"), + TypeData::Simple(TypeRef::named_list(format!("{}Edge", type_name))), + ), + (Name::new("totalCount"), TypeData::Simple(TypeRef::named_nn(TypeRef::INT))), ]); Self { @@ -93,8 +95,8 @@ pub fn connection_arguments(field: Field) -> Field { field .argument(InputValue::new("first", TypeRef::named(TypeRef::INT))) .argument(InputValue::new("last", TypeRef::named(TypeRef::INT))) - .argument(InputValue::new("before", TypeRef::named(ScalarType::Cursor.to_string()))) - .argument(InputValue::new("after", TypeRef::named(ScalarType::Cursor.to_string()))) + .argument(InputValue::new("before", TypeRef::named(GraphqlType::Cursor.to_string()))) + .argument(InputValue::new("after", TypeRef::named(GraphqlType::Cursor.to_string()))) } pub fn connection_output(data: Vec, total_count: i64) -> ValueMapping { diff --git a/crates/torii/graphql/src/object/connection/page_info.rs b/crates/torii/graphql/src/object/connection/page_info.rs index ca88d744de..e125a6fb66 100644 --- a/crates/torii/graphql/src/object/connection/page_info.rs +++ b/crates/torii/graphql/src/object/connection/page_info.rs @@ -1,9 +1,8 @@ use async_graphql::dynamic::TypeRef; use async_graphql::Name; -use indexmap::IndexMap; use crate::object::{ObjectTrait, TypeMapping}; -use crate::types::ScalarType; +use crate::types::{GraphqlType, TypeData}; pub struct PageInfoObject { pub type_mapping: TypeMapping, @@ -12,11 +11,17 @@ pub struct PageInfoObject { impl Default for PageInfoObject { fn default() -> Self { Self { - type_mapping: IndexMap::from([ - (Name::new("hasPreviousPage"), TypeRef::named(TypeRef::BOOLEAN)), - (Name::new("hasNextPage"), TypeRef::named(TypeRef::BOOLEAN)), - (Name::new("startCursor"), TypeRef::named(ScalarType::Cursor.to_string())), - (Name::new("endCursor"), TypeRef::named(ScalarType::Cursor.to_string())), + type_mapping: TypeMapping::from([ + (Name::new("hasPreviousPage"), TypeData::Simple(TypeRef::named(TypeRef::BOOLEAN))), + (Name::new("hasNextPage"), TypeData::Simple(TypeRef::named(TypeRef::BOOLEAN))), + ( + Name::new("startCursor"), + TypeData::Simple(TypeRef::named(GraphqlType::Cursor.to_string())), + ), + ( + Name::new("endCursor"), + TypeData::Simple(TypeRef::named(GraphqlType::Cursor.to_string())), + ), ]), } } diff --git a/crates/torii/graphql/src/object/entity.rs b/crates/torii/graphql/src/object/entity.rs index c722195c62..fba6a8503e 100644 --- a/crates/torii/graphql/src/object/entity.rs +++ b/crates/torii/graphql/src/object/entity.rs @@ -13,11 +13,11 @@ use super::connection::{ connection_arguments, connection_output, decode_cursor, parse_connection_arguments, ConnectionArguments, }; -use super::model_state::{model_state_by_id_query, type_mapping_query}; +use super::model_data::model_data_by_id_query; use super::{ObjectTrait, TypeMapping, ValueMapping}; use crate::constants::DEFAULT_LIMIT; -use crate::query::{query_by_id, ID}; -use crate::types::ScalarType; +use crate::query::{query_by_id, type_mapping_query}; +use crate::types::{GraphqlType, TypeData}; use crate::utils::csv_to_vec; use crate::utils::extract_value::extract; @@ -29,11 +29,17 @@ impl Default for EntityObject { fn default() -> Self { Self { type_mapping: IndexMap::from([ - (Name::new("id"), TypeRef::named(TypeRef::ID)), - (Name::new("keys"), TypeRef::named_list(TypeRef::STRING)), - (Name::new("modelNames"), TypeRef::named(TypeRef::STRING)), - (Name::new("createdAt"), TypeRef::named(ScalarType::DateTime.to_string())), - (Name::new("updatedAt"), TypeRef::named(ScalarType::DateTime.to_string())), + (Name::new("id"), TypeData::Simple(TypeRef::named(TypeRef::ID))), + (Name::new("keys"), TypeData::Simple(TypeRef::named_list(TypeRef::STRING))), + (Name::new("modelNames"), TypeData::Simple(TypeRef::named(TypeRef::STRING))), + ( + Name::new("createdAt"), + TypeData::Simple(TypeRef::named(GraphqlType::DateTime.to_string())), + ), + ( + Name::new("updatedAt"), + TypeData::Simple(TypeRef::named(GraphqlType::DateTime.to_string())), + ), ]), } } @@ -71,34 +77,8 @@ impl ObjectTrait for EntityObject { &self.type_mapping } - fn nested_fields(&self) -> Option> { - Some(vec![Field::new("models", TypeRef::named_list("ModelUnion"), move |ctx| { - FieldFuture::new(async move { - match ctx.parent_value.try_to_value()? { - Value::Object(indexmap) => { - let mut conn = ctx.data::>()?.acquire().await?; - let models = csv_to_vec(&extract::(indexmap, "modelNames")?); - let id = extract::(indexmap, "id")?; - - let mut results: Vec> = Vec::new(); - for model_name in models { - let table_name = model_name.to_lowercase(); - let type_mapping = type_mapping_query(&mut conn, &table_name).await?; - let state = - model_state_by_id_query(&mut conn, &table_name, &id, &type_mapping) - .await?; - results.push(FieldValue::with_type( - FieldValue::owned_any(state), - model_name, - )); - } - - Ok(Some(FieldValue::list(results))) - } - _ => Err("incorrect value, requires Value::Object".into()), - } - }) - })]) + fn related_fields(&self) -> Option> { + Some(vec![model_union_field()]) } fn resolve_one(&self) -> Option { @@ -107,7 +87,7 @@ impl ObjectTrait for EntityObject { FieldFuture::new(async move { let mut conn = ctx.data::>()?.acquire().await?; let id = ctx.args.try_get("id")?.string()?.to_string(); - let entity = query_by_id(&mut conn, "entities", ID::Str(id)).await?; + let entity = query_by_id(&mut conn, "entities", &id).await?; let result = EntityObject::value_mapping(entity); Ok(Some(Value::Object(result))) }) @@ -134,6 +114,7 @@ impl ObjectTrait for EntityObject { }); let (entities, total_count) = entities_by_sk(&mut conn, keys, args).await?; + Ok(Some(Value::Object(connection_output(entities, total_count)))) }) }, @@ -172,6 +153,32 @@ impl ObjectTrait for EntityObject { } } +fn model_union_field() -> Field { + Field::new("models", TypeRef::named_list("ModelUnion"), move |ctx| { + FieldFuture::new(async move { + match ctx.parent_value.try_to_value()? { + Value::Object(indexmap) => { + let mut conn = ctx.data::>()?.acquire().await?; + let model_names = csv_to_vec(&extract::(indexmap, "modelNames")?); + let entity_id = extract::(indexmap, "id")?; + + let mut results: Vec> = Vec::new(); + for name in model_names { + let type_mapping = type_mapping_query(&mut conn, &name).await?; + let state = + model_data_by_id_query(&mut conn, &name, &entity_id, &type_mapping) + .await?; + results.push(FieldValue::with_type(FieldValue::owned_any(state), name)); + } + + Ok(Some(FieldValue::list(results))) + } + _ => Err("incorrect value, requires Value::Object".into()), + } + }) + }) +} + async fn entities_by_sk( conn: &mut PoolConnection, keys: Option>, diff --git a/crates/torii/graphql/src/object/event.rs b/crates/torii/graphql/src/object/event.rs index decac57326..d3e628db2a 100644 --- a/crates/torii/graphql/src/object/event.rs +++ b/crates/torii/graphql/src/object/event.rs @@ -9,8 +9,8 @@ use super::connection::connection_output; use super::system_call::{SystemCall, SystemCallObject}; use super::{ObjectTrait, TypeMapping, ValueMapping}; use crate::constants::DEFAULT_LIMIT; -use crate::query::{query_all, query_by_id, query_total_count, ID}; -use crate::types::ScalarType; +use crate::query::{query_all, query_by_id, query_total_count}; +use crate::types::{GraphqlType, TypeData}; use crate::utils::extract_value::extract; #[derive(FromRow, Deserialize)] @@ -31,11 +31,14 @@ impl Default for EventObject { fn default() -> Self { Self { type_mapping: IndexMap::from([ - (Name::new("id"), TypeRef::named(TypeRef::ID)), - (Name::new("keys"), TypeRef::named(TypeRef::STRING)), - (Name::new("data"), TypeRef::named(TypeRef::STRING)), - (Name::new("createdAt"), TypeRef::named(ScalarType::DateTime.to_string())), - (Name::new("transactionHash"), TypeRef::named(TypeRef::STRING)), + (Name::new("id"), TypeData::Simple(TypeRef::named(TypeRef::ID))), + (Name::new("keys"), TypeData::Simple(TypeRef::named(TypeRef::STRING))), + (Name::new("data"), TypeData::Simple(TypeRef::named(TypeRef::STRING))), + ( + Name::new("createdAt"), + TypeData::Simple(TypeRef::named(GraphqlType::DateTime.to_string())), + ), + (Name::new("transactionHash"), TypeData::Simple(TypeRef::named(TypeRef::STRING))), ]), } } @@ -74,7 +77,7 @@ impl ObjectTrait for EventObject { FieldFuture::new(async move { let mut conn = ctx.data::>()?.acquire().await?; let id = ctx.args.try_get("id")?.string()?.to_string(); - let event = query_by_id(&mut conn, "events", ID::Str(id)).await?; + let event = query_by_id(&mut conn, "events", &id).await?; let result = EventObject::value_mapping(event); Ok(Some(Value::Object(result))) }) @@ -101,14 +104,14 @@ impl ObjectTrait for EventObject { )) } - fn nested_fields(&self) -> Option> { + fn related_fields(&self) -> Option> { Some(vec![Field::new("systemCall", TypeRef::named_nn("SystemCall"), |ctx| { FieldFuture::new(async move { let mut conn = ctx.data::>()?.acquire().await?; let event_values = ctx.parent_value.try_downcast_ref::()?; let syscall_id = extract::(event_values, "system_call_id")?; let system_call: SystemCall = - query_by_id(&mut conn, "system_calls", ID::I64(syscall_id)).await?; + query_by_id(&mut conn, "system_calls", &syscall_id.to_string()).await?; let result = SystemCallObject::value_mapping(system_call); Ok(Some(Value::Object(result))) }) diff --git a/crates/torii/graphql/src/object/inputs/where_input.rs b/crates/torii/graphql/src/object/inputs/where_input.rs index a5b2c804b6..85cae8bd07 100644 --- a/crates/torii/graphql/src/object/inputs/where_input.rs +++ b/crates/torii/graphql/src/object/inputs/where_input.rs @@ -2,11 +2,11 @@ use std::str::FromStr; use async_graphql::dynamic::{Field, InputObject, InputValue, ResolverContext, TypeRef}; use async_graphql::{Error, Name}; +use dojo_types::primitive::Primitive; use super::InputObjectTrait; use crate::object::TypeMapping; use crate::query::filter::{parse_filter, Filter, FilterValue}; -use crate::types::ScalarType; pub struct WhereInputObject { pub type_name: String, @@ -22,23 +22,23 @@ impl WhereInputObject { pub fn new(type_name: &str, object_types: &TypeMapping) -> Self { let where_mapping = object_types .iter() - .filter_map(|(ty_name, ty)| { - ScalarType::from_str(ty.to_string().as_str()).ok().map(|scalar_type| { - let ty = - if scalar_type.is_numeric_type() { TypeRef::INT } else { TypeRef::STRING }; - - let mut comparators = ["GT", "GTE", "LT", "LTE", "NEQ"] - .iter() - .map(|comparator| { - let name = format!("{}{}", ty_name, comparator); - (Name::new(name), TypeRef::named(ty)) - }) - .collect::>(); - - comparators.push((Name::new(ty_name), TypeRef::named(ty))); - - comparators - }) + .filter_map(|(type_name, type_data)| { + // TODO: filter on nested objects + if type_data.is_nested() { + return None; + } + + let mut comparators = ["GT", "GTE", "LT", "LTE", "NEQ"] + .iter() + .map(|comparator| { + let name = format!("{}{}", type_name, comparator); + (Name::new(name), type_data.clone()) + }) + .collect::>(); + + comparators.push((Name::new(type_name), type_data.clone())); + + Some(comparators) }) .flatten() .collect(); @@ -58,7 +58,7 @@ impl InputObjectTrait for WhereInputObject { fn input_object(&self) -> InputObject { self.type_mapping.iter().fold(InputObject::new(self.type_name()), |acc, (ty_name, ty)| { - acc.field(InputValue::new(ty_name.to_string(), TypeRef::named(ty.to_string()))) + acc.field(InputValue::new(ty_name.to_string(), ty.type_ref())) }) } } @@ -73,25 +73,23 @@ pub fn parse_where_argument( ) -> Result, Error> { let where_input = match ctx.args.try_get("where") { Ok(input) => input, - Err(_) => return Ok(Vec::new()), + Err(_) => return Ok(vec![]), }; let input_object = where_input.object()?; - - let filters = where_mapping + where_mapping .iter() - .filter_map(|(ty_name, ty)| { - input_object.get(ty_name).map(|input_filter| { - let data_type = match ty.to_string().as_str() { - TypeRef::STRING => FilterValue::String(input_filter.string()?.to_string()), - TypeRef::INT => FilterValue::Int(input_filter.i64()?), + .filter_map(|(type_name, type_data)| { + input_object.get(type_name).map(|input_filter| { + let primitive = Primitive::from_str(&type_data.type_ref().to_string())?; + let data = match primitive.to_sql_type().as_str() { + "TEXT" => FilterValue::String(input_filter.string()?.to_string()), + "INTEGER" => FilterValue::Int(input_filter.i64()?), _ => return Err(Error::from("Unsupported `where` argument type")), }; - Ok(parse_filter(ty_name, data_type)) + Ok(parse_filter(type_name, data)) }) }) - .collect::, _>>()?; - - Ok(filters) + .collect::, _>>() } diff --git a/crates/torii/graphql/src/object/mod.rs b/crates/torii/graphql/src/object/mod.rs index a8c61b038e..f67ea4a770 100644 --- a/crates/torii/graphql/src/object/mod.rs +++ b/crates/torii/graphql/src/object/mod.rs @@ -3,22 +3,16 @@ pub mod entity; pub mod event; pub mod inputs; pub mod model; -pub mod model_state; +pub mod model_data; pub mod system; pub mod system_call; -use async_graphql::dynamic::{ - Enum, Field, FieldFuture, InputObject, Object, SubscriptionField, TypeRef, -}; -use async_graphql::{Error, Name, Value}; -use indexmap::IndexMap; +use async_graphql::dynamic::{Enum, Field, FieldFuture, InputObject, Object, SubscriptionField}; +use async_graphql::Value; use self::connection::edge::EdgeObject; use self::connection::ConnectionObject; - -// Type aliases for GraphQL fields -pub type TypeMapping = IndexMap; -pub type ValueMapping = IndexMap; +use crate::types::{TypeMapping, ValueMapping}; pub trait ObjectTrait { // Name of the graphql object (eg "player") @@ -30,8 +24,8 @@ pub trait ObjectTrait { // Type mapping defines the fields of the graphql object and their corresponding type fn type_mapping(&self) -> &TypeMapping; - // Related graphql objects - fn nested_fields(&self) -> Option> { + // Related field resolve to sibling graphql objects + fn related_fields(&self) -> Option> { None } @@ -40,15 +34,16 @@ pub trait ObjectTrait { None } - // Resolves subscriptions, returns current object (eg "PlayerAdded") - fn subscriptions(&self) -> Option> { - None - } // Resolves plural object queries, returns type of {type_name}Connection (eg "PlayerConnection") fn resolve_many(&self) -> Option { None } + // Resolves subscriptions, returns current object (eg "PlayerAdded") + fn subscriptions(&self) -> Option> { + None + } + // Input objects consist of {type_name}WhereInput for filtering and {type_name}Order for // ordering fn input_objects(&self) -> Option> { @@ -69,41 +64,31 @@ pub trait ObjectTrait { let connection = ConnectionObject::new(self.name().to_string(), self.type_name().to_string()); - Some(vec![edge.create(), connection.create()]) + let mut objects = Vec::new(); + objects.extend(edge.objects()); + objects.extend(connection.objects()); + + Some(objects) } - // Create a new graphql object and also define its fields from type mapping - fn create(&self) -> Object { + fn objects(&self) -> Vec { let mut object = Object::new(self.type_name()); - for (field_name, field_type) in self.type_mapping() { - let field_name = field_name.clone(); - let field_type = field_type.clone(); + for (field_name, type_data) in self.type_mapping().clone() { + if type_data.is_nested() { + continue; + } - let field = Field::new(field_name.to_string(), field_type, move |ctx| { + let field = Field::new(field_name.to_string(), type_data.type_ref(), move |ctx| { let field_name = field_name.clone(); FieldFuture::new(async move { - // All direct queries, single and plural, passes down results as Value of type - // Object, and Object is an indexmap that contains fields - // and their corresponding result. The result can also be - // another Object. This is evaluated repeatedly until Value is a string or - // number. - if let Some(value) = ctx.parent_value.as_value() { - return match value { - Value::Object(indexmap) => field_value(indexmap, field_name.as_str()), - _ => Err("Incorrect value, requires Value::Object".into()), - }; - } - - // Model union queries is a special case, it instead passes down a - // IndexMap. This could be avoided if - // async-graphql allowed union resolver to be passed down as Value. - if let Some(indexmap) = ctx.parent_value.downcast_ref::() { - return field_value(indexmap, field_name.as_str()); + match ctx.parent_value.try_to_value()? { + Value::Object(values) => { + Ok(Some(values.get(&field_name).unwrap().clone())) // safe unwrap + } + _ => Err("incorrect value, requires Value::Object".into()), } - - Err("Field resolver only accepts Value or IndexMap".into()) }) }); @@ -111,19 +96,12 @@ pub trait ObjectTrait { } // Add related graphql objects (eg event, system) - if let Some(nested_fields) = self.nested_fields() { - for field in nested_fields { + if let Some(fields) = self.related_fields() { + for field in fields { object = object.field(field); } } - object - } -} - -fn field_value(value_mapping: &ValueMapping, field_name: &str) -> Result, Error> { - match value_mapping.get(field_name) { - Some(value) => Ok(Some(value.clone())), - _ => Err(format!("{} field not found", field_name).into()), + vec![object] } } diff --git a/crates/torii/graphql/src/object/model.rs b/crates/torii/graphql/src/object/model.rs index 35a5ada2ce..172a54b2ed 100644 --- a/crates/torii/graphql/src/object/model.rs +++ b/crates/torii/graphql/src/object/model.rs @@ -2,6 +2,7 @@ use async_graphql::dynamic::{ Field, FieldFuture, InputValue, SubscriptionField, SubscriptionFieldFuture, TypeRef, }; use async_graphql::{Name, Value}; +use dojo_types::primitive::Primitive; use indexmap::IndexMap; use sqlx::{Pool, Sqlite}; use tokio_stream::StreamExt; @@ -11,8 +12,8 @@ use torii_core::types::Model; use super::connection::connection_output; use super::{ObjectTrait, TypeMapping, ValueMapping}; use crate::constants::DEFAULT_LIMIT; -use crate::query::{query_all, query_by_id, query_total_count, ID}; -use crate::types::ScalarType; +use crate::query::{query_all, query_by_id, query_total_count}; +use crate::types::{GraphqlType, TypeData}; pub struct ModelObject { pub type_mapping: TypeMapping, @@ -23,11 +24,20 @@ impl Default for ModelObject { fn default() -> Self { Self { type_mapping: IndexMap::from([ - (Name::new("id"), TypeRef::named(TypeRef::ID)), - (Name::new("name"), TypeRef::named(TypeRef::STRING)), - (Name::new("classHash"), TypeRef::named(ScalarType::Felt252.to_string())), - (Name::new("transactionHash"), TypeRef::named(ScalarType::Felt252.to_string())), - (Name::new("createdAt"), TypeRef::named(ScalarType::DateTime.to_string())), + (Name::new("id"), TypeData::Simple(TypeRef::named(TypeRef::ID))), + (Name::new("name"), TypeData::Simple(TypeRef::named(TypeRef::STRING))), + ( + Name::new("classHash"), + TypeData::Simple(TypeRef::named(Primitive::Felt252(None).to_string())), + ), + ( + Name::new("transactionHash"), + TypeData::Simple(TypeRef::named(Primitive::Felt252(None).to_string())), + ), + ( + Name::new("createdAt"), + TypeData::Simple(TypeRef::named(GraphqlType::DateTime.to_string())), + ), ]), } } @@ -67,7 +77,7 @@ impl ObjectTrait for ModelObject { FieldFuture::new(async move { let mut conn = ctx.data::>()?.acquire().await?; let id = ctx.args.try_get("id")?.string()?.to_string(); - let model = query_by_id(&mut conn, "models", ID::Str(id)).await?; + let model = query_by_id(&mut conn, "models", &id).await?; let result = ModelObject::value_mapping(model); Ok(Some(Value::Object(result))) }) diff --git a/crates/torii/graphql/src/object/model_state.rs b/crates/torii/graphql/src/object/model_data.rs similarity index 56% rename from crates/torii/graphql/src/object/model_state.rs rename to crates/torii/graphql/src/object/model_data.rs index 81596d96f2..00c7f481e6 100644 --- a/crates/torii/graphql/src/object/model_state.rs +++ b/crates/torii/graphql/src/object/model_data.rs @@ -1,12 +1,13 @@ use std::str::FromStr; -use async_graphql::dynamic::{Enum, Field, FieldFuture, InputObject, TypeRef}; +use async_graphql::dynamic::{Enum, Field, FieldFuture, InputObject, Object, TypeRef}; use async_graphql::{Name, Value}; use chrono::{DateTime, Utc}; +use dojo_types::primitive::Primitive; use serde::Deserialize; use sqlx::pool::PoolConnection; use sqlx::sqlite::SqliteRow; -use sqlx::{FromRow, Pool, QueryBuilder, Row, Sqlite}; +use sqlx::{FromRow, Pool, Row, Sqlite}; use torii_core::types::Entity; use super::connection::{ @@ -21,15 +22,17 @@ use crate::constants::DEFAULT_LIMIT; use crate::object::entity::EntityObject; use crate::query::filter::{Filter, FilterValue}; use crate::query::order::{Direction, Order}; -use crate::query::{query_by_id, query_total_count, ID}; -use crate::types::ScalarType; +use crate::query::{query_by_id, query_total_count}; +use crate::types::TypeData; use crate::utils::extract_value::extract; const BOOLEAN_TRUE: i64 = 1; -#[derive(FromRow, Deserialize)] -pub struct ModelMembers { +#[derive(FromRow, Deserialize, PartialEq, Eq)] +pub struct ModelMember { + pub id: String, pub model_id: String, + pub model_idx: i64, pub name: String, #[serde(rename = "type")] pub ty: String, @@ -37,7 +40,7 @@ pub struct ModelMembers { pub created_at: DateTime, } -pub struct ModelStateObject { +pub struct ModelDataObject { pub name: String, pub type_name: String, pub type_mapping: TypeMapping, @@ -45,7 +48,7 @@ pub struct ModelStateObject { pub order_input: OrderInputObject, } -impl ModelStateObject { +impl ModelDataObject { pub fn new(name: String, type_name: String, type_mapping: TypeMapping) -> Self { let where_input = WhereInputObject::new(type_name.as_str(), &type_mapping); let order_input = OrderInputObject::new(type_name.as_str(), &type_mapping); @@ -53,7 +56,7 @@ impl ModelStateObject { } } -impl ObjectTrait for ModelStateObject { +impl ObjectTrait for ModelDataObject { fn name(&self) -> &str { &self.name } @@ -62,16 +65,10 @@ impl ObjectTrait for ModelStateObject { &self.type_name } - // Type mapping contains all model members and their corresponding type fn type_mapping(&self) -> &TypeMapping { &self.type_mapping } - // Associate model to its parent entity - fn nested_fields(&self) -> Option> { - Some(vec![entity_field()]) - } - fn input_objects(&self) -> Option> { Some(vec![self.where_input.input_object(), self.order_input.input_object()]) } @@ -81,7 +78,7 @@ impl ObjectTrait for ModelStateObject { } fn resolve_many(&self) -> Option { - let name = self.name.clone(); + let type_name = self.type_name.clone(); let type_mapping = self.type_mapping.clone(); let where_mapping = self.where_input.type_mapping.clone(); let field_name = format!("{}Models", self.name()); @@ -90,18 +87,18 @@ impl ObjectTrait for ModelStateObject { let mut field = Field::new(field_name, TypeRef::named(field_type), move |ctx| { let type_mapping = type_mapping.clone(); let where_mapping = where_mapping.clone(); - let name = name.clone(); + let type_name = type_name.clone(); FieldFuture::new(async move { let mut conn = ctx.data::>()?.acquire().await?; - let table_name = format!("external_{}", name); let order = parse_order_argument(&ctx); let filters = parse_where_argument(&ctx, &where_mapping)?; let connection = parse_connection_arguments(&ctx)?; + let data = - model_states_query(&mut conn, &table_name, &order, &filters, &connection) - .await?; - let total_count = query_total_count(&mut conn, &table_name, &filters).await?; + models_data_query(&mut conn, &type_name, &order, &filters, &connection).await?; + + let total_count = query_total_count(&mut conn, &type_name, &filters).await?; let connection = model_connection(&data, &type_mapping, total_count)?; Ok(Some(Value::Object(connection))) @@ -115,6 +112,102 @@ impl ObjectTrait for ModelStateObject { Some(field) } + + fn objects(&self) -> Vec { + let mut path_array = vec![self.type_name().to_string()]; + let mut objects = data_objects(self.type_name(), self.type_mapping(), &mut path_array); + + // root object requires entity_field association + let mut root = objects.pop().unwrap(); + root = root.field(entity_field()); + + objects.push(root); + objects + } +} + +fn data_objects( + type_name: &str, + type_mapping: &TypeMapping, + path_array: &mut Vec, +) -> Vec { + let mut objects = Vec::::new(); + + for (_, type_data) in type_mapping { + if let TypeData::Nested((nested_type, nested_mapping)) = type_data { + path_array.push(nested_type.to_string()); + objects.extend(data_objects( + &nested_type.to_string(), + nested_mapping, + &mut path_array.clone(), + )); + } + } + + objects.push(object(type_name, type_mapping, path_array)); + objects +} + +pub fn object(type_name: &str, type_mapping: &TypeMapping, path_array: &[String]) -> Object { + let mut object = Object::new(type_name); + + for (field_name, type_data) in type_mapping.clone() { + let table_name = path_array.join("$"); + + let field = Field::new(field_name.to_string(), type_data.type_ref(), move |ctx| { + let field_name = field_name.clone(); + let type_data = type_data.clone(); + let table_name = table_name.clone(); + + // Field resolver for nested types + if let TypeData::Nested((_, nested_mapping)) = type_data { + return FieldFuture::new(async move { + match ctx.parent_value.try_to_value()? { + Value::Object(indexmap) => { + let mut conn = ctx.data::>()?.acquire().await?; + let entity_id = extract::(indexmap, "entity_id")?; + + // TODO: remove subqueries and use JOIN in parent query + let result = model_data_by_id_query( + &mut conn, + &table_name, + &entity_id, + &nested_mapping, + ) + .await?; + + Ok(Some(Value::Object(result))) + } + _ => Err("incorrect value, requires Value::Object".into()), + } + }); + } + + // Field resolver for simple types and model union + FieldFuture::new(async move { + if let Some(value) = ctx.parent_value.as_value() { + return match value { + Value::Object(value_mapping) => { + Ok(Some(value_mapping.get(&field_name).unwrap().clone())) + } + _ => Err("Incorrect value, requires Value::Object".into()), + }; + } + + // Catch model union resolutions, async-graphql sends union types as IndexMap + if let Some(value_mapping) = ctx.parent_value.downcast_ref::() { + return Ok(Some(value_mapping.get(&field_name).unwrap().clone())); + } + + Err("Field resolver only accepts Value or IndexMap".into()) + }) + }); + + object = object.field(field); + } + + object } fn entity_field() -> Field { @@ -123,8 +216,8 @@ fn entity_field() -> Field { match ctx.parent_value.try_to_value()? { Value::Object(indexmap) => { let mut conn = ctx.data::>()?.acquire().await?; - let id = extract::(indexmap, "entity_id")?; - let entity: Entity = query_by_id(&mut conn, "entities", ID::Str(id)).await?; + let entity_id = extract::(indexmap, "entity_id")?; + let entity: Entity = query_by_id(&mut conn, "entities", &entity_id).await?; let result = EntityObject::value_mapping(entity); Ok(Some(Value::Object(result))) @@ -135,20 +228,18 @@ fn entity_field() -> Field { }) } -pub async fn model_state_by_id_query( +pub async fn model_data_by_id_query( conn: &mut PoolConnection, - name: &str, - id: &str, - fields: &TypeMapping, + table_name: &str, + entity_id: &str, + type_mapping: &TypeMapping, ) -> sqlx::Result { - let table_name = format!("external_{}", name); - let mut builder: QueryBuilder<'_, Sqlite> = QueryBuilder::new("SELECT * FROM "); - builder.push(table_name).push(" WHERE entity_id = ").push_bind(id); - let row = builder.build().fetch_one(conn).await?; - value_mapping_from_row(&row, fields) + let query = format!("SELECT * FROM {} WHERE entity_id = '{}'", table_name, entity_id); + let row = sqlx::query(&query).fetch_one(conn).await?; + value_mapping_from_row(&row, type_mapping) } -pub async fn model_states_query( +pub async fn models_data_query( conn: &mut PoolConnection, table_name: &str, order: &Option, @@ -216,8 +307,7 @@ pub async fn model_states_query( sqlx::query(&query).fetch_all(conn).await } -// TODO: make `connection_output()` more generic. Currently, `model_connection()` method -// required as we need to explicity add `entity_id` to each edge. +// TODO: make `connection_output()` more generic. pub fn model_connection( data: &[SqliteRow], types: &TypeMapping, @@ -230,10 +320,7 @@ pub fn model_connection( let entity_id = row.try_get::("entity_id")?; let created_at = row.try_get::("created_at")?; let cursor = encode_cursor(&created_at, &entity_id); - - // insert entity_id because it needs to be queriable - let mut value_mapping = value_mapping_from_row(row, types)?; - value_mapping.insert(Name::new("entity_id"), Value::String(entity_id)); + let value_mapping = value_mapping_from_row(row, types)?; let mut edge = ValueMapping::new(); edge.insert(Name::new("node"), Value::Object(value_mapping)); @@ -251,58 +338,36 @@ pub fn model_connection( } fn value_mapping_from_row(row: &SqliteRow, types: &TypeMapping) -> sqlx::Result { - types + let mut value_mapping = types .iter() - .map(|(name, ty)| Ok((Name::new(name), fetch_value(row, name, &ty.to_string())?))) - .collect::>() -} - -fn fetch_value(row: &SqliteRow, field_name: &str, field_type: &str) -> sqlx::Result { - let column_name = format!("external_{}", field_name); - match ScalarType::from_str(field_type) { - Ok(ScalarType::Bool) => fetch_boolean(row, &column_name), - Ok(ty) if ty.is_numeric_type() => fetch_numeric(row, &column_name), - Ok(_) => fetch_string(row, &column_name), - _ => Err(sqlx::Error::TypeNotFound { type_name: field_type.to_string() }), - } -} - -fn fetch_string(row: &SqliteRow, column_name: &str) -> sqlx::Result { - row.try_get::(column_name).map(Value::from) -} + .filter(|(_, type_data)| type_data.is_simple()) + .map(|(field_name, type_data)| { + let column_name = format!("external_{}", field_name); + Ok(( + Name::new(field_name), + fetch_value(row, &column_name, &type_data.type_ref().to_string())?, + )) + }) + .collect::>()?; -fn fetch_numeric(row: &SqliteRow, column_name: &str) -> sqlx::Result { - row.try_get::(column_name).map(Value::from) -} + // entity_id column is a foreign key associating back to original entity and is not prefixed + // with `external_` + value_mapping.insert(Name::new("entity_id"), fetch_value(row, "entity_id", TypeRef::STRING)?); -fn fetch_boolean(row: &SqliteRow, column_name: &str) -> sqlx::Result { - let result = row.try_get::(column_name); - Ok(Value::from(matches!(result?, BOOLEAN_TRUE))) + Ok(value_mapping) } -pub async fn type_mapping_query( - conn: &mut PoolConnection, - model_id: &str, -) -> sqlx::Result { - let model_members: Vec = sqlx::query_as( - r#" - SELECT - model_id, - name, - type AS ty, - key, - created_at - FROM model_members WHERE model_id = ? - "#, - ) - .bind(model_id) - .fetch_all(conn) - .await?; - - let mut type_mapping = TypeMapping::new(); - for member in model_members { - type_mapping.insert(Name::new(member.name), TypeRef::named(member.ty)); +fn fetch_value(row: &SqliteRow, column_name: &str, field_type: &str) -> sqlx::Result { + match Primitive::from_str(field_type) { + // fetch boolean + Ok(Primitive::Bool(_)) => { + Ok(Value::from(matches!(row.try_get::(column_name)?, BOOLEAN_TRUE))) + } + // fetch integer + Ok(ty) if ty.to_sql_type() == "INTEGER" => { + row.try_get::(column_name).map(Value::from) + } + // fetch string + _ => row.try_get::(column_name).map(Value::from), } - - Ok(type_mapping) } diff --git a/crates/torii/graphql/src/object/system.rs b/crates/torii/graphql/src/object/system.rs index 0e796f5da5..3d53fd5004 100644 --- a/crates/torii/graphql/src/object/system.rs +++ b/crates/torii/graphql/src/object/system.rs @@ -1,6 +1,7 @@ use async_graphql::dynamic::{Field, FieldFuture, FieldValue, InputValue, TypeRef}; use async_graphql::{Name, Value}; use chrono::{DateTime, Utc}; +use dojo_types::primitive::Primitive; use indexmap::IndexMap; use serde::Deserialize; use sqlx::{FromRow, Pool, Sqlite}; @@ -9,8 +10,8 @@ use super::connection::connection_output; use super::system_call::system_calls_by_system_id; use super::{ObjectTrait, TypeMapping, ValueMapping}; use crate::constants::DEFAULT_LIMIT; -use crate::query::{query_all, query_by_id, query_total_count, ID}; -use crate::types::ScalarType; +use crate::query::{query_all, query_by_id, query_total_count}; +use crate::types::{GraphqlType, TypeData}; use crate::utils::extract_value::extract; #[derive(FromRow, Deserialize)] @@ -31,11 +32,20 @@ impl Default for SystemObject { fn default() -> Self { Self { type_mapping: IndexMap::from([ - (Name::new("id"), TypeRef::named(TypeRef::ID)), - (Name::new("name"), TypeRef::named(TypeRef::STRING)), - (Name::new("classHash"), TypeRef::named(ScalarType::Felt252.to_string())), - (Name::new("transactionHash"), TypeRef::named(ScalarType::Felt252.to_string())), - (Name::new("createdAt"), TypeRef::named(ScalarType::DateTime.to_string())), + (Name::new("id"), TypeData::Simple(TypeRef::named(TypeRef::ID))), + (Name::new("name"), TypeData::Simple(TypeRef::named(TypeRef::STRING))), + ( + Name::new("classHash"), + TypeData::Simple(TypeRef::named(Primitive::Felt252(None).to_string())), + ), + ( + Name::new("transactionHash"), + TypeData::Simple(TypeRef::named(Primitive::Felt252(None).to_string())), + ), + ( + Name::new("createdAt"), + TypeData::Simple(TypeRef::named(GraphqlType::DateTime.to_string())), + ), ]), } } @@ -74,7 +84,7 @@ impl ObjectTrait for SystemObject { FieldFuture::new(async move { let mut conn = ctx.data::>()?.acquire().await?; let id = ctx.args.try_get("id")?.string()?.to_string(); - let system = query_by_id(&mut conn, "systems", ID::Str(id)).await?; + let system = query_by_id(&mut conn, "systems", &id).await?; let result = SystemObject::value_mapping(system); Ok(Some(Value::Object(result))) }) @@ -101,7 +111,7 @@ impl ObjectTrait for SystemObject { )) } - fn nested_fields(&self) -> Option> { + fn related_fields(&self) -> Option> { Some(vec![Field::new("systemCalls", TypeRef::named_nn_list_nn("SystemCall"), |ctx| { FieldFuture::new(async move { let mut conn = ctx.data::>()?.acquire().await?; diff --git a/crates/torii/graphql/src/object/system_call.rs b/crates/torii/graphql/src/object/system_call.rs index 62bd14303f..4fcf0d6df3 100644 --- a/crates/torii/graphql/src/object/system_call.rs +++ b/crates/torii/graphql/src/object/system_call.rs @@ -10,8 +10,8 @@ use super::connection::connection_output; use super::system::SystemObject; use super::{ObjectTrait, TypeMapping, ValueMapping}; use crate::constants::DEFAULT_LIMIT; -use crate::query::{query_all, query_by_id, query_total_count, ID}; -use crate::types::ScalarType; +use crate::query::{query_all, query_by_id, query_total_count}; +use crate::types::{GraphqlType, TypeData}; use crate::utils::extract_value::extract; #[derive(FromRow, Deserialize)] @@ -31,11 +31,14 @@ impl Default for SystemCallObject { fn default() -> Self { Self { type_mapping: IndexMap::from([ - (Name::new("id"), TypeRef::named(TypeRef::ID)), - (Name::new("transactionHash"), TypeRef::named(TypeRef::STRING)), - (Name::new("data"), TypeRef::named(TypeRef::STRING)), - (Name::new("systemId"), TypeRef::named(TypeRef::ID)), - (Name::new("createdAt"), TypeRef::named(ScalarType::DateTime.to_string())), + (Name::new("id"), TypeData::Simple(TypeRef::named(TypeRef::ID))), + (Name::new("transactionHash"), TypeData::Simple(TypeRef::named(TypeRef::STRING))), + (Name::new("data"), TypeData::Simple(TypeRef::named(TypeRef::STRING))), + (Name::new("systemId"), TypeData::Simple(TypeRef::named(TypeRef::ID))), + ( + Name::new("createdAt"), + TypeData::Simple(TypeRef::named(GraphqlType::DateTime.to_string())), + ), ]), } } @@ -74,7 +77,8 @@ impl ObjectTrait for SystemCallObject { FieldFuture::new(async move { let mut conn = ctx.data::>()?.acquire().await?; let id = ctx.args.try_get("id")?.i64()?; - let system_call = query_by_id(&mut conn, "system_calls", ID::I64(id)).await?; + let system_call = + query_by_id(&mut conn, "system_calls", &id.to_string()).await?; let result = SystemCallObject::value_mapping(system_call); Ok(Some(Value::Object(result))) }) @@ -103,13 +107,13 @@ impl ObjectTrait for SystemCallObject { )) } - fn nested_fields(&self) -> Option> { + fn related_fields(&self) -> Option> { Some(vec![Field::new("system", TypeRef::named_nn("System"), |ctx| { FieldFuture::new(async move { let mut conn = ctx.data::>()?.acquire().await?; let syscall_values = ctx.parent_value.try_downcast_ref::()?; let system_id = extract::(syscall_values, "systemId")?; - let system = query_by_id(&mut conn, "systems", ID::Str(system_id)).await?; + let system = query_by_id(&mut conn, "systems", &system_id).await?; let result = SystemObject::value_mapping(system); Ok(Some(Value::Object(result))) }) diff --git a/crates/torii/graphql/src/query/mod.rs b/crates/torii/graphql/src/query/mod.rs index 6cafb46f6d..11d2867f12 100644 --- a/crates/torii/graphql/src/query/mod.rs +++ b/crates/torii/graphql/src/query/mod.rs @@ -1,30 +1,30 @@ +use std::str::FromStr; + +use async_graphql::dynamic::TypeRef; +use async_graphql::Name; +use dojo_types::primitive::Primitive; use sqlx::pool::PoolConnection; use sqlx::sqlite::SqliteRow; use sqlx::{FromRow, QueryBuilder, Result, Sqlite}; use self::filter::{Filter, FilterValue}; +use crate::object::model_data::ModelMember; +use crate::types::{TypeData, TypeMapping}; pub mod filter; pub mod order; -pub enum ID { - Str(String), - I64(i64), -} - pub async fn query_by_id( conn: &mut PoolConnection, table_name: &str, - id: ID, + id: &str, ) -> Result where T: Send + Unpin + for<'a> FromRow<'a, SqliteRow>, { let query = format!("SELECT * FROM {} WHERE id = ?", table_name); - let result = match id { - ID::Str(id) => sqlx::query_as::<_, T>(&query).bind(id).fetch_one(conn).await?, - ID::I64(id) => sqlx::query_as::<_, T>(&query).bind(id).fetch_one(conn).await?, - }; + let result = sqlx::query_as::<_, T>(&query).bind(id).fetch_one(conn).await?; + Ok(result) } @@ -66,3 +66,67 @@ pub async fn query_total_count( let result: (i64,) = sqlx::query_as(&query).fetch_one(conn).await?; Ok(result.0) } + +pub async fn type_mapping_query( + conn: &mut PoolConnection, + model_id: &str, +) -> sqlx::Result { + let model_members: Vec = sqlx::query_as( + r#" + SELECT + id, + model_id, + model_idx, + name, + type AS ty, + key, + created_at + from model_members WHERE model_id = ? + "#, + ) + .bind(model_id) + .fetch_all(conn) + .await?; + + let (root_members, nested_members): (Vec<&ModelMember>, Vec<&ModelMember>) = + model_members.iter().partition(|member| member.model_idx == 0); + + let type_mapping: TypeMapping = root_members + .iter() + .map(|member| { + let type_data = match Primitive::from_str(&member.ty) { + Ok(_) => TypeData::Simple(TypeRef::named(member.ty.clone())), + _ => parse_nested_type(&member.model_id, &member.ty, &nested_members), + }; + + (Name::new(&member.name), type_data) + }) + .collect(); + + Ok(type_mapping) +} + +fn parse_nested_type( + target_id: &str, + target_type: &str, + nested_members: &Vec<&ModelMember>, +) -> TypeData { + let nested_mapping: TypeMapping = nested_members + .iter() + .filter_map(|member| { + // search for target type in nested members + if target_id == member.model_id && member.id.ends_with(target_type) { + let type_data = match Primitive::from_str(&member.ty) { + Ok(_) => TypeData::Simple(TypeRef::named(member.ty.clone())), + _ => parse_nested_type(&member.model_id, &member.ty, nested_members), + }; + + Some((Name::new(&member.name), type_data)) + } else { + None + } + }) + .collect(); + + TypeData::Nested((TypeRef::named(target_type), nested_mapping)) +} diff --git a/crates/torii/graphql/src/schema.rs b/crates/torii/graphql/src/schema.rs index a5c4a2cda7..245f54112e 100644 --- a/crates/torii/graphql/src/schema.rs +++ b/crates/torii/graphql/src/schema.rs @@ -8,17 +8,17 @@ use torii_core::types::Model; use super::object::connection::page_info::PageInfoObject; use super::object::entity::EntityObject; use super::object::event::EventObject; -use super::object::model_state::{type_mapping_query, ModelStateObject}; +use super::object::model_data::ModelDataObject; use super::object::system::SystemObject; use super::object::system_call::SystemCallObject; use super::object::ObjectTrait; use super::types::ScalarType; -use super::utils::format_name; use crate::object::model::ModelObject; +use crate::query::type_mapping_query; // The graphql schema is built dynamically at runtime, this is because we won't know the schema of // the models until runtime. There are however, predefined objects such as entities and -// system_calls, their schema is known but we generate them dynamically as well since async-graphql +// events, their schema is known but we generate them dynamically as well because async-graphql // does not allow mixing of static and dynamic schemas. pub async fn build_schema(pool: &SqlitePool) -> Result { let mut schema_builder = Schema::build("Query", None, Some("Subscription")); @@ -33,32 +33,28 @@ pub async fn build_schema(pool: &SqlitePool) -> Result { Box::::default(), ]; - // register dynamic model objects - let (model_objects, model_union) = model_objects(pool).await?; - objects.extend(model_objects); + // build model data gql objects + let (data_objects, data_union) = build_data_objects(pool).await?; + objects.extend(data_objects); - schema_builder = schema_builder.register(model_union); + // register model data unions + schema_builder = schema_builder.register(data_union); - // collect resolvers for single and plural queries - let mut fields: Vec = Vec::new(); - for object in &objects { - if let Some(resolve_one) = object.resolve_one() { - fields.push(resolve_one); - } - if let Some(resolve_many) = object.resolve_many() { - fields.push(resolve_many); - } + // register default scalars + for scalar_type in ScalarType::all().iter() { + schema_builder = schema_builder.register(Scalar::new(scalar_type)); } + // collect resolvers for single and plural queries + let queries: Vec = objects + .iter() + .flat_map(|object| vec![object.resolve_one(), object.resolve_many()].into_iter().flatten()) + .collect(); + // add field resolvers to query root let mut query_root = Object::new("Query"); - for field in fields { - query_root = query_root.field(field); - } - - // register custom scalars - for scalar_type in ScalarType::types().iter() { - schema_builder = schema_builder.register(Scalar::new(scalar_type.to_string())); + for query in queries { + query_root = query_root.field(query); } for object in &objects { @@ -84,7 +80,10 @@ pub async fn build_schema(pool: &SqlitePool) -> Result { } // register gql objects - schema_builder = schema_builder.register(object.create()); + let object_collection = object.objects(); + for object in object_collection { + schema_builder = schema_builder.register(object); + } } // collect resolvers for single subscriptions @@ -111,30 +110,28 @@ pub async fn build_schema(pool: &SqlitePool) -> Result { .map_err(|e| e.into()) } -async fn model_objects(pool: &SqlitePool) -> Result<(Vec>, Union)> { +async fn build_data_objects(pool: &SqlitePool) -> Result<(Vec>, Union)> { let mut conn = pool.acquire().await?; let mut objects: Vec> = Vec::new(); let models: Vec = sqlx::query_as("SELECT * FROM models").fetch_all(&mut conn).await?; // model union object - let mut model_union = Union::new("ModelUnion"); + let mut union = Union::new("ModelUnion"); // model state objects - for model_metadata in models { - let field_type_mapping = type_mapping_query(&mut conn, &model_metadata.id).await?; - if !field_type_mapping.is_empty() { - let (name, type_name) = format_name(&model_metadata.name); - let state_object = Box::new(ModelStateObject::new( - name.clone(), - type_name.clone(), - field_type_mapping, - )); - - model_union = model_union.possible_type(&type_name); - objects.push(state_object); + for model in models { + let type_mapping = type_mapping_query(&mut conn, &model.id).await?; + + if !type_mapping.is_empty() { + let field_name = model.name.to_lowercase(); + let type_name = model.name; + + union = union.possible_type(&type_name); + + objects.push(Box::new(ModelDataObject::new(field_name, type_name, type_mapping))); } } - Ok((objects, model_union)) + Ok((objects, union)) } diff --git a/crates/torii/graphql/src/tests/types-test/Scarb.toml b/crates/torii/graphql/src/tests/types-test/Scarb.toml new file mode 100644 index 0000000000..7ad4b362e0 --- /dev/null +++ b/crates/torii/graphql/src/tests/types-test/Scarb.toml @@ -0,0 +1,24 @@ +[package] +cairo-version = "2.1.1" +name = "types_test" +version = "0.1.0" + +[cairo] +sierra-replace-ids = true + +[dependencies] +dojo = { path = "../../../../../dojo-core" } + +[[target.dojo]] + +[tool.dojo] +initializer_class_hash = "0xbeef" + +[tool.dojo.env] +rpc_url = "http://localhost:5050/" +# Default account for katana with seed = 0 +account_address = "0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973" +private_key = "0x1800000000300000180000000000030000000000003006001800006600" +# world_address = "0x789c94ef39aeebc7f8c4c4633030faefb8bee454e358ae53d06ced36136d7d6" +# keystore_password = "password" +# keystore_path = "../keystore.json" diff --git a/crates/torii/graphql/src/tests/types-test/src/lib.cairo b/crates/torii/graphql/src/tests/types-test/src/lib.cairo new file mode 100644 index 0000000000..224a04b02d --- /dev/null +++ b/crates/torii/graphql/src/tests/types-test/src/lib.cairo @@ -0,0 +1,2 @@ +mod models; +mod systems; diff --git a/crates/torii/graphql/src/tests/types-test/src/models.cairo b/crates/torii/graphql/src/tests/types-test/src/models.cairo new file mode 100644 index 0000000000..5bfa5b9f95 --- /dev/null +++ b/crates/torii/graphql/src/tests/types-test/src/models.cairo @@ -0,0 +1,42 @@ +use array::ArrayTrait; +use starknet::{ContractAddress, ClassHash}; + +#[derive(Model, Copy, Drop, Serde)] +struct Record { + #[key] + record_id: u32, + type_u8: u8, + type_u16: u16, + type_u32: u32, + type_u64: u64, + type_u128: u128, + type_u256: u256, + type_bool: bool, + type_felt: felt252, + type_class_hash: ClassHash, + type_contract_address: ContractAddress, + type_nested: Nested, +} + +#[derive(Copy, Drop, Serde, Introspect)] +struct Nested { + depth: u8, + type_number: u8, + type_string: felt252, + type_nested_more: NestedMore, +} + +#[derive(Copy, Drop, Serde, Introspect)] +struct NestedMore { + depth: u8, + type_number: u8, + type_string: felt252, + type_nested_more_more: NestedMoreMore, +} + +#[derive(Copy, Drop, Serde, Introspect)] +struct NestedMoreMore { + depth: u8, + type_number: u8, + type_string: felt252, +} diff --git a/crates/torii/graphql/src/tests/types-test/src/systems.cairo b/crates/torii/graphql/src/tests/types-test/src/systems.cairo new file mode 100644 index 0000000000..c0acfcfa6a --- /dev/null +++ b/crates/torii/graphql/src/tests/types-test/src/systems.cairo @@ -0,0 +1,66 @@ +use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; +use starknet::{ContractAddress, ClassHash}; + +#[starknet::interface] +trait IRecords { + fn create(self: @TContractState, world: IWorldDispatcher, num_records: u8); +} + +#[system] +mod records { + use types_test::models::{Record, Nested, NestedMore, NestedMoreMore}; + use super::IRecords; + + #[external(v0)] + impl RecordsImpl of IRecords { + fn create(self: @ContractState, world: IWorldDispatcher, num_records: u8) { + let mut record_idx = 0; + loop { + if record_idx == num_records { + break (); + } + + let type_felt: felt252 = record_idx.into(); + + set!( + world, + (Record { + record_id: world.uuid(), + type_u8: record_idx.into(), + type_u16: record_idx.into(), + type_u32: record_idx.into(), + type_u64: record_idx.into(), + type_u128: record_idx.into(), + type_u256: type_felt.into(), + type_bool: if record_idx % 2 == 0 { + true + } else { + false + }, + type_felt: record_idx.into(), + type_class_hash: type_felt.try_into().unwrap(), + type_contract_address: type_felt.try_into().unwrap(), + type_nested: Nested { + depth: 1, + type_number: record_idx.into(), + type_string: type_felt, + type_nested_more: NestedMore { + depth: 2, + type_number: record_idx.into(), + type_string: type_felt, + type_nested_more_more: NestedMoreMore { + depth: 3, + type_number: record_idx.into(), + type_string: type_felt, + } + } + } + }) + ); + + record_idx += 1; + }; + return (); + } + } +} diff --git a/crates/torii/graphql/src/types.rs b/crates/torii/graphql/src/types.rs index fe7b22e3ff..60279f0096 100644 --- a/crates/torii/graphql/src/types.rs +++ b/crates/torii/graphql/src/types.rs @@ -1,125 +1,65 @@ -use std::collections::HashSet; -use std::fmt; -use std::str::FromStr; +use async_graphql::dynamic::TypeRef; +use async_graphql::{Name, Value}; +use dojo_types::primitive::Primitive; +use indexmap::IndexMap; +use strum::IntoEnumIterator; +use strum_macros::{AsRefStr, Display, EnumIter, EnumString}; -// NOTE: If adding/removing types, corresponding change needs to be made to torii-core `src/sql.rs` -// in method sql_type() -#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)] -pub enum ScalarType { - U8, - U16, - U32, - U64, - U128, - U256, - USize, - Bool, - Cursor, - Address, - ClassHash, - DateTime, - Felt252, - Enum, +// ValueMapping is used to map the values of the fields of a model and TypeMapping their +// correpsonding types. Both are used at runtime to dynamically build/resolve graphql +// queries/schema. `Value` from async-graphql supports nesting, but TypeRef does not. TypeData is +// used to support nesting. +pub type ValueMapping = IndexMap; +pub type TypeMapping = IndexMap; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TypeData { + Simple(TypeRef), + Nested((TypeRef, IndexMap)), + // TODO: Enum, could be combined with Simple } -impl fmt::Display for ScalarType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match *self { - ScalarType::U8 => write!(f, "u8"), - ScalarType::U16 => write!(f, "u16"), - ScalarType::U32 => write!(f, "u32"), - ScalarType::U64 => write!(f, "u64"), - ScalarType::U128 => write!(f, "u128"), - ScalarType::U256 => write!(f, "u256"), - ScalarType::USize => write!(f, "usize"), - ScalarType::Bool => write!(f, "bool"), - ScalarType::Cursor => write!(f, "Cursor"), - ScalarType::Address => write!(f, "ContractAddress"), - ScalarType::ClassHash => write!(f, "ClassHash"), - ScalarType::DateTime => write!(f, "DateTime"), - ScalarType::Felt252 => write!(f, "felt252"), - ScalarType::Enum => write!(f, "Enum"), +impl TypeData { + pub fn type_ref(&self) -> TypeRef { + match self { + TypeData::Simple(ty) | TypeData::Nested((ty, _)) => ty.clone(), } } -} -impl ScalarType { - pub fn types() -> HashSet { - vec![ - ScalarType::U8, - ScalarType::U16, - ScalarType::U32, - ScalarType::U64, - ScalarType::U128, - ScalarType::U256, - ScalarType::USize, - ScalarType::Bool, - ScalarType::Cursor, - ScalarType::Address, - ScalarType::ClassHash, - ScalarType::DateTime, - ScalarType::Felt252, - ScalarType::Enum, - ] - .into_iter() - .collect() + pub fn is_simple(&self) -> bool { + matches!(self, TypeData::Simple(_)) } - pub fn numeric_types() -> HashSet { - vec![ - ScalarType::U8, - ScalarType::U16, - ScalarType::U32, - ScalarType::U64, - ScalarType::USize, - ScalarType::Bool, - ScalarType::Enum, - ] - .into_iter() - .collect() + pub fn is_nested(&self) -> bool { + matches!(self, TypeData::Nested(_)) } - // u128 and u256 are non numeric here due to - // sqlite constraint on integer columns - pub fn _non_numeric_types() -> HashSet { - vec![ - ScalarType::U128, - ScalarType::U256, - ScalarType::Cursor, - ScalarType::Address, - ScalarType::ClassHash, - ScalarType::DateTime, - ScalarType::Felt252, - ] - .into_iter() - .collect() + pub fn type_mapping(&self) -> Option<&IndexMap> { + match self { + TypeData::Simple(_) => None, + TypeData::Nested((_, type_mapping)) => Some(type_mapping), + } } +} - pub fn is_numeric_type(&self) -> bool { - ScalarType::numeric_types().contains(self) - } +#[derive(Debug)] +pub enum ScalarType { + Cairo(Primitive), + Torii(GraphqlType), } -impl FromStr for ScalarType { - type Err = anyhow::Error; +// basic types like ID and Int are handled by async-graphql +#[derive(AsRefStr, Display, EnumIter, EnumString, Debug)] +pub enum GraphqlType { + Cursor, + DateTime, +} - fn from_str(s: &str) -> Result { - match s { - "u8" => Ok(ScalarType::U8), - "u16" => Ok(ScalarType::U16), - "u32" => Ok(ScalarType::U32), - "u64" => Ok(ScalarType::U64), - "u128" => Ok(ScalarType::U128), - "u256" => Ok(ScalarType::U256), - "usize" => Ok(ScalarType::USize), - "bool" => Ok(ScalarType::Bool), - "Cursor" => Ok(ScalarType::Cursor), - "ContractAddress" => Ok(ScalarType::Address), - "ClassHash" => Ok(ScalarType::ClassHash), - "DateTime" => Ok(ScalarType::DateTime), - "felt252" => Ok(ScalarType::Felt252), - "Enum" => Ok(ScalarType::Enum), - _ => Err(anyhow::anyhow!("Unknown type {}", s.to_string())), - } +impl ScalarType { + pub fn all() -> Vec { + Primitive::iter() + .map(|ty| ty.to_string()) + .chain(GraphqlType::iter().map(|ty| ty.to_string())) + .collect() } } diff --git a/crates/torii/graphql/src/utils/extract_value.rs b/crates/torii/graphql/src/utils/extract_value.rs index 01cde56206..c307fb82b3 100644 --- a/crates/torii/graphql/src/utils/extract_value.rs +++ b/crates/torii/graphql/src/utils/extract_value.rs @@ -3,7 +3,7 @@ use std::borrow::Cow; use async_graphql::Result; use super::value_accessor::{ObjectAccessor, ValueAccessor}; -use crate::object::ValueMapping; +use crate::types::ValueMapping; pub trait ExtractValue: Sized { fn extract(value_accessor: ValueAccessor<'_>) -> Result; diff --git a/crates/torii/graphql/src/utils/mod.rs b/crates/torii/graphql/src/utils/mod.rs index af7702a5eb..65df2fbe18 100644 --- a/crates/torii/graphql/src/utils/mod.rs +++ b/crates/torii/graphql/src/utils/mod.rs @@ -2,12 +2,6 @@ pub mod extract_value; pub mod parse_argument; pub mod value_accessor; -pub fn format_name(input: &str) -> (String, String) { - let name = input.to_lowercase(); - let type_name = input.to_string(); - (name, type_name) -} - pub fn csv_to_vec(csv: &str) -> Vec { csv.split(',').map(|s| s.trim().to_string()).collect() } diff --git a/crates/torii/migrations/20230316154230_setup.sql b/crates/torii/migrations/20230316154230_setup.sql index 6d06ca0aef..729724dfe3 100644 --- a/crates/torii/migrations/20230316154230_setup.sql +++ b/crates/torii/migrations/20230316154230_setup.sql @@ -33,7 +33,7 @@ CREATE TABLE model_members( type TEXT NOT NULL, key BOOLEAN NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (id, model_idx) FOREIGN KEY (model_id) REFERENCES models(id) UNIQUE (id, member_idx) + PRIMARY KEY (id, member_idx) FOREIGN KEY (model_id) REFERENCES models(id) ); CREATE INDEX idx_model_members_model_id ON model_members (model_id);