diff --git a/crates/torii/graphql/src/constants.rs b/crates/torii/graphql/src/constants.rs index f309b82b91..fc7d7c0285 100644 --- a/crates/torii/graphql/src/constants.rs +++ b/crates/torii/graphql/src/constants.rs @@ -12,8 +12,8 @@ pub const METADATA_TABLE: &str = "metadata"; pub const ID_COLUMN: &str = "id"; pub const EVENT_ID_COLUMN: &str = "event_id"; -pub const ENTITY_ID_COLUMN: &str = "entity_id"; -pub const EVENT_MESSAGE_ID_COLUMN: &str = "event_message_id"; +pub const ENTITY_ID_COLUMN: &str = "internal_entity_id"; +pub const EVENT_MESSAGE_ID_COLUMN: &str = "internal_event_message_id"; pub const JSON_COLUMN: &str = "json"; pub const TRANSACTION_HASH_COLUMN: &str = "transaction_hash"; diff --git a/crates/torii/graphql/src/object/connection/mod.rs b/crates/torii/graphql/src/object/connection/mod.rs index dc9931ac58..d979106333 100644 --- a/crates/torii/graphql/src/object/connection/mod.rs +++ b/crates/torii/graphql/src/object/connection/mod.rs @@ -125,8 +125,8 @@ pub fn connection_output( .map(|row| { let order_field = match order { Some(order) => { - if is_external { - format!("external_{}", order.field) + if !is_external { + format!("internal_{}", order.field) } else { order.field.to_string() } diff --git a/crates/torii/graphql/src/object/entity.rs b/crates/torii/graphql/src/object/entity.rs index 15ef06d46c..ddd0bfb622 100644 --- a/crates/torii/graphql/src/object/entity.rs +++ b/crates/torii/graphql/src/object/entity.rs @@ -67,8 +67,10 @@ impl ResolvableObject for EntityObject { } fn subscriptions(&self) -> Option> { - Some(vec![ - SubscriptionField::new("entityUpdated", TypeRef::named_nn(self.type_name()), |ctx| { + Some(vec![SubscriptionField::new( + "entityUpdated", + TypeRef::named_nn(self.type_name()), + |ctx| { SubscriptionFieldFuture::new(async move { let id = match ctx.args.get("id") { Some(id) => Some(id.string()?.to_string()), @@ -85,9 +87,9 @@ impl ResolvableObject for EntityObject { } })) }) - }) - .argument(InputValue::new("id", TypeRef::named(TypeRef::ID))), - ]) + }, + ) + .argument(InputValue::new("id", TypeRef::named(TypeRef::ID)))]) } } @@ -144,21 +146,16 @@ fn model_union_field() -> Field { })?; let type_mapping = build_type_mapping(&namespace, &schema); - // but the table name for the model data is the unhashed model name - let data: ValueMapping = match model_data_recursive_query( - &mut conn, - ENTITY_ID_COLUMN, - vec![get_tag(&namespace, &name)], - &entity_id, - &[], - &type_mapping, - false, - ) - .await? - { - Value::Object(map) => map, - _ => unreachable!(), - }; + // Get the table name + let table_name = get_tag(&namespace, &name); + + // Fetch the row data + let query = format!("SELECT * FROM [{}] WHERE internal_entity_id = ?", table_name); + let row = + sqlx::query(&query).bind(&entity_id).fetch_one(&mut *conn).await?; + + // Use value_mapping_from_row to handle nested structures + let data = value_mapping_from_row(&row, &type_mapping, true)?; results.push(FieldValue::with_type( FieldValue::owned_any(data), @@ -173,116 +170,3 @@ fn model_union_field() -> Field { }) }) } - -// TODO: flatten query -#[async_recursion] -pub async fn model_data_recursive_query( - conn: &mut PoolConnection, - entity_id_column: &str, - path_array: Vec, - entity_id: &str, - indexes: &[i64], - type_mapping: &TypeMapping, - is_list: bool, -) -> sqlx::Result { - // For nested types, we need to remove prefix in path array - let namespace = format!("{}_", path_array[0]); - let table_name = &path_array.join("$").replace(&namespace, ""); - let mut query = - format!("SELECT * FROM [{}] WHERE {entity_id_column} = '{}' ", table_name, entity_id); - for (column_idx, index) in indexes.iter().enumerate() { - query.push_str(&format!("AND idx_{} = {} ", column_idx, index)); - } - - let rows = sqlx::query(&query).fetch_all(conn.as_mut()).await?; - if rows.is_empty() { - return Ok(Value::List(vec![])); - } - - let value_mapping: Value; - let mut nested_value_mappings = Vec::new(); - - for (idx, row) in rows.iter().enumerate() { - let mut nested_value_mapping = value_mapping_from_row(row, type_mapping, true)?; - - for (field_name, type_data) in type_mapping { - if let TypeData::Nested((_, nested_mapping)) = type_data { - let mut nested_path = path_array.clone(); - nested_path.push(field_name.to_string()); - - let nested_values = model_data_recursive_query( - conn, - entity_id_column, - nested_path, - entity_id, - &if is_list { - let mut indexes = indexes.to_vec(); - indexes.push(idx as i64); - indexes - } else { - indexes.to_vec() - }, - nested_mapping, - false, - ) - .await?; - - nested_value_mapping.insert(Name::new(field_name), nested_values); - } else if let TypeData::List(inner) = type_data { - let mut nested_path = path_array.clone(); - nested_path.push(field_name.to_string()); - - let data = match model_data_recursive_query( - conn, - entity_id_column, - nested_path, - entity_id, - // this might need to be changed to support 2d+ arrays - &if is_list { - let mut indexes = indexes.to_vec(); - indexes.push(idx as i64); - indexes - } else { - indexes.to_vec() - }, - &IndexMap::from([(Name::new("data"), *inner.clone())]), - true, - ) - .await? - { - // map our list which uses a data field as a place holder - // for all elements to get the elemnt directly - Value::List(data) => data - .iter() - .map(|v| match v { - Value::Object(map) => map.get(&Name::new("data")).unwrap().clone(), - ty => unreachable!( - "Expected Value::Object for list \"data\" field, got {:?}", - ty - ), - }) - .collect(), - Value::Object(map) => map.get(&Name::new("data")).unwrap().clone(), - ty => { - unreachable!( - "Expected Value::List or Value::Object for list, got {:?}", - ty - ); - } - }; - - nested_value_mapping.insert(Name::new(field_name), data); - } - } - - nested_value_mappings.push(Value::Object(nested_value_mapping)); - } - - if is_list { - value_mapping = Value::List(nested_value_mappings); - } else { - value_mapping = nested_value_mappings.pop().unwrap(); - } - - Ok(value_mapping) -} diff --git a/crates/torii/graphql/src/object/event_message.rs b/crates/torii/graphql/src/object/event_message.rs index 34508f8c1b..945bde5bb8 100644 --- a/crates/torii/graphql/src/object/event_message.rs +++ b/crates/torii/graphql/src/object/event_message.rs @@ -10,7 +10,6 @@ use tokio_stream::StreamExt; use torii_core::simple_broker::SimpleBroker; use torii_core::types::EventMessage; -use super::entity::model_data_recursive_query; use super::inputs::keys_input::keys_argument; use super::{BasicObject, ResolvableObject, TypeMapping, ValueMapping}; use crate::constants::{ @@ -19,8 +18,9 @@ use crate::constants::{ }; use crate::mapping::ENTITY_TYPE_MAPPING; use crate::object::{resolve_many, resolve_one}; -use crate::query::build_type_mapping; +use crate::query::{build_type_mapping, value_mapping_from_row}; use crate::utils; + #[derive(Debug)] pub struct EventMessageObject; @@ -75,8 +75,6 @@ impl ResolvableObject for EventMessageObject { Some(id) => Some(id.string()?.to_string()), None => None, }; - // if id is None, then subscribe to all entities - // if id is Some, then subscribe to only the entity with that id Ok(SimpleBroker::::subscribe().filter_map( move |entity: EventMessage| { if id.is_none() || id == Some(entity.id.clone()) { @@ -84,7 +82,6 @@ impl ResolvableObject for EventMessageObject { entity, )))) } else { - // id != entity.id , then don't send anything, still listening None } }, @@ -129,7 +126,6 @@ fn model_union_field() -> Field { let entity_id = utils::extract::(indexmap, "id")?; // fetch name from the models table - // using the model id (hashed model name) let model_ids: Vec<(String, String, String)> = sqlx::query_as( "SELECT namespace, name, schema FROM models @@ -150,21 +146,21 @@ fn model_union_field() -> Field { })?; let type_mapping = build_type_mapping(&namespace, &schema); - // but the table name for the model data is the unhashed model name - let data: ValueMapping = match model_data_recursive_query( - &mut conn, - EVENT_MESSAGE_ID_COLUMN, - vec![get_tag(&namespace, &name)], - &entity_id, - &[], - &type_mapping, - false, - ) - .await? - { - Value::Object(map) => map, - _ => unreachable!(), - }; + // Get the table name + let table_name = get_tag(&namespace, &name); + + // Fetch the row data + let query = format!( + "SELECT * FROM [{}] WHERE internal_event_message_id = ?", + table_name + ); + let row = sqlx::query(&query) + .bind(&entity_id) + .fetch_one(&mut *conn) + .await?; + + // Use value_mapping_from_row to handle nested structures + let data = value_mapping_from_row(&row, &type_mapping, true)?; results.push(FieldValue::with_type( FieldValue::owned_any(data), diff --git a/crates/torii/graphql/src/object/mod.rs b/crates/torii/graphql/src/object/mod.rs index b4201e0989..b2a8d4063e 100644 --- a/crates/torii/graphql/src/object/mod.rs +++ b/crates/torii/graphql/src/object/mod.rs @@ -261,7 +261,7 @@ pub fn resolve_one( let id: String = extract::(ctx.args.as_index_map(), &id_column.to_case(Case::Camel))?; let data = fetch_single_row(&mut conn, &table_name, &id_column, &id).await?; - let model = value_mapping_from_row(&data, &type_mapping, false)?; + let model = value_mapping_from_row(&data, &type_mapping, true)?; Ok(Some(Value::Object(model))) }) }) @@ -310,7 +310,7 @@ pub fn resolve_many( &order, &id_column, total_count, - false, + true, page_info, )?; diff --git a/crates/torii/graphql/src/object/model_data.rs b/crates/torii/graphql/src/object/model_data.rs index 73e9319b70..3d93ea031a 100644 --- a/crates/torii/graphql/src/object/model_data.rs +++ b/crates/torii/graphql/src/object/model_data.rs @@ -1,6 +1,7 @@ use async_graphql::dynamic::{Enum, Field, FieldFuture, InputObject, Object, TypeRef}; use async_graphql::Value; use dojo_types::naming::get_tag; +use dojo_types::schema::Ty; use sqlx::Pool; use sqlx::Sqlite; @@ -25,16 +26,17 @@ pub struct ModelDataObject { pub plural_name: String, pub type_name: String, pub type_mapping: TypeMapping, + pub schema: Ty, pub where_input: WhereInputObject, pub order_input: OrderInputObject, } impl ModelDataObject { - pub fn new(name: String, type_name: String, type_mapping: TypeMapping) -> Self { + pub fn new(name: String, type_name: String, type_mapping: TypeMapping, schema: Ty) -> Self { let where_input = WhereInputObject::new(type_name.as_str(), &type_mapping); let order_input = OrderInputObject::new(type_name.as_str(), &type_mapping); let plural_name = format!("{}Models", name); - Self { name, plural_name, type_name, type_mapping, where_input, order_input } + Self { name, plural_name, type_name, type_mapping, schema, where_input, order_input } } } @@ -104,7 +106,7 @@ impl ResolvableObject for ModelDataObject { let (data, page_info) = fetch_multiple_rows( &mut conn, &table_name, - EVENT_ID_COLUMN, + "internal_event_id", &None, &order, &filters, @@ -116,7 +118,7 @@ impl ResolvableObject for ModelDataObject { &data, &type_mapping, &order, - EVENT_ID_COLUMN, + "internal_event_id", total_count, true, page_info, diff --git a/crates/torii/graphql/src/query/mod.rs b/crates/torii/graphql/src/query/mod.rs index 990bf8b616..49e8b3e954 100644 --- a/crates/torii/graphql/src/query/mod.rs +++ b/crates/torii/graphql/src/query/mod.rs @@ -112,29 +112,61 @@ pub fn value_mapping_from_row( types: &TypeMapping, is_external: bool, ) -> sqlx::Result { - let mut value_mapping = types - .iter() - .filter(|(_, type_data)| { - type_data.is_simple() - // ignore Enum fields because the column is not stored in this row. we inejct it later - // && !(type_data.type_ref().to_string() == "Enum") - }) - .map(|(field_name, type_data)| { - let mut value = - fetch_value(row, field_name, &type_data.type_ref().to_string(), is_external)?; - - // handles felt arrays stored as string (ex: keys) - if let (TypeRef::List(_), Value::String(s)) = (&type_data.type_ref(), &value) { - let mut felts: Vec<_> = s.split(SQL_FELT_DELIMITER).map(Value::from).collect(); - felts.pop(); // removes empty item - value = Value::List(felts); + println!("types: {:?}", types); + fn build_value_mapping( + row: &SqliteRow, + types: &TypeMapping, + prefix: &str, + is_external: bool, + ) -> sqlx::Result { + let mut value_mapping = ValueMapping::new(); + + for (field_name, type_data) in types { + let column_name = if prefix.is_empty() { + field_name.to_string() + } else { + format!("{}.{}", prefix, field_name) + }; + + match type_data { + TypeData::Simple(type_ref) => { + let mut value = fetch_value(row, &column_name, &type_ref.to_string(), is_external)?; + + // handles felt arrays stored as string (ex: keys) + if let (TypeRef::List(_), Value::String(s)) = (type_ref, &value) { + let mut felts: Vec<_> = s.split(SQL_FELT_DELIMITER).map(Value::from).collect(); + felts.pop(); // removes empty item + value = Value::List(felts); + } + + value_mapping.insert(Name::new(field_name), value); + } + TypeData::List(inner) => { + let value = fetch_value(row, &column_name, "String", is_external)?; + if let Value::String(json_str) = value { + let array_value: Value = serde_json::from_str(&json_str) + .map_err(|e| sqlx::Error::Protocol(format!("JSON parse error: {}", e)))?; + value_mapping.insert(Name::new(field_name), array_value); + } + } + TypeData::Nested((_, nested_mapping)) => { + let nested_values = build_value_mapping( + row, + nested_mapping, + &column_name, + is_external, + )?; + value_mapping.insert(Name::new(field_name), Value::Object(nested_values)); + } } + } - Ok((Name::new(field_name), value)) - }) - .collect::>()?; + Ok(value_mapping) + } + + let mut value_mapping = build_value_mapping(row, types, "", is_external)?; - // entity_id is not part of a model's type_mapping but needed to relate to parent entity + // Add internal entity ID if present if let Ok(entity_id) = row.try_get::(ENTITY_ID_COLUMN) { value_mapping.insert(Name::new(INTERNAL_ENTITY_ID_KEY), Value::from(entity_id)); } else if let Ok(event_message_id) = row.try_get::(EVENT_MESSAGE_ID_COLUMN) { @@ -150,12 +182,18 @@ fn fetch_value( type_name: &str, is_external: bool, ) -> sqlx::Result { - let column_name = if is_external { - format!("external_{}", field_name) + let mut column_name = if !is_external { + format!("internal_{}", field_name) } else { - field_name.to_string().to_case(Case::Snake) + field_name.to_string() }; + // for enum options, remove the ".option" suffix to get the variant + // through the enum itself field name + if type_name == "Enum" && column_name.ends_with(".option") { + column_name = column_name.trim_end_matches(".option").to_string(); + } + match Primitive::from_str(type_name) { // fetch boolean Ok(Primitive::Bool(_)) => { diff --git a/crates/torii/graphql/src/schema.rs b/crates/torii/graphql/src/schema.rs index 95a776275e..8e1aba65c3 100644 --- a/crates/torii/graphql/src/schema.rs +++ b/crates/torii/graphql/src/schema.rs @@ -156,6 +156,7 @@ async fn build_objects(pool: &SqlitePool) -> Result<(Vec, Vec Result<(), Error> { let mut closed_stream = Vec::new(); - println!("entity: {:?}", entity); let hashed = Felt::from_str(&entity.id).map_err(ParseError::FromStr)?; // sometimes for some reason keys isxx empty. investigate the issue let keys = entity