diff --git a/examples/sqlite/tests/guard_mutation_tests.rs b/examples/sqlite/tests/guard_mutation_tests.rs new file mode 100644 index 00000000..ced4915a --- /dev/null +++ b/examples/sqlite/tests/guard_mutation_tests.rs @@ -0,0 +1,152 @@ +use std::collections::BTreeMap; + +use async_graphql::{dynamic::*, Response}; +use sea_orm::{Database, DatabaseConnection}; +use seaography::{Builder, BuilderContext, FnGuard, GuardsConfig}; +use seaography_sqlite_example::entities::*; + +lazy_static::lazy_static! { + static ref CONTEXT : BuilderContext = { + let context = BuilderContext::default(); + let mut entity_guards: BTreeMap = BTreeMap::new(); + entity_guards.insert("FilmCategory".into(), Box::new(|_ctx| { + seaography::GuardAction::Block(None) + })); + let mut field_guards: BTreeMap = BTreeMap::new(); + field_guards.insert("Language.name".into(), Box::new(|_ctx| { + seaography::GuardAction::Block(None) + })); + BuilderContext { + guards: GuardsConfig { + entity_guards, + field_guards, + }, + ..context + } + }; +} + +pub fn schema( + database: DatabaseConnection, + depth: Option, + complexity: Option, +) -> Result { + let mut builder = Builder::new(&CONTEXT, database.clone()); + seaography::register_entities!( + builder, + [ + actor, + address, + category, + city, + country, + customer, + film, + film_actor, + film_category, + film_text, + inventory, + language, + payment, + rental, + staff, + store, + ] + ); + let schema = builder.schema_builder(); + let schema = if let Some(depth) = depth { + schema.limit_depth(depth) + } else { + schema + }; + let schema = if let Some(complexity) = complexity { + schema.limit_complexity(complexity) + } else { + schema + }; + schema.data(database).finish() +} + +pub async fn get_schema() -> Schema { + let database = Database::connect("sqlite://sakila.db").await.unwrap(); + let schema = schema(database, None, None).unwrap(); + + schema +} + +pub fn assert_eq(a: Response, b: &str) { + assert_eq!( + a.data.into_json().unwrap(), + serde_json::from_str::(b).unwrap() + ) +} + +#[tokio::test] +async fn entity_guard_mutation() { + let schema = get_schema().await; + + assert_eq( + schema + .execute( + r#" + mutation LanguageUpdate { + languageUpdate( + data: { lastUpdate: "2030-01-01 11:11:11 UTC" } + filter: { languageId: { eq: 6 } } + ) { + languageId + } + } + "#, + ) + .await, + r#" + { + "languageUpdate": [ + { + "languageId": 6 + } + ] + } + "#, + ); + + let response = schema + .execute( + r#" + mutation FilmCategoryUpdate { + filmCategoryUpdate( + data: { filmId: 1, categoryId: 1, lastUpdate: "2030-01-01 11:11:11 UTC" } + ) { + filmId + } + } +"#, + ) + .await; + + assert_eq!(response.errors.len(), 1); + + assert_eq!(response.errors[0].message, "Entity guard triggered."); +} + +#[tokio::test] +async fn field_guard_mutation() { + let schema = get_schema().await; + + let response = schema + .execute( + r#" + mutation LanguageUpdate { + languageUpdate(data: { name: "Cantonese" }, filter: { languageId: { eq: 6 } }) { + languageId + } + } + "#, + ) + .await; + + assert_eq!(response.errors.len(), 1); + + assert_eq!(response.errors[0].message, "Field guard triggered."); +} diff --git a/src/mutation/entity_create_batch_mutation.rs b/src/mutation/entity_create_batch_mutation.rs index ac95489c..08ab954d 100644 --- a/src/mutation/entity_create_batch_mutation.rs +++ b/src/mutation/entity_create_batch_mutation.rs @@ -5,7 +5,7 @@ use sea_orm::{ use crate::{ prepare_active_model, BuilderContext, EntityInputBuilder, EntityObjectBuilder, - EntityQueryFieldBuilder, + EntityQueryFieldBuilder, GuardAction, }; /// The configuration structure of EntityCreateBatchMutationBuilder @@ -64,11 +64,32 @@ impl EntityCreateBatchMutationBuilder { let context = self.context; + let object_name: String = entity_object_builder.type_name::(); + let guard = self.context.guards.entity_guards.get(&object_name); + let field_guards = &self.context.guards.field_guards; + Field::new( self.type_name::(), TypeRef::named_nn_list_nn(entity_object_builder.basic_type_name::()), move |ctx| { FieldFuture::new(async move { + let guard_flag = if let Some(guard) = guard { + (*guard)(&ctx) + } else { + GuardAction::Allow + }; + + if let GuardAction::Block(reason) = guard_flag { + return match reason { + Some(reason) => Err::, async_graphql::Error>( + async_graphql::Error::new(reason), + ), + None => Err::, async_graphql::Error>( + async_graphql::Error::new("Entity guard triggered."), + ), + }; + } + let db = ctx.data::()?; let transaction = db.begin().await?; @@ -76,17 +97,41 @@ impl EntityCreateBatchMutationBuilder { let entity_object_builder = EntityObjectBuilder { context }; let mut results: Vec<_> = Vec::new(); - for input_object in ctx + for input in ctx .args .get(&context.entity_create_batch_mutation.data_field) .unwrap() .list()? .iter() { + let input_object = &input.object()?; + for (column, _) in input_object.iter() { + let field_guard = field_guards.get(&format!( + "{}.{}", + entity_object_builder.type_name::(), + column + )); + let field_guard_flag = if let Some(field_guard) = field_guard { + (*field_guard)(&ctx) + } else { + GuardAction::Allow + }; + if let GuardAction::Block(reason) = field_guard_flag { + return match reason { + Some(reason) => Err::, async_graphql::Error>( + async_graphql::Error::new(reason), + ), + None => Err::, async_graphql::Error>( + async_graphql::Error::new("Field guard triggered."), + ), + }; + } + } + let active_model = prepare_active_model::( &entity_input_builder, &entity_object_builder, - &(input_object.object()?), + input_object, )?; let result = active_model.insert(&transaction).await?; results.push(result); diff --git a/src/mutation/entity_create_one_mutation.rs b/src/mutation/entity_create_one_mutation.rs index 70fc8ccf..110af657 100644 --- a/src/mutation/entity_create_one_mutation.rs +++ b/src/mutation/entity_create_one_mutation.rs @@ -4,7 +4,9 @@ use sea_orm::{ PrimaryKeyToColumn, PrimaryKeyTrait, }; -use crate::{BuilderContext, EntityInputBuilder, EntityObjectBuilder, EntityQueryFieldBuilder}; +use crate::{ + BuilderContext, EntityInputBuilder, EntityObjectBuilder, EntityQueryFieldBuilder, GuardAction, +}; /// The configuration structure of EntityCreateOneMutationBuilder pub struct EntityCreateOneMutationConfig { @@ -62,11 +64,32 @@ impl EntityCreateOneMutationBuilder { let context = self.context; + let object_name: String = entity_object_builder.type_name::(); + let guard = self.context.guards.entity_guards.get(&object_name); + let field_guards = &self.context.guards.field_guards; + Field::new( self.type_name::(), TypeRef::named_nn(entity_object_builder.basic_type_name::()), move |ctx| { FieldFuture::new(async move { + let guard_flag = if let Some(guard) = guard { + (*guard)(&ctx) + } else { + GuardAction::Allow + }; + + if let GuardAction::Block(reason) = guard_flag { + return match reason { + Some(reason) => Err::, async_graphql::Error>( + async_graphql::Error::new(reason), + ), + None => Err::, async_graphql::Error>( + async_graphql::Error::new("Entity guard triggered."), + ), + }; + } + let entity_input_builder = EntityInputBuilder { context }; let entity_object_builder = EntityObjectBuilder { context }; let db = ctx.data::()?; @@ -76,6 +99,29 @@ impl EntityCreateOneMutationBuilder { .unwrap(); let input_object = &value_accessor.object()?; + for (column, _) in input_object.iter() { + let field_guard = field_guards.get(&format!( + "{}.{}", + entity_object_builder.type_name::(), + column + )); + let field_guard_flag = if let Some(field_guard) = field_guard { + (*field_guard)(&ctx) + } else { + GuardAction::Allow + }; + if let GuardAction::Block(reason) = field_guard_flag { + return match reason { + Some(reason) => Err::, async_graphql::Error>( + async_graphql::Error::new(reason), + ), + None => Err::, async_graphql::Error>( + async_graphql::Error::new("Field guard triggered."), + ), + }; + } + } + let active_model = prepare_active_model::( &entity_input_builder, &entity_object_builder, diff --git a/src/mutation/entity_update_mutation.rs b/src/mutation/entity_update_mutation.rs index 11db0b83..b2a92f69 100644 --- a/src/mutation/entity_update_mutation.rs +++ b/src/mutation/entity_update_mutation.rs @@ -6,7 +6,7 @@ use sea_orm::{ use crate::{ get_filter_conditions, prepare_active_model, BuilderContext, EntityInputBuilder, - EntityObjectBuilder, EntityQueryFieldBuilder, FilterInputBuilder, + EntityObjectBuilder, EntityQueryFieldBuilder, FilterInputBuilder, GuardAction, }; /// The configuration structure of EntityUpdateMutationBuilder @@ -75,11 +75,31 @@ impl EntityUpdateMutationBuilder { let context = self.context; + let guard = self.context.guards.entity_guards.get(&object_name); + let field_guards = &self.context.guards.field_guards; + Field::new( self.type_name::(), TypeRef::named_nn_list_nn(entity_object_builder.basic_type_name::()), move |ctx| { FieldFuture::new(async move { + let guard_flag = if let Some(guard) = guard { + (*guard)(&ctx) + } else { + GuardAction::Allow + }; + + if let GuardAction::Block(reason) = guard_flag { + return match reason { + Some(reason) => Err::, async_graphql::Error>( + async_graphql::Error::new(reason), + ), + None => Err::, async_graphql::Error>( + async_graphql::Error::new("Entity guard triggered."), + ), + }; + } + let db = ctx.data::()?; let transaction = db.begin().await?; @@ -95,6 +115,30 @@ impl EntityUpdateMutationBuilder { .get(&context.entity_create_one_mutation.data_field) .unwrap(); let input_object = &value_accessor.object()?; + + for (column, _) in input_object.iter() { + let field_guard = field_guards.get(&format!( + "{}.{}", + entity_object_builder.type_name::(), + column + )); + let field_guard_flag = if let Some(field_guard) = field_guard { + (*field_guard)(&ctx) + } else { + GuardAction::Allow + }; + if let GuardAction::Block(reason) = field_guard_flag { + return match reason { + Some(reason) => Err::, async_graphql::Error>( + async_graphql::Error::new(reason), + ), + None => Err::, async_graphql::Error>( + async_graphql::Error::new("Field guard triggered."), + ), + }; + } + } + let active_model = prepare_active_model::( &entity_input_builder, &entity_object_builder,