diff --git a/crates/torii/graphql/src/object/entity.rs b/crates/torii/graphql/src/object/entity.rs index 7d3c66bf3e..8170ca5903 100644 --- a/crates/torii/graphql/src/object/entity.rs +++ b/crates/torii/graphql/src/object/entity.rs @@ -11,9 +11,10 @@ use torii_core::simple_broker::SimpleBroker; use torii_core::types::Entity; use super::connection::{connection_arguments, connection_output, parse_connection_arguments}; +use super::inputs::keys_input::{keys_argument, parse_keys_argument}; use super::{ObjectTrait, TypeMapping, ValueMapping}; use crate::mapping::ENTITY_TYPE_MAPPING; -use crate::query::constants::ENTITY_TABLE; +use crate::query::constants::{ENTITY_TABLE, EVENT_ID_COLUMN}; use crate::query::data::{count_rows, fetch_multiple_rows}; use crate::query::{type_mapping_query, value_mapping_from_row}; use crate::types::TypeData; @@ -71,12 +72,12 @@ impl ObjectTrait for EntityObject { FieldFuture::new(async move { let mut conn = ctx.data::>()?.acquire().await?; let connection = parse_connection_arguments(&ctx)?; - let keys = extract::>(ctx.args.as_index_map(), "keys").ok(); + let keys = parse_keys_argument(&ctx)?; let total_count = count_rows(&mut conn, ENTITY_TABLE, &keys, &None).await?; let data = fetch_multiple_rows( &mut conn, ENTITY_TABLE, - "event_id", + EVENT_ID_COLUMN, &keys, &None, &None, @@ -87,7 +88,7 @@ impl ObjectTrait for EntityObject { &data, &ENTITY_TYPE_MAPPING, &None, - "event_id", + EVENT_ID_COLUMN, total_count, false, )?; @@ -95,10 +96,10 @@ impl ObjectTrait for EntityObject { Ok(Some(Value::Object(results))) }) }, - ) - .argument(InputValue::new("keys", TypeRef::named_list(TypeRef::STRING))); + ); field = connection_arguments(field); + field = keys_argument(field); Some(field) } diff --git a/crates/torii/graphql/src/object/event.rs b/crates/torii/graphql/src/object/event.rs index ae6f3d0f76..855e2fbc5d 100644 --- a/crates/torii/graphql/src/object/event.rs +++ b/crates/torii/graphql/src/object/event.rs @@ -2,10 +2,12 @@ use async_graphql::dynamic::{Field, FieldFuture, TypeRef}; use async_graphql::Value; use sqlx::{Pool, Sqlite}; +use super::connection::{connection_arguments, connection_output, parse_connection_arguments}; +use super::inputs::keys_input::{keys_argument, parse_keys_argument}; use super::{ObjectTrait, TypeMapping, ValueMapping}; use crate::mapping::{EVENT_TYPE_MAPPING, SYSTEM_CALL_TYPE_MAPPING}; -use crate::query::constants::EVENT_TABLE; -use crate::query::data::fetch_single_row; +use crate::query::constants::{EVENT_TABLE, ID_COLUMN}; +use crate::query::data::{count_rows, fetch_multiple_rows, fetch_single_row}; use crate::query::value_mapping_from_row; use crate::utils::extract; @@ -32,6 +34,46 @@ impl ObjectTrait for EventObject { None } + fn resolve_many(&self) -> Option { + let mut field = Field::new( + self.name().1, + TypeRef::named(format!("{}Connection", self.type_name())), + |ctx| { + FieldFuture::new(async move { + let mut conn = ctx.data::>()?.acquire().await?; + let connection = parse_connection_arguments(&ctx)?; + let keys = parse_keys_argument(&ctx)?; + let total_count = count_rows(&mut conn, EVENT_TABLE, &keys, &None).await?; + let data = fetch_multiple_rows( + &mut conn, + EVENT_TABLE, + ID_COLUMN, + &keys, + &None, + &None, + &connection, + ) + .await?; + let results = connection_output( + &data, + &EVENT_TYPE_MAPPING, + &None, + ID_COLUMN, + total_count, + false, + )?; + + Ok(Some(Value::Object(results))) + }) + }, + ); + + field = connection_arguments(field); + field = keys_argument(field); + + Some(field) + } + fn related_fields(&self) -> Option> { Some(vec![Field::new("systemCall", TypeRef::named_nn("SystemCall"), |ctx| { FieldFuture::new(async move { diff --git a/crates/torii/graphql/src/object/inputs/keys_input.rs b/crates/torii/graphql/src/object/inputs/keys_input.rs new file mode 100644 index 0000000000..a1a4098258 --- /dev/null +++ b/crates/torii/graphql/src/object/inputs/keys_input.rs @@ -0,0 +1,31 @@ +use async_graphql::dynamic::{Field, InputValue, ResolverContext, TypeRef}; +use async_graphql::Error; + +use crate::utils::extract; + +pub fn keys_argument(field: Field) -> Field { + field.argument(InputValue::new("keys", TypeRef::named_list(TypeRef::STRING))) +} + +pub fn parse_keys_argument(ctx: &ResolverContext<'_>) -> Result>, Error> { + let keys = extract::>(ctx.args.as_index_map(), "keys"); + + if let Ok(keys) = keys { + if !keys.iter().all(|s| is_hex_or_star(s)) { + return Err("Key parts can only be hex string or wild card `*`".into()); + } + + return Ok(Some(keys)); + } + + Ok(None) +} + +fn is_hex_or_star(s: &str) -> bool { + if s == "*" { + return true; + } + let s = if let Some(stripped) = s.strip_prefix("0x") { stripped } else { s }; + + s.chars().all(|c| c.is_ascii_hexdigit()) +} diff --git a/crates/torii/graphql/src/object/inputs/mod.rs b/crates/torii/graphql/src/object/inputs/mod.rs index d27f2fdf26..6bc305905f 100644 --- a/crates/torii/graphql/src/object/inputs/mod.rs +++ b/crates/torii/graphql/src/object/inputs/mod.rs @@ -2,6 +2,7 @@ use async_graphql::dynamic::{Enum, InputObject}; use super::TypeMapping; +pub mod keys_input; pub mod order_input; pub mod where_input; diff --git a/crates/torii/graphql/src/query/constants.rs b/crates/torii/graphql/src/query/constants.rs index 40c1ae6317..49802ed183 100644 --- a/crates/torii/graphql/src/query/constants.rs +++ b/crates/torii/graphql/src/query/constants.rs @@ -9,6 +9,7 @@ pub const SYSTEM_TABLE: &str = "systems"; 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 INTERNAL_ENTITY_ID_KEY: &str = "$entity_id$"; diff --git a/crates/torii/graphql/src/query/data.rs b/crates/torii/graphql/src/query/data.rs index d790cc8672..2e79171f93 100644 --- a/crates/torii/graphql/src/query/data.rs +++ b/crates/torii/graphql/src/query/data.rs @@ -14,25 +14,7 @@ pub async fn count_rows( filters: &Option>, ) -> Result { let mut query = format!("SELECT COUNT(*) FROM {}", table_name); - let mut conditions = Vec::new(); - - if let Some(keys) = keys { - let keys_str = keys.join("/"); - conditions.push(format!("keys LIKE '{}/%'", keys_str)); - } - - if let Some(filters) = filters { - for filter in filters { - let condition = match filter.value { - FilterValue::Int(i) => format!("{} {} {}", filter.field, filter.comparator, i), - FilterValue::String(ref s) => { - format!("{} {} '{}'", filter.field, filter.comparator, s) - } - }; - - conditions.push(condition); - } - } + let conditions = build_conditions(keys, filters); if !conditions.is_empty() { query.push_str(&format!(" WHERE {}", conditions.join(" AND "))); @@ -61,12 +43,7 @@ pub async fn fetch_multiple_rows( filters: &Option>, connection: &ConnectionArguments, ) -> Result> { - let mut conditions = Vec::new(); - - if let Some(keys) = &keys { - let keys_str = keys.join("/"); - conditions.push(format!("keys LIKE '{}/%'", keys_str)); - } + let mut conditions = build_conditions(keys, filters); if let Some(after_cursor) = &connection.after { conditions.push(handle_cursor(after_cursor, order, CursorDirection::After, id_column)?); @@ -76,10 +53,6 @@ pub async fn fetch_multiple_rows( conditions.push(handle_cursor(before_cursor, order, CursorDirection::Before, id_column)?); } - if let Some(filters) = filters { - conditions.extend(filters.iter().map(handle_filter)); - } - let mut query = format!("SELECT * FROM {}", table_name); if !conditions.is_empty() { query.push_str(&format!(" WHERE {}", conditions.join(" AND "))); @@ -147,9 +120,20 @@ fn handle_cursor( } } -fn handle_filter(filter: &Filter) -> String { - match &filter.value { - FilterValue::Int(i) => format!("{} {} {}", filter.field, filter.comparator, i), - FilterValue::String(s) => format!("{} {} '{}'", filter.field, filter.comparator, s), +fn build_conditions(keys: &Option>, filters: &Option>) -> Vec { + let mut conditions = Vec::new(); + + if let Some(keys) = &keys { + let keys_str = keys.join("/").replace('*', "%"); + conditions.push(format!("keys LIKE '{}/%'", keys_str)); + } + + if let Some(filters) = filters { + conditions.extend(filters.iter().map(|filter| match &filter.value { + FilterValue::Int(i) => format!("{} {} {}", filter.field, filter.comparator, i), + FilterValue::String(s) => format!("{} {} '{}'", filter.field, filter.comparator, s), + })); } + + conditions } diff --git a/crates/torii/graphql/src/tests/types-test/src/systems.cairo b/crates/torii/graphql/src/tests/types-test/src/systems.cairo index be57506a21..eb622f4625 100644 --- a/crates/torii/graphql/src/tests/types-test/src/systems.cairo +++ b/crates/torii/graphql/src/tests/types-test/src/systems.cairo @@ -12,12 +12,25 @@ mod records { use types_test::{seed, random}; use super::IRecords; + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + RecordLogged: RecordLogged + } + + #[derive(Drop, starknet::Event)] + struct RecordLogged { + record_id: u32, + type_u8: u8, + type_felt: felt252, + } + #[external(v0)] impl RecordsImpl of IRecords { fn create(self: @ContractState, num_records: u8) { let world = self.world_dispatcher.read(); let mut record_idx = 0; - + loop { if record_idx == num_records { break (); @@ -80,6 +93,8 @@ mod records { ); record_idx += 1; + + emit!(world, RecordLogged { record_id, type_u8: record_idx.into(), type_felt }); }; return (); }