diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c2cc26c..beac9e43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,15 @@ and this project adheres to [Semantic Versioning](http://semver.org/). * add `update_mutation` - This module enabled the update mutation for entities. The update mutation takes an entity data object with a filter condition object, + This module enables the update mutation for entities. The update mutation takes an entity data object with a filter condition object, applies the update to the database and returns the modified entities. + +* add `delete_mutation` + + This module enables the delete mutation for entities. The delete mutation takes an entity condition filter object, + deletes the selected entities from database and returns the number of deleted items. + ## 1.0.2 - Pending * add `create_one_mutation` diff --git a/README.md b/README.md index f06c17a1..036a4961 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ * Order by any column * Guard fields, queries or relations * Rename fields +* Mutations (create, update, delete) (Right now there is no mutation, but it's on our plan!) diff --git a/examples/mysql/tests/mutation_tests.rs b/examples/mysql/tests/mutation_tests.rs index c3aad593..aca98c00 100644 --- a/examples/mysql/tests/mutation_tests.rs +++ b/examples/mysql/tests/mutation_tests.rs @@ -532,3 +532,91 @@ async fn test_update_mutation() { "#, ); } + +#[tokio::test] +async fn test_delete_mutation() { + let schema = get_schema().await; + + assert_eq( + schema + .execute( + r#" + { + filmText(filters: { filmId: { gte: 998 } }, orderBy: { filmId: ASC }) { + nodes { + filmId + title + } + } + } + "#, + ) + .await, + r#" + { + "filmText": { + "nodes": [ + { + "filmId": 998, + "title": "ZHIVAGO CORE" + }, + { + "filmId": 999, + "title": "ZOOLANDER FICTION" + }, + { + "filmId": 1000, + "title": "ZORRO ARK" + } + ] + } + } + "#, + ); + + assert_eq( + schema + .execute( + r#" + mutation { + filmTextDelete(filter: { filmId: { gte: 999 } }) + } + "#, + ) + .await, + r#" + { + "filmTextDelete": 2 + } + "#, + ); + + assert_eq( + schema + .execute( + r#" + { + filmText(filters: { filmId: { gte: 998 } }, orderBy: { filmId: ASC }) { + nodes { + filmId + title + } + } + } + "#, + ) + .await, + r#" + { + "filmText": { + "nodes": [ + { + "filmId": 998, + "title": "ZHIVAGO CORE" + } + ] + } + } + "#, + ); +} diff --git a/examples/postgres/tests/mutation_tests.rs b/examples/postgres/tests/mutation_tests.rs index 87931b28..dc7d92bb 100644 --- a/examples/postgres/tests/mutation_tests.rs +++ b/examples/postgres/tests/mutation_tests.rs @@ -218,7 +218,7 @@ async fn test_create_batch_mutation() { .execute( r#" { - language { + language(filters: { languageId: { lte: 8 } }, orderBy: { languageId: ASC }) { nodes { languageId name @@ -269,11 +269,10 @@ async fn test_create_batch_mutation() { mutation { languageCreateBatch( data: [ - { languageId: 7, name: "Swedish", lastUpdate: "2030-01-12 21:50:05" } - { languageId: 8, name: "Danish", lastUpdate: "2030-01-12 21:50:05" } + { languageId: 1, name: "Swedish", lastUpdate: "2030-01-12 21:50:05" } + { languageId: 1, name: "Danish", lastUpdate: "2030-01-12 21:50:05" } ] ) { - languageId name } } @@ -284,11 +283,9 @@ async fn test_create_batch_mutation() { { "languageCreateBatch": [ { - "languageId": 7, "name": "Swedish " }, { - "languageId": 8, "name": "Danish " } ] @@ -301,9 +298,8 @@ async fn test_create_batch_mutation() { .execute( r#" { - language { + language(filters: { languageId: { lte: 8 } }, orderBy: { languageId: ASC }) { nodes { - languageId name } } @@ -317,35 +313,27 @@ async fn test_create_batch_mutation() { "language": { "nodes": [ { - "languageId": 1, "name": "English " }, { - "languageId": 2, "name": "Italian " }, { - "languageId": 3, "name": "Japanese " }, { - "languageId": 4, "name": "Mandarin " }, { - "languageId": 5, "name": "French " }, { - "languageId": 6, "name": "German " }, { - "languageId": 7, "name": "Swedish " }, { - "languageId": 8, "name": "Danish " } ] @@ -501,3 +489,143 @@ async fn test_update_mutation() { "#, ); } + +#[tokio::test] +async fn test_delete_mutation() { + let schema = get_schema().await; + + assert_eq( + schema + .execute( + r#" + { + language(filters: { languageId: { gte: 9 } }, orderBy: { languageId: ASC }) { + nodes { + languageId + } + } + } + "#, + ) + .await, + r#" + { + "language": { + "nodes": [] + } + } + "#, + ); + + assert_eq( + schema + .execute( + r#" + mutation { + languageCreateBatch( + data: [ + { languageId: 9, name: "9", lastUpdate: "2030-01-12 21:50:05" } + { languageId: 10, name: "10", lastUpdate: "2030-01-12 21:50:05" } + { languageId: 11, name: "11", lastUpdate: "2030-01-12 21:50:05" } + ] + ) { + languageId + } + } + "#, + ) + .await, + r#" + { + "languageCreateBatch": [ + { + "languageId": 9 + }, + { + "languageId": 10 + }, + { + "languageId": 11 + } + ] + } + "#, + ); + + assert_eq( + schema + .execute( + r#" + { + language(filters: { languageId: { gte: 9 } }, orderBy: { languageId: ASC }) { + nodes { + languageId + } + } + } + "#, + ) + .await, + r#" + { + "language": { + "nodes": [ + { + "languageId": 9 + }, + { + "languageId": 10 + }, + { + "languageId": 11 + } + ] + } + } + "#, + ); + + assert_eq( + schema + .execute( + r#" + mutation { + languageDelete(filter: { languageId: { gte: 10 } }) + } + "#, + ) + .await, + r#" + { + "languageDelete": 2 + } + "#, + ); + + assert_eq( + schema + .execute( + r#" + { + language(filters: { languageId: { gte: 9 } }, orderBy: { languageId: ASC }) { + nodes { + languageId + } + } + } + "#, + ) + .await, + r#" + { + "language": { + "nodes": [ + { + "languageId": 9 + } + ] + } + } + "#, + ); +} \ No newline at end of file diff --git a/examples/sqlite/tests/mutation_tests.rs b/examples/sqlite/tests/mutation_tests.rs index 4b4dfe0f..564acd3b 100644 --- a/examples/sqlite/tests/mutation_tests.rs +++ b/examples/sqlite/tests/mutation_tests.rs @@ -220,10 +220,10 @@ async fn test_create_batch_mutation() { .execute( r#" { - filmText{ + filmText(filters: { filmId: { lte: 5 } }, orderBy: { filmId: ASC }) { nodes { filmId - title + title description } } @@ -282,32 +282,32 @@ async fn test_create_batch_mutation() { .execute( r#" { - filmText{ + filmText(filters: { filmId: { lte: 5 } }, orderBy: { filmId: ASC }) { nodes { filmId - title + title description } } - } + } "#, ) .await, r#" { "filmText": { - "nodes": [ - { - "filmId": 1, - "title": "TEST 1", - "description": "TEST DESC 1" - }, - { - "filmId": 2, - "title": "TEST 2", - "description": "TEST DESC 2" - } - ] + "nodes": [ + { + "filmId": 1, + "title": "TEST 1", + "description": "TEST DESC 1" + }, + { + "filmId": 2, + "title": "TEST 2", + "description": "TEST DESC 2" + } + ] } } "#, @@ -492,3 +492,108 @@ async fn test_update_mutation() { "#, ); } + +#[tokio::test] +async fn test_delete_mutation() { + let schema = get_schema().await; + + schema.execute( + r#" + mutation { + filmTextCreateBatch( + data: [ + { filmId: 6, title: "TEST 6", description: "TEST DESC 6" } + { filmId: 7, title: "TEST 7", description: "TEST DESC 7" } + { filmId: 8, title: "TEST 8", description: "TEST DESC 8" } + ] + ) { + filmId + } + } + "# + ) + .await; + + assert_eq( + schema + .execute( + r#" + { + filmText(filters: { filmId: { gte: 6 } }, orderBy: { filmId: ASC }) { + nodes { + filmId + title + } + } + } + "#, + ) + .await, + r#" + { + "filmText": { + "nodes": [ + { + "filmId": 6, + "title": "TEST 6" + }, + { + "filmId": 7, + "title": "TEST 7" + }, + { + "filmId": 8, + "title": "TEST 8" + } + ] + } + } + "#, + ); + + assert_eq( + schema + .execute( + r#" + mutation { + filmTextDelete(filter: { filmId: { gte: 7 } }) + } + "#, + ) + .await, + r#" + { + "filmTextDelete": 2 + } + "#, + ); + + assert_eq( + schema + .execute( + r#" + { + filmText(filters: { filmId: { gte: 6 } }, orderBy: { filmId: ASC }) { + nodes { + filmId + title + } + } + } + "#, + ) + .await, + r#" + { + "filmText": { + "nodes": [ + { + "filmId": 6, + "title": "TEST 6" + } + ] + } + } + "#, + ); +} diff --git a/src/builder.rs b/src/builder.rs index af42a602..2de75389 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -7,10 +7,11 @@ use sea_orm::{ActiveEnum, ActiveModelTrait, EntityTrait, IntoActiveModel}; use crate::{ ActiveEnumBuilder, ActiveEnumFilterInputBuilder, BuilderContext, ConnectionObjectBuilder, CursorInputBuilder, EdgeObjectBuilder, EntityCreateBatchMutationBuilder, - EntityCreateOneMutationBuilder, EntityInputBuilder, EntityObjectBuilder, - EntityQueryFieldBuilder, EntityUpdateMutationBuilder, FilterInputBuilder, FilterTypesMapHelper, - OffsetInputBuilder, OneToManyLoader, OneToOneLoader, OrderByEnumBuilder, OrderInputBuilder, - PageInfoObjectBuilder, PageInputBuilder, PaginationInfoObjectBuilder, PaginationInputBuilder, + EntityCreateOneMutationBuilder, EntityDeleteMutationBuilder, EntityInputBuilder, + EntityObjectBuilder, EntityQueryFieldBuilder, EntityUpdateMutationBuilder, FilterInputBuilder, + FilterTypesMapHelper, OffsetInputBuilder, OneToManyLoader, OneToOneLoader, OrderByEnumBuilder, + OrderInputBuilder, PageInfoObjectBuilder, PageInputBuilder, PaginationInfoObjectBuilder, + PaginationInputBuilder, }; /// The Builder is used to create the Schema for GraphQL @@ -155,6 +156,12 @@ impl Builder { }; let update_mutation = entity_update_mutation_builder.to_field::(); self.mutations.push(update_mutation); + + let entity_delete_mutation_builder = EntityDeleteMutationBuilder { + context: self.context, + }; + let delete_mutation = entity_delete_mutation_builder.to_field::(); + self.mutations.push(delete_mutation); } pub fn register_entity_dataloader_one_to_one(mut self, _entity: T, spawner: S) -> Self diff --git a/src/builder_context.rs b/src/builder_context.rs index c0ef88dd..7beaf99c 100644 --- a/src/builder_context.rs +++ b/src/builder_context.rs @@ -1,9 +1,10 @@ use crate::{ ActiveEnumConfig, ActiveEnumFilterInputConfig, ConnectionObjectConfig, CursorInputConfig, EdgeObjectConfig, EntityCreateBatchMutationConfig, EntityCreateOneMutationConfig, - EntityInputConfig, EntityObjectConfig, EntityQueryFieldConfig, EntityUpdateMutationConfig, - FilterInputConfig, OffsetInputConfig, OrderByEnumConfig, OrderInputConfig, - PageInfoObjectConfig, PageInputConfig, PaginationInfoObjectConfig, PaginationInputConfig, + EntityDeleteMutationConfig, EntityInputConfig, EntityObjectConfig, EntityQueryFieldConfig, + EntityUpdateMutationConfig, FilterInputConfig, OffsetInputConfig, OrderByEnumConfig, + OrderInputConfig, PageInfoObjectConfig, PageInputConfig, PaginationInfoObjectConfig, + PaginationInputConfig, }; pub mod guards; @@ -44,6 +45,7 @@ pub struct BuilderContext { pub entity_create_one_mutation: EntityCreateOneMutationConfig, pub entity_create_batch_mutation: EntityCreateBatchMutationConfig, pub entity_update_mutation: EntityUpdateMutationConfig, + pub entity_delete_mutation: EntityDeleteMutationConfig, pub entity_input: EntityInputConfig, diff --git a/src/mutation/entity_delete_mutation.rs b/src/mutation/entity_delete_mutation.rs new file mode 100644 index 00000000..fa1ed0cd --- /dev/null +++ b/src/mutation/entity_delete_mutation.rs @@ -0,0 +1,91 @@ +use async_graphql::dynamic::{Field, FieldFuture, InputValue, TypeRef}; +use sea_orm::{ + ActiveModelTrait, DatabaseConnection, DeleteResult, EntityTrait, IntoActiveModel, QueryFilter, +}; + +use crate::{ + get_filter_conditions, BuilderContext, EntityObjectBuilder, EntityQueryFieldBuilder, + FilterInputBuilder, +}; + +/// The configuration structure of EntityDeleteMutationBuilder +pub struct EntityDeleteMutationConfig { + /// suffix that is appended on delete mutations + pub mutation_suffix: String, + + /// name for `filter` field + pub filter_field: String, +} + +impl std::default::Default for EntityDeleteMutationConfig { + fn default() -> Self { + Self { + mutation_suffix: "Delete".into(), + filter_field: "filter".into(), + } + } +} + +/// This builder produces the delete mutation for an entity +pub struct EntityDeleteMutationBuilder { + pub context: &'static BuilderContext, +} + +impl EntityDeleteMutationBuilder { + /// used to get mutation name for a SeaORM entity + pub fn type_name(&self) -> String + where + T: EntityTrait, + ::Model: Sync, + { + let entity_query_field_builder = EntityQueryFieldBuilder { + context: self.context, + }; + format!( + "{}{}", + entity_query_field_builder.type_name::(), + self.context.entity_delete_mutation.mutation_suffix + ) + } + + /// used to get the delete mutation field for a SeaORM entity + pub fn to_field(&self) -> Field + where + T: EntityTrait, + ::Model: Sync, + ::Model: IntoActiveModel, + A: ActiveModelTrait + sea_orm::ActiveModelBehavior + std::marker::Send, + { + let entity_filter_input_builder = FilterInputBuilder { + context: self.context, + }; + let entity_object_builder = EntityObjectBuilder { + context: self.context, + }; + let object_name: String = entity_object_builder.type_name::(); + + let context = self.context; + + Field::new( + self.type_name::(), + TypeRef::named_nn(TypeRef::INT), + move |ctx| { + FieldFuture::new(async move { + let db = ctx.data::()?; + + let filters = ctx.args.get(&context.entity_delete_mutation.filter_field); + let filter_condition = get_filter_conditions::(context, filters); + + let res: DeleteResult = + T::delete_many().filter(filter_condition).exec(db).await?; + + Ok(Some(async_graphql::Value::from(res.rows_affected))) + }) + }, + ) + .argument(InputValue::new( + &context.entity_delete_mutation.filter_field, + TypeRef::named(entity_filter_input_builder.type_name(&object_name)), + )) + } +} diff --git a/src/mutation/entity_update_mutation.rs b/src/mutation/entity_update_mutation.rs index 11db0b83..96c307ab 100644 --- a/src/mutation/entity_update_mutation.rs +++ b/src/mutation/entity_update_mutation.rs @@ -54,7 +54,6 @@ impl EntityUpdateMutationBuilder { } /// used to get the update mutation field for a SeaORM entity - /// used to get the create mutation field for a SeaORM entity pub fn to_field(&self) -> Field where T: EntityTrait, @@ -88,11 +87,10 @@ impl EntityUpdateMutationBuilder { let filters = ctx.args.get(&context.entity_update_mutation.filter_field); let filter_condition = get_filter_conditions::(context, filters); - println!("{:?}", filter_condition); let value_accessor = ctx .args - .get(&context.entity_create_one_mutation.data_field) + .get(&context.entity_update_mutation.data_field) .unwrap(); let input_object = &value_accessor.object()?; let active_model = prepare_active_model::( diff --git a/src/mutation/mod.rs b/src/mutation/mod.rs index 5ce2ffc5..d765a448 100644 --- a/src/mutation/mod.rs +++ b/src/mutation/mod.rs @@ -6,3 +6,6 @@ pub use entity_create_batch_mutation::*; pub mod entity_update_mutation; pub use entity_update_mutation::*; + +pub mod entity_delete_mutation; +pub use entity_delete_mutation::*;