From 0300d7baf62d4926fbfa5e9748175469e5fc59b2 Mon Sep 17 00:00:00 2001 From: lonelyhentxi Date: Mon, 28 Apr 2025 02:44:16 +0800 Subject: [PATCH] feature: add mutation input object transformer --- Cargo.lock | 2 +- Cargo.toml | 2 +- apps/recorder/src/graphql/guard.rs | 99 ++++++++++--------- apps/recorder/src/graphql/schema_root.rs | 18 +++- apps/recorder/src/graphql/transformer.rs | 62 +++++++++++- apps/webui/src/app/auth/context.ts | 2 +- apps/webui/src/app/auth/hooks.ts | 4 +- apps/webui/src/domains/auth/auth.service.ts | 2 +- apps/webui/src/infra/auth/auth.provider.ts | 2 +- .../infra/auth/basic/basic-auth.provider.ts | 2 +- .../src/infra/auth/oidc/oidc-auth.provider.ts | 2 +- .../routes/_app/subscriptions/create.tsx | 13 +-- 12 files changed, 145 insertions(+), 65 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0d14cd3..b6a1375 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5632,7 +5632,7 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" [[package]] name = "seaography" version = "1.1.4" -source = "git+https://github.com/lonelyhentxi/seaography.git?rev=0c4cc5c#0c4cc5ccd43730338802f98401120d5faeccd096" +source = "git+https://github.com/dumtruck/seaography.git?rev=10ba248#10ba2487fb356a0385c598290668a01e0ef21734" dependencies = [ "async-graphql", "fnv", diff --git a/Cargo.toml b/Cargo.toml index 509934e..494a289 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,4 +50,4 @@ recorder = { path = "./apps/recorder" } [patch.crates-io] jwt-authorizer = { git = "https://github.com/blablacio/jwt-authorizer.git", rev = "e956774" } -seaography = { git = "https://github.com/lonelyhentxi/seaography.git", rev = "0c4cc5c" } +seaography = { git = "https://github.com/dumtruck/seaography.git", rev = "10ba248" } diff --git a/apps/recorder/src/graphql/guard.rs b/apps/recorder/src/graphql/guard.rs index 5452da4..14a1ece 100644 --- a/apps/recorder/src/graphql/guard.rs +++ b/apps/recorder/src/graphql/guard.rs @@ -97,64 +97,75 @@ where Ok(user_info) => { let subscriber_id = user_info.subscriber_auth.subscriber_id; let validation_result = match context.field().name() { - field if field == entity_create_one_mutation_field_name.as_str() => context - .args - .try_get(&entity_create_one_mutation_data_field_name) - .and_then(|data_value| { + field if field == entity_create_one_mutation_field_name.as_str() => { + if let Some(data_value) = context + .args + .get(&entity_create_one_mutation_data_field_name) + { guard_data_object_accessor_with_subscriber_id( data_value, &column_name, subscriber_id, ) - }) - .map_err(|inner_error| { - AuthError::from_graphql_subscribe_id_guard( - inner_error, - context, - &entity_create_one_mutation_data_field_name, - &column_name, - ) - }), - field if field == entity_create_batch_mutation_field_name.as_str() => context - .args - .try_get(&entity_create_batch_mutation_data_field_name) - .and_then(|data_value| { - data_value.list().and_then(|data_list| { - data_list.iter().try_for_each(|data_item_value| { - guard_data_object_accessor_with_subscriber_id( - data_item_value, - &column_name, - subscriber_id, - ) - }) - }) - }) - .map_err(|inner_error| { - AuthError::from_graphql_subscribe_id_guard( - inner_error, - context, - &entity_create_batch_mutation_data_field_name, - &column_name, - ) - }), - field if field == entity_update_mutation_field_name.as_str() => { - match context.args.get(&entity_update_mutation_data_field_name) { - Some(data_value) => { - guard_data_object_accessor_with_optional_subscriber_id( - data_value, + .map_err(|inner_error| { + AuthError::from_graphql_subscribe_id_guard( + inner_error, + context, + &entity_create_one_mutation_data_field_name, &column_name, - subscriber_id, ) + }) + } else { + Ok(()) + } + } + field if field == entity_create_batch_mutation_field_name.as_str() => { + if let Some(data_value) = context + .args + .get(&entity_create_batch_mutation_data_field_name) + { + data_value + .list() + .and_then(|data_list| { + data_list.iter().try_for_each(|data_item_value| { + guard_data_object_accessor_with_optional_subscriber_id( + data_item_value, + &column_name, + subscriber_id, + ) + }) + }) .map_err(|inner_error| { AuthError::from_graphql_subscribe_id_guard( inner_error, context, - &entity_update_mutation_data_field_name, + &entity_create_batch_mutation_data_field_name, &column_name, ) }) - } - None => Ok(()), + } else { + Ok(()) + } + } + field if field == entity_update_mutation_field_name.as_str() => { + if let Some(data_value) = + context.args.get(&entity_update_mutation_data_field_name) + { + guard_data_object_accessor_with_optional_subscriber_id( + data_value, + &column_name, + subscriber_id, + ) + .map_err(|inner_error| { + AuthError::from_graphql_subscribe_id_guard( + inner_error, + context, + &entity_update_mutation_data_field_name, + &column_name, + ) + }) + } else { + Ok(()) } } _ => Ok(()), diff --git a/apps/recorder/src/graphql/schema_root.rs b/apps/recorder/src/graphql/schema_root.rs index d51ad03..6d81d11 100644 --- a/apps/recorder/src/graphql/schema_root.rs +++ b/apps/recorder/src/graphql/schema_root.rs @@ -3,7 +3,7 @@ use once_cell::sync::OnceCell; use sea_orm::{DatabaseConnection, EntityTrait, Iterable}; use seaography::{Builder, BuilderContext, FilterType, FilterTypesMapHelper}; -use super::transformer::filter_condition_transformer; +use super::transformer::{filter_condition_transformer, mutation_input_object_transformer}; use crate::graphql::{ filter::{ SUBSCRIBER_ID_FILTER_INFO, init_custom_filter_info, subscriber_id_condition_function, @@ -48,13 +48,25 @@ where )), ); context.filter_types.condition_functions.insert( - entity_column_key, + entity_column_key.clone(), subscriber_id_condition_function::(context, column), ); context.transformers.filter_conditions_transformers.insert( - entity_key, + entity_key.clone(), filter_condition_transformer::(context, column), ); + context + .transformers + .mutation_input_object_transformers + .insert( + entity_key, + mutation_input_object_transformer::(context, column), + ); + context + .entity_input + .insert_skips + .push(entity_column_key.clone()); + context.entity_input.update_skips.push(entity_column_key); } pub fn schema( diff --git a/apps/recorder/src/graphql/transformer.rs b/apps/recorder/src/graphql/transformer.rs index ab1f03f..69c8146 100644 --- a/apps/recorder/src/graphql/transformer.rs +++ b/apps/recorder/src/graphql/transformer.rs @@ -1,7 +1,10 @@ -use async_graphql::dynamic::ResolverContext; -use sea_orm::{ColumnTrait, Condition, EntityTrait}; -use seaography::{BuilderContext, FnFilterConditionsTransformer}; +use std::{collections::BTreeMap, sync::Arc}; +use async_graphql::dynamic::ResolverContext; +use sea_orm::{ColumnTrait, Condition, EntityTrait, Value}; +use seaography::{BuilderContext, FnFilterConditionsTransformer, FnMutationInputObjectTransformer}; + +use super::util::{get_column_key, get_entity_key}; use crate::auth::AuthUserInfo; pub fn filter_condition_transformer( @@ -25,3 +28,56 @@ where }, ) } + +pub fn mutation_input_object_transformer( + context: &BuilderContext, + column: &T::Column, +) -> FnMutationInputObjectTransformer +where + T: EntityTrait, + ::Model: Sync, +{ + let entity_key = get_entity_key::(context); + let entity_name = context.entity_query_field.type_name.as_ref()(&entity_key); + let column_key = get_column_key::(context, column); + let column_name = Arc::new(context.entity_object.column_name.as_ref()( + &entity_key, + &column_key, + )); + let entity_create_one_mutation_field_name = Arc::new(format!( + "{}{}", + entity_name, context.entity_create_one_mutation.mutation_suffix + )); + let entity_create_batch_mutation_field_name = Arc::new(format!( + "{}{}", + entity_name, + context.entity_create_batch_mutation.mutation_suffix.clone() + )); + Box::new( + move |context: &ResolverContext, + mut input: BTreeMap| + -> BTreeMap { + let field_name = context.field().name(); + if field_name == entity_create_one_mutation_field_name.as_str() + || field_name == entity_create_batch_mutation_field_name.as_str() + { + match context.ctx.data::() { + Ok(user_info) => { + let subscriber_id = user_info.subscriber_auth.subscriber_id; + let value = input.get_mut(column_name.as_str()); + if value.is_none() { + input.insert( + column_name.as_str().to_string(), + Value::Int(Some(subscriber_id)), + ); + } + input + } + Err(err) => unreachable!("auth user info must be guarded: {:?}", err), + } + } else { + input + } + }, + ) +} diff --git a/apps/webui/src/app/auth/context.ts b/apps/webui/src/app/auth/context.ts index 5115fd7..051b6f3 100644 --- a/apps/webui/src/app/auth/context.ts +++ b/apps/webui/src/app/auth/context.ts @@ -66,7 +66,7 @@ export function authContextFromInjector(injector: Injector): AuthContext { return { type: authProvider.authMethod, isAuthenticated$: authService.isAuthenticated$, - userData$: authService.userData$, + userData$: authService.authData$, checkAuthResultEvent$: authService.checkAuthResultEvent$, authService, authProvider, diff --git a/apps/webui/src/app/auth/hooks.ts b/apps/webui/src/app/auth/hooks.ts index 2b50f14..b34aa0f 100644 --- a/apps/webui/src/app/auth/hooks.ts +++ b/apps/webui/src/app/auth/hooks.ts @@ -20,14 +20,14 @@ export function useAuth() { [authContext.isAuthenticated$] ); - const userData = useMemo( + const authData = useMemo( () => atomWithObservable(() => authContext.userData$ as Observable), [authContext] ); return { ...authContext, - userData, + authData, injector, isAuthenticated, }; diff --git a/apps/webui/src/domains/auth/auth.service.ts b/apps/webui/src/domains/auth/auth.service.ts index 5e22166..5568bdc 100644 --- a/apps/webui/src/domains/auth/auth.service.ts +++ b/apps/webui/src/domains/auth/auth.service.ts @@ -6,8 +6,8 @@ export class AuthService { private authProvider = inject(AUTH_PROVIDER); isAuthenticated$ = this.authProvider.isAuthenticated$; - userData$ = this.authProvider.userData$; checkAuthResultEvent$ = this.authProvider.checkAuthResultEvent$; + authData$ = this.authProvider.authData$; setup() { this.authProvider.setup(); diff --git a/apps/webui/src/infra/auth/auth.provider.ts b/apps/webui/src/infra/auth/auth.provider.ts index 6e9f5d2..888592d 100644 --- a/apps/webui/src/infra/auth/auth.provider.ts +++ b/apps/webui/src/infra/auth/auth.provider.ts @@ -7,7 +7,7 @@ export abstract class AuthProvider { abstract authMethod: AuthMethodType; abstract checkAuthResultEvent$: Observable; abstract isAuthenticated$: Observable; - abstract userData$: Observable; + abstract authData$: Observable; abstract getAccessToken(): Observable; abstract setup(): void; abstract autoLoginPartialRoutesGuard(): Observable; diff --git a/apps/webui/src/infra/auth/basic/basic-auth.provider.ts b/apps/webui/src/infra/auth/basic/basic-auth.provider.ts index b653d99..1bdbf06 100644 --- a/apps/webui/src/infra/auth/basic/basic-auth.provider.ts +++ b/apps/webui/src/infra/auth/basic/basic-auth.provider.ts @@ -7,7 +7,7 @@ import { AUTH_METHOD } from '../defs'; export class BasicAuthProvider extends AuthProvider { authMethod = AUTH_METHOD.BASIC; isAuthenticated$ = of(true); - userData$ = of({}); + authData$ = of({}); checkAuthResultEvent$: Observable = NEVER; getAccessToken(): Observable { diff --git a/apps/webui/src/infra/auth/oidc/oidc-auth.provider.ts b/apps/webui/src/infra/auth/oidc/oidc-auth.provider.ts index 283e696..015c0c6 100644 --- a/apps/webui/src/infra/auth/oidc/oidc-auth.provider.ts +++ b/apps/webui/src/infra/auth/oidc/oidc-auth.provider.ts @@ -25,7 +25,7 @@ export class OidcAuthProvider extends AuthProvider { ); } - get userData$() { + get authData$() { return this.oidcSecurityService.userData$.pipe(map((s) => s.userData)); } diff --git a/apps/webui/src/views/routes/_app/subscriptions/create.tsx b/apps/webui/src/views/routes/_app/subscriptions/create.tsx index abe100f..2859f8e 100644 --- a/apps/webui/src/views/routes/_app/subscriptions/create.tsx +++ b/apps/webui/src/views/routes/_app/subscriptions/create.tsx @@ -56,20 +56,21 @@ const CREATE_SUBSCRIPTION_MUTATION = gql` sourceUrl enabled category + subscriberId } } `; function SubscriptionCreateRouteComponent() { - const { userData } = useAuth(); - console.log(JSON.stringify(userData, null, 2)); + const { authData } = useAuth(); + console.log(JSON.stringify(authData, null, 2)); const [isSubmitting, setIsSubmitting] = useState(false); const navigate = useNavigate(); const form = useForm({ defaultValues: { displayName: '', sourceUrl: '', - category: 'Mikan', + category: 'mikan', enabled: true, }, }); @@ -129,7 +130,7 @@ function SubscriptionCreateRouteComponent() { disabled value={field.value} onValueChange={field.onChange} - defaultValue="Mikan" + defaultValue="mikan" > @@ -137,11 +138,11 @@ function SubscriptionCreateRouteComponent() { - Mikan + mikan - Currently only Mikan source is supported + Currently only mikan source is supported