diff --git a/bin/torii/src/main.rs b/bin/torii/src/main.rs index 7621e23bce..9406c51e25 100644 --- a/bin/torii/src/main.rs +++ b/bin/torii/src/main.rs @@ -29,6 +29,7 @@ use tokio_stream::StreamExt; use torii_core::engine::{Engine, EngineConfig, Processors}; use torii_core::processors::metadata_update::MetadataUpdateProcessor; use torii_core::processors::register_model::RegisterModelProcessor; +use torii_core::processors::store_del_record::StoreDelRecordProcessor; use torii_core::processors::store_set_record::StoreSetRecordProcessor; use torii_core::processors::store_transaction::StoreTransactionProcessor; use torii_core::simple_broker::SimpleBroker; @@ -144,6 +145,7 @@ async fn main() -> anyhow::Result<()> { Box::new(RegisterModelProcessor), Box::new(StoreSetRecordProcessor), Box::new(MetadataUpdateProcessor), + Box::new(StoreDelRecordProcessor), ], transaction: vec![Box::new(StoreTransactionProcessor)], ..Processors::default() diff --git a/crates/dojo-core/src/world.cairo b/crates/dojo-core/src/world.cairo index 1271a3a621..4432f17dbe 100644 --- a/crates/dojo-core/src/world.cairo +++ b/crates/dojo-core/src/world.cairo @@ -47,7 +47,7 @@ trait IWorld { #[starknet::interface] trait IUpgradeableWorld { - fn upgrade(ref self: T, new_class_hash : ClassHash); + fn upgrade(ref self: T, new_class_hash: ClassHash); } #[starknet::interface] @@ -70,15 +70,15 @@ mod world { use starknet::{ get_caller_address, get_contract_address, get_tx_info, contract_address::ContractAddressIntoFelt252, ClassHash, Zeroable, ContractAddress, - syscalls::{deploy_syscall, emit_event_syscall, replace_class_syscall}, SyscallResult, SyscallResultTrait, - SyscallResultTraitImpl + syscalls::{deploy_syscall, emit_event_syscall, replace_class_syscall}, SyscallResult, + SyscallResultTrait, SyscallResultTraitImpl }; use dojo::database; use dojo::database::index::WhereCondition; use dojo::executor::{IExecutorDispatcher, IExecutorDispatcherTrait}; use dojo::world::{IWorldDispatcher, IWorld, IUpgradeableWorld}; - + use dojo::components::upgradeable::{IUpgradeableDispatcher, IUpgradeableDispatcherTrait}; const NAME_ENTRYPOINT: felt252 = @@ -112,7 +112,7 @@ mod world { struct WorldUpgraded { class_hash: ClassHash, } - + #[derive(Drop, starknet::Event)] struct ContractDeployed { salt: felt252, @@ -641,17 +641,18 @@ mod world { /// # Arguments /// /// * `new_class_hash` - The new world class hash. - fn upgrade(ref self: ContractState, new_class_hash : ClassHash){ + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) { assert(new_class_hash.is_non_zero(), 'invalid class_hash'); - assert(IWorld::is_owner(@self, get_tx_info().unbox().account_contract_address, WORLD), 'only owner can upgrade'); + assert( + IWorld::is_owner(@self, get_tx_info().unbox().account_contract_address, WORLD), + 'only owner can upgrade' + ); // upgrade to new_class_hash replace_class_syscall(new_class_hash).unwrap(); // emit Upgrade Event - EventEmitter::emit( - ref self, WorldUpgraded {class_hash: new_class_hash } - ); + EventEmitter::emit(ref self, WorldUpgraded { class_hash: new_class_hash }); } } diff --git a/crates/torii/core/src/processors/mod.rs b/crates/torii/core/src/processors/mod.rs index d503671b7d..a6c2d2c868 100644 --- a/crates/torii/core/src/processors/mod.rs +++ b/crates/torii/core/src/processors/mod.rs @@ -8,9 +8,13 @@ use crate::sql::Sql; pub mod metadata_update; pub mod register_model; +pub mod store_del_record; pub mod store_set_record; pub mod store_transaction; +const MODEL_INDEX: usize = 0; +const NUM_KEYS_INDEX: usize = 1; + #[async_trait] pub trait EventProcessor

where diff --git a/crates/torii/core/src/processors/store_del_record.rs b/crates/torii/core/src/processors/store_del_record.rs new file mode 100644 index 0000000000..3417152b58 --- /dev/null +++ b/crates/torii/core/src/processors/store_del_record.rs @@ -0,0 +1,58 @@ +use anyhow::{Error, Ok, Result}; +use async_trait::async_trait; +use dojo_world::contracts::model::ModelReader; +use dojo_world::contracts::world::WorldContractReader; +use starknet::core::types::{BlockWithTxs, Event, InvokeTransactionReceipt}; +use starknet::core::utils::parse_cairo_short_string; +use starknet::providers::Provider; +use tracing::info; + +use super::EventProcessor; +use crate::processors::{MODEL_INDEX, NUM_KEYS_INDEX}; +use crate::sql::Sql; + +#[derive(Default)] +pub struct StoreDelRecordProcessor; + +#[async_trait] +impl

EventProcessor

for StoreDelRecordProcessor +where + P: Provider + Send + Sync, +{ + fn event_key(&self) -> String { + "StoreDelRecord".to_string() + } + + fn validate(&self, event: &Event) -> bool { + if event.keys.len() > 1 { + info!( + "invalid keys for event {}: {}", + >::event_key(self), + >::event_keys_as_string(self, event), + ); + return false; + } + true + } + + async fn process( + &self, + _world: &WorldContractReader

, + db: &mut Sql, + _block: &BlockWithTxs, + _transaction_receipt: &InvokeTransactionReceipt, + _event_id: &str, + event: &Event, + ) -> Result<(), Error> { + let name = parse_cairo_short_string(&event.data[MODEL_INDEX])?; + info!("store delete record: {}", name); + + let model = db.model(&name).await?; + + let keys_start = NUM_KEYS_INDEX + 1; + let keys = event.data[keys_start..].to_vec(); + let entity = model.schema().await?; + db.delete_entity(keys, entity).await?; + Ok(()) + } +} diff --git a/crates/torii/core/src/processors/store_set_record.rs b/crates/torii/core/src/processors/store_set_record.rs index 9e538dc428..1da641a5be 100644 --- a/crates/torii/core/src/processors/store_set_record.rs +++ b/crates/torii/core/src/processors/store_set_record.rs @@ -8,14 +8,12 @@ use starknet::providers::Provider; use tracing::info; use super::EventProcessor; +use crate::processors::{MODEL_INDEX, NUM_KEYS_INDEX}; use crate::sql::Sql; #[derive(Default)] pub struct StoreSetRecordProcessor; -const MODEL_INDEX: usize = 0; -const NUM_KEYS_INDEX: usize = 1; - #[async_trait] impl

EventProcessor

for StoreSetRecordProcessor where diff --git a/crates/torii/core/src/query_queue.rs b/crates/torii/core/src/query_queue.rs index 7103425df3..106a57cf17 100644 --- a/crates/torii/core/src/query_queue.rs +++ b/crates/torii/core/src/query_queue.rs @@ -27,6 +27,10 @@ impl QueryQueue { self.queue.push_back((statement.into(), arguments)); } + pub fn push_front>(&mut self, statement: S, arguments: Vec) { + self.queue.push_front((statement.into(), arguments)); + } + pub async fn execute_all(&mut self) -> sqlx::Result { let mut total_affected = 0_u64; let mut tx = self.pool.begin().await?; diff --git a/crates/torii/core/src/sql.rs b/crates/torii/core/src/sql.rs index 33df27b40c..946f0994ce 100644 --- a/crates/torii/core/src/sql.rs +++ b/crates/torii/core/src/sql.rs @@ -149,11 +149,12 @@ impl Sql { Ok(()) } - pub fn delete_entity(&mut self, model: String, key: FieldElement) { - let model = Argument::String(model); - let id = Argument::FieldElement(key); - - self.query_queue.enqueue("DELETE FROM ? WHERE id = ?", vec![model, id]); + pub async fn delete_entity(&mut self, keys: Vec, entity: Ty) -> Result<()> { + let entity_id = format!("{:#x}", poseidon_hash_many(&keys)); + let path = vec![entity.name()]; + self.build_delete_entity_queries_recursive(path, &entity_id, &entity); + self.query_queue.execute_all().await?; + Ok(()) } pub fn set_metadata(&mut self, resource: &FieldElement, uri: &str) { @@ -344,7 +345,6 @@ impl Sql { if let Ty::Struct(_) = &member.ty { let mut path_clone = path.clone(); path_clone.push(member.name.clone()); - self.build_set_entity_queries_recursive( path_clone, event_id, entity_id, &member.ty, ); @@ -364,6 +364,39 @@ impl Sql { } } + fn build_delete_entity_queries_recursive( + &mut self, + path: Vec, + entity_id: &str, + entity: &Ty, + ) { + match entity { + Ty::Struct(s) => { + let table_id = path.join("$"); + let statement = format!("DELETE FROM [{table_id}] WHERE entity_id = ?"); + self.query_queue + .push_front(statement, vec![Argument::String(entity_id.to_string())]); + for member in s.children.iter() { + if let Ty::Struct(_) = &member.ty { + let mut path_clone = path.clone(); + path_clone.push(member.name.clone()); + self.build_delete_entity_queries_recursive( + path_clone, entity_id, &member.ty, + ); + } + } + } + Ty::Enum(e) => { + for child in e.options.iter() { + let mut path_clone = path.clone(); + path_clone.push(child.name.clone()); + self.build_delete_entity_queries_recursive(path_clone, entity_id, &child.ty); + } + } + _ => {} + } + } + fn build_model_query(&mut self, path: Vec, model: &Ty, model_idx: i64) { let table_id = path.join("$"); let mut indices = Vec::new(); diff --git a/crates/torii/graphql/src/tests/entities_test.rs b/crates/torii/graphql/src/tests/entities_test.rs index d2e3f31879..2948b58da0 100644 --- a/crates/torii/graphql/src/tests/entities_test.rs +++ b/crates/torii/graphql/src/tests/entities_test.rs @@ -1,6 +1,5 @@ #[cfg(test)] mod tests { - use anyhow::Result; use async_graphql::dynamic::Schema; use serde_json::Value; @@ -235,7 +234,6 @@ mod tests { let subrecord: Subrecord = serde_json::from_value(models[0].clone()).unwrap(); assert_eq!(&subrecord.__typename, "Subrecord"); assert_eq!(subrecord.subrecord_id, 1); - Ok(()) } } diff --git a/crates/torii/graphql/src/tests/mod.rs b/crates/torii/graphql/src/tests/mod.rs index 9d2c71e16f..fda7c9e444 100644 --- a/crates/torii/graphql/src/tests/mod.rs +++ b/crates/torii/graphql/src/tests/mod.rs @@ -27,6 +27,7 @@ use tokio::sync::broadcast; use tokio_stream::StreamExt; use torii_core::engine::{Engine, EngineConfig, Processors}; use torii_core::processors::register_model::RegisterModelProcessor; +use torii_core::processors::store_del_record::StoreDelRecordProcessor; use torii_core::processors::store_set_record::StoreSetRecordProcessor; use torii_core::sql::Sql; @@ -139,6 +140,14 @@ pub struct Subrecord { pub entity: Option, } +#[derive(Deserialize, Debug, PartialEq)] +pub struct RecordSibling { + pub __typename: String, + pub record_id: u32, + pub random_u8: u8, + pub entity: Option, +} + #[derive(Deserialize, Debug, PartialEq)] pub struct Social { pub name: String, @@ -270,13 +279,14 @@ pub async fn spinup_types_test() -> Result { let manifest = Manifest::load_from_remote(&provider, migration.world_address().unwrap()).await.unwrap(); - // Execute `create` and insert 10 records into storage + // Execute `create` and insert 11 records into storage let records_contract = manifest.contracts.iter().find(|contract| contract.name.eq("records")).unwrap(); + let record_contract_address = records_contract.address.unwrap(); let InvokeTransactionResult { transaction_hash } = account .execute(vec![Call { calldata: vec![FieldElement::from_str("0xa").unwrap()], - to: records_contract.address.unwrap(), + to: record_contract_address, selector: selector!("create"), }]) .send() @@ -285,13 +295,30 @@ pub async fn spinup_types_test() -> Result { TransactionWaiter::new(transaction_hash, &provider).await?; + // Execute `delete` and delete Record with id 20 + let InvokeTransactionResult { transaction_hash } = account + .execute(vec![Call { + calldata: vec![FieldElement::from_str("0x14").unwrap()], + to: record_contract_address, + selector: selector!("delete"), + }]) + .send() + .await + .unwrap(); + + TransactionWaiter::new(transaction_hash, &provider).await?; + let (shutdown_tx, _) = broadcast::channel(1); let mut engine = Engine::new( world, &mut db, &provider, Processors { - event: vec![Box::new(RegisterModelProcessor), Box::new(StoreSetRecordProcessor)], + event: vec![ + Box::new(RegisterModelProcessor), + Box::new(StoreSetRecordProcessor), + Box::new(StoreDelRecordProcessor), + ], ..Processors::default() }, EngineConfig::default(), diff --git a/crates/torii/graphql/src/tests/models_test.rs b/crates/torii/graphql/src/tests/models_test.rs index cc04ded6d2..ecf9a1721d 100644 --- a/crates/torii/graphql/src/tests/models_test.rs +++ b/crates/torii/graphql/src/tests/models_test.rs @@ -8,7 +8,71 @@ mod tests { use starknet_crypto::FieldElement; use crate::schema::build_schema; - use crate::tests::{run_graphql_query, spinup_types_test, Connection, Record}; + use crate::tests::{ + run_graphql_query, spinup_types_test, Connection, Record, RecordSibling, Subrecord, + }; + + async fn record_sibling_query(schema: &Schema, arg: &str) -> Value { + let query = format!( + r#" + {{ + recordSiblingModels {} {{ + totalCount + edges {{ + cursor + node {{ + __typename + record_id + random_u8 + }} + }} + pageInfo {{ + hasPreviousPage + hasNextPage + startCursor + endCursor + }} + }} + }} + "#, + arg, + ); + + let result = run_graphql_query(schema, &query).await; + result.get("recordSiblingModels").ok_or("recordSiblingModels not found").unwrap().clone() + } + + async fn subrecord_model_query(schema: &Schema, arg: &str) -> Value { + let query = format!( + r#" + {{ + subrecordModels {} {{ + totalCount + edges {{ + cursor + node {{ + __typename + record_id + subrecord_id + type_u8 + random_u8 + }} + }} + pageInfo {{ + hasPreviousPage + hasNextPage + startCursor + endCursor + }} + }} + }} + "#, + arg, + ); + + let result = run_graphql_query(schema, &query).await; + result.get("subrecordModels").ok_or("subrecordModels not found").unwrap().clone() + } async fn records_model_query(schema: &Schema, arg: &str) -> Value { let query = format!( @@ -115,12 +179,12 @@ mod tests { // *** WHERE FILTER TESTING *** - // where filter EQ on u8 - let records = records_model_query(&schema, "(where: { type_u8: 0 })").await; + // where filter EQ on record_id + let records = records_model_query(&schema, "(where: { record_id: 0 })").await; let connection: Connection = serde_json::from_value(records).unwrap(); let first_record = connection.edges.first().unwrap(); assert_eq!(connection.total_count, 1); - assert_eq!(first_record.node.record_id, 0); + assert_eq!(first_record.node.type_u8, 0); // where filter GTE on u16 let records = records_model_query(&schema, "(where: { type_u16GTE: 5 })").await; @@ -318,22 +382,33 @@ mod tests { let connection: Connection = serde_json::from_value(records).unwrap(); assert_eq!(connection.edges.len(), 0); - let result = run_graphql_query( - &schema, - r#" - { - recordSiblingModels { - edges { - node { - __typename - } - } - } - } - "#, - ) - .await; - assert!(result.get("recordSiblingModels").is_some()); + // *** SIBLING TESTING *** + let sibling = record_sibling_query(&schema, "").await; + let connection: Connection = serde_json::from_value(sibling).unwrap(); + assert_eq!(connection.total_count, 10); + + // *** SUBRECORD TESTING *** + let subrecord = subrecord_model_query(&schema, "").await; + let connection: Connection = serde_json::from_value(subrecord).unwrap(); + let last_record = connection.edges.first().unwrap(); + assert_eq!(last_record.node.record_id, 18); + assert_eq!(last_record.node.subrecord_id, 19); + + // *** DELETE TESTING *** + // where filter EQ on record_id, test Record with id 20 is deleted + let records = records_model_query(&schema, "(where: { record_id: 20 })").await; + let connection: Connection = serde_json::from_value(records).unwrap(); + assert_eq!(connection.edges.len(), 0); + + // where filter GTE on record_id, test Sibling with id 20 is deleted + let sibling = record_sibling_query(&schema, "(where: { record_id: 20 })").await; + let connection: Connection = serde_json::from_value(sibling).unwrap(); + assert_eq!(connection.edges.len(), 0); + + // where filter GTE on record_id, test Subrecord with id 20 is deleted + let subrecord = subrecord_model_query(&schema, "(where: { record_id: 20 })").await; + let connection: Connection = serde_json::from_value(subrecord).unwrap(); + assert_eq!(connection.edges.len(), 0); Ok(()) } diff --git a/crates/torii/types-test/Scarb.lock b/crates/torii/types-test/Scarb.lock index 5cb79dfe8e..86f181d66b 100644 --- a/crates/torii/types-test/Scarb.lock +++ b/crates/torii/types-test/Scarb.lock @@ -15,7 +15,7 @@ source = "git+https://github.com/dojoengine/dojo?tag=v0.3.11#1e651b5d4d3b79b14a7 [[package]] name = "types_test" -version = "0.4.4" +version = "0.5.0" dependencies = [ "dojo", ] diff --git a/crates/torii/types-test/src/contracts.cairo b/crates/torii/types-test/src/contracts.cairo index 166ad6d26a..ed41161ec2 100644 --- a/crates/torii/types-test/src/contracts.cairo +++ b/crates/torii/types-test/src/contracts.cairo @@ -3,12 +3,15 @@ use starknet::{ContractAddress, ClassHash}; #[starknet::interface] trait IRecords { fn create(self: @TContractState, num_records: u8); + fn delete(self: @TContractState, record_id: u32); } #[dojo::contract] mod records { use starknet::{ContractAddress, get_caller_address}; - use types_test::models::{Record, RecordSibling, Subrecord, Nested, NestedMore, NestedMost, Depth}; + use types_test::models::{ + Record, RecordSibling, Subrecord, Nested, NestedMore, NestedMost, Depth + }; use types_test::{seed, random}; use super::IRecords; @@ -48,10 +51,7 @@ mod records { 0, 0xffffffffffffffffffffffffffffffff_u128 ); - let composite_u256 = u256 { - low: random_u128, - high: random_u128 - }; + let composite_u256 = u256 { low: random_u128, high: random_u128 }; let record_id = world.uuid(); let subrecord_id = world.uuid(); @@ -92,22 +92,16 @@ mod records { } }, type_nested_one: NestedMost { - depth: Depth::One, - type_number: 1, - type_string: 1, + depth: Depth::One, type_number: 1, type_string: 1, }, type_nested_two: NestedMost { - depth: Depth::One, - type_number: 2, - type_string: 2, + depth: Depth::One, type_number: 2, type_string: 2, }, random_u8, random_u128, composite_u256 }, - RecordSibling { - record_id, random_u8 - }, + RecordSibling { record_id, random_u8 }, Subrecord { record_id, subrecord_id, type_u8: record_idx.into(), random_u8, } @@ -116,9 +110,20 @@ mod records { record_idx += 1; - emit!(world, RecordLogged { record_id, type_u8: record_idx.into(), type_felt, random_u128 }); + emit!( + world, + RecordLogged { record_id, type_u8: record_idx.into(), type_felt, random_u128 } + ); }; return (); } + // Implemment fn delete, input param: record_id + fn delete(self: @ContractState, record_id: u32) { + let world = self.world_dispatcher.read(); + let (record, record_sibling) = get!(world, record_id, (Record, RecordSibling)); + let subrecord_id = record_id + 1; + let subrecord = get!(world, (record_id, subrecord_id), (Subrecord)); + delete!(world, (record, record_sibling, subrecord)); + } } } diff --git a/examples/spawn-and-move/src/actions.cairo b/examples/spawn-and-move/src/actions.cairo index ea94cde00f..747dfdd510 100644 --- a/examples/spawn-and-move/src/actions.cairo +++ b/examples/spawn-and-move/src/actions.cairo @@ -7,7 +7,6 @@ trait IActions { #[dojo::contract] mod actions { use super::IActions; - use starknet::{ContractAddress, get_caller_address}; use dojo_examples::models::{Position, Moves, Direction, Vec2}; use dojo_examples::utils::next_position;