diff --git a/Cargo.lock b/Cargo.lock index d73ba29093..8c03828a29 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8403,12 +8403,12 @@ version = "0.1.0" dependencies = [ "anyhow", "async-graphql", - "async-graphql-parser", "async-graphql-poem", "async-trait", "chrono", "clap", "ctrlc", + "dojo-world", "indexmap", "log", "num", diff --git a/crates/torii/Cargo.toml b/crates/torii/Cargo.toml index 99e0394c71..4af2ffc908 100644 --- a/crates/torii/Cargo.toml +++ b/crates/torii/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +dojo-world = { path = "../dojo-world" } async-trait.workspace = true anyhow.workspace = true clap.workspace = true @@ -30,7 +31,6 @@ tracing-subscriber.workspace = true url = "2.2.2" chrono.workspace = true async-graphql = { version = "5.0.8", features = ["chrono", "dynamic-schema"] } -async-graphql-parser = "5.0.8" async-graphql-poem = "5.0.8" poem = "1.3.48" indexmap = "1.9.3" diff --git a/crates/torii/migrations/20230316154230_setup.sql b/crates/torii/migrations/20230316154230_setup.sql index a44a961efb..9ac6cecfcd 100644 --- a/crates/torii/migrations/20230316154230_setup.sql +++ b/crates/torii/migrations/20230316154230_setup.sql @@ -11,7 +11,7 @@ CREATE TABLE components ( address TEXT NOT NULL, class_hash TEXT NOT NULL, transaction_hash TEXT NOT NULL, - storage_schema TEXT NOT NULL, + storage_definition TEXT NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); diff --git a/crates/torii/scripts/test.sh b/crates/torii/scripts/test.sh new file mode 100755 index 0000000000..b8f2e06cc6 --- /dev/null +++ b/crates/torii/scripts/test.sh @@ -0,0 +1,4 @@ +#!/bin/bash +pushd $(dirname "$0")/.. + +cargo test --bin torii -- --nocapture \ No newline at end of file diff --git a/crates/torii/sqlx-data.json b/crates/torii/sqlx-data.json index 67c8a7c190..93f70359c5 100644 --- a/crates/torii/sqlx-data.json +++ b/crates/torii/sqlx-data.json @@ -1,279 +1,3 @@ { - "db": "SQLite", - "10810b95e7e69620fcd832eee2b9342277df3641f84d8b88998d78a9c4b0f5d1": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "name", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "address", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "class_hash", - "ordinal": 3, - "type_info": "Text" - }, - { - "name": "transaction_hash", - "ordinal": 4, - "type_info": "Text" - }, - { - "name": "created_at: _", - "ordinal": 5, - "type_info": "Datetime" - } - ], - "nullable": [ - false, - false, - false, - false, - false, - false - ], - "parameters": { - "Right": 1 - } - }, - "query": "\n SELECT\n id,\n name,\n address,\n class_hash,\n transaction_hash,\n created_at as \"created_at: _\"\n FROM systems WHERE id = $1\n " - }, - "17b656fd8893b11234fb631d819e11a21acc26d2ee1e12c1c7338843e6c8a1d0": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int64" - }, - { - "name": "data", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "transaction_hash", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "system_id", - "ordinal": 3, - "type_info": "Text" - }, - { - "name": "created_at: _", - "ordinal": 4, - "type_info": "Datetime" - } - ], - "nullable": [ - false, - false, - false, - false, - false - ], - "parameters": { - "Right": 1 - } - }, - "query": "\n SELECT\n id,\n data,\n transaction_hash,\n system_id,\n created_at as \"created_at: _\"\n FROM system_calls WHERE system_id = $1\n " - }, - "2327896fe4f909df1221a2ce203cded743b34566fbb94ad803c963c22b341a0c": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "name", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "partition_id", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "keys", - "ordinal": 3, - "type_info": "Text" - }, - { - "name": "transaction_hash", - "ordinal": 4, - "type_info": "Text" - }, - { - "name": "created_at: _", - "ordinal": 5, - "type_info": "Datetime" - } - ], - "nullable": [ - false, - false, - false, - true, - false, - false - ], - "parameters": { - "Right": 1 - } - }, - "query": "\n SELECT \n id,\n name,\n partition_id,\n keys,\n transaction_hash,\n created_at as \"created_at: _\"\n FROM entities \n WHERE id = $1\n " - }, - "30dedd997ece2ccf165a611708f7ddf4e7987a3d53ded127a76a6e5dc4de3cbd": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "system_call_id", - "ordinal": 1, - "type_info": "Int64" - }, - { - "name": "keys", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "data", - "ordinal": 3, - "type_info": "Text" - }, - { - "name": "created_at: _", - "ordinal": 4, - "type_info": "Datetime" - } - ], - "nullable": [ - false, - false, - false, - false, - false - ], - "parameters": { - "Right": 1 - } - }, - "query": "\n SELECT \n id,\n system_call_id,\n keys,\n data,\n created_at as \"created_at: _\"\n FROM events \n WHERE id = $1\n " - }, - "9874396c6908c9499799e95a31b482c2dedcde7ea856419370b8e83a75636583": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "name", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "address", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "class_hash", - "ordinal": 3, - "type_info": "Text" - }, - { - "name": "transaction_hash", - "ordinal": 4, - "type_info": "Text" - }, - { - "name": "storage_schema", - "ordinal": 5, - "type_info": "Text" - }, - { - "name": "created_at: _", - "ordinal": 6, - "type_info": "Datetime" - } - ], - "nullable": [ - false, - false, - false, - false, - false, - false, - false - ], - "parameters": { - "Right": 1 - } - }, - "query": "\n SELECT \n id,\n name,\n address,\n class_hash,\n transaction_hash,\n storage_schema,\n created_at as \"created_at: _\"\n FROM components WHERE id = $1\n " - }, - "f7b294431f97b0186f93a660ff986788f94febd42c44ebcceee2db9fa10a854f": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int64" - }, - { - "name": "data", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "transaction_hash", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "system_id", - "ordinal": 3, - "type_info": "Text" - }, - { - "name": "created_at: _", - "ordinal": 4, - "type_info": "Datetime" - } - ], - "nullable": [ - false, - false, - false, - false, - false - ], - "parameters": { - "Right": 1 - } - }, - "query": "\n SELECT\n id,\n data,\n transaction_hash,\n system_id,\n created_at as \"created_at: _\"\n FROM system_calls WHERE id = $1\n " - } + "db": "SQLite" } \ No newline at end of file diff --git a/crates/torii/src/graphql/base.graphql b/crates/torii/src/graphql/base.graphql index f36c1ad51e..5d20d50c1f 100644 --- a/crates/torii/src/graphql/base.graphql +++ b/crates/torii/src/graphql/base.graphql @@ -27,7 +27,7 @@ type Component { address: Address! classHash: Address! transactionHash: FieldElement! - storageSchema: String! + storageDefinition: String! storageStates: [Storage_NAME_!] createdAt: DateTime! } diff --git a/crates/torii/src/graphql/component.rs b/crates/torii/src/graphql/component.rs index 990cbbe8f3..69e0646818 100644 --- a/crates/torii/src/graphql/component.rs +++ b/crates/torii/src/graphql/component.rs @@ -7,7 +7,8 @@ use sqlx::pool::PoolConnection; use sqlx::{FromRow, Pool, Result, Sqlite}; use super::types::ScalarType; -use super::{ObjectTraitInstance, ObjectTraitStatic, TypeMapping, ValueMapping}; +use super::utils::{format_name, remove_quotes}; +use super::{ObjectTrait, TypeMapping, ValueMapping}; #[derive(FromRow, Deserialize)] #[serde(rename_all = "camelCase")] @@ -17,35 +18,35 @@ pub struct Component { pub address: String, pub class_hash: String, pub transaction_hash: String, - pub storage_schema: String, + pub storage_definition: String, pub created_at: DateTime, } pub struct ComponentObject { pub field_type_mapping: TypeMapping, + pub storage_names: Vec, } -impl ObjectTraitStatic for ComponentObject { - fn new() -> Self { +impl ComponentObject { + // Storage names are passed in on new because + // it builds the related fields dynamically + pub fn new(storage_names: Vec) -> Self { Self { field_type_mapping: IndexMap::from([ - (Name::new("id"), TypeRef::ID), - (Name::new("name"), TypeRef::STRING), - (Name::new("address"), ScalarType::ADDRESS), - (Name::new("classHash"), ScalarType::FELT), - (Name::new("transactionHash"), ScalarType::FELT), - (Name::new("storageSchema"), TypeRef::STRING), - (Name::new("createdAt"), ScalarType::DATE_TIME), + (Name::new("id"), TypeRef::ID.to_string()), + (Name::new("name"), TypeRef::STRING.to_string()), + (Name::new("address"), ScalarType::ADDRESS.to_string()), + (Name::new("classHash"), ScalarType::FELT.to_string()), + (Name::new("transactionHash"), ScalarType::FELT.to_string()), + (Name::new("storageDefinition"), TypeRef::STRING.to_string()), + (Name::new("createdAt"), ScalarType::DATE_TIME.to_string()), ]), + storage_names, } } - - fn from(field_type_mapping: TypeMapping) -> Self { - Self { field_type_mapping } - } } -impl ObjectTraitInstance for ComponentObject { +impl ObjectTrait for ComponentObject { fn name(&self) -> &str { "component" } @@ -63,7 +64,7 @@ impl ObjectTraitInstance for ComponentObject { Field::new(self.name(), TypeRef::named_nn(self.type_name()), |ctx| { FieldFuture::new(async move { let mut conn = ctx.data::>()?.acquire().await?; - let id = ctx.args.try_get("id")?.string()?.replace('\"', ""); + let id = remove_quotes(ctx.args.try_get("id")?.string()?); let component_values = component_by_id(&mut conn, &id).await?; Ok(Some(FieldValue::owned_any(component_values))) }) @@ -71,30 +72,40 @@ impl ObjectTraitInstance for ComponentObject { .argument(InputValue::new("id", TypeRef::named_nn(TypeRef::ID))), ] } + + fn related_fields(&self) -> Option> { + Some( + self.storage_names + .iter() + .map(|storage| { + let (name, type_name) = format_name(storage); + Field::new(name, TypeRef::named(type_name), |_| { + FieldFuture::new(async move { + // TODO: implement + Ok(Some(Value::Null)) + }) + }) + }) + .collect(), + ) + } } async fn component_by_id(conn: &mut PoolConnection, id: &str) -> Result { - let component = sqlx::query_as!( - Component, - r#" - SELECT - id, - name, - address, - class_hash, - transaction_hash, - storage_schema, - created_at as "created_at: _" - FROM components WHERE id = $1 - "#, - id - ) - .fetch_one(conn) - .await?; + let component: Component = + sqlx::query_as("SELECT * FROM components WHERE id = $1").bind(id).fetch_one(conn).await?; Ok(value_mapping(component)) } +#[allow(dead_code)] +pub async fn components(conn: &mut PoolConnection) -> Result> { + let components: Vec = + sqlx::query_as("SELECT * FROM components").fetch_all(conn).await?; + + Ok(components.into_iter().map(value_mapping).collect()) +} + fn value_mapping(component: Component) -> ValueMapping { IndexMap::from([ (Name::new("id"), Value::from(component.id)), @@ -102,7 +113,7 @@ fn value_mapping(component: Component) -> ValueMapping { (Name::new("address"), Value::from(component.address)), (Name::new("classHash"), Value::from(component.class_hash)), (Name::new("transactionHash"), Value::from(component.transaction_hash)), - (Name::new("storageSchema"), Value::from(component.storage_schema)), + (Name::new("storageDefinition"), Value::from(component.storage_definition)), ( Name::new("createdAt"), Value::from(component.created_at.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)), diff --git a/crates/torii/src/graphql/entity.rs b/crates/torii/src/graphql/entity.rs index d7aaf087a3..3562958dfb 100644 --- a/crates/torii/src/graphql/entity.rs +++ b/crates/torii/src/graphql/entity.rs @@ -7,7 +7,8 @@ use sqlx::pool::PoolConnection; use sqlx::{FromRow, Pool, Result, Sqlite}; use super::types::ScalarType; -use super::{ObjectTraitInstance, ObjectTraitStatic, TypeMapping, ValueMapping}; +use super::utils::remove_quotes; +use super::{ObjectTrait, TypeMapping, ValueMapping}; #[derive(FromRow, Deserialize)] #[serde(rename_all = "camelCase")] @@ -24,25 +25,22 @@ pub struct EntityObject { pub field_type_mapping: TypeMapping, } -impl ObjectTraitStatic for EntityObject { - fn new() -> Self { +impl EntityObject { + pub fn new() -> Self { Self { field_type_mapping: IndexMap::from([ - (Name::new("id"), TypeRef::ID), - (Name::new("name"), TypeRef::STRING), - (Name::new("partitionId"), ScalarType::FELT), - (Name::new("keys"), TypeRef::STRING), - (Name::new("transactionHash"), ScalarType::FELT), - (Name::new("createdAt"), ScalarType::DATE_TIME), + (Name::new("id"), TypeRef::ID.to_string()), + (Name::new("name"), TypeRef::STRING.to_string()), + (Name::new("partitionId"), ScalarType::FELT.to_string()), + (Name::new("keys"), TypeRef::STRING.to_string()), + (Name::new("transactionHash"), ScalarType::FELT.to_string()), + (Name::new("createdAt"), ScalarType::DATE_TIME.to_string()), ]), } } - fn from(field_type_mapping: TypeMapping) -> Self { - Self { field_type_mapping } - } } -impl ObjectTraitInstance for EntityObject { +impl ObjectTrait for EntityObject { fn name(&self) -> &str { "entity" } @@ -60,7 +58,7 @@ impl ObjectTraitInstance for EntityObject { Field::new(self.name(), TypeRef::named_nn(self.type_name()), |ctx| { FieldFuture::new(async move { let mut conn = ctx.data::>()?.acquire().await?; - let id = ctx.args.try_get("id")?.string()?.replace('\"', ""); + let id = remove_quotes(ctx.args.try_get("id")?.string()?); let entity_values = entity_by_id(&mut conn, &id).await?; Ok(Some(FieldValue::owned_any(entity_values))) }) @@ -71,23 +69,8 @@ impl ObjectTraitInstance for EntityObject { } async fn entity_by_id(conn: &mut PoolConnection, id: &str) -> Result { - let entity = sqlx::query_as!( - Entity, - r#" - SELECT - id, - name, - partition_id, - keys, - transaction_hash, - created_at as "created_at: _" - FROM entities - WHERE id = $1 - "#, - id, - ) - .fetch_one(conn) - .await?; + let entity: Entity = + sqlx::query_as("SELECT * FROM entities WHERE id = $1").bind(id).fetch_one(conn).await?; Ok(value_mapping(entity)) } diff --git a/crates/torii/src/graphql/event.rs b/crates/torii/src/graphql/event.rs index 0c457325a8..12b170ef57 100644 --- a/crates/torii/src/graphql/event.rs +++ b/crates/torii/src/graphql/event.rs @@ -10,8 +10,9 @@ use sqlx::{FromRow, Pool, Result, Sqlite}; use super::system_call::system_call_by_id; use super::types::ScalarType; +use super::utils::remove_quotes; use super::utils::value_accessor::ObjectAccessor; -use super::{ObjectTraitInstance, ObjectTraitStatic, TypeMapping, ValueMapping}; +use super::{ObjectTrait, TypeMapping, ValueMapping}; #[derive(FromRow, Deserialize)] #[serde(rename_all = "camelCase")] @@ -27,25 +28,21 @@ pub struct EventObject { pub field_type_mapping: TypeMapping, } -impl ObjectTraitStatic for EventObject { - fn new() -> Self { +impl EventObject { + pub fn new() -> Self { Self { field_type_mapping: IndexMap::from([ - (Name::new("id"), TypeRef::ID), - (Name::new("keys"), TypeRef::STRING), - (Name::new("data"), TypeRef::STRING), - (Name::new("systemCallId"), TypeRef::INT), - (Name::new("createdAt"), ScalarType::DATE_TIME), + (Name::new("id"), TypeRef::ID.to_string()), + (Name::new("keys"), TypeRef::STRING.to_string()), + (Name::new("data"), TypeRef::STRING.to_string()), + (Name::new("systemCallId"), TypeRef::INT.to_string()), + (Name::new("createdAt"), ScalarType::DATE_TIME.to_string()), ]), } } - - fn from(field_type_mapping: TypeMapping) -> Self { - Self { field_type_mapping } - } } -impl ObjectTraitInstance for EventObject { +impl ObjectTrait for EventObject { fn name(&self) -> &str { "event" } @@ -63,7 +60,7 @@ impl ObjectTraitInstance for EventObject { Field::new(self.name(), TypeRef::named_nn(self.type_name()), |ctx| { FieldFuture::new(async move { let mut conn = ctx.data::>()?.acquire().await?; - let id = ctx.args.try_get("id")?.string()?.replace('\"', ""); + let id = remove_quotes(ctx.args.try_get("id")?.string()?); let event_values = event_by_id(&mut conn, &id).await?; Ok(Some(FieldValue::owned_any(event_values))) @@ -90,22 +87,8 @@ impl ObjectTraitInstance for EventObject { } async fn event_by_id(conn: &mut PoolConnection, id: &str) -> Result { - let event = sqlx::query_as!( - Event, - r#" - SELECT - id, - system_call_id, - keys, - data, - created_at as "created_at: _" - FROM events - WHERE id = $1 - "#, - id - ) - .fetch_one(conn) - .await?; + let event: Event = + sqlx::query_as("SELECT * FROM events WHERE id = $1").bind(id).fetch_one(conn).await?; Ok(value_mapping(event)) } diff --git a/crates/torii/src/graphql/mod.rs b/crates/torii/src/graphql/mod.rs index c469fb341b..da1da36541 100644 --- a/crates/torii/src/graphql/mod.rs +++ b/crates/torii/src/graphql/mod.rs @@ -4,25 +4,22 @@ pub mod entity; pub mod event; pub mod schema; pub mod server; +pub mod storage; pub mod system; pub mod system_call; -pub mod types; -pub mod utils; + +mod types; +mod utils; use async_graphql::dynamic::{Field, FieldFuture, Object, TypeRef}; use async_graphql::{Name, Value}; use indexmap::IndexMap; // Type aliases for GraphQL fields -pub type TypeMapping = IndexMap; +pub type TypeMapping = IndexMap; pub type ValueMapping = IndexMap; -pub trait ObjectTraitStatic { - fn new() -> Self; - fn from(field_type_mapping: TypeMapping) -> Self; -} - -pub trait ObjectTraitInstance { +pub trait ObjectTrait { fn name(&self) -> &str; fn type_name(&self) -> &str; fn field_type_mapping(&self) -> &TypeMapping; diff --git a/crates/torii/src/graphql/schema.rs b/crates/torii/src/graphql/schema.rs index c1e9f49c40..10b2e94976 100644 --- a/crates/torii/src/graphql/schema.rs +++ b/crates/torii/src/graphql/schema.rs @@ -1,23 +1,25 @@ -use async_graphql::dynamic::{Object, Scalar, Schema, SchemaError}; +use anyhow::Result; +use async_graphql::dynamic::{Object, Scalar, Schema}; +use async_graphql::Name; +use dojo_world::manifest::Member; use sqlx::SqlitePool; -use super::component::ComponentObject; +use super::component::{Component, ComponentObject}; use super::entity::EntityObject; use super::event::EventObject; +use super::storage::StorageObject; use super::system::SystemObject; use super::system_call::SystemCallObject; use super::types::ScalarType; -use super::{ObjectTraitInstance, ObjectTraitStatic}; +use super::utils::format_name; +use super::{ObjectTrait, TypeMapping}; -pub async fn build_schema(pool: &SqlitePool) -> Result { - // base gql objects - let objects: Vec> = vec![ - Box::new(EntityObject::new()), - Box::new(ComponentObject::new()), - Box::new(SystemObject::new()), - Box::new(EventObject::new()), - Box::new(SystemCallObject::new()), - ]; +pub async fn build_schema(pool: &SqlitePool) -> Result { + let mut schema_builder = Schema::build("Query", None, None); + + // static objects + dynamic objects (component and storage objects) + let mut objects = static_objects(); + objects.extend(dynamic_objects(pool).await?); // collect field resolvers let mut fields = Vec::new(); @@ -32,15 +34,59 @@ pub async fn build_schema(pool: &SqlitePool) -> Result { } // register custom scalars - let mut schema_builder = Schema::build("Query", None, None); for scalar_type in ScalarType::types().iter() { schema_builder = schema_builder.register(Scalar::new(*scalar_type)); } - // register base gql objects + // register gql objects for object in &objects { schema_builder = schema_builder.register(object.create()); } - schema_builder.register(query_root).data(pool.clone()).finish() + schema_builder.register(query_root).data(pool.clone()).finish().map_err(|e| e.into()) +} + +// predefined base objects +fn static_objects() -> Vec> { + vec![ + Box::new(EntityObject::new()), + Box::new(SystemObject::new()), + Box::new(EventObject::new()), + Box::new(SystemCallObject::new()), + ] +} + +async fn dynamic_objects(pool: &SqlitePool) -> Result>> { + let mut conn = pool.acquire().await?; + let mut objects = Vec::new(); + + // storage objects + let components: Vec = + sqlx::query_as("SELECT * FROM components").fetch_all(&mut conn).await?; + for component in components { + let storage_object = process_component(component)?; + objects.push(storage_object); + } + + let storage_names: Vec = objects.iter().map(|obj| obj.name().to_string()).collect(); + + // component object + let component = ComponentObject::new(storage_names); + objects.push(Box::new(component)); + + Ok(objects) +} + +fn process_component(component: Component) -> Result> { + let members: Vec = serde_json::from_str(&component.storage_definition)?; + + let field_type_mapping = members.iter().fold(TypeMapping::new(), |mut mapping, member| { + // TODO: check if member type exists in scalar types + mapping.insert(Name::new(&member.name), member.ty.to_string()); + mapping + }); + + let (name, type_name) = format_name(component.name.as_str()); + + Ok(Box::new(StorageObject::new(name, type_name, field_type_mapping))) } diff --git a/crates/torii/src/graphql/server.rs b/crates/torii/src/graphql/server.rs index d577a1d91e..323157aa15 100644 --- a/crates/torii/src/graphql/server.rs +++ b/crates/torii/src/graphql/server.rs @@ -17,8 +17,8 @@ async fn graphql_playground() -> impl IntoResponse { Html(playground_source(GraphQLPlaygroundConfig::new("/playground"))) } -pub async fn start_graphql(pool: &Pool) -> std::io::Result<()> { - let schema = build_schema(pool).await.expect("failed to build schema"); +pub async fn start_graphql(pool: &Pool) -> anyhow::Result<()> { + let schema = build_schema(pool).await?; let app = Route::new() .at("/query", get(graphiql).post(GraphQL::new(schema.clone()))) diff --git a/crates/torii/src/graphql/storage.rs b/crates/torii/src/graphql/storage.rs index 190f27bfad..815762f4db 100644 --- a/crates/torii/src/graphql/storage.rs +++ b/crates/torii/src/graphql/storage.rs @@ -1,9 +1,13 @@ -use indexmap::IndexMap; -use async_graphql::dynamic::{Field, FieldFuture, FieldValue, Object, TypeRef}; +use async_graphql::dynamic::{Field, FieldFuture, FieldValue, InputValue, TypeRef}; use async_graphql::Value; -use sqlx::{Pool, Sqlite}; +use sqlx::pool::PoolConnection; +use sqlx::sqlite::SqliteRow; +use sqlx::{Error, Pool, Result, Row, Sqlite}; -use super::{TypeMapping, ObjectTraitInstance}; +use super::{ObjectTrait, TypeMapping, ValueMapping}; +use crate::graphql::types::ScalarType; + +const BOOLEAN_TRUE: i64 = 1; pub struct StorageObject { pub name: String, @@ -12,12 +16,12 @@ pub struct StorageObject { } impl StorageObject { - pub fn from(name: String, type_name: String, field_type_mapping: TypeMapping) -> Self { + pub fn new(name: String, type_name: String, field_type_mapping: TypeMapping) -> Self { Self { name, type_name, field_type_mapping } } } -impl ObjectTraitInstance for StorageObject { +impl ObjectTrait for StorageObject { fn name(&self) -> &str { &self.name } @@ -31,7 +35,62 @@ impl ObjectTraitInstance for StorageObject { } fn field_resolvers(&self) -> Vec { - // TODO: implement - vec![] + let name = self.name.clone(); + let type_mapping = self.field_type_mapping.clone(); + vec![ + Field::new(self.name(), TypeRef::named_nn(self.type_name()), move |ctx| { + let inner_name = name.clone(); + let inner_type_mapping = type_mapping.clone(); + + FieldFuture::new(async move { + let mut conn = ctx.data::>()?.acquire().await?; + let id = ctx.args.try_get("id")?.i64()?; + let storage_values = + storage_by_id(&mut conn, &inner_name, &inner_type_mapping, id).await?; + Ok(Some(FieldValue::owned_any(storage_values))) + }) + }) + .argument(InputValue::new("id", TypeRef::named_nn(TypeRef::INT))), + ] } } + +async fn storage_by_id( + conn: &mut PoolConnection, + name: &str, + fields: &TypeMapping, + id: i64, +) -> Result { + let query = format!("SELECT * FROM storage_{} WHERE id = ?", name); + let storage = sqlx::query(&query).bind(id).fetch_one(conn).await?; + let result = value_mapping_from_row(&storage, fields)?; + Ok(result) +} + +fn value_mapping_from_row(row: &SqliteRow, fields: &TypeMapping) -> Result { + let mut value_mapping = ValueMapping::new(); + + // Cairo's data types are stored as either int or str in sqlite db, + // int's max size is 64bit so we retrieve all types above u64 as str + for (field_name, field_type) in fields { + let value = match field_type.as_str() { + ScalarType::U8 | ScalarType::U16 | ScalarType::U32 | ScalarType::U64 => { + let result = row.try_get::(field_name.as_str()); + Value::from(result?) + } + ScalarType::U128 | ScalarType::U250 | ScalarType::U256 | ScalarType::FELT => { + let result = row.try_get::(field_name.as_str()); + Value::from(result?) + } + TypeRef::BOOLEAN => { + // sqlite stores booleans as 0 or 1 + let result = row.try_get::(field_name.as_str()); + Value::from(matches!(result?, BOOLEAN_TRUE)) + } + _ => return Err(Error::TypeNotFound { type_name: field_type.clone() }), + }; + value_mapping.insert(field_name.clone(), value); + } + + Ok(value_mapping) +} diff --git a/crates/torii/src/graphql/system.rs b/crates/torii/src/graphql/system.rs index e7e8615469..7d008f26f5 100644 --- a/crates/torii/src/graphql/system.rs +++ b/crates/torii/src/graphql/system.rs @@ -10,8 +10,9 @@ use sqlx::{FromRow, Pool, Result, Sqlite}; use super::system_call::system_calls_by_system_id; use super::types::ScalarType; +use super::utils::remove_quotes; use super::utils::value_accessor::ObjectAccessor; -use super::{ObjectTraitInstance, ObjectTraitStatic, TypeMapping, ValueMapping}; +use super::{ObjectTrait, TypeMapping, ValueMapping}; #[derive(FromRow, Deserialize)] #[serde(rename_all = "camelCase")] @@ -28,26 +29,22 @@ pub struct SystemObject { pub field_type_mapping: TypeMapping, } -impl ObjectTraitStatic for SystemObject { - fn new() -> Self { +impl SystemObject { + pub fn new() -> Self { Self { field_type_mapping: IndexMap::from([ - (Name::new("id"), TypeRef::ID), - (Name::new("name"), TypeRef::STRING), - (Name::new("address"), ScalarType::ADDRESS), - (Name::new("classHash"), ScalarType::FELT), - (Name::new("transactionHash"), ScalarType::FELT), - (Name::new("createdAt"), ScalarType::DATE_TIME), + (Name::new("id"), TypeRef::ID.to_string()), + (Name::new("name"), TypeRef::STRING.to_string()), + (Name::new("address"), ScalarType::ADDRESS.to_string()), + (Name::new("classHash"), ScalarType::FELT.to_string()), + (Name::new("transactionHash"), ScalarType::FELT.to_string()), + (Name::new("createdAt"), ScalarType::DATE_TIME.to_string()), ]), } } - - fn from(field_type_mapping: TypeMapping) -> Self { - Self { field_type_mapping } - } } -impl ObjectTraitInstance for SystemObject { +impl ObjectTrait for SystemObject { fn name(&self) -> &str { "system" } @@ -65,7 +62,7 @@ impl ObjectTraitInstance for SystemObject { Field::new(self.name(), TypeRef::named_nn(self.type_name()), |ctx| { FieldFuture::new(async move { let mut conn = ctx.data::>()?.acquire().await?; - let id = ctx.args.try_get("id")?.string()?.replace('\"', ""); + let id = remove_quotes(ctx.args.try_get("id")?.string()?); let system_values = system_by_id(&mut conn, &id).await?; Ok(Some(FieldValue::owned_any(system_values))) }) @@ -91,22 +88,8 @@ impl ObjectTraitInstance for SystemObject { } pub async fn system_by_id(conn: &mut PoolConnection, id: &str) -> Result { - let system = sqlx::query_as!( - System, - r#" - SELECT - id, - name, - address, - class_hash, - transaction_hash, - created_at as "created_at: _" - FROM systems WHERE id = $1 - "#, - id - ) - .fetch_one(conn) - .await?; + let system: System = + sqlx::query_as("SELECT * FROM systems WHERE id = $1").bind(id).fetch_one(conn).await?; Ok(value_mapping(system)) } diff --git a/crates/torii/src/graphql/system_call.rs b/crates/torii/src/graphql/system_call.rs index efcb63275c..84b9992396 100644 --- a/crates/torii/src/graphql/system_call.rs +++ b/crates/torii/src/graphql/system_call.rs @@ -12,7 +12,7 @@ use sqlx::{FromRow, Pool, Result, Sqlite}; use super::system::system_by_id; use super::types::ScalarType; use super::utils::value_accessor::ObjectAccessor; -use super::{ObjectTraitInstance, ObjectTraitStatic, TypeMapping, ValueMapping}; +use super::{ObjectTrait, TypeMapping, ValueMapping}; #[derive(FromRow, Deserialize)] #[serde(rename_all = "camelCase")] @@ -27,25 +27,21 @@ pub struct SystemCallObject { pub field_type_mapping: TypeMapping, } -impl ObjectTraitStatic for SystemCallObject { - fn new() -> Self { +impl SystemCallObject { + pub fn new() -> Self { Self { field_type_mapping: IndexMap::from([ - (Name::new("id"), TypeRef::ID), - (Name::new("transactionHash"), TypeRef::STRING), - (Name::new("data"), TypeRef::STRING), - (Name::new("system_id"), TypeRef::ID), - (Name::new("createdAt"), ScalarType::DATE_TIME), + (Name::new("id"), TypeRef::ID.to_string()), + (Name::new("transactionHash"), TypeRef::STRING.to_string()), + (Name::new("data"), TypeRef::STRING.to_string()), + (Name::new("system_id"), TypeRef::ID.to_string()), + (Name::new("createdAt"), ScalarType::DATE_TIME.to_string()), ]), } } - - fn from(field_type_mapping: TypeMapping) -> Self { - Self { field_type_mapping } - } } -impl ObjectTraitInstance for SystemCallObject { +impl ObjectTrait for SystemCallObject { fn name(&self) -> &str { "systemCall" } @@ -89,21 +85,8 @@ impl ObjectTraitInstance for SystemCallObject { } pub async fn system_call_by_id(conn: &mut PoolConnection, id: i64) -> Result { - let system_call = sqlx::query_as!( - SystemCall, - r#" - SELECT - id, - data, - transaction_hash, - system_id, - created_at as "created_at: _" - FROM system_calls WHERE id = $1 - "#, - id - ) - .fetch_one(conn) - .await?; + let system_call: SystemCall = + sqlx::query_as("SELECT * FROM system_calls WHERE id = $1").bind(id).fetch_one(conn).await?; Ok(value_mapping(system_call)) } @@ -112,21 +95,11 @@ pub async fn system_calls_by_system_id( conn: &mut PoolConnection, id: &str, ) -> Result> { - let system_calls = sqlx::query_as!( - SystemCall, - r#" - SELECT - id, - data, - transaction_hash, - system_id, - created_at as "created_at: _" - FROM system_calls WHERE system_id = $1 - "#, - id - ) - .fetch_all(conn) - .await?; + let system_calls: Vec = + sqlx::query_as("SELECT * FROM system_calls WHERE system_id = $1") + .bind(id) + .fetch_all(conn) + .await?; Ok(system_calls.into_iter().map(value_mapping).collect()) } diff --git a/crates/torii/src/graphql/types.rs b/crates/torii/src/graphql/types.rs index 7e48080812..a4b5521162 100644 --- a/crates/torii/src/graphql/types.rs +++ b/crates/torii/src/graphql/types.rs @@ -1,28 +1,23 @@ +use std::collections::HashSet; + pub struct ScalarType; // Custom scalar types impl ScalarType { - pub const U8: &'static str = "U8"; - pub const U16: &'static str = "U16"; - pub const U32: &'static str = "U32"; - pub const U64: &'static str = "U64"; - pub const U128: &'static str = "U128"; - pub const U250: &'static str = "U250"; - pub const U256: &'static str = "U256"; + pub const U8: &'static str = "u8"; + pub const U16: &'static str = "u16"; + pub const U32: &'static str = "u32"; + pub const U64: &'static str = "u64"; + pub const U128: &'static str = "u128"; + pub const U250: &'static str = "u250"; + pub const U256: &'static str = "u256"; pub const CURSOR: &'static str = "Cursor"; pub const ADDRESS: &'static str = "Address"; pub const DATE_TIME: &'static str = "DateTime"; pub const FELT: &'static str = "FieldElement"; - // NOTE: default types from async_graphql - // TypeRef::ID - // TypeRef::INT - // TypeRef::FLOAT - // TypeRef::STRING - // TypeRef::BOOLEAN - - pub fn types() -> Vec<&'static str> { - vec![ + pub fn types() -> HashSet<&'static str> { + HashSet::from([ ScalarType::U8, ScalarType::U16, ScalarType::U32, @@ -34,6 +29,6 @@ impl ScalarType { ScalarType::ADDRESS, ScalarType::DATE_TIME, ScalarType::FELT, - ] + ]) } } diff --git a/crates/torii/src/graphql/utils/mod.rs b/crates/torii/src/graphql/utils/mod.rs index 461d18db5f..e36dfe9eb1 100644 --- a/crates/torii/src/graphql/utils/mod.rs +++ b/crates/torii/src/graphql/utils/mod.rs @@ -1 +1,21 @@ pub mod value_accessor; + +pub fn remove_quotes(s: &str) -> String { + s.replace(&['\"', '\''][..], "") +} + +pub fn format_name(input: &str) -> (String, String) { + let name = input.to_lowercase(); + let type_name = input + .chars() + .enumerate() + .map(|(i, c)| { + if i == 0 { + c.to_uppercase().collect::() + } else { + c.to_lowercase().collect::() + } + }) + .collect::(); + (name, type_name) +} diff --git a/crates/torii/src/tests/components_test.rs b/crates/torii/src/tests/components_test.rs index 98c233a041..ea9bc3ab9d 100644 --- a/crates/torii/src/tests/components_test.rs +++ b/crates/torii/src/tests/components_test.rs @@ -1,14 +1,37 @@ -// #[cfg(test)] -// mod tests { -// use async_graphql_parser::parse_schema; -// use sqlx::SqlitePool; - -// use crate::graphql::entity::Entity; -// use crate::graphql::schema::{self, build_schema}; -// use crate::tests::common::run_graphql_query; - -// #[sqlx::test(migrations = "./migrations", fixtures("components"))] -// async fn test_dynamic_component(pool: SqlitePool) { -// let schema = build_schema(&pool).await; -// } -// } +#[cfg(test)] +mod tests { + use serde::Deserialize; + use sqlx::SqlitePool; + + use crate::tests::common::run_graphql_query; + + #[derive(Deserialize)] + struct Game { + name: String, + is_finished: bool, + } + + #[derive(Deserialize)] + struct Stats { + health: i64, + mana: i64, + } + + #[sqlx::test(migrations = "./migrations", fixtures("entities", "components"))] + async fn test_storage_components(pool: SqlitePool) { + let _ = pool.acquire().await; + + let query = "{ game(id: 1) { name is_finished } stats(id: 1) { health mana } }"; + let value = run_graphql_query(&pool, query).await; + + let game = value.get("game").ok_or("no game found").unwrap(); + let game: Game = serde_json::from_value(game.clone()).unwrap(); + let stats = value.get("stats").ok_or("no stats found").unwrap(); + let stats: Stats = serde_json::from_value(stats.clone()).unwrap(); + + assert!(!game.is_finished); + assert_eq!(game.name, "0x594F4C4F"); + assert_eq!(stats.health, 42); + assert_eq!(stats.mana, 69); + } +} diff --git a/crates/torii/src/tests/fixtures/components.sql b/crates/torii/src/tests/fixtures/components.sql index 89f8e72b5f..58652f3d06 100644 --- a/crates/torii/src/tests/fixtures/components.sql +++ b/crates/torii/src/tests/fixtures/components.sql @@ -1,11 +1,37 @@ -INSERT INTO components (id, name, address, class_hash, transaction_hash, storage_schema) +INSERT INTO components (id, name, address, class_hash, transaction_hash, storage_definition) VALUES ('component_1', 'Game', '0x0', '0x0', '0x0', - 'type GameComponent { isFinished: Boolean! entity: Entity! component: Component! createdAt: DateTime! }'); + '[{"name":"name","type":"FieldElement","slot":0,"offset":0},{"name":"is_finished","type":"Boolean","slot":0,"offset":0}]'); -INSERT INTO components (id, name, address, class_hash, transaction_hash, storage_schema) +INSERT INTO components (id, name, address, class_hash, transaction_hash, storage_definition) VALUES ('component_2', 'Stats', '0x0', '0x0', '0x0', - 'type StatsComponent { health: U8! entity: Entity! component: Component! createdAt: DateTime! }'); + '[{"name":"health","type":"u8","slot":0,"offset":0},{"name":"mana","type":"u8","slot":0,"offset":0}]'); + + +CREATE TABLE storage_game ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + is_finished BOOLEAN NOT NULL, + version TEXT NOT NULL, + entity_id TEXT NOT NULL, + component_id TEXT NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (entity_id) REFERENCES entities(id), + FOREIGN KEY (component_id) REFERENCES components(id) +); +CREATE TABLE storage_stats ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + health INTEGER NOT NULL, + mana INTEGER NOT NULL, + version TEXT NOT NULL, + entity_id TEXT NOT NULL, + component_id TEXT NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (entity_id) REFERENCES entities(id), + FOREIGN KEY (component_id) REFERENCES components(id) +); + +INSERT INTO storage_game (id, name, is_finished, version, entity_id, component_id, created_at) +VALUES (1, '0x594F4C4F', 0, '0.0.0', 'entity_1', 'component_1', '2023-05-19T21:04:04Z'); +INSERT INTO storage_stats (id, health, mana, version, entity_id, component_id, created_at) +VALUES (1, 42, 69, '0.0.0', 'entity_2', 'component_2', '2023-05-19T21:05:44Z'); -INSERT INTO components (id, name, address, class_hash, transaction_hash, storage_schema) -VALUES ('component_3', 'Cash', '0x0', '0x0', '0x0', - 'type CashComponent { amount: U32! entity: Entity! component: Component! createdAt: DateTime! }'); \ No newline at end of file diff --git a/crates/torii/src/tests/fixtures/seed.sql b/crates/torii/src/tests/fixtures/seed.sql index fb26652197..d1e178640b 100644 --- a/crates/torii/src/tests/fixtures/seed.sql +++ b/crates/torii/src/tests/fixtures/seed.sql @@ -2,15 +2,15 @@ INSERT INTO indexer (head) VALUES (0); /* register components and systems */ -INSERT INTO components (id, name, address, class_hash, transaction_hash, storage_schema) +INSERT INTO components (id, name, address, class_hash, transaction_hash, storage_definition) VALUES ('component_1', 'Game', '0x0', '0x0', '0x0', - 'type GameComponent { isFinished: Boolean! entity: Entity! component: Component! createdAt: DateTime! }'); -INSERT INTO components (id, name, address, class_hash, transaction_hash, storage_schema) + '[{"name":"name","type":"FieldElement","slot":0,"offset":0},{"name":"is_finished","type":"Boolean","slot":0,"offset":0}]'); +INSERT INTO components (id, name, address, class_hash, transaction_hash, storage_definition) VALUES ('component_2', 'Stats', '0x0', '0x0', '0x0', - 'type StatsComponent { health: U8! entity: Entity! component: Component! createdAt: DateTime! }'); -INSERT INTO components (id, name, address, class_hash, transaction_hash, storage_schema) + '[{"name":"health","type":"u8","slot":0,"offset":0},{"name":"mana","type":"u8","slot":0,"offset":0}]'); +INSERT INTO components (id, name, address, class_hash, transaction_hash, storage_definition) VALUES ('component_3', 'Cash', '0x0', '0x0', '0x0', - 'type CashComponent { amount: U32! entity: Entity! component: Component! createdAt: DateTime! }'); + '[{"name":"amount","type":"u32","slot":0,"offset":0}]'); INSERT INTO systems (id, name, address, class_hash, transaction_hash) VALUES ('system_1', 'SpawnGame', '0x0', '0x0', '0x0'); INSERT INTO systems (id, name, address, class_hash, transaction_hash) VALUES ('system_2', 'SpawnPlayer', '0x0', '0x0', '0x0'); INSERT INTO systems (id, name, address, class_hash, transaction_hash) VALUES ('system_3', 'SpawnPlayer', '0x0', '0x0', '0x0'); @@ -43,6 +43,7 @@ VALUES ( 'entity_3', 'Player', 'game_1', 'player_2', '0x0', '2023-05-19T21:08:12 /* tables for component storage, created at runtime by processor */ CREATE TABLE storage_game ( id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, is_finished BOOLEAN NOT NULL, version TEXT NOT NULL, entity_id TEXT NOT NULL, @@ -53,7 +54,8 @@ CREATE TABLE storage_game ( ); CREATE TABLE storage_stats ( id INTEGER PRIMARY KEY AUTOINCREMENT, - health STRING NOT NULL, + health INTEGER NOT NULL, + mana INTEGER NOT NULL, version TEXT NOT NULL, entity_id TEXT NOT NULL, component_id TEXT NOT NULL, @@ -72,13 +74,13 @@ CREATE TABLE storage_cash ( FOREIGN KEY (component_id) REFERENCES components(id) ); -INSERT INTO storage_game (id, is_finished, version, entity_id, component_id, created_at) -VALUES (1, 0, '0.0.0', 'entity_1', 'component_1', '2023-05-19T21:04:04Z'); -INSERT INTO storage_stats (id, health, version, entity_id, component_id, created_at) -VALUES (1, '100', '0.0.0', 'entity_2', 'component_2', '2023-05-19T21:05:44Z'); -INSERT INTO storage_stats (id, health, version, entity_id, component_id, created_at) -VALUES (2, '100', '0.0.0', 'entity_3', 'component_2', '2023-05-19T21:08:12Z'); +INSERT INTO storage_game (id, name, is_finished, version, entity_id, component_id, created_at) +VALUES (1, '0x594F4C4F', 0, '0.0.0', 'entity_1', 'component_1', '2023-05-19T21:04:04Z'); +INSERT INTO storage_stats (id, health, mana, version, entity_id, component_id, created_at) +VALUES (1, 100, 100, '0.0.0', 'entity_2', 'component_2', '2023-05-19T21:05:44Z'); +INSERT INTO storage_stats (id, health, mana, version, entity_id, component_id, created_at) +VALUES (2, 50, 50, '0.0.0', 'entity_3', 'component_2', '2023-05-19T21:08:12Z'); INSERT INTO storage_cash (id, amount, version, entity_id, component_id, created_at) -VALUES (1, 50, '0.0.0', 'entity_2', 'component_3', '2023-05-19T21:05:44Z'); +VALUES (1, 77, '0.0.0', 'entity_2', 'component_3', '2023-05-19T21:05:44Z'); INSERT INTO storage_cash (id, amount, version, entity_id, component_id, created_at) -VALUES (2, 50, '0.0.0', 'entity_3', 'component_3', '2023-05-19T21:08:12Z'); +VALUES (2, 88, '0.0.0', 'entity_3', 'component_3', '2023-05-19T21:08:12Z');