diff --git a/CHANGELOG.md b/CHANGELOG.md index 22f9e6e4..4c2cc26c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -## 1.0.2 - 2023-10-09 +## 1.0.3 - Pending + +* add `update_mutation` + + This module enabled 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. + +## 1.0.2 - Pending * add `create_one_mutation` @@ -41,7 +48,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). * start error handling -## 1.0.1 - 2023-03-25 +## 1.0.1 - Pending * slim down code generation for the `query_root.rs` file of a generated project @@ -49,6 +56,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/). * update examples +## 1.0.0 - Pending +======= + ## 1.0.0 - 2023-03-25 Introduction the functional API of Seaography. Warning, this version has breaking changes, but it was a sacrifice in order to make the project easier to maintain. With this version we have support for field guards and field renames. diff --git a/examples/mysql/tests/mutation_tests.rs b/examples/mysql/tests/mutation_tests.rs index 73e0b407..b0c4e2e3 100644 --- a/examples/mysql/tests/mutation_tests.rs +++ b/examples/mysql/tests/mutation_tests.rs @@ -359,3 +359,182 @@ async fn test_create_batch_mutation() { "#, ) } + +#[tokio::test] +async fn test_update_mutation() { + let schema = get_schema().await; + + assert_eq( + schema + .execute( + r#" + { + country(pagination: { page: { limit: 10, page: 0 } }) { + nodes { + country + countryId + } + } + } + "#, + ) + .await, + r#" + { + "country": { + "nodes": [ + { + "country": "Afghanistan", + "countryId": 1 + }, + { + "country": "Algeria", + "countryId": 2 + }, + { + "country": "American Samoa", + "countryId": 3 + }, + { + "country": "Angola", + "countryId": 4 + }, + { + "country": "Anguilla", + "countryId": 5 + }, + { + "country": "Argentina", + "countryId": 6 + }, + { + "country": "Armenia", + "countryId": 7 + }, + { + "country": "Australia", + "countryId": 8 + }, + { + "country": "Austria", + "countryId": 9 + }, + { + "country": "Azerbaijan", + "countryId": 10 + } + ] + } + } + "#, + ); + + assert_eq( + schema + .execute( + r#" + mutation { + countryUpdate( + data: { country: "[DELETED]" } + filter: { countryId: { lt: 6 } } + ) { + countryId + country + } + } + "#, + ) + .await, + r#" + { + "countryUpdate": [ + { + "countryId": 1, + "country": "[DELETED]" + }, + { + "countryId": 2, + "country": "[DELETED]" + }, + { + "countryId": 3, + "country": "[DELETED]" + }, + { + "countryId": 4, + "country": "[DELETED]" + }, + { + "countryId": 5, + "country": "[DELETED]" + } + ] + } + "#, + ); + + assert_eq( + schema + .execute( + r#" + { + country(pagination: { page: { limit: 10, page: 0 } }) { + nodes { + country + countryId + } + } + } + "#, + ) + .await, + r#" + { + "country": { + "nodes": [ + { + "country": "[DELETED]", + "countryId": 1 + }, + { + "country": "[DELETED]", + "countryId": 2 + }, + { + "country": "[DELETED]", + "countryId": 3 + }, + { + "country": "[DELETED]", + "countryId": 4 + }, + { + "country": "[DELETED]", + "countryId": 5 + }, + { + "country": "Argentina", + "countryId": 6 + }, + { + "country": "Armenia", + "countryId": 7 + }, + { + "country": "Australia", + "countryId": 8 + }, + { + "country": "Austria", + "countryId": 9 + }, + { + "country": "Azerbaijan", + "countryId": 10 + } + ] + } + } + "#, + ); +} diff --git a/examples/postgres/tests/mutation_tests.rs b/examples/postgres/tests/mutation_tests.rs index 5a8ceeb0..97b13a31 100644 --- a/examples/postgres/tests/mutation_tests.rs +++ b/examples/postgres/tests/mutation_tests.rs @@ -62,7 +62,7 @@ async fn test_simple_insert_one() { filmActorCreateOne(data: { actorId: 1, filmId: 2, lastUpdate: "2030-01-01 11:11:11"}) { actorId filmId - __typename + __typename } } "#, @@ -361,3 +361,150 @@ async fn test_create_batch_mutation() { "#, ) } + +#[tokio::test] +async fn test_update_mutation() { + let schema = get_schema().await; + + assert_eq( + schema + .execute( + r#" + { + country(filters: { countryId: { lt: 7 } }, orderBy: { countryId: ASC }) { + nodes { + country + countryId + } + } + } + "#, + ) + .await, + r#" + { + "country": { + "nodes": [ + { + "country": "Afghanistan", + "countryId": 1 + }, + { + "country": "Algeria", + "countryId": 2 + }, + { + "country": "American Samoa", + "countryId": 3 + }, + { + "country": "Angola", + "countryId": 4 + }, + { + "country": "Anguilla", + "countryId": 5 + }, + { + "country": "Argentina", + "countryId": 6 + } + ] + } + } + "#, + ); + + assert_eq( + schema + .execute( + r#" + mutation { + countryUpdate( + data: { country: "[DELETED]" } + filter: { countryId: { lt: 6 } } + ) { + countryId + country + } + } + "#, + ) + .await, + r#" + { + "countryUpdate": [ + { + "countryId": 1, + "country": "[DELETED]" + }, + { + "countryId": 2, + "country": "[DELETED]" + }, + { + "countryId": 3, + "country": "[DELETED]" + }, + { + "countryId": 4, + "country": "[DELETED]" + }, + { + "countryId": 5, + "country": "[DELETED]" + } + ] + } + "#, + ); + + assert_eq( + schema + .execute( + r#" + { + country(filters: { countryId: { lt: 7 } }, orderBy: { countryId: ASC }) { + nodes { + country + countryId + } + } + } + "#, + ) + .await, + r#" + { + "country": { + "nodes": [ + { + "country": "[DELETED]", + "countryId": 1 + }, + { + "country": "[DELETED]", + "countryId": 2 + }, + { + "country": "[DELETED]", + "countryId": 3 + }, + { + "country": "[DELETED]", + "countryId": 4 + }, + { + "country": "[DELETED]", + "countryId": 5 + }, + { + "country": "Argentina", + "countryId": 6 + } + ] + } + } + "#, + ); +} diff --git a/examples/sqlite/tests/mutation_tests.rs b/examples/sqlite/tests/mutation_tests.rs index 270c7098..5a24bd7c 100644 --- a/examples/sqlite/tests/mutation_tests.rs +++ b/examples/sqlite/tests/mutation_tests.rs @@ -322,3 +322,182 @@ async fn test_create_batch_mutation() { "#, ); } + +#[tokio::test] +async fn test_update_mutation() { + let schema = get_schema().await; + + assert_eq( + schema + .execute( + r#" + { + country(pagination: { page: { limit: 10, page: 0 } }) { + nodes { + country + countryId + } + } + } + "#, + ) + .await, + r#" + { + "country": { + "nodes": [ + { + "country": "Afghanistan", + "countryId": 1 + }, + { + "country": "Algeria", + "countryId": 2 + }, + { + "country": "American Samoa", + "countryId": 3 + }, + { + "country": "Angola", + "countryId": 4 + }, + { + "country": "Anguilla", + "countryId": 5 + }, + { + "country": "Argentina", + "countryId": 6 + }, + { + "country": "Armenia", + "countryId": 7 + }, + { + "country": "Australia", + "countryId": 8 + }, + { + "country": "Austria", + "countryId": 9 + }, + { + "country": "Azerbaijan", + "countryId": 10 + } + ] + } + } + "#, + ); + + assert_eq( + schema + .execute( + r#" + mutation { + countryUpdate( + data: { country: "[DELETED]" } + filter: { countryId: { lt: 6 } } + ) { + countryId + country + } + } + "#, + ) + .await, + r#" + { + "countryUpdate": [ + { + "countryId": 1, + "country": "[DELETED]" + }, + { + "countryId": 2, + "country": "[DELETED]" + }, + { + "countryId": 3, + "country": "[DELETED]" + }, + { + "countryId": 4, + "country": "[DELETED]" + }, + { + "countryId": 5, + "country": "[DELETED]" + } + ] + } + "#, + ); + + assert_eq( + schema + .execute( + r#" + { + country(pagination: { page: { limit: 10, page: 0 } }) { + nodes { + country + countryId + } + } + } + "#, + ) + .await, + r#" + { + "country": { + "nodes": [ + { + "country": "[DELETED]", + "countryId": 1 + }, + { + "country": "[DELETED]", + "countryId": 2 + }, + { + "country": "[DELETED]", + "countryId": 3 + }, + { + "country": "[DELETED]", + "countryId": 4 + }, + { + "country": "[DELETED]", + "countryId": 5 + }, + { + "country": "Argentina", + "countryId": 6 + }, + { + "country": "Armenia", + "countryId": 7 + }, + { + "country": "Australia", + "countryId": 8 + }, + { + "country": "Austria", + "countryId": 9 + }, + { + "country": "Azerbaijan", + "countryId": 10 + } + ] + } + } + "#, + ); +} diff --git a/src/builder.rs b/src/builder.rs index b333deb6..dcb81885 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -5,9 +5,9 @@ use crate::{ ActiveEnumBuilder, ActiveEnumFilterInputBuilder, BuilderContext, ConnectionObjectBuilder, CursorInputBuilder, EdgeObjectBuilder, EntityCreateBatchMutationBuilder, EntityCreateOneMutationBuilder, EntityInputBuilder, EntityObjectBuilder, - EntityQueryFieldBuilder, FilterInputBuilder, FilterTypesMapHelper, OffsetInputBuilder, - OrderByEnumBuilder, OrderInputBuilder, PageInfoObjectBuilder, PageInputBuilder, - PaginationInfoObjectBuilder, PaginationInputBuilder, + EntityQueryFieldBuilder, EntityUpdateMutationBuilder, FilterInputBuilder, FilterTypesMapHelper, + OffsetInputBuilder, OrderByEnumBuilder, OrderInputBuilder, PageInfoObjectBuilder, + PageInputBuilder, PaginationInfoObjectBuilder, PaginationInputBuilder, }; /// The Builder is used to create the Schema for GraphQL @@ -107,15 +107,12 @@ impl Builder { context: self.context, }; - if self.context.entity_input.unified { - let entity_input_object = entity_input_builder.insert_input_object::(); - self.inputs.push(entity_input_object); - } else { - let entity_insert_input_object = entity_input_builder.insert_input_object::(); - let entity_update_input_object = entity_input_builder.update_input_object::(); - self.inputs - .extend(vec![entity_insert_input_object, entity_update_input_object]); - } + let entity_insert_input_object = entity_input_builder.insert_input_object::(); + let entity_update_input_object = entity_input_builder.update_input_object::(); + self.inputs + .extend(vec![entity_insert_input_object, entity_update_input_object]); + + // create one mutation let entity_create_one_mutation_builder = EntityCreateOneMutationBuilder { context: self.context, @@ -123,12 +120,20 @@ impl Builder { let create_one_mutation = entity_create_one_mutation_builder.to_field::(); self.mutations.push(create_one_mutation); + // create batch mutation let entity_create_batch_mutation_builder: EntityCreateBatchMutationBuilder = EntityCreateBatchMutationBuilder { context: self.context, }; let create_batch_mutation = entity_create_batch_mutation_builder.to_field::(); self.mutations.push(create_batch_mutation); + + // update mutation + let entity_update_mutation_builder = EntityUpdateMutationBuilder { + context: self.context, + }; + let update_mutation = entity_update_mutation_builder.to_field::(); + self.mutations.push(update_mutation); } /// used to register a new enumeration to the builder context diff --git a/src/builder_context.rs b/src/builder_context.rs index 8aa1a2a5..c0ef88dd 100644 --- a/src/builder_context.rs +++ b/src/builder_context.rs @@ -1,9 +1,9 @@ use crate::{ ActiveEnumConfig, ActiveEnumFilterInputConfig, ConnectionObjectConfig, CursorInputConfig, EdgeObjectConfig, EntityCreateBatchMutationConfig, EntityCreateOneMutationConfig, - EntityInputConfig, EntityObjectConfig, EntityQueryFieldConfig, FilterInputConfig, - OffsetInputConfig, OrderByEnumConfig, OrderInputConfig, PageInfoObjectConfig, PageInputConfig, - PaginationInfoObjectConfig, PaginationInputConfig, + EntityInputConfig, EntityObjectConfig, EntityQueryFieldConfig, EntityUpdateMutationConfig, + FilterInputConfig, OffsetInputConfig, OrderByEnumConfig, OrderInputConfig, + PageInfoObjectConfig, PageInputConfig, PaginationInfoObjectConfig, PaginationInputConfig, }; pub mod guards; @@ -43,6 +43,8 @@ pub struct BuilderContext { pub entity_create_one_mutation: EntityCreateOneMutationConfig, pub entity_create_batch_mutation: EntityCreateBatchMutationConfig, + pub entity_update_mutation: EntityUpdateMutationConfig, + pub entity_input: EntityInputConfig, pub guards: GuardsConfig, diff --git a/src/inputs/entity_input.rs b/src/inputs/entity_input.rs index 02c70418..241f106e 100644 --- a/src/inputs/entity_input.rs +++ b/src/inputs/entity_input.rs @@ -7,8 +7,6 @@ use crate::{BuilderContext, EntityObjectBuilder, SeaResult, TypesMapHelper}; /// The configuration structure of EntityInputBuilder pub struct EntityInputConfig { - /// if true both insert and update are the same input object - pub unified: bool, /// suffix that is appended on insert input objects pub insert_suffix: String, /// names of "{entity}.{column}" you want to skip the insert input to be generated @@ -22,7 +20,6 @@ pub struct EntityInputConfig { impl std::default::Default for EntityInputConfig { fn default() -> Self { EntityInputConfig { - unified: true, insert_suffix: "InsertInput".into(), insert_skips: Vec::new(), update_suffix: "UpdateInput".into(), @@ -56,9 +53,6 @@ impl EntityInputBuilder { T: EntityTrait, ::Model: Sync, { - if self.context.entity_input.unified { - return self.insert_type_name::(); - } let entity_object_builder = EntityObjectBuilder { context: self.context, }; @@ -67,12 +61,12 @@ impl EntityInputBuilder { } /// used to produce the SeaORM entity input object - fn input_object(&self, insert: bool) -> InputObject + fn input_object(&self, is_insert: bool) -> InputObject where T: EntityTrait, ::Model: Sync, { - let name = if insert { + let name = if is_insert { self.insert_type_name::() } else { self.update_type_name::() @@ -90,7 +84,7 @@ impl EntityInputBuilder { let full_name = format!("{}.{}", entity_object_builder.type_name::(), column_name); - let skip = if insert { + let skip = if is_insert { self.context.entity_input.insert_skips.contains(&full_name) } else { self.context.entity_input.update_skips.contains(&full_name) @@ -110,7 +104,7 @@ impl EntityInputBuilder { None => return object, }; - let graphql_type = if column_def.is_null() { + let graphql_type = if column_def.is_null() || !is_insert { TypeRef::named(type_name) } else { TypeRef::named_nn(type_name) @@ -135,7 +129,7 @@ impl EntityInputBuilder { T: EntityTrait, ::Model: Sync, { - self.input_object::(self.context.entity_input.unified) + self.input_object::(false) } pub fn parse_object( diff --git a/src/mutation/entity_update_mutation.rs b/src/mutation/entity_update_mutation.rs new file mode 100644 index 00000000..11db0b83 --- /dev/null +++ b/src/mutation/entity_update_mutation.rs @@ -0,0 +1,130 @@ +use async_graphql::dynamic::{Field, FieldFuture, FieldValue, InputValue, TypeRef}; +use sea_orm::{ + ActiveModelTrait, DatabaseConnection, EntityTrait, IntoActiveModel, QueryFilter, + TransactionTrait, +}; + +use crate::{ + get_filter_conditions, prepare_active_model, BuilderContext, EntityInputBuilder, + EntityObjectBuilder, EntityQueryFieldBuilder, FilterInputBuilder, +}; + +/// The configuration structure of EntityUpdateMutationBuilder +pub struct EntityUpdateMutationConfig { + /// suffix that is appended on update mutations + pub mutation_suffix: String, + + /// name for `data` field + pub data_field: String, + + /// name for `filter` field + pub filter_field: String, +} + +impl std::default::Default for EntityUpdateMutationConfig { + fn default() -> Self { + Self { + mutation_suffix: "Update".into(), + data_field: "data".into(), + filter_field: "filter".into(), + } + } +} + +/// This builder produces the update mutation for an entity +pub struct EntityUpdateMutationBuilder { + pub context: &'static BuilderContext, +} + +impl EntityUpdateMutationBuilder { + /// 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_update_mutation.mutation_suffix + ) + } + + /// 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, + ::Model: Sync, + ::Model: IntoActiveModel, + A: ActiveModelTrait + sea_orm::ActiveModelBehavior + std::marker::Send, + { + let entity_input_builder = EntityInputBuilder { + context: self.context, + }; + 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_list_nn(entity_object_builder.basic_type_name::()), + move |ctx| { + FieldFuture::new(async move { + let db = ctx.data::()?; + let transaction = db.begin().await?; + + let entity_input_builder = EntityInputBuilder { context }; + let entity_object_builder = EntityObjectBuilder { context }; + + 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) + .unwrap(); + let input_object = &value_accessor.object()?; + let active_model = prepare_active_model::( + &entity_input_builder, + &entity_object_builder, + input_object, + )?; + + T::update_many() + .set(active_model) + .filter(filter_condition.clone()) + .exec(&transaction) + .await?; + + let result: Vec = + T::find().filter(filter_condition).all(&transaction).await?; + + transaction.commit().await?; + + Ok(Some(FieldValue::list( + result.into_iter().map(FieldValue::owned_any), + ))) + }) + }, + ) + .argument(InputValue::new( + &context.entity_update_mutation.data_field, + TypeRef::named_nn(entity_input_builder.update_type_name::()), + )) + .argument(InputValue::new( + &context.entity_update_mutation.filter_field, + TypeRef::named(entity_filter_input_builder.type_name(&object_name)), + )) + } +} diff --git a/src/mutation/mod.rs b/src/mutation/mod.rs index c95acc28..5ce2ffc5 100644 --- a/src/mutation/mod.rs +++ b/src/mutation/mod.rs @@ -3,3 +3,6 @@ pub use entity_create_one_mutation::*; pub mod entity_create_batch_mutation; pub use entity_create_batch_mutation::*; + +pub mod entity_update_mutation; +pub use entity_update_mutation::*;