From 278901e38af2c6de9bdf1e3cc949fb1dc9119574 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Wed, 2 Jul 2025 20:08:37 +0600 Subject: [PATCH 01/59] feat: batch resolver type for appsync-graphql --- .../src/types/appsync-graphql.ts | 78 ++++++++++++++++++- 1 file changed, 76 insertions(+), 2 deletions(-) diff --git a/packages/event-handler/src/types/appsync-graphql.ts b/packages/event-handler/src/types/appsync-graphql.ts index d799df13e9..5e1adbcb95 100644 --- a/packages/event-handler/src/types/appsync-graphql.ts +++ b/packages/event-handler/src/types/appsync-graphql.ts @@ -3,6 +3,47 @@ import type { AppSyncResolverEvent, Context } from 'aws-lambda'; import type { RouteHandlerRegistry } from '../appsync-graphql/RouteHandlerRegistry.js'; import type { Router } from '../appsync-graphql/Router.js'; +// #region BatchResolver fn + +type BatchResolverSyncHandlerFn> = ( + args: TParams, + options: { + event: AppSyncResolverEvent; + context: Context; + } +) => unknown; + +type BatchResolverHandlerFn> = ( + args: TParams, + options: { + event: AppSyncResolverEvent; + context: Context; + } +) => Promise; + +type BatchResolverAggregateHandlerFn> = ( + events: AppSyncResolverEvent[], + options: { + context: Context; + } +) => Promise; + +type BatchResolverSyncAggregateHandlerFn> = ( + event: AppSyncResolverEvent[], + options: { + context: Context; + } +) => unknown; + +type BatchResolverHandler< + TParams = Record, + T extends boolean | undefined = undefined, +> = T extends true + ? + | BatchResolverAggregateHandlerFn + | BatchResolverSyncAggregateHandlerFn + : BatchResolverHandlerFn | BatchResolverSyncHandlerFn; + // #region Resolver fn type ResolverSyncHandlerFn> = ( @@ -46,11 +87,15 @@ type RouteHandlerRegistryOptions = { * @property fieldName - The name of the field to be registered * @property typeName - The name of the type to be registered */ -type RouteHandlerOptions> = { +type RouteHandlerOptions< + TParams = Record, + T extends boolean = true, + R extends boolean = false, +> = { /** * The handler function to be called when the event is received */ - handler: ResolverHandler; + handler: BatchResolverHandler | ResolverHandler; /** * The field name of the event to be registered */ @@ -59,6 +104,16 @@ type RouteHandlerOptions> = { * The type name of the event to be registered */ typeName: string; + /** + * Whether the route handler will send all the events to the route handler at once or one by one + * @default true + */ + aggregate?: T; + /** + * Whether to raise an error if the handler fails + * @default false + */ + raiseOnError?: R; }; // #region Router @@ -89,10 +144,29 @@ type GraphQlRouteOptions = { typeName?: string; }; +type GraphQlBatchRouteOptions< + T extends boolean = false, + R extends boolean = true, +> = GraphQlRouteOptions & { + /** + * Whether the route handler will send all the events to the route handler at once or one by one + * @default false + */ + aggregate?: T; + /** + * Whether to raise an error if the handler fails + * @default true + */ + raiseOnError?: R; +}; + export type { RouteHandlerRegistryOptions, RouteHandlerOptions, GraphQlRouterOptions, GraphQlRouteOptions, + GraphQlBatchRouteOptions, ResolverHandler, + BatchResolverHandler, + BatchResolverAggregateHandlerFn, }; From 7b279a71dfe04d019523b1d0b3dfac621cd164b2 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Wed, 2 Jul 2025 20:15:43 +0600 Subject: [PATCH 02/59] feat: update default values for GraphQlBatchRouteOptions --- packages/event-handler/src/types/appsync-graphql.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/event-handler/src/types/appsync-graphql.ts b/packages/event-handler/src/types/appsync-graphql.ts index 5e1adbcb95..b44e2489fe 100644 --- a/packages/event-handler/src/types/appsync-graphql.ts +++ b/packages/event-handler/src/types/appsync-graphql.ts @@ -145,17 +145,17 @@ type GraphQlRouteOptions = { }; type GraphQlBatchRouteOptions< - T extends boolean = false, - R extends boolean = true, + T extends boolean = true, + R extends boolean = false, > = GraphQlRouteOptions & { /** * Whether the route handler will send all the events to the route handler at once or one by one - * @default false + * @default true */ aggregate?: T; /** * Whether to raise an error if the handler fails - * @default true + * @default false */ raiseOnError?: R; }; From aa5c7ab4628738e424e36e21b1a65fdf11f0c1a7 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sat, 5 Jul 2025 15:29:55 +0600 Subject: [PATCH 03/59] fix: correct parameter type for BatchResolverAggregateHandlerFn --- packages/event-handler/src/types/appsync-graphql.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/event-handler/src/types/appsync-graphql.ts b/packages/event-handler/src/types/appsync-graphql.ts index b44e2489fe..234d66c15e 100644 --- a/packages/event-handler/src/types/appsync-graphql.ts +++ b/packages/event-handler/src/types/appsync-graphql.ts @@ -22,8 +22,9 @@ type BatchResolverHandlerFn> = ( ) => Promise; type BatchResolverAggregateHandlerFn> = ( - events: AppSyncResolverEvent[], + event: AppSyncResolverEvent[], options: { + event: AppSyncResolverEvent[]; context: Context; } ) => Promise; From 054d69321092fff62375ec02fb753019dd9f8716 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 6 Jul 2025 11:59:44 +0600 Subject: [PATCH 04/59] feat: implement batch resolver functionality in Router and AppSyncGraphQLResolver --- .../appsync-graphql/AppSyncGraphQLResolver.ts | 126 +++++++++++++++++- .../appsync-graphql/RouteHandlerRegistry.ts | 4 +- .../src/appsync-graphql/Router.ts | 64 +++++++++ 3 files changed, 187 insertions(+), 7 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts index 9c2679dcea..ce9483c0ec 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -1,4 +1,9 @@ import type { AppSyncResolverEvent, Context } from 'aws-lambda'; +import type { + BatchResolverAggregateHandlerFn, + ResolverHandler, + RouteHandlerOptions, +} from '../types/appsync-graphql.js'; import type { ResolveOptions } from '../types/common.js'; import { ResolverNotFoundException } from './errors.js'; import { Router } from './Router.js'; @@ -98,8 +103,13 @@ class AppSyncGraphQLResolver extends Router { options?: ResolveOptions ): Promise { if (Array.isArray(event)) { - this.logger.warn('Batch resolver is not implemented yet'); - return; + if (event.some((singleEvent) => !isAppSyncGraphQLEvent(singleEvent))) { + this.logger.warn( + 'Received an event that is not compatible with this resolver' + ); + return; + } + return await this.#executeBatchResolvers(event, context, options); } if (!isAppSyncGraphQLEvent(event)) { this.logger.warn( @@ -119,6 +129,110 @@ class AppSyncGraphQLResolver extends Router { } } + async #executeBatchResolvers( + events: AppSyncResolverEvent>[], + context: Context, + options?: ResolveOptions + ): Promise { + const results: unknown[] = []; + const { fieldName, parentTypeName: typeName } = events[0].info; + const batchHandlerOptions = this.batchResolverRegistry.resolve( + typeName, + fieldName + ); + + if (batchHandlerOptions) { + try { + const result = await this.#callBatchResolver( + events, + context, + batchHandlerOptions, + options + ); + results.push(...result); + } catch (error) { + this.logger.error( + `An error occurred in batch handler ${fieldName}`, + error + ); + throw error; + } + } + + return results; + } + + async #callBatchResolver( + events: AppSyncResolverEvent>[], + context: Context, + options: RouteHandlerOptions, + resolveOptions?: ResolveOptions + ): Promise { + const { aggregate, raiseOnError } = options; + this.logger.debug( + `Graceful error handling flag raiseOnError=${raiseOnError}` + ); + + // Checks whether the entire batch should be processed at once + if (aggregate) { + const response = await ( + options.handler as BatchResolverAggregateHandlerFn + ).apply(resolveOptions?.scope ?? this, [ + events, + { event: events, context }, + ]); + + if (!Array.isArray(response)) { + throw new Error( + 'The response must be a List when using batch resolvers' + ); + } + + return response; + } + + const handler = options.handler as ResolverHandler; + + /** + * Non aggregated events, so we call this event list x times and + * process each event individually. + */ + + // If `raiseOnError` is true, stop on first exception we encounter + if (raiseOnError) { + const results: unknown[] = []; + for (const event of events) { + const result = await handler.apply(resolveOptions?.scope ?? this, [ + event.arguments, + { event, context }, + ]); + results.push(result); + } + return results; + } + + // By default, we gracefully append `null` for any records that failed processing + const results: unknown[] = []; + for (let idx = 0; idx < events.length; idx++) { + const event = events[idx]; + try { + const result = await handler.apply(resolveOptions?.scope ?? this, [ + event.arguments, + { event, context }, + ]); + results.push(result); + } catch (error) { + this.logger.error(error); + this.logger.debug( + `Failed to process event number ${idx} from field '${event.info.fieldName}'` + ); + results.push(null); + } + } + + return results; + } + /** * Executes the appropriate resolver for a given AppSync GraphQL event. * @@ -143,10 +257,10 @@ class AppSyncGraphQLResolver extends Router { fieldName ); if (resolverHandlerOptions) { - return resolverHandlerOptions.handler.apply(options?.scope ?? this, [ - event.arguments, - { event, context }, - ]); + return (resolverHandlerOptions.handler as ResolverHandler).apply( + options?.scope ?? this, + [event.arguments, { event, context }] + ); } throw new ResolverNotFoundException( diff --git a/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts b/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts index 7113ccfd80..866b210429 100644 --- a/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts +++ b/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts @@ -35,7 +35,7 @@ class RouteHandlerRegistry { * */ public register(options: RouteHandlerOptions): void { - const { fieldName, handler, typeName } = options; + const { fieldName, handler, typeName, raiseOnError, aggregate } = options; this.#logger.debug(`Adding resolver for field ${typeName}.${fieldName}`); const cacheKey = this.#makeKey(typeName, fieldName); if (this.resolvers.has(cacheKey)) { @@ -47,6 +47,8 @@ class RouteHandlerRegistry { fieldName, handler, typeName, + raiseOnError, + aggregate, }); } diff --git a/packages/event-handler/src/appsync-graphql/Router.ts b/packages/event-handler/src/appsync-graphql/Router.ts index b1d0feb4e6..5d810bf87e 100644 --- a/packages/event-handler/src/appsync-graphql/Router.ts +++ b/packages/event-handler/src/appsync-graphql/Router.ts @@ -4,6 +4,8 @@ import { isDevMode, } from '@aws-lambda-powertools/commons/utils/env'; import type { + BatchResolverHandler, + GraphQlBatchRouteOptions, GraphQlRouteOptions, GraphQlRouterOptions, ResolverHandler, @@ -18,6 +20,10 @@ class Router { * A map of registered routes for all GraphQL events, keyed by their fieldNames. */ protected readonly resolverRegistry: RouteHandlerRegistry; + /** + * A map of registered routes for GraphQL batch events, keyed by their fieldNames. + */ + protected readonly batchResolverRegistry: RouteHandlerRegistry; /** * A logger instance to be used for logging debug, warning, and error messages. * @@ -42,6 +48,9 @@ class Router { this.resolverRegistry = new RouteHandlerRegistry({ logger: this.logger, }); + this.batchResolverRegistry = new RouteHandlerRegistry({ + logger: this.logger, + }); this.isDev = isDevMode(); } @@ -318,6 +327,61 @@ class Router { return descriptor; }; } + + public batchResolver< + TParams extends Record, + T extends boolean = false, + >( + handler: BatchResolverHandler, + options: GraphQlBatchRouteOptions + ): void; + public batchResolver< + TParams extends Record, + T extends boolean = false, + >(options: GraphQlBatchRouteOptions): MethodDecorator; + public batchResolver< + TParams extends Record, + T extends boolean = false, + >( + handler: BatchResolverHandler | GraphQlBatchRouteOptions, + options?: GraphQlBatchRouteOptions + ): MethodDecorator | undefined { + if (typeof handler === 'function') { + const batchResolverOptions = options as GraphQlBatchRouteOptions; + const { + typeName = 'Query', + fieldName, + aggregate = true, + raiseOnError = false, + } = batchResolverOptions; + this.batchResolverRegistry.register({ + fieldName, + handler: handler as BatchResolverHandler, + typeName, + aggregate, + raiseOnError, + }); + return; + } + + const batchResolverOptions = handler; + return (target, _propertyKey, descriptor: PropertyDescriptor) => { + const { + typeName = 'Query', + fieldName, + aggregate = true, + raiseOnError = false, + } = batchResolverOptions; + this.batchResolverRegistry.register({ + fieldName, + handler: descriptor?.value, + typeName, + aggregate, + raiseOnError, + }); + return descriptor; + }; + } } export { Router }; From 47717af4ad7e37845fa81fc9335709e0de2305a3 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 6 Jul 2025 12:45:54 +0600 Subject: [PATCH 05/59] refactor: enhance type definitions for RouteHandlerOptions and GraphQlBatchRouteOptions --- .../appsync-graphql/AppSyncGraphQLResolver.ts | 2 +- .../appsync-graphql/RouteHandlerRegistry.ts | 13 +++++-- .../src/appsync-graphql/Router.ts | 39 ++++++++----------- .../src/types/appsync-graphql.ts | 10 ++--- 4 files changed, 30 insertions(+), 34 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts index ce9483c0ec..9db7d52feb 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -165,7 +165,7 @@ class AppSyncGraphQLResolver extends Router { async #callBatchResolver( events: AppSyncResolverEvent>[], context: Context, - options: RouteHandlerOptions, + options: RouteHandlerOptions, boolean, boolean>, resolveOptions?: ResolveOptions ): Promise { const { aggregate, raiseOnError } = options; diff --git a/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts b/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts index 866b210429..4e79a0c6a5 100644 --- a/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts +++ b/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts @@ -15,7 +15,10 @@ class RouteHandlerRegistry { /** * A map of registered route handlers, keyed by their type & field name. */ - protected readonly resolvers: Map = new Map(); + protected readonly resolvers: Map< + string, + RouteHandlerOptions, boolean, boolean> + > = new Map(); /** * A logger instance to be used for logging debug and warning messages. */ @@ -34,7 +37,9 @@ class RouteHandlerRegistry { * @param options.typeName - The name of the GraphQL type to be registered * */ - public register(options: RouteHandlerOptions): void { + public register( + options: RouteHandlerOptions, boolean, boolean> + ): void { const { fieldName, handler, typeName, raiseOnError, aggregate } = options; this.#logger.debug(`Adding resolver for field ${typeName}.${fieldName}`); const cacheKey = this.#makeKey(typeName, fieldName); @@ -61,7 +66,9 @@ class RouteHandlerRegistry { public resolve( typeName: string, fieldName: string - ): RouteHandlerOptions | undefined { + ): + | RouteHandlerOptions, boolean, boolean> + | undefined { this.#logger.debug( `Looking for resolver for type=${typeName}, field=${fieldName}` ); diff --git a/packages/event-handler/src/appsync-graphql/Router.ts b/packages/event-handler/src/appsync-graphql/Router.ts index 5d810bf87e..a682416225 100644 --- a/packages/event-handler/src/appsync-graphql/Router.ts +++ b/packages/event-handler/src/appsync-graphql/Router.ts @@ -330,54 +330,47 @@ class Router { public batchResolver< TParams extends Record, - T extends boolean = false, + T extends boolean = true, + R extends boolean = false, >( handler: BatchResolverHandler, - options: GraphQlBatchRouteOptions + options: GraphQlBatchRouteOptions ): void; public batchResolver< TParams extends Record, - T extends boolean = false, - >(options: GraphQlBatchRouteOptions): MethodDecorator; + T extends boolean = true, + R extends boolean = false, + >(options: GraphQlBatchRouteOptions): MethodDecorator; public batchResolver< TParams extends Record, - T extends boolean = false, + T extends boolean = true, + R extends boolean = false, >( - handler: BatchResolverHandler | GraphQlBatchRouteOptions, - options?: GraphQlBatchRouteOptions + handler: BatchResolverHandler | GraphQlBatchRouteOptions, + options?: GraphQlBatchRouteOptions ): MethodDecorator | undefined { if (typeof handler === 'function') { const batchResolverOptions = options as GraphQlBatchRouteOptions; - const { - typeName = 'Query', - fieldName, - aggregate = true, - raiseOnError = false, - } = batchResolverOptions; + const { typeName = 'Query', fieldName } = batchResolverOptions; this.batchResolverRegistry.register({ fieldName, handler: handler as BatchResolverHandler, typeName, - aggregate, - raiseOnError, + aggregate: (batchResolverOptions?.aggregate ?? true) as T, + raiseOnError: (batchResolverOptions?.raiseOnError ?? false) as R, }); return; } const batchResolverOptions = handler; return (target, _propertyKey, descriptor: PropertyDescriptor) => { - const { - typeName = 'Query', - fieldName, - aggregate = true, - raiseOnError = false, - } = batchResolverOptions; + const { typeName = 'Query', fieldName } = batchResolverOptions; this.batchResolverRegistry.register({ fieldName, handler: descriptor?.value, typeName, - aggregate, - raiseOnError, + aggregate: (batchResolverOptions?.aggregate ?? true) as T, + raiseOnError: (batchResolverOptions?.raiseOnError ?? false) as R, }); return descriptor; }; diff --git a/packages/event-handler/src/types/appsync-graphql.ts b/packages/event-handler/src/types/appsync-graphql.ts index 234d66c15e..8879667a0d 100644 --- a/packages/event-handler/src/types/appsync-graphql.ts +++ b/packages/event-handler/src/types/appsync-graphql.ts @@ -88,11 +88,7 @@ type RouteHandlerRegistryOptions = { * @property fieldName - The name of the field to be registered * @property typeName - The name of the type to be registered */ -type RouteHandlerOptions< - TParams = Record, - T extends boolean = true, - R extends boolean = false, -> = { +type RouteHandlerOptions = { /** * The handler function to be called when the event is received */ @@ -146,8 +142,8 @@ type GraphQlRouteOptions = { }; type GraphQlBatchRouteOptions< - T extends boolean = true, - R extends boolean = false, + T extends boolean | undefined = true, + R extends boolean | undefined = false, > = GraphQlRouteOptions & { /** * Whether the route handler will send all the events to the route handler at once or one by one From 079354b563b8aea198e6d23768ecdeda30f5ee28 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 6 Jul 2025 12:55:15 +0600 Subject: [PATCH 06/59] fix: simplify aggregate and raiseOnError default value handling in batchResolver --- packages/event-handler/src/appsync-graphql/Router.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/Router.ts b/packages/event-handler/src/appsync-graphql/Router.ts index a682416225..b57ca131e4 100644 --- a/packages/event-handler/src/appsync-graphql/Router.ts +++ b/packages/event-handler/src/appsync-graphql/Router.ts @@ -356,8 +356,8 @@ class Router { fieldName, handler: handler as BatchResolverHandler, typeName, - aggregate: (batchResolverOptions?.aggregate ?? true) as T, - raiseOnError: (batchResolverOptions?.raiseOnError ?? false) as R, + aggregate: batchResolverOptions?.aggregate ?? true, + raiseOnError: batchResolverOptions?.raiseOnError ?? false, }); return; } @@ -369,8 +369,8 @@ class Router { fieldName, handler: descriptor?.value, typeName, - aggregate: (batchResolverOptions?.aggregate ?? true) as T, - raiseOnError: (batchResolverOptions?.raiseOnError ?? false) as R, + aggregate: batchResolverOptions?.aggregate ?? true, + raiseOnError: batchResolverOptions?.raiseOnError ?? false, }); return descriptor; }; From 00c62f5fdcf000a2502397431ba70a68b83d5d62 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Thu, 17 Jul 2025 20:37:15 +0600 Subject: [PATCH 07/59] feat: add InvalidBatchResponseException and enhance error handling in AppSyncGraphQLResolver --- .../appsync-graphql/AppSyncGraphQLResolver.ts | 54 +++++++++---------- .../src/appsync-graphql/errors.ts | 9 +++- .../src/appsync-graphql/index.ts | 5 +- 3 files changed, 38 insertions(+), 30 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts index 9db7d52feb..49dfab76cd 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -5,8 +5,11 @@ import type { RouteHandlerOptions, } from '../types/appsync-graphql.js'; import type { ResolveOptions } from '../types/common.js'; -import { ResolverNotFoundException } from './errors.js'; import { Router } from './Router.js'; +import { + InvalidBatchResponseException, + ResolverNotFoundException, +} from './errors.js'; import { isAppSyncGraphQLEvent } from './utils.js'; /** @@ -109,7 +112,17 @@ class AppSyncGraphQLResolver extends Router { ); return; } - return await this.#executeBatchResolvers(event, context, options); + try { + return await this.#executeBatchResolvers(event, context, options); + } catch (error) { + this.logger.error( + `An error occurred in batch handler ${event[0].info.fieldName}`, + error + ); + if (error instanceof InvalidBatchResponseException) throw error; + if (error instanceof ResolverNotFoundException) throw error; + return this.#formatErrorResponse(error); + } } if (!isAppSyncGraphQLEvent(event)) { this.logger.warn( @@ -134,7 +147,6 @@ class AppSyncGraphQLResolver extends Router { context: Context, options?: ResolveOptions ): Promise { - const results: unknown[] = []; const { fieldName, parentTypeName: typeName } = events[0].info; const batchHandlerOptions = this.batchResolverRegistry.resolve( typeName, @@ -142,24 +154,17 @@ class AppSyncGraphQLResolver extends Router { ); if (batchHandlerOptions) { - try { - const result = await this.#callBatchResolver( - events, - context, - batchHandlerOptions, - options - ); - results.push(...result); - } catch (error) { - this.logger.error( - `An error occurred in batch handler ${fieldName}`, - error - ); - throw error; - } + return await this.#callBatchResolver( + events, + context, + batchHandlerOptions, + options + ); } - return results; + throw new ResolverNotFoundException( + `No resolver found for ${typeName}-${fieldName}` + ); } async #callBatchResolver( @@ -173,7 +178,6 @@ class AppSyncGraphQLResolver extends Router { `Graceful error handling flag raiseOnError=${raiseOnError}` ); - // Checks whether the entire batch should be processed at once if (aggregate) { const response = await ( options.handler as BatchResolverAggregateHandlerFn @@ -183,7 +187,7 @@ class AppSyncGraphQLResolver extends Router { ]); if (!Array.isArray(response)) { - throw new Error( + throw new InvalidBatchResponseException( 'The response must be a List when using batch resolvers' ); } @@ -193,12 +197,6 @@ class AppSyncGraphQLResolver extends Router { const handler = options.handler as ResolverHandler; - /** - * Non aggregated events, so we call this event list x times and - * process each event individually. - */ - - // If `raiseOnError` is true, stop on first exception we encounter if (raiseOnError) { const results: unknown[] = []; for (const event of events) { @@ -211,7 +209,6 @@ class AppSyncGraphQLResolver extends Router { return results; } - // By default, we gracefully append `null` for any records that failed processing const results: unknown[] = []; for (let idx = 0; idx < events.length; idx++) { const event = events[idx]; @@ -226,6 +223,7 @@ class AppSyncGraphQLResolver extends Router { this.logger.debug( `Failed to process event number ${idx} from field '${event.info.fieldName}'` ); + // By default, we gracefully append `null` for any records that failed processing results.push(null); } } diff --git a/packages/event-handler/src/appsync-graphql/errors.ts b/packages/event-handler/src/appsync-graphql/errors.ts index 73825cf2b7..b2f601608e 100644 --- a/packages/event-handler/src/appsync-graphql/errors.ts +++ b/packages/event-handler/src/appsync-graphql/errors.ts @@ -8,4 +8,11 @@ class ResolverNotFoundException extends Error { } } -export { ResolverNotFoundException }; +class InvalidBatchResponseException extends Error { + constructor(message: string, options?: ErrorOptions) { + super(message, options); + this.name = 'InvalidBatchResponseException'; + } +} + +export { ResolverNotFoundException, InvalidBatchResponseException }; diff --git a/packages/event-handler/src/appsync-graphql/index.ts b/packages/event-handler/src/appsync-graphql/index.ts index 40524b71aa..9fe91122f4 100644 --- a/packages/event-handler/src/appsync-graphql/index.ts +++ b/packages/event-handler/src/appsync-graphql/index.ts @@ -1,5 +1,8 @@ export { AppSyncGraphQLResolver } from './AppSyncGraphQLResolver.js'; -export { ResolverNotFoundException } from './errors.js'; +export { + ResolverNotFoundException, + InvalidBatchResponseException, +} from './errors.js'; export { awsDate, awsDateTime, From 8160cc18ad0e366702485853419d4a155aa447aa Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Wed, 9 Jul 2025 10:17:54 +0600 Subject: [PATCH 08/59] fix: clarify error message for missing batch resolvers in AppSyncGraphQLResolver --- .../event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts index 49dfab76cd..ea0eb68d7f 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -163,7 +163,7 @@ class AppSyncGraphQLResolver extends Router { } throw new ResolverNotFoundException( - `No resolver found for ${typeName}-${fieldName}` + `No batch resolver found for ${typeName}-${fieldName}` ); } From 0a1026c0086d58b86432593c44317c4d16d4d9e1 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Wed, 9 Jul 2025 10:18:10 +0600 Subject: [PATCH 09/59] test: batch resolver unit tests for graphql --- .../AppSyncGraphQLResolver.test.ts | 331 +++++++++++++++++- 1 file changed, 312 insertions(+), 19 deletions(-) diff --git a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts index 6e6c2574f9..df3ffb9477 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts @@ -1,8 +1,11 @@ import context from '@aws-lambda-powertools/testing-utils/context'; -import type { Context } from 'aws-lambda'; +import type { AppSyncResolverEvent, Context } from 'aws-lambda'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { AppSyncGraphQLResolver } from '../../../src/appsync-graphql/AppSyncGraphQLResolver.js'; -import { ResolverNotFoundException } from '../../../src/appsync-graphql/index.js'; +import { + InvalidBatchResponseException, + ResolverNotFoundException, +} from '../../../src/appsync-graphql/index.js'; import { onGraphqlEventFactory } from '../../helpers/factories.js'; describe('Class: AppSyncGraphQLResolver', () => { @@ -10,23 +13,6 @@ describe('Class: AppSyncGraphQLResolver', () => { vi.clearAllMocks(); }); - it('logs a warning and returns early if the event is batched', async () => { - // Prepare - const app = new AppSyncGraphQLResolver({ logger: console }); - - // Act - const result = await app.resolve( - [onGraphqlEventFactory('getPost', 'Query')], - context - ); - - // Assess - expect(console.warn).toHaveBeenCalledWith( - 'Batch resolver is not implemented yet' - ); - expect(result).toBeUndefined(); - }); - it('logs a warning and returns early if the event is not compatible', async () => { // Prepare const app = new AppSyncGraphQLResolver({ logger: console }); @@ -67,6 +53,21 @@ describe('Class: AppSyncGraphQLResolver', () => { expect(console.error).toHaveBeenCalled(); }); + it('throws error if there are no handlers for batch events', async () => { + // Prepare + const app = new AppSyncGraphQLResolver({ logger: console }); + + // Act && Assess + await expect( + app.resolve([onGraphqlEventFactory('relatedPosts', 'Query')], context) + ).rejects.toThrow( + new ResolverNotFoundException( + 'No batch resolver found for Query-relatedPosts' + ) + ); + expect(console.error).toHaveBeenCalled(); + }); + it('returns the response of the `Query` handler', async () => { // Prepare const app = new AppSyncGraphQLResolver({ logger: console }); @@ -265,6 +266,131 @@ describe('Class: AppSyncGraphQLResolver', () => { }); }); + it('preserves the scope when using `batchResolver` decorator', async () => { + // Prepare + const app = new AppSyncGraphQLResolver({ logger: console }); + + class Lambda { + public scope = 'scoped'; + + @app.batchResolver({ fieldName: 'batchGet' }) + public async handleBatchGet( + events: AppSyncResolverEvent>[] + ) { + const ids = events.map((event) => event.arguments.id); + return ids.map((id) => ({ + id, + scope: `${this.scope} id=${id}`, + })); + } + + public async handler(event: unknown, context: Context) { + return app.resolve(event, context, { scope: this }); + } + } + const lambda = new Lambda(); + const handler = lambda.handler.bind(lambda); + + // Act + const result = await handler( + [ + onGraphqlEventFactory('batchGet', 'Query', { id: 1 }), + onGraphqlEventFactory('batchGet', 'Query', { id: 2 }), + ], + context + ); + + // Assess + expect(result).toEqual([ + { id: 1, scope: 'scoped id=1' }, + { id: 2, scope: 'scoped id=2' }, + ]); + }); + + it('preserves the scope when using `batchResolver` decorator when aggregate=false and raiseOnError=true', async () => { + // Prepare + const app = new AppSyncGraphQLResolver({ logger: console }); + + class Lambda { + public scope = 'scoped'; + + @app.batchResolver({ + fieldName: 'batchGet', + raiseOnError: true, + aggregate: false, + }) + public async handleBatchGet({ id }: { id: string }) { + return { + id, + scope: `${this.scope} id=${id}`, + }; + } + + public async handler(event: unknown, context: Context) { + return app.resolve(event, context, { scope: this }); + } + } + const lambda = new Lambda(); + const handler = lambda.handler.bind(lambda); + + // Act + const result = await handler( + [ + onGraphqlEventFactory('batchGet', 'Query', { id: 1 }), + onGraphqlEventFactory('batchGet', 'Query', { id: 2 }), + ], + context + ); + + // Assess + expect(result).toEqual([ + { id: 1, scope: 'scoped id=1' }, + { id: 2, scope: 'scoped id=2' }, + ]); + }); + + it('preserves the scope when using `batchResolver` decorator when aggregate=false and raiseOnError=false', async () => { + // Prepare + const app = new AppSyncGraphQLResolver({ logger: console }); + + class Lambda { + public scope = 'scoped'; + + @app.batchResolver({ + fieldName: 'batchGet', + raiseOnError: false, + aggregate: false, + }) + public async handleBatchGet({ id }: { id: string }) { + return { + id, + scope: `${this.scope} id=${id}`, + }; + } + + public async handler(event: unknown, context: Context) { + return app.resolve(event, context, { scope: this }); + } + } + const lambda = new Lambda(); + const handler = lambda.handler.bind(lambda); + + // Act + const result = await handler( + [ + onGraphqlEventFactory('batchGet', 'Query', { id: 1 }), + onGraphqlEventFactory('batchGet', 'Query', { id: 2 }), + ], + context + ); + + // Assess + expect(result).toEqual([ + { id: 1, scope: 'scoped id=1' }, + { id: 2, scope: 'scoped id=2' }, + ]); + }); + it('emits debug message when AWS_LAMBDA_LOG_LEVEL is set to DEBUG', async () => { // Prepare vi.stubEnv('AWS_LAMBDA_LOG_LEVEL', 'DEBUG'); @@ -346,4 +472,171 @@ describe('Class: AppSyncGraphQLResolver', () => { }); } ); + + it('logs a warning and returns early if one of the batch event is not compatible', async () => { + // Prepare + const app = new AppSyncGraphQLResolver({ logger: console }); + app.batchResolver(vi.fn(), { + fieldName: 'batchGet', + typeName: 'Query', + aggregate: true, + }); + + // Act + const result = await app.resolve( + [ + onGraphqlEventFactory('batchGet', 'Query', { id: '1' }), + { + key: 'notCompatible', + type: 'unknown', + }, + ], + context + ); + + // Assess + expect(console.warn).toHaveBeenCalledWith( + 'Received an event that is not compatible with this resolver' + ); + expect(result).toBeUndefined(); + }); + + it('registers a batch resolver via direct function call and invokes it (aggregate=true)', async () => { + // Prepare + const app = new AppSyncGraphQLResolver({ logger: console }); + const handler = vi.fn().mockResolvedValue([ + { id: '1', value: 'A' }, + { id: '2', value: 'B' }, + ]); + app.batchResolver(handler, { + fieldName: 'batchGet', + typeName: 'Query', + aggregate: true, + }); + const events = [ + onGraphqlEventFactory('batchGet', 'Query', { id: '1' }), + onGraphqlEventFactory('batchGet', 'Query', { id: '2' }), + ]; + + // Act + const result = await app.resolve(events, context); + + // Assess + expect(handler).toHaveBeenCalledWith(events, { event: events, context }); + expect(result).toEqual([ + { id: '1', value: 'A' }, + { id: '2', value: 'B' }, + ]); + }); + + it('registers a batch resolver via direct function call and invokes it (aggregate=false) and (raiseOnError=true)', async () => { + // Prepare + const app = new AppSyncGraphQLResolver({ logger: console }); + const handler = vi + .fn() + .mockResolvedValueOnce({ id: '1', value: 'A' }) + .mockResolvedValueOnce({ id: '2', value: 'B' }); + app.batchResolver(handler, { + fieldName: 'batchGet', + typeName: 'Query', + aggregate: false, + raiseOnError: true, + }); + const events = [ + onGraphqlEventFactory('batchGet', 'Query', { id: '1' }), + onGraphqlEventFactory('batchGet', 'Query', { id: '2' }), + ]; + + // Act + const result = await app.resolve(events, context); + + // Assess + expect(handler).toHaveBeenCalledTimes(2); + expect(result).toEqual([ + { id: '1', value: 'A' }, + { id: '2', value: 'B' }, + ]); + }); + + it('returns null for failed records when aggregate=false and raiseOnError=false', async () => { + // Prepare + const app = new AppSyncGraphQLResolver({ logger: console }); + const handler = vi + .fn() + .mockResolvedValueOnce({ id: '1', value: 'A' }) + .mockRejectedValueOnce(new Error('fail')) + .mockResolvedValueOnce({ id: '3', value: 'C' }); + + app.batchResolver(handler, { + fieldName: 'batchGet', + typeName: 'Query', + aggregate: false, + raiseOnError: false, + }); + const events = [ + onGraphqlEventFactory('batchGet', 'Query', { id: '1' }), + onGraphqlEventFactory('batchGet', 'Query', { id: '2' }), + onGraphqlEventFactory('batchGet', 'Query', { id: '3' }), + ]; + + // Act + const result = await app.resolve(events, context); + + // Assess + expect(result).toEqual([ + { id: '1', value: 'A' }, + null, + { id: '3', value: 'C' }, + ]); + }); + + it('stops on first error when aggregate=false and raiseOnError=true', async () => { + // Prepare + const app = new AppSyncGraphQLResolver({ logger: console }); + const handler = vi + .fn() + .mockResolvedValueOnce({ id: '1', value: 'A' }) + .mockRejectedValueOnce(new Error('fail')) + .mockResolvedValueOnce(new Error('fail again')); + app.batchResolver(handler, { + fieldName: 'batchGet', + typeName: 'Query', + aggregate: false, + raiseOnError: true, + }); + const events = [ + onGraphqlEventFactory('batchGet', 'Query', { id: '1' }), + onGraphqlEventFactory('batchGet', 'Query', { id: '2' }), + onGraphqlEventFactory('batchGet', 'Query', { id: '3' }), + ]; + + // Act && Assess + const result = await app.resolve(events, context); + expect(result).toEqual({ + error: 'Error - fail', + }); + }); + + it('throws if aggregate handler does not return an array', async () => { + // Prepare + const app = new AppSyncGraphQLResolver({ logger: console }); + const handler = vi.fn().mockResolvedValue({ id: '1', value: 'A' }); + app.batchResolver(handler, { + fieldName: 'batchGet', + typeName: 'Query', + aggregate: true, + }); + + // Act && Assess + await expect( + app.resolve( + [onGraphqlEventFactory('batchGet', 'Query', { id: '1' })], + context + ) + ).rejects.toThrow( + new InvalidBatchResponseException( + 'The response must be a List when using batch resolvers' + ) + ); + }); }); From c6259ef1e45ba63db054e97773c3dc3133dc5f00 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Wed, 9 Jul 2025 10:23:50 +0600 Subject: [PATCH 10/59] refactor: enhance GraphQlBatchRouteOptions to clarify aggregation and error handling behavior --- .../src/types/appsync-graphql.ts | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/packages/event-handler/src/types/appsync-graphql.ts b/packages/event-handler/src/types/appsync-graphql.ts index 8879667a0d..dee9f9355a 100644 --- a/packages/event-handler/src/types/appsync-graphql.ts +++ b/packages/event-handler/src/types/appsync-graphql.ts @@ -141,21 +141,31 @@ type GraphQlRouteOptions = { typeName?: string; }; +/** + * Options for configuring a batch GraphQL route handler. + * + * @template T - Determines if aggregation is enabled. If `true` (default), the `aggregate` property is set to `true` and `raiseOnError` is not allowed. + * @template R - Determines if errors should be raised when the handler fails. Defaults to `false`. + * + * If `T` is `true`, the options enforce aggregation and disallow `raiseOnError`. + * If `T` is `false` or `undefined`, both `aggregate` and `raiseOnError` can be set. + */ type GraphQlBatchRouteOptions< T extends boolean | undefined = true, R extends boolean | undefined = false, -> = GraphQlRouteOptions & { - /** - * Whether the route handler will send all the events to the route handler at once or one by one - * @default true - */ - aggregate?: T; - /** - * Whether to raise an error if the handler fails - * @default false - */ - raiseOnError?: R; -}; +> = T extends true + ? GraphQlRouteOptions & { + aggregate?: true; + raiseOnError?: never; + } + : GraphQlRouteOptions & { + aggregate?: T; + /** + * Whether to raise an error if the handler fails + * @default false + */ + raiseOnError?: R; + }; export type { RouteHandlerRegistryOptions, From 0f404f9dc435299e22062e2bc977bf8697c0cd6c Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Wed, 9 Jul 2025 12:02:18 +0600 Subject: [PATCH 11/59] fix: improve error handling and logging for batch events in AppSyncGraphQLResolver --- .../appsync-graphql/AppSyncGraphQLResolver.ts | 66 +++++++++++++------ .../AppSyncGraphQLResolver.test.ts | 2 +- 2 files changed, 47 insertions(+), 21 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts index ea0eb68d7f..ab80363e56 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -106,23 +106,16 @@ class AppSyncGraphQLResolver extends Router { options?: ResolveOptions ): Promise { if (Array.isArray(event)) { - if (event.some((singleEvent) => !isAppSyncGraphQLEvent(singleEvent))) { + if (!event.every((e) => isAppSyncGraphQLEvent(e))) { this.logger.warn( - 'Received an event that is not compatible with this resolver' + 'Received a batch event that is not compatible with this resolver' ); return; } - try { - return await this.#executeBatchResolvers(event, context, options); - } catch (error) { - this.logger.error( - `An error occurred in batch handler ${event[0].info.fieldName}`, - error - ); - if (error instanceof InvalidBatchResponseException) throw error; - if (error instanceof ResolverNotFoundException) throw error; - return this.#formatErrorResponse(error); - } + return this.#withErrorHandling( + () => this.#executeBatchResolvers(event, context, options), + `An error occurred in handler ${event[0].info.fieldName}` + ); } if (!isAppSyncGraphQLEvent(event)) { this.logger.warn( @@ -130,18 +123,51 @@ class AppSyncGraphQLResolver extends Router { ); return; } + + return this.#withErrorHandling( + () => this.#executeSingleResolver(event, context, options), + `An error occurred in handler ${event.info.fieldName}` + ); + } + + /** + * Executes the provided asynchronous function with error handling. + * If the function throws an error, it delegates error processing to `#handleError` + * and returns its result cast to the expected type. + * + * @typeParam T - The return type of the asynchronous function. + * @param fn - A function returning a Promise of type `T` to be executed. + * @param errorMessage - A custom error message to be used if an error occurs. + */ + async #withErrorHandling( + fn: () => Promise, + errorMessage: string + ): Promise { try { - return await this.#executeSingleResolver(event, context, options); + return await fn(); } catch (error) { - this.logger.error( - `An error occurred in handler ${event.info.fieldName}`, - error - ); - if (error instanceof ResolverNotFoundException) throw error; - return this.#formatErrorResponse(error); + return this.#handleError(error, errorMessage) as T; } } + /** + * Handles errors encountered during resolver execution. + * + * Logs the provided error message and error object. If the error is an instance of + * `InvalidBatchResponseException` or `ResolverNotFoundException`, it is re-thrown. + * Otherwise, the error is formatted into a response using `#formatErrorResponse`. + * + * @param error - The error object to handle. + * @param errorMessage - A descriptive message to log alongside the error. + * @throws InvalidBatchResponseException | ResolverNotFoundException + */ + #handleError(error: unknown, errorMessage: string) { + this.logger.error(errorMessage, error); + if (error instanceof InvalidBatchResponseException) throw error; + if (error instanceof ResolverNotFoundException) throw error; + return this.#formatErrorResponse(error); + } + async #executeBatchResolvers( events: AppSyncResolverEvent>[], context: Context, diff --git a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts index df3ffb9477..eb2c6e5ca8 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts @@ -496,7 +496,7 @@ describe('Class: AppSyncGraphQLResolver', () => { // Assess expect(console.warn).toHaveBeenCalledWith( - 'Received an event that is not compatible with this resolver' + 'Received a batch event that is not compatible with this resolver' ); expect(result).toBeUndefined(); }); From 192015e1d58dc9fce040a690a66235d600d84a62 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Wed, 9 Jul 2025 12:18:22 +0600 Subject: [PATCH 12/59] fix: improve batch event validation in AppSyncGraphQLResolver --- .../event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts index ab80363e56..595f7a8110 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -106,7 +106,7 @@ class AppSyncGraphQLResolver extends Router { options?: ResolveOptions ): Promise { if (Array.isArray(event)) { - if (!event.every((e) => isAppSyncGraphQLEvent(e))) { + if (event.some((e) => !isAppSyncGraphQLEvent(e))) { this.logger.warn( 'Received a batch event that is not compatible with this resolver' ); From e976acb4ec68cd96bf19bc27116e4c713dbe827e Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Wed, 9 Jul 2025 18:46:48 +0600 Subject: [PATCH 13/59] refactor: streamline GraphQlBatchRouteOptions type definition for clarity --- .../event-handler/src/types/appsync-graphql.ts | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/packages/event-handler/src/types/appsync-graphql.ts b/packages/event-handler/src/types/appsync-graphql.ts index dee9f9355a..93066a659a 100644 --- a/packages/event-handler/src/types/appsync-graphql.ts +++ b/packages/event-handler/src/types/appsync-graphql.ts @@ -153,19 +153,10 @@ type GraphQlRouteOptions = { type GraphQlBatchRouteOptions< T extends boolean | undefined = true, R extends boolean | undefined = false, -> = T extends true - ? GraphQlRouteOptions & { - aggregate?: true; - raiseOnError?: never; - } - : GraphQlRouteOptions & { - aggregate?: T; - /** - * Whether to raise an error if the handler fails - * @default false - */ - raiseOnError?: R; - }; +> = GraphQlRouteOptions & + (T extends true + ? { aggregate?: true; raiseOnError?: never } + : { aggregate?: T; raiseOnError?: R }); export type { RouteHandlerRegistryOptions, From a00f66c9ed95824e208d0ce88b1d4b1fc71020ae Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Thu, 17 Jul 2025 20:42:45 +0600 Subject: [PATCH 14/59] feat: add batch resolver support and detailed documentation for AppSyncGraphQLResolver --- .../appsync-graphql/AppSyncGraphQLResolver.ts | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts index 595f7a8110..79112cff82 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -5,11 +5,11 @@ import type { RouteHandlerOptions, } from '../types/appsync-graphql.js'; import type { ResolveOptions } from '../types/common.js'; -import { Router } from './Router.js'; import { InvalidBatchResponseException, ResolverNotFoundException, } from './errors.js'; +import { Router } from './Router.js'; import { isAppSyncGraphQLEvent } from './utils.js'; /** @@ -66,6 +66,29 @@ class AppSyncGraphQLResolver extends Router { * app.resolve(event, context); * ``` * + * Resolves the response based on the provided batch event and route handlers configured. + * + * @example + * ```ts + * import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; + * import type { AppSyncResolverEvent, Context } from 'aws-lambda'; + * + * const app = new AppSyncGraphQLResolver(); + * + * app.batchResolver(async (events: AppSyncResolverEvent>[]) => { + * // your business logic here + * const ids = events.map((event) => event.source.id); + * return ids.map((id) => ({ + * id, + * title: 'Post Title', + * content: 'Post Content', + * })); + * }); + * + * export const handler = async (event, context) => + * app.resolve(event, context); + * ``` + * * The method works also as class method decorator, so you can use it like this: * * @example @@ -193,6 +216,21 @@ class AppSyncGraphQLResolver extends Router { ); } + /** + * Handles batch invocation of AppSync GraphQL resolvers with support for aggregation and error handling. + * + * @param events - An array of AppSyncResolverEvent objects representing the batch of incoming events. + * @param context - The Lambda context object. + * @param options - Route handler options, including the handler function, aggregation, and error handling flags. + * @param resolveOptions - Optional resolve options, such as custom scope for handler invocation. + * + * @throws {InvalidBatchResponseException} If the aggregate handler does not return an array. + * + * @remarks + * - If `aggregate` is true, invokes the handler once with the entire batch and expects an array response. + * - If `raiseOnError` is true, errors are propagated and will cause the function to throw. + * - If `raiseOnError` is false, errors are logged and `null` is appended for failed events, allowing graceful degradation. + */ async #callBatchResolver( events: AppSyncResolverEvent>[], context: Context, From 9ba7e66c3f2e4d1f842df2137b48456a5cad0936 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Thu, 17 Jul 2025 20:49:42 +0600 Subject: [PATCH 15/59] feat: add batch resolver execution method with detailed documentation in AppSyncGraphQLResolver --- .../src/appsync-graphql/AppSyncGraphQLResolver.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts index 79112cff82..5d284ab338 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -191,6 +191,19 @@ class AppSyncGraphQLResolver extends Router { return this.#formatErrorResponse(error); } + /** + * Executes batch resolvers for multiple AppSync GraphQL events. + * + * This method processes an array of AppSync resolver events as a batch operation. + * It looks up the appropriate batch resolver from the registry using the field name + * and parent type name from the first event, then delegates to the batch resolver + * if found. + * + * @param events - Array of AppSync resolver events to process as a batch + * @param context - AWS Lambda context object + * @param options - Optional resolve options for customizing resolver behavior + * @throws {ResolverNotFoundException} When no batch resolver is registered for the given type and field combination + */ async #executeBatchResolvers( events: AppSyncResolverEvent>[], context: Context, From b5e24b2cd1260e0341a3d3b2cfb6cedfa3ab1ddf Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 20 Jul 2025 12:14:38 +0600 Subject: [PATCH 16/59] refactor: remove typing of aggregate handler --- .../event-handler/src/types/appsync-graphql.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/event-handler/src/types/appsync-graphql.ts b/packages/event-handler/src/types/appsync-graphql.ts index 93066a659a..db14784510 100644 --- a/packages/event-handler/src/types/appsync-graphql.ts +++ b/packages/event-handler/src/types/appsync-graphql.ts @@ -21,16 +21,16 @@ type BatchResolverHandlerFn> = ( } ) => Promise; -type BatchResolverAggregateHandlerFn> = ( - event: AppSyncResolverEvent[], +type BatchResolverAggregateHandlerFn = ( + event: AppSyncResolverEvent>[], options: { - event: AppSyncResolverEvent[]; + event: AppSyncResolverEvent>[]; context: Context; } ) => Promise; -type BatchResolverSyncAggregateHandlerFn> = ( - event: AppSyncResolverEvent[], +type BatchResolverSyncAggregateHandlerFn = ( + event: AppSyncResolverEvent>[], options: { context: Context; } @@ -40,9 +40,7 @@ type BatchResolverHandler< TParams = Record, T extends boolean | undefined = undefined, > = T extends true - ? - | BatchResolverAggregateHandlerFn - | BatchResolverSyncAggregateHandlerFn + ? BatchResolverAggregateHandlerFn | BatchResolverSyncAggregateHandlerFn : BatchResolverHandlerFn | BatchResolverSyncHandlerFn; // #region Resolver fn From af35ca656ad1002c5e309e3822ca271d42830103 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 20 Jul 2025 12:15:15 +0600 Subject: [PATCH 17/59] refactor: specify type parameters for resolvers in MockRouteHandlerRegistry --- .../tests/unit/appsync-graphql/RouteHandlerRegistry.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/event-handler/tests/unit/appsync-graphql/RouteHandlerRegistry.test.ts b/packages/event-handler/tests/unit/appsync-graphql/RouteHandlerRegistry.test.ts index 215a05445d..4de557fd63 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/RouteHandlerRegistry.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/RouteHandlerRegistry.test.ts @@ -4,7 +4,10 @@ import type { RouteHandlerOptions } from '../../../src/types/appsync-graphql.js' describe('Class: RouteHandlerRegistry', () => { class MockRouteHandlerRegistry extends RouteHandlerRegistry { - public declare resolvers: Map; + public declare resolvers: Map< + string, + RouteHandlerOptions, boolean, boolean> + >; } const getRegistry = () => new MockRouteHandlerRegistry({ logger: console }); From edffb08bd2ace2401bf2482d3846637749d5b58f Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 20 Jul 2025 12:24:09 +0600 Subject: [PATCH 18/59] feat: add detailed documentation for batch resolver registration in Router class --- .../src/appsync-graphql/Router.ts | 102 +++++++++++++++++- 1 file changed, 97 insertions(+), 5 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/Router.ts b/packages/event-handler/src/appsync-graphql/Router.ts index b57ca131e4..3b9e196322 100644 --- a/packages/event-handler/src/appsync-graphql/Router.ts +++ b/packages/event-handler/src/appsync-graphql/Router.ts @@ -328,6 +328,100 @@ class Router { }; } + /** + * Register a batch resolver function for GraphQL events that support batching. + * + * Registers a handler for a specific GraphQL field that can process multiple requests in a batch. + * The handler will be invoked when requests are made for the specified field, and can either + * process requests individually or aggregate them for batch processing. + * + * @example + * ```ts + * import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; + * import type { AppSyncResolverEvent } from 'aws-lambda'; + * + * const app = new AppSyncGraphQLResolver(); + * + * // Register a batch Query resolver with aggregation (default) + * app.batchResolver(async (events: AppSyncResolverEvent>[]) => { + * // Process all events in batch + * return events.map(event => ({ id: event.source.id, data: 'processed' })); + * }, { + * fieldName: 'getPosts' + * }); + * + * // Register a batch resolver without aggregation + * app.batchResolver(async (args, { event, context }) => { + * // Process individual request + * return { id: args.id, data: 'processed' }; + * }, { + * fieldName: 'getPost', + * aggregate: false + * }); + * + * export const handler = async (event, context) => + * app.resolve(event, context); + * ``` + * + * You can also specify the type of the arguments using generic type parameters for non-aggregated handlers: + * + * @example + * ```ts + * import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; + * + * const app = new AppSyncGraphQLResolver() + * + * app.batchResolver<{ postId: string }, false>(async (args, { event, context }) => { + * // args is typed as { postId: string } + * return { id: args.postId }; + * }, { + * fieldName: 'getPost', + * aggregate: false + * }); + * + * export const handler = async (event, context) => + * app.resolve(event, context); + * ``` + * + * As a decorator: + * + * @example + * ```ts + * import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; + * + * const app = new AppSyncGraphQLResolver(); + * + * class Lambda { + * ⁣@app.batchResolver({ fieldName: 'getPosts' }) + * async handleGetPosts(events) { + * // Process batch of events + * return events.map(event => ({ id: event.arguments.id, data: 'processed' })); + * } + * + * ⁣@app.batchResolver({ fieldName: 'getPost', aggregate: false }) + * async handleGetPost(args, { event, context }) { + * // Process individual request + * return { id: args.id, data: 'processed' }; + * } + * + * async handler(event, context) { + * return app.resolve(event, context, { + * scope: this, // bind decorated methods to the class instance + * }); + * } + * } + * + * const lambda = new Lambda(); + * export const handler = lambda.handler.bind(lambda); + * ``` + * + * @param handler - The batch handler function to be called when events are received. + * @param options - Batch route options including the required fieldName and optional configuration. + * @param options.fieldName - The name of the field to register the handler for. + * @param options.typeName - The name of the GraphQL type to use for the resolver, defaults to `Query`. + * @param options.aggregate - Whether to aggregate multiple requests into a single handler call, defaults to `true`. + * @param options.raiseOnError - Whether to raise errors when processing individual requests (only available when aggregate is false), defaults to `false`. + */ public batchResolver< TParams extends Record, T extends boolean = true, @@ -336,11 +430,9 @@ class Router { handler: BatchResolverHandler, options: GraphQlBatchRouteOptions ): void; - public batchResolver< - TParams extends Record, - T extends boolean = true, - R extends boolean = false, - >(options: GraphQlBatchRouteOptions): MethodDecorator; + public batchResolver( + options: GraphQlBatchRouteOptions + ): MethodDecorator; public batchResolver< TParams extends Record, T extends boolean = true, From 094fcb156fcdb0aedbe8ba29cb43413c8a1bdc2a Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 20 Jul 2025 16:38:16 +0600 Subject: [PATCH 19/59] feat: add batch resolver registration methods for query and mutation events in Router class --- .../src/appsync-graphql/Router.ts | 228 ++++++++++++++++++ .../tests/unit/appsync-graphql/Router.test.ts | 95 ++++++++ 2 files changed, 323 insertions(+) diff --git a/packages/event-handler/src/appsync-graphql/Router.ts b/packages/event-handler/src/appsync-graphql/Router.ts index 3b9e196322..8c7e240ed1 100644 --- a/packages/event-handler/src/appsync-graphql/Router.ts +++ b/packages/event-handler/src/appsync-graphql/Router.ts @@ -467,6 +467,234 @@ class Router { return descriptor; }; } + + /** + * Register a batch handler function for the `query` event. + * + * Registers a batch handler for a specific GraphQL Query field that can process multiple requests in a batch. + * The handler will be invoked when requests are made for the specified field in the Query type. + * + * @example + * ```ts + * import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; + * import type { AppSyncResolverEvent } from 'aws-lambda'; + * + * const app = new AppSyncGraphQLResolver(); + * + * // Register a batch Query resolver with aggregation (default) + * app.onBatchQuery('getPosts', async (events: AppSyncResolverEvent>[]) => { + * // Process all events in batch + * return events.map(event => ({ id: event.source.id, data: 'processed' })); + * }); + * + * // Register a batch Query resolver without aggregation + * app.onBatchQuery('getPost', async (args, { event, context }) => { + * // Process individual request + * return { id: args.id, data: 'processed' }; + * }, { aggregate: false }); + * + * export const handler = async (event, context) => + * app.resolve(event, context); + * ``` + * + * As a decorator: + * + * @example + * ```ts + * import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; + * + * const app = new AppSyncGraphQLResolver(); + * + * class Lambda { + * ⁣@app.onBatchQuery('getPosts') + * async handleGetPosts(events) { + * // Process batch of events + * return events.map(event => ({ id: event.arguments.id, data: 'processed' })); + * } + * + * ⁣@app.onBatchQuery('getPost', { aggregate: false }) + * async handleGetPost(args, { event, context }) { + * // Process individual request + * return { id: args.id, data: 'processed' }; + * } + * + * async handler(event, context) { + * return app.resolve(event, context, { + * scope: this, // bind decorated methods to the class instance + * }); + * } + * } + * + * const lambda = new Lambda(); + * export const handler = lambda.handler.bind(lambda); + * ``` + * + * @param fieldName - The name of the Query field to register the batch handler for. + * @param handler - The batch handler function to be called when events are received. + * @param options - Optional batch configuration including aggregate and raiseOnError settings. + */ + public onBatchQuery< + TParams extends Record, + T extends boolean = true, + R extends boolean = false, + >( + fieldName: string, + handler: BatchResolverHandler, + options?: Omit, 'fieldName' | 'typeName'> + ): void; + public onBatchQuery( + fieldName: string, + options?: Omit, 'fieldName' | 'typeName'> + ): MethodDecorator; + public onBatchQuery< + TParams extends Record, + T extends boolean = true, + R extends boolean = false, + >( + fieldName: string, + handlerOrOptions?: + | BatchResolverHandler + | Omit, 'fieldName' | 'typeName'>, + options?: Omit, 'fieldName' | 'typeName'> + ): MethodDecorator | undefined { + if (typeof handlerOrOptions === 'function') { + this.batchResolverRegistry.register({ + fieldName, + handler: handlerOrOptions as BatchResolverHandler, + typeName: 'Query', + aggregate: options?.aggregate ?? true, + raiseOnError: options?.raiseOnError ?? false, + }); + + return; + } + + return (_target, _propertyKey, descriptor: PropertyDescriptor) => { + this.batchResolverRegistry.register({ + fieldName, + handler: descriptor?.value, + typeName: 'Query', + aggregate: handlerOrOptions?.aggregate ?? true, + raiseOnError: handlerOrOptions?.raiseOnError ?? false, + }); + + return descriptor; + }; + } + + /** + * Register a batch handler function for the `mutation` event. + * + * Registers a batch handler for a specific GraphQL Mutation field that can process multiple requests in a batch. + * The handler will be invoked when requests are made for the specified field in the Mutation type. + * + * @example + * ```ts + * import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; + * import type { AppSyncResolverEvent } from 'aws-lambda'; + * + * const app = new AppSyncGraphQLResolver(); + * + * // Register a batch Mutation resolver with aggregation (default) + * app.onBatchMutation('createPosts', async (events: AppSyncResolverEvent>[]) => { + * // Process all events in batch + * return events.map(event => ({ id: event.arguments.id, status: 'created' })); + * }); + * + * // Register a batch Mutation resolver without aggregation + * app.onBatchMutation('createPost', async (args, { event, context }) => { + * // Process individual request + * return { id: args.id, status: 'created' }; + * }, { aggregate: false }); + * + * export const handler = async (event, context) => + * app.resolve(event, context); + * ``` + * + * As a decorator: + * + * @example + * ```ts + * import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; + * + * const app = new AppSyncGraphQLResolver(); + * + * class Lambda { + * ⁣@app.onBatchMutation('createPosts') + * async handleCreatePosts(events) { + * // Process batch of events + * return events.map(event => ({ id: event.arguments.id, status: 'created' })); + * } + * + * ⁣@app.onBatchMutation('createPost', { aggregate: false }) + * async handleCreatePost(args, { event, context }) { + * // Process individual request + * return { id: args.id, status: 'created' }; + * } + * + * async handler(event, context) { + * return app.resolve(event, context, { + * scope: this, // bind decorated methods to the class instance + * }); + * } + * } + * + * const lambda = new Lambda(); + * export const handler = lambda.handler.bind(lambda); + * ``` + * + * @param fieldName - The name of the Mutation field to register the batch handler for. + * @param handler - The batch handler function to be called when events are received. + * @param options - Optional batch configuration including aggregate and raiseOnError settings. + */ + public onBatchMutation< + TParams extends Record, + T extends boolean = true, + R extends boolean = false, + >( + fieldName: string, + handler: BatchResolverHandler, + options?: Omit, 'fieldName' | 'typeName'> + ): void; + public onBatchMutation( + fieldName: string, + options?: Omit, 'fieldName' | 'typeName'> + ): MethodDecorator; + public onBatchMutation< + TParams extends Record, + T extends boolean = true, + R extends boolean = false, + >( + fieldName: string, + handlerOrOptions?: + | BatchResolverHandler + | Omit, 'fieldName' | 'typeName'>, + options?: Omit, 'fieldName' | 'typeName'> + ): MethodDecorator | undefined { + if (typeof handlerOrOptions === 'function') { + this.batchResolverRegistry.register({ + fieldName, + handler: handlerOrOptions as BatchResolverHandler, + typeName: 'Mutation', + aggregate: options?.aggregate ?? true, + raiseOnError: options?.raiseOnError ?? false, + }); + + return; + } + + return (_target, _propertyKey, descriptor: PropertyDescriptor) => { + this.batchResolverRegistry.register({ + fieldName, + handler: descriptor?.value, + typeName: 'Mutation', + aggregate: handlerOrOptions?.aggregate ?? true, + raiseOnError: handlerOrOptions?.raiseOnError ?? false, + }); + + return descriptor; + }; + } } export { Router }; diff --git a/packages/event-handler/tests/unit/appsync-graphql/Router.test.ts b/packages/event-handler/tests/unit/appsync-graphql/Router.test.ts index 29c6a02aae..b46cc653ff 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/Router.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/Router.test.ts @@ -122,6 +122,101 @@ describe('Class: Router', () => { ]); }); + it('registers batch resolvers using the functional approach', () => { + // Prepare + const app = new Router({ logger: console }); + const getPosts = vi.fn(() => [{ id: 1 }]); + const addPosts = vi.fn(async () => [{ id: 2 }]); + + // Act + app.onBatchQuery('getPosts', getPosts); + app.onBatchMutation('addPosts', addPosts); + + // Assess + expect(console.debug).toHaveBeenNthCalledWith( + 1, + 'Adding resolver for field Query.getPosts' + ); + expect(console.debug).toHaveBeenNthCalledWith( + 2, + 'Adding resolver for field Mutation.addPosts' + ); + }); + + it('registers batch resolvers using the decorator pattern', () => { + // Prepare + const app = new Router({ logger: console }); + + // Act + class Lambda { + readonly prop = 'value'; + + @app.onBatchQuery('getPosts') + public getPosts() { + return `${this.prop} batchQuery`; + } + + @app.onBatchMutation('addPosts') + public addPosts() { + return `${this.prop} batchMutation`; + } + } + const lambda = new Lambda(); + const res1 = lambda.getPosts(); + const res2 = lambda.addPosts(); + + // Assess + expect(console.debug).toHaveBeenNthCalledWith( + 1, + 'Adding resolver for field Query.getPosts' + ); + expect(console.debug).toHaveBeenNthCalledWith( + 2, + 'Adding resolver for field Mutation.addPosts' + ); + + // verify that class scope is preserved after decorating + expect(res1).toBe('value batchQuery'); + expect(res2).toBe('value batchMutation'); + }); + + it('registers nested batch resolvers using the decorator pattern', () => { + // Prepare + const app = new Router({ logger: console }); + + // Act + class Lambda { + readonly prop = 'value'; + + @app.onBatchQuery('listLocations') + @app.onBatchQuery('locations') + public getLocations() { + return [ + { + name: 'Location 1', + description: 'Description 1', + }, + ]; + } + } + const lambda = new Lambda(); + const response = lambda.getLocations(); + + // Assess + expect(console.debug).toHaveBeenNthCalledWith( + 1, + 'Adding resolver for field Query.locations' + ); + expect(console.debug).toHaveBeenNthCalledWith( + 2, + 'Adding resolver for field Query.listLocations' + ); + + expect(response).toEqual([ + { name: 'Location 1', description: 'Description 1' }, + ]); + }); + it('uses a default logger with only warnings if none is provided', () => { // Prepare const app = new Router(); From 3610c9dfd42fb82bebb76288a8c73841646a4045 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 20 Jul 2025 16:43:08 +0600 Subject: [PATCH 20/59] fix: correct documentation for InvalidBatchResponseException in errors.ts --- packages/event-handler/src/appsync-graphql/errors.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/event-handler/src/appsync-graphql/errors.ts b/packages/event-handler/src/appsync-graphql/errors.ts index b2f601608e..e005499e78 100644 --- a/packages/event-handler/src/appsync-graphql/errors.ts +++ b/packages/event-handler/src/appsync-graphql/errors.ts @@ -8,6 +8,9 @@ class ResolverNotFoundException extends Error { } } +/** + * Error thrown when the response from a batch resolver is invalid. + */ class InvalidBatchResponseException extends Error { constructor(message: string, options?: ErrorOptions) { super(message, options); From f01d34e7eb8f6e074225bc15546215c00054a5b1 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 20 Jul 2025 19:39:59 +0600 Subject: [PATCH 21/59] refactor: simplify GraphQlBatchRouteOptions type definition --- packages/event-handler/src/types/appsync-graphql.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/event-handler/src/types/appsync-graphql.ts b/packages/event-handler/src/types/appsync-graphql.ts index db14784510..95465d1823 100644 --- a/packages/event-handler/src/types/appsync-graphql.ts +++ b/packages/event-handler/src/types/appsync-graphql.ts @@ -144,17 +144,11 @@ type GraphQlRouteOptions = { * * @template T - Determines if aggregation is enabled. If `true` (default), the `aggregate` property is set to `true` and `raiseOnError` is not allowed. * @template R - Determines if errors should be raised when the handler fails. Defaults to `false`. - * - * If `T` is `true`, the options enforce aggregation and disallow `raiseOnError`. - * If `T` is `false` or `undefined`, both `aggregate` and `raiseOnError` can be set. */ type GraphQlBatchRouteOptions< T extends boolean | undefined = true, R extends boolean | undefined = false, -> = GraphQlRouteOptions & - (T extends true - ? { aggregate?: true; raiseOnError?: never } - : { aggregate?: T; raiseOnError?: R }); +> = GraphQlRouteOptions & { aggregate?: T; raiseOnError?: R }; export type { RouteHandlerRegistryOptions, From 29de9374d3aa9bcb969b63b6db5cce1f1ec90cad Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 20 Jul 2025 21:04:47 +0600 Subject: [PATCH 22/59] fix: update error message for batch resolver response validation --- .../src/appsync-graphql/AppSyncGraphQLResolver.ts | 4 ++-- .../tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts index 5d284ab338..56ade8bfab 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -252,7 +252,7 @@ class AppSyncGraphQLResolver extends Router { ): Promise { const { aggregate, raiseOnError } = options; this.logger.debug( - `Graceful error handling flag raiseOnError=${raiseOnError}` + `Aggregate flag aggregate=${aggregate} & Graceful error handling flag raiseOnError=${raiseOnError}` ); if (aggregate) { @@ -265,7 +265,7 @@ class AppSyncGraphQLResolver extends Router { if (!Array.isArray(response)) { throw new InvalidBatchResponseException( - 'The response must be a List when using batch resolvers' + 'The response must be an array when using batch resolvers' ); } diff --git a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts index eb2c6e5ca8..dbb61be058 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts @@ -635,7 +635,7 @@ describe('Class: AppSyncGraphQLResolver', () => { ) ).rejects.toThrow( new InvalidBatchResponseException( - 'The response must be a List when using batch resolvers' + 'The response must be an array when using batch resolvers' ) ); }); From 22a0a4c486b5c4326e893356eb983cdafddeabe8 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 20 Jul 2025 21:11:41 +0600 Subject: [PATCH 23/59] fix: update loop index variable for event processing in AppSyncGraphQLResolver --- .../src/appsync-graphql/AppSyncGraphQLResolver.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts index 56ade8bfab..7817da3d28 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -287,8 +287,8 @@ class AppSyncGraphQLResolver extends Router { } const results: unknown[] = []; - for (let idx = 0; idx < events.length; idx++) { - const event = events[idx]; + for (let i = 0; i < events.length; i++) { + const event = events[i]; try { const result = await handler.apply(resolveOptions?.scope ?? this, [ event.arguments, @@ -298,7 +298,7 @@ class AppSyncGraphQLResolver extends Router { } catch (error) { this.logger.error(error); this.logger.debug( - `Failed to process event number ${idx} from field '${event.info.fieldName}'` + `Failed to process event number ${i} from field '${event.info.fieldName}'` ); // By default, we gracefully append `null` for any records that failed processing results.push(null); From 587818f122cee0a0a11dad1e25ee247389cd9829 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Mon, 21 Jul 2025 22:29:41 +0600 Subject: [PATCH 24/59] fix: clarify template parameter descriptions in GraphQlBatchRouteOptions --- packages/event-handler/src/types/appsync-graphql.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/event-handler/src/types/appsync-graphql.ts b/packages/event-handler/src/types/appsync-graphql.ts index 95465d1823..fab85d5f14 100644 --- a/packages/event-handler/src/types/appsync-graphql.ts +++ b/packages/event-handler/src/types/appsync-graphql.ts @@ -142,8 +142,8 @@ type GraphQlRouteOptions = { /** * Options for configuring a batch GraphQL route handler. * - * @template T - Determines if aggregation is enabled. If `true` (default), the `aggregate` property is set to `true` and `raiseOnError` is not allowed. - * @template R - Determines if errors should be raised when the handler fails. Defaults to `false`. + * @template T - If `true`, the handler receives all events at once. If `false`, the handler is called for each event individually. Defaults to `true`. + * @template R - If `true`, errors thrown by the handler will be raised. Defaults to `false`. */ type GraphQlBatchRouteOptions< T extends boolean | undefined = true, From 92d0768bd4d0af5dd6f2fcb5c2576121dea052f0 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Mon, 21 Jul 2025 22:35:07 +0600 Subject: [PATCH 25/59] docs: add example for batch resolver usage in AppSyncGraphQLResolver --- .../appsync-graphql/AppSyncGraphQLResolver.ts | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts index 7817da3d28..48cb8f4ec4 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -119,6 +119,35 @@ class AppSyncGraphQLResolver extends Router { * export const handler = lambda.handler.bind(lambda); * ``` * + * @example + * ```ts + * import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; + * import type { AppSyncResolverEvent } from 'aws-lambda'; + * + * const app = new AppSyncGraphQLResolver(); + * + * class Lambda { + * ⁣@app.batchResolver({ fieldName: 'getPosts', typeName: 'Query' }) + * async getPosts(events: AppSyncResolverEvent>[]) { + * // your business logic here + * const ids = events.map((event) => event.source.id); + * return ids.map((id) => ({ + * id, + * title: 'Post Title', + * content: 'Post Content', + * })); + * } + * + * async handler(event, context) { + * return app.resolve(event, context, { + * scope: this, // bind decorated methods to the class instance + * }); + * } + * } + * + * const lambda = new Lambda(); + * export const handler = lambda.handler.bind(lambda); + * ``` * @param event - The incoming event, which may be an AppSync GraphQL event or an array of events. * @param context - The AWS Lambda context object. * @param options - Optional parameters for the resolver, such as the scope of the handler. From 0dcc54c3542d6a59c2942f5186bdd179308f99fb Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Wed, 23 Jul 2025 09:30:29 +0600 Subject: [PATCH 26/59] fix: update batch resolver to use event.arguments.id for ID extraction --- .../src/appsync-graphql/AppSyncGraphQLResolver.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts index 48cb8f4ec4..09c20a6ce6 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -75,9 +75,9 @@ class AppSyncGraphQLResolver extends Router { * * const app = new AppSyncGraphQLResolver(); * - * app.batchResolver(async (events: AppSyncResolverEvent>[]) => { + * app.batchResolver(async (events: AppSyncResolverEvent<{ id: number }>[]) => { * // your business logic here - * const ids = events.map((event) => event.source.id); + * const ids = events.map((event) => event.arguments.id); * return ids.map((id) => ({ * id, * title: 'Post Title', @@ -128,9 +128,9 @@ class AppSyncGraphQLResolver extends Router { * * class Lambda { * ⁣@app.batchResolver({ fieldName: 'getPosts', typeName: 'Query' }) - * async getPosts(events: AppSyncResolverEvent>[]) { + * async getPosts(events: AppSyncResolverEvent<{ id: number }>[]) { * // your business logic here - * const ids = events.map((event) => event.source.id); + * const ids = events.map((event) => event.arguments.id); * return ids.map((id) => ({ * id, * title: 'Post Title', From 8df5f8c9b714233e19731c5033b585ba667d3a25 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Wed, 23 Jul 2025 09:46:23 +0600 Subject: [PATCH 27/59] fix: remove unused import and optimize event processing loop in AppSyncGraphQLResolver --- .../src/appsync-graphql/AppSyncGraphQLResolver.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts index 09c20a6ce6..6e7d786eed 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -71,7 +71,7 @@ class AppSyncGraphQLResolver extends Router { * @example * ```ts * import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; - * import type { AppSyncResolverEvent, Context } from 'aws-lambda'; + * import type { AppSyncResolverEvent } from 'aws-lambda'; * * const app = new AppSyncGraphQLResolver(); * @@ -302,9 +302,9 @@ class AppSyncGraphQLResolver extends Router { } const handler = options.handler as ResolverHandler; + const results: unknown[] = []; if (raiseOnError) { - const results: unknown[] = []; for (const event of events) { const result = await handler.apply(resolveOptions?.scope ?? this, [ event.arguments, @@ -315,9 +315,7 @@ class AppSyncGraphQLResolver extends Router { return results; } - const results: unknown[] = []; - for (let i = 0; i < events.length; i++) { - const event = events[i]; + for (const [i, event] of events.entries()) { try { const result = await handler.apply(resolveOptions?.scope ?? this, [ event.arguments, From 1220f19dae83998f0d7ee73c148285d564e1d23c Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Wed, 23 Jul 2025 10:04:25 +0600 Subject: [PATCH 28/59] fix: parameterize BatchResolverAggregateHandlerFn and BatchResolverSyncAggregateHandlerFn types --- .../event-handler/src/types/appsync-graphql.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/event-handler/src/types/appsync-graphql.ts b/packages/event-handler/src/types/appsync-graphql.ts index fab85d5f14..905b4f321f 100644 --- a/packages/event-handler/src/types/appsync-graphql.ts +++ b/packages/event-handler/src/types/appsync-graphql.ts @@ -21,16 +21,16 @@ type BatchResolverHandlerFn> = ( } ) => Promise; -type BatchResolverAggregateHandlerFn = ( - event: AppSyncResolverEvent>[], +type BatchResolverAggregateHandlerFn> = ( + event: AppSyncResolverEvent[], options: { - event: AppSyncResolverEvent>[]; + event: AppSyncResolverEvent[]; context: Context; } ) => Promise; -type BatchResolverSyncAggregateHandlerFn = ( - event: AppSyncResolverEvent>[], +type BatchResolverSyncAggregateHandlerFn> = ( + event: AppSyncResolverEvent[], options: { context: Context; } @@ -40,7 +40,9 @@ type BatchResolverHandler< TParams = Record, T extends boolean | undefined = undefined, > = T extends true - ? BatchResolverAggregateHandlerFn | BatchResolverSyncAggregateHandlerFn + ? + | BatchResolverAggregateHandlerFn + | BatchResolverSyncAggregateHandlerFn : BatchResolverHandlerFn | BatchResolverSyncHandlerFn; // #region Resolver fn From fa88d05985a01483e1dc1410ee1a500801de883e Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Fri, 25 Jul 2025 12:51:40 +0600 Subject: [PATCH 29/59] fix: clarify GraphQlBatchRouteOptions template parameter behavior regarding raiseOnError --- packages/event-handler/src/types/appsync-graphql.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/event-handler/src/types/appsync-graphql.ts b/packages/event-handler/src/types/appsync-graphql.ts index 905b4f321f..99f7fe64d3 100644 --- a/packages/event-handler/src/types/appsync-graphql.ts +++ b/packages/event-handler/src/types/appsync-graphql.ts @@ -144,13 +144,18 @@ type GraphQlRouteOptions = { /** * Options for configuring a batch GraphQL route handler. * - * @template T - If `true`, the handler receives all events at once. If `false`, the handler is called for each event individually. Defaults to `true`. + * @template T - If `true`, the handler receives all events at once and `raiseOnError` cannot be specified. + * If `false`, the handler is called for each event individually and `raiseOnError` can be specified. + * Defaults to `true`. * @template R - If `true`, errors thrown by the handler will be raised. Defaults to `false`. */ type GraphQlBatchRouteOptions< T extends boolean | undefined = true, R extends boolean | undefined = false, -> = GraphQlRouteOptions & { aggregate?: T; raiseOnError?: R }; +> = GraphQlRouteOptions & + (T extends true + ? { aggregate?: T; raiseOnError?: never } + : { aggregate?: T; raiseOnError?: R }); export type { RouteHandlerRegistryOptions, From 1292348abedcd4243cc2a383f4ee024bfd9601d2 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Fri, 25 Jul 2025 12:59:46 +0600 Subject: [PATCH 30/59] fix: update batchResolver, onBatchQuery, and onBatchMutation signatures for improved type safety and clarity --- .../src/appsync-graphql/Router.ts | 84 +++++++++++++------ 1 file changed, 59 insertions(+), 25 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/Router.ts b/packages/event-handler/src/appsync-graphql/Router.ts index 8c7e240ed1..c4ccfaf15a 100644 --- a/packages/event-handler/src/appsync-graphql/Router.ts +++ b/packages/event-handler/src/appsync-graphql/Router.ts @@ -422,13 +422,13 @@ class Router { * @param options.aggregate - Whether to aggregate multiple requests into a single handler call, defaults to `true`. * @param options.raiseOnError - Whether to raise errors when processing individual requests (only available when aggregate is false), defaults to `false`. */ - public batchResolver< - TParams extends Record, - T extends boolean = true, - R extends boolean = false, - >( - handler: BatchResolverHandler, - options: GraphQlBatchRouteOptions + public batchResolver>( + handler: BatchResolverHandler, + options: GraphQlBatchRouteOptions + ): void; + public batchResolver>( + handler: BatchResolverHandler, + options: GraphQlBatchRouteOptions ): void; public batchResolver( options: GraphQlBatchRouteOptions @@ -533,18 +533,35 @@ class Router { * @param handler - The batch handler function to be called when events are received. * @param options - Optional batch configuration including aggregate and raiseOnError settings. */ - public onBatchQuery< - TParams extends Record, - T extends boolean = true, - R extends boolean = false, - >( + public onBatchQuery>( fieldName: string, - handler: BatchResolverHandler, - options?: Omit, 'fieldName' | 'typeName'> + handler: BatchResolverHandler, + options?: Omit< + GraphQlBatchRouteOptions, + 'fieldName' | 'typeName' + > ): void; - public onBatchQuery( + public onBatchQuery>( fieldName: string, - options?: Omit, 'fieldName' | 'typeName'> + handler: BatchResolverHandler, + options?: Omit< + GraphQlBatchRouteOptions, + 'fieldName' | 'typeName' + > + ): void; + public onBatchQuery( + fieldName: string, + options: Omit< + GraphQlBatchRouteOptions, + 'fieldName' | 'typeName' + > + ): MethodDecorator; + public onBatchQuery( + fieldName: string, + options?: Omit< + GraphQlBatchRouteOptions, + 'fieldName' | 'typeName' + > ): MethodDecorator; public onBatchQuery< TParams extends Record, @@ -647,18 +664,35 @@ class Router { * @param handler - The batch handler function to be called when events are received. * @param options - Optional batch configuration including aggregate and raiseOnError settings. */ - public onBatchMutation< - TParams extends Record, - T extends boolean = true, - R extends boolean = false, - >( + public onBatchMutation>( fieldName: string, - handler: BatchResolverHandler, - options?: Omit, 'fieldName' | 'typeName'> + handler: BatchResolverHandler, + options?: Omit< + GraphQlBatchRouteOptions, + 'fieldName' | 'typeName' + > ): void; - public onBatchMutation( + public onBatchMutation>( fieldName: string, - options?: Omit, 'fieldName' | 'typeName'> + handler: BatchResolverHandler, + options?: Omit< + GraphQlBatchRouteOptions, + 'fieldName' | 'typeName' + > + ): void; + public onBatchMutation( + fieldName: string, + options: Omit< + GraphQlBatchRouteOptions, + 'fieldName' | 'typeName' + > + ): MethodDecorator; + public onBatchMutation( + fieldName: string, + options?: Omit< + GraphQlBatchRouteOptions, + 'fieldName' | 'typeName' + > ): MethodDecorator; public onBatchMutation< TParams extends Record, From 1311ced485ce851ff5e1efa1e8c8e4371acdf966 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Fri, 25 Jul 2025 15:55:31 +0600 Subject: [PATCH 31/59] fix: enhance type safety in batch resolver examples by using AppSyncResolverEvent --- .../src/appsync-graphql/Router.ts | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/Router.ts b/packages/event-handler/src/appsync-graphql/Router.ts index c4ccfaf15a..c25b357933 100644 --- a/packages/event-handler/src/appsync-graphql/Router.ts +++ b/packages/event-handler/src/appsync-graphql/Router.ts @@ -63,6 +63,7 @@ class Router { * @example * ```ts * import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; + * import type { AppSyncResolverEvent } from 'aws-lambda'; * * const app = new AppSyncGraphQLResolver(); * @@ -83,6 +84,13 @@ class Router { * typeName: 'Mutation' * }); * + * // Register a batch resolver + * app.batchResolver(async (events: AppSyncResolverEvent<{ id: number }>[]) => { + * return events.map(event => ({ id: event.arguments.id, data: 'processed' })); + * }, { + * fieldName: 'getPosts', + * }); + * * export const handler = async (event, context) => * app.resolve(event, context); * ``` @@ -111,7 +119,7 @@ class Router { * @example * ```ts * import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; - * + * import type { AppSyncResolverEvent } from 'aws-lambda'; * const app = new AppSyncGraphQLResolver(); * * class Lambda { @@ -121,6 +129,12 @@ class Router { * return payload; * } * + * @app.batchResolver({ fieldName: 'getPosts' }) + * async handleGetPosts(events: AppSyncResolverEvent<{ id: number }>[]) { + * // Process batch of events + * return events.map(event => ({ id: event.arguments.id, data: 'processed' })); + * } + * * async handler(event, context) { * return app.resolve(event, context, { * scope: this, // bind decorated methods to the class instance @@ -343,9 +357,9 @@ class Router { * const app = new AppSyncGraphQLResolver(); * * // Register a batch Query resolver with aggregation (default) - * app.batchResolver(async (events: AppSyncResolverEvent>[]) => { + * app.batchResolver(async (events: AppSyncResolverEvent<{id: number}>[]) => { * // Process all events in batch - * return events.map(event => ({ id: event.source.id, data: 'processed' })); + * return events.map(event => ({ id: event.arguments.id, data: 'processed' })); * }, { * fieldName: 'getPosts' * }); @@ -371,7 +385,7 @@ class Router { * * const app = new AppSyncGraphQLResolver() * - * app.batchResolver<{ postId: string }, false>(async (args, { event, context }) => { + * app.batchResolver<{ postId: string }>(async (args, { event, context }) => { * // args is typed as { postId: string } * return { id: args.postId }; * }, { @@ -482,9 +496,9 @@ class Router { * const app = new AppSyncGraphQLResolver(); * * // Register a batch Query resolver with aggregation (default) - * app.onBatchQuery('getPosts', async (events: AppSyncResolverEvent>[]) => { + * app.onBatchQuery('getPosts', async (events: AppSyncResolverEvent<{ id: number }>[]) => { * // Process all events in batch - * return events.map(event => ({ id: event.source.id, data: 'processed' })); + * return events.map(event => ({ id: event.arguments.id, data: 'processed' })); * }); * * // Register a batch Query resolver without aggregation @@ -502,12 +516,13 @@ class Router { * @example * ```ts * import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; + * import type { AppSyncResolverEvent } from 'aws-lambda'; * * const app = new AppSyncGraphQLResolver(); * * class Lambda { * ⁣@app.onBatchQuery('getPosts') - * async handleGetPosts(events) { + * async handleGetPosts(events: AppSyncResolverEvent<{ id: number }>[]) { * // Process batch of events * return events.map(event => ({ id: event.arguments.id, data: 'processed' })); * } @@ -613,7 +628,7 @@ class Router { * const app = new AppSyncGraphQLResolver(); * * // Register a batch Mutation resolver with aggregation (default) - * app.onBatchMutation('createPosts', async (events: AppSyncResolverEvent>[]) => { + * app.onBatchMutation('createPosts', async (events: AppSyncResolverEvent<{ id: number }>[]) => { * // Process all events in batch * return events.map(event => ({ id: event.arguments.id, status: 'created' })); * }); @@ -633,12 +648,13 @@ class Router { * @example * ```ts * import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; + * import type { AppSyncResolverEvent } from 'aws-lambda'; * * const app = new AppSyncGraphQLResolver(); * * class Lambda { * ⁣@app.onBatchMutation('createPosts') - * async handleCreatePosts(events) { + * async handleCreatePosts(events: AppSyncResolverEvent<{ id: number }>[]) { * // Process batch of events * return events.map(event => ({ id: event.arguments.id, status: 'created' })); * } From 27a2d3a0976f318101e0b71581758ccdbbc0baf7 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Fri, 25 Jul 2025 16:14:00 +0600 Subject: [PATCH 32/59] fix: enhance documentation for batchResolver with detailed examples and error handling explanations --- .../src/appsync-graphql/Router.ts | 68 ++++++++++++++++++- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/Router.ts b/packages/event-handler/src/appsync-graphql/Router.ts index c25b357933..bd1ce9eb9d 100644 --- a/packages/event-handler/src/appsync-graphql/Router.ts +++ b/packages/event-handler/src/appsync-graphql/Router.ts @@ -356,7 +356,6 @@ class Router { * * const app = new AppSyncGraphQLResolver(); * - * // Register a batch Query resolver with aggregation (default) * app.batchResolver(async (events: AppSyncResolverEvent<{id: number}>[]) => { * // Process all events in batch * return events.map(event => ({ id: event.arguments.id, data: 'processed' })); @@ -364,7 +363,32 @@ class Router { * fieldName: 'getPosts' * }); * - * // Register a batch resolver without aggregation + * export const handler = async (event, context) => + * app.resolve(event, context); + * ``` + * + * By default, the handler will receive all batch events at once as an array and you are responsible for processing + * them and returning an array of results. The first parameter is an array of events, while the second parameter + * provides the original event array and context. + * + * If your function throws an error, we catch it and format the error response to be sent back to AppSync. This helps + * the client understand what went wrong and handle the error accordingly. + * + * It's important to note that if your function throws an error when processing in aggregate mode, the entire + * batch of events will be affected. + * + * **Process events individually** + * + * If you want to process each event individually instead of receiving all events at once, you can set the + * `aggregate` option to `false`. In this case, the handler will be called once for each event in the batch, + * similar to regular resolvers. + * + * @example + * ```ts + * import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; + * + * const app = new AppSyncGraphQLResolver(); + * * app.batchResolver(async (args, { event, context }) => { * // Process individual request * return { id: args.id, data: 'processed' }; @@ -377,6 +401,40 @@ class Router { * app.resolve(event, context); * ``` * + * When the handler is called, the first parameter contains the arguments from the GraphQL request, while the second + * parameter provides the original event and context, similar to regular resolvers. + * + * When `aggregate` is `false`, by default if one of the events in the batch throws an error, we catch it + * and append `null` for that specific event in the results array, allowing other events to be processed successfully. + * This provides graceful error handling where partial failures don't affect the entire batch. + * + * **Strict error handling** + * + * If you want stricter error handling when processing events individually, you can set the `raiseOnError` option + * to `true`. In this case, if any event throws an error, the entire batch processing will stop and the error + * will be propagated. + * + * @example + * ```ts + * import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; + * + * const app = new AppSyncGraphQLResolver(); + * + * app.batchResolver(async (args, { event, context }) => { + * // Process individual request + * return { id: args.id, data: 'processed' }; + * }, { + * fieldName: 'getPost', + * aggregate: false, + * raiseOnError: true + * }); + * + * export const handler = async (event, context) => + * app.resolve(event, context); + * ``` + * + * Note that `raiseOnError` can only be used when `aggregate` is set to `false`. + * * You can also specify the type of the arguments using generic type parameters for non-aggregated handlers: * * @example @@ -418,6 +476,12 @@ class Router { * return { id: args.id, data: 'processed' }; * } * + * ⁣@app.batchResolver({ fieldName: 'getPost', aggregate: false, raiseOnError: true }) + * async handleGetPostStrict(args, { event, context }) { + * // Process individual request with strict error handling + * return { id: args.id, data: 'processed' }; + * } + * * async handler(event, context) { * return app.resolve(event, context, { * scope: this, // bind decorated methods to the class instance From c5e5bbbd401af3242ef5b3a6941a0951b4a5d160 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Fri, 25 Jul 2025 16:26:58 +0600 Subject: [PATCH 33/59] fix: enhance documentation for batchResolver and onBatchQuery/onBatchMutation with detailed error handling and processing options --- .../src/appsync-graphql/Router.ts | 151 ++++++++++++++++-- 1 file changed, 137 insertions(+), 14 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/Router.ts b/packages/event-handler/src/appsync-graphql/Router.ts index bd1ce9eb9d..bf17d6442f 100644 --- a/packages/event-handler/src/appsync-graphql/Router.ts +++ b/packages/event-handler/src/appsync-graphql/Router.ts @@ -349,6 +349,16 @@ class Router { * The handler will be invoked when requests are made for the specified field, and can either * process requests individually or aggregate them for batch processing. * + * By default, the handler will receive all batch events at once as an array and you are responsible for processing + * them and returning an array of results. The first parameter is an array of events, while the second parameter + * provides the original event array and context. + * + * If your function throws an error, we catch it and format the error response to be sent back to AppSync. This helps + * the client understand what went wrong and handle the error accordingly. + * + * It's important to note that if your function throws an error when processing in aggregate mode, the entire + * batch of events will be affected. + * * @example * ```ts * import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; @@ -367,16 +377,6 @@ class Router { * app.resolve(event, context); * ``` * - * By default, the handler will receive all batch events at once as an array and you are responsible for processing - * them and returning an array of results. The first parameter is an array of events, while the second parameter - * provides the original event array and context. - * - * If your function throws an error, we catch it and format the error response to be sent back to AppSync. This helps - * the client understand what went wrong and handle the error accordingly. - * - * It's important to note that if your function throws an error when processing in aggregate mode, the entire - * batch of events will be affected. - * * **Process events individually** * * If you want to process each event individually instead of receiving all events at once, you can set the @@ -559,13 +559,37 @@ class Router { * * const app = new AppSyncGraphQLResolver(); * - * // Register a batch Query resolver with aggregation (default) * app.onBatchQuery('getPosts', async (events: AppSyncResolverEvent<{ id: number }>[]) => { * // Process all events in batch * return events.map(event => ({ id: event.arguments.id, data: 'processed' })); * }); * - * // Register a batch Query resolver without aggregation + * export const handler = async (event, context) => + * app.resolve(event, context); + * ``` + * + * By default, the handler will receive all batch events at once as an array and you are responsible for processing + * them and returning an array of results. The first parameter is an array of events, while the second parameter + * provides the original event array and context. + * + * If your function throws an error, we catch it and format the error response to be sent back to AppSync. This helps + * the client understand what went wrong and handle the error accordingly. + * + * It's important to note that if your function throws an error when processing in aggregate mode, the entire + * batch of events will be affected. + * + * **Process events individually** + * + * If you want to process each event individually instead of receiving all events at once, you can set the + * `aggregate` option to `false`. In this case, the handler will be called once for each event in the batch, + * similar to regular resolvers. + * + * @example + * ```ts + * import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; + * + * const app = new AppSyncGraphQLResolver(); + * * app.onBatchQuery('getPost', async (args, { event, context }) => { * // Process individual request * return { id: args.id, data: 'processed' }; @@ -575,6 +599,36 @@ class Router { * app.resolve(event, context); * ``` * + * When the handler is called, the first parameter contains the arguments from the GraphQL request, while the second + * parameter provides the original event and context, similar to regular resolvers. + * + * When `aggregate` is `false`, by default if one of the events in the batch throws an error, we catch it + * and append `null` for that specific event in the results array, allowing other events to be processed successfully. + * This provides graceful error handling where partial failures don't affect the entire batch. + * + * **Strict error handling** + * + * If you want stricter error handling when processing events individually, you can set the `raiseOnError` option + * to `true`. In this case, if any event throws an error, the entire batch processing will stop and the error + * will be propagated. + * + * @example + * ```ts + * import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; + * + * const app = new AppSyncGraphQLResolver(); + * + * app.onBatchQuery('getPost', async (args, { event, context }) => { + * // Process individual request + * return { id: args.id, data: 'processed' }; + * }, { aggregate: false, raiseOnError: true }); + * + * export const handler = async (event, context) => + * app.resolve(event, context); + * ``` + * + * Note that `raiseOnError` can only be used when `aggregate` is set to `false`. + * * As a decorator: * * @example @@ -597,6 +651,12 @@ class Router { * return { id: args.id, data: 'processed' }; * } * + * ⁣@app.onBatchQuery('getPost', { aggregate: false, raiseOnError: true }) + * async handleGetPostStrict(args, { event, context }) { + * // Process individual request with strict error handling + * return { id: args.id, data: 'processed' }; + * } + * * async handler(event, context) { * return app.resolve(event, context, { * scope: this, // bind decorated methods to the class instance @@ -611,6 +671,8 @@ class Router { * @param fieldName - The name of the Query field to register the batch handler for. * @param handler - The batch handler function to be called when events are received. * @param options - Optional batch configuration including aggregate and raiseOnError settings. + * @param options.aggregate - Whether to aggregate multiple requests into a single handler call, defaults to `true`. + * @param options.raiseOnError - Whether to raise errors when processing individual requests (only available when aggregate is false), defaults to `false`. */ public onBatchQuery>( fieldName: string, @@ -684,6 +746,15 @@ class Router { * Registers a batch handler for a specific GraphQL Mutation field that can process multiple requests in a batch. * The handler will be invoked when requests are made for the specified field in the Mutation type. * + * By default, the handler will receive all batch events at once as an array and you are responsible for processing + * them and returning an array of results. The first parameter is an array of events, while the second parameter + * provides the original event array and context. + * + * If your function throws an error, we catch it and format the error response to be sent back to AppSync. + * + * It's important to note that if your function throws an error when processing in aggregate mode, the entire + * batch of events will be affected. + * * @example * ```ts * import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; @@ -691,13 +762,27 @@ class Router { * * const app = new AppSyncGraphQLResolver(); * - * // Register a batch Mutation resolver with aggregation (default) * app.onBatchMutation('createPosts', async (events: AppSyncResolverEvent<{ id: number }>[]) => { * // Process all events in batch * return events.map(event => ({ id: event.arguments.id, status: 'created' })); * }); * - * // Register a batch Mutation resolver without aggregation + * export const handler = async (event, context) => + * app.resolve(event, context); + * ``` + * + * **Process events individually** + * + * If you want to process each event individually instead of receiving all events at once, you can set the + * `aggregate` option to `false`. In this case, the handler will be called once for each event in the batch, + * similar to regular resolvers. + * + * @example + * ```ts + * import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; + * + * const app = new AppSyncGraphQLResolver(); + * * app.onBatchMutation('createPost', async (args, { event, context }) => { * // Process individual request * return { id: args.id, status: 'created' }; @@ -707,6 +792,36 @@ class Router { * app.resolve(event, context); * ``` * + * When the handler is called, the first parameter contains the arguments from the GraphQL request, while the second + * parameter provides the original event and context, similar to regular resolvers. + * + * When `aggregate` is `false`, by default if one of the events in the batch throws an error, we catch it + * and append `null` for that specific event in the results array, allowing other events to be processed successfully. + * This provides graceful error handling where partial failures don't affect the entire batch. + * + * **Strict error handling** + * + * If you want stricter error handling when processing events individually, you can set the `raiseOnError` option + * to `true`. In this case, if any event throws an error, the entire batch processing will stop and the error + * will be propagated. + * + * @example + * ```ts + * import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; + * + * const app = new AppSyncGraphQLResolver(); + * + * app.onBatchMutation('createPost', async (args, { event, context }) => { + * // Process individual request + * return { id: args.id, status: 'created' }; + * }, { aggregate: false, raiseOnError: true }); + * + * export const handler = async (event, context) => + * app.resolve(event, context); + * ``` + * + * Note that `raiseOnError` can only be used when `aggregate` is set to `false`. + * * As a decorator: * * @example @@ -729,6 +844,12 @@ class Router { * return { id: args.id, status: 'created' }; * } * + * ⁣@app.onBatchMutation('createPost', { aggregate: false, raiseOnError: true }) + * async handleCreatePostStrict(args, { event, context }) { + * // Process individual request with strict error handling + * return { id: args.id, status: 'created' }; + * } + * * async handler(event, context) { * return app.resolve(event, context, { * scope: this, // bind decorated methods to the class instance @@ -743,6 +864,8 @@ class Router { * @param fieldName - The name of the Mutation field to register the batch handler for. * @param handler - The batch handler function to be called when events are received. * @param options - Optional batch configuration including aggregate and raiseOnError settings. + * @param options.aggregate - Whether to aggregate multiple requests into a single handler call, defaults to `true`. + * @param options.raiseOnError - Whether to raise errors when processing individual requests (only available when aggregate is false), defaults to `false`. */ public onBatchMutation>( fieldName: string, From 0012c592ae7fee8adb316e9c22578a2156509908 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Fri, 25 Jul 2025 18:36:01 +0600 Subject: [PATCH 34/59] fix: improve type safety in handleBatchGet by specifying event argument structure --- .../tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts index dbb61be058..57a1fce5f5 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts @@ -275,7 +275,7 @@ describe('Class: AppSyncGraphQLResolver', () => { @app.batchResolver({ fieldName: 'batchGet' }) public async handleBatchGet( - events: AppSyncResolverEvent>[] + events: AppSyncResolverEvent<{ id: number }>[] ) { const ids = events.map((event) => event.arguments.id); return ids.map((id) => ({ From 2de1c827492d32bfd2549ab5580cbe7cade23936 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Fri, 25 Jul 2025 18:48:07 +0600 Subject: [PATCH 35/59] fix: update batch event handling to improve error logging and simplify test cases --- .../src/appsync-graphql/AppSyncGraphQLResolver.ts | 8 ++++---- .../appsync-graphql/AppSyncGraphQLResolver.test.ts | 11 +++++++---- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts index 6e7d786eed..02c5a1f3f8 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -315,17 +315,17 @@ class AppSyncGraphQLResolver extends Router { return results; } - for (const [i, event] of events.entries()) { + for (let i = 0; i < events.length; i++) { try { const result = await handler.apply(resolveOptions?.scope ?? this, [ - event.arguments, - { event, context }, + events[i].arguments, + { event: events[i], context }, ]); results.push(result); } catch (error) { this.logger.error(error); this.logger.debug( - `Failed to process event number ${i} from field '${event.info.fieldName}'` + `Failed to process event #${i + 1} from field '${events[i].info.fieldName}'` ); // By default, we gracefully append `null` for any records that failed processing results.push(null); diff --git a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts index 57a1fce5f5..a87d5ef5ae 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts @@ -558,7 +558,7 @@ describe('Class: AppSyncGraphQLResolver', () => { ]); }); - it('returns null for failed records when aggregate=false and raiseOnError=false', async () => { + it('returns null for failed records when aggregate=false', async () => { // Prepare const app = new AppSyncGraphQLResolver({ logger: console }); const handler = vi @@ -571,7 +571,6 @@ describe('Class: AppSyncGraphQLResolver', () => { fieldName: 'batchGet', typeName: 'Query', aggregate: false, - raiseOnError: false, }); const events = [ onGraphqlEventFactory('batchGet', 'Query', { id: '1' }), @@ -583,6 +582,10 @@ describe('Class: AppSyncGraphQLResolver', () => { const result = await app.resolve(events, context); // Assess + expect(console.debug).toHaveBeenNthCalledWith( + 4, + "Failed to process event #2 from field 'batchGet'" + ); expect(result).toEqual([ { id: '1', value: 'A' }, null, @@ -597,7 +600,7 @@ describe('Class: AppSyncGraphQLResolver', () => { .fn() .mockResolvedValueOnce({ id: '1', value: 'A' }) .mockRejectedValueOnce(new Error('fail')) - .mockResolvedValueOnce(new Error('fail again')); + .mockResolvedValueOnce({ id: '3', value: 'C' }); app.batchResolver(handler, { fieldName: 'batchGet', typeName: 'Query', @@ -617,7 +620,7 @@ describe('Class: AppSyncGraphQLResolver', () => { }); }); - it('throws if aggregate handler does not return an array', async () => { + it('throws error if aggregate handler does not return an array', async () => { // Prepare const app = new AppSyncGraphQLResolver({ logger: console }); const handler = vi.fn().mockResolvedValue({ id: '1', value: 'A' }); From 0ba04962c3091d70c29071ed7d9dec6f3b89944f Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Fri, 25 Jul 2025 19:12:39 +0600 Subject: [PATCH 36/59] fix: enhance type safety in batch resolver and related handlers by introducing TSource parameter --- .../src/appsync-graphql/Router.ts | 53 +++++++++++++------ .../src/types/appsync-graphql.ts | 30 +++++++---- 2 files changed, 59 insertions(+), 24 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/Router.ts b/packages/event-handler/src/appsync-graphql/Router.ts index bf17d6442f..c688e8ad45 100644 --- a/packages/event-handler/src/appsync-graphql/Router.ts +++ b/packages/event-handler/src/appsync-graphql/Router.ts @@ -500,12 +500,18 @@ class Router { * @param options.aggregate - Whether to aggregate multiple requests into a single handler call, defaults to `true`. * @param options.raiseOnError - Whether to raise errors when processing individual requests (only available when aggregate is false), defaults to `false`. */ - public batchResolver>( - handler: BatchResolverHandler, + public batchResolver< + TParams extends Record, + TSource = Record, + >( + handler: BatchResolverHandler, options: GraphQlBatchRouteOptions ): void; - public batchResolver>( - handler: BatchResolverHandler, + public batchResolver< + TParams extends Record, + TSource = Record, + >( + handler: BatchResolverHandler, options: GraphQlBatchRouteOptions ): void; public batchResolver( @@ -513,10 +519,13 @@ class Router { ): MethodDecorator; public batchResolver< TParams extends Record, + TSource = Record, T extends boolean = true, R extends boolean = false, >( - handler: BatchResolverHandler | GraphQlBatchRouteOptions, + handler: + | BatchResolverHandler + | GraphQlBatchRouteOptions, options?: GraphQlBatchRouteOptions ): MethodDecorator | undefined { if (typeof handler === 'function') { @@ -674,17 +683,23 @@ class Router { * @param options.aggregate - Whether to aggregate multiple requests into a single handler call, defaults to `true`. * @param options.raiseOnError - Whether to raise errors when processing individual requests (only available when aggregate is false), defaults to `false`. */ - public onBatchQuery>( + public onBatchQuery< + TParams extends Record, + TSource = Record, + >( fieldName: string, - handler: BatchResolverHandler, + handler: BatchResolverHandler, options?: Omit< GraphQlBatchRouteOptions, 'fieldName' | 'typeName' > ): void; - public onBatchQuery>( + public onBatchQuery< + TParams extends Record, + TSource = Record, + >( fieldName: string, - handler: BatchResolverHandler, + handler: BatchResolverHandler, options?: Omit< GraphQlBatchRouteOptions, 'fieldName' | 'typeName' @@ -706,12 +721,13 @@ class Router { ): MethodDecorator; public onBatchQuery< TParams extends Record, + TSource = Record, T extends boolean = true, R extends boolean = false, >( fieldName: string, handlerOrOptions?: - | BatchResolverHandler + | BatchResolverHandler | Omit, 'fieldName' | 'typeName'>, options?: Omit, 'fieldName' | 'typeName'> ): MethodDecorator | undefined { @@ -867,17 +883,23 @@ class Router { * @param options.aggregate - Whether to aggregate multiple requests into a single handler call, defaults to `true`. * @param options.raiseOnError - Whether to raise errors when processing individual requests (only available when aggregate is false), defaults to `false`. */ - public onBatchMutation>( + public onBatchMutation< + TParams extends Record, + TSource = Record, + >( fieldName: string, - handler: BatchResolverHandler, + handler: BatchResolverHandler, options?: Omit< GraphQlBatchRouteOptions, 'fieldName' | 'typeName' > ): void; - public onBatchMutation>( + public onBatchMutation< + TParams extends Record, + TSource = Record, + >( fieldName: string, - handler: BatchResolverHandler, + handler: BatchResolverHandler, options?: Omit< GraphQlBatchRouteOptions, 'fieldName' | 'typeName' @@ -899,12 +921,13 @@ class Router { ): MethodDecorator; public onBatchMutation< TParams extends Record, + TSource = Record, T extends boolean = true, R extends boolean = false, >( fieldName: string, handlerOrOptions?: - | BatchResolverHandler + | BatchResolverHandler | Omit, 'fieldName' | 'typeName'>, options?: Omit, 'fieldName' | 'typeName'> ): MethodDecorator | undefined { diff --git a/packages/event-handler/src/types/appsync-graphql.ts b/packages/event-handler/src/types/appsync-graphql.ts index 99f7fe64d3..a578363c4e 100644 --- a/packages/event-handler/src/types/appsync-graphql.ts +++ b/packages/event-handler/src/types/appsync-graphql.ts @@ -21,16 +21,22 @@ type BatchResolverHandlerFn> = ( } ) => Promise; -type BatchResolverAggregateHandlerFn> = ( - event: AppSyncResolverEvent[], +type BatchResolverAggregateHandlerFn< + TParams = Record, + TSource = Record, +> = ( + event: AppSyncResolverEvent[], options: { - event: AppSyncResolverEvent[]; + event: AppSyncResolverEvent[]; context: Context; } ) => Promise; -type BatchResolverSyncAggregateHandlerFn> = ( - event: AppSyncResolverEvent[], +type BatchResolverSyncAggregateHandlerFn< + TParams = Record, + TSource = Record, +> = ( + event: AppSyncResolverEvent[], options: { context: Context; } @@ -38,11 +44,12 @@ type BatchResolverSyncAggregateHandlerFn> = ( type BatchResolverHandler< TParams = Record, + TSource = Record, T extends boolean | undefined = undefined, > = T extends true ? - | BatchResolverAggregateHandlerFn - | BatchResolverSyncAggregateHandlerFn + | BatchResolverAggregateHandlerFn + | BatchResolverSyncAggregateHandlerFn : BatchResolverHandlerFn | BatchResolverSyncHandlerFn; // #region Resolver fn @@ -88,11 +95,16 @@ type RouteHandlerRegistryOptions = { * @property fieldName - The name of the field to be registered * @property typeName - The name of the type to be registered */ -type RouteHandlerOptions = { +type RouteHandlerOptions< + TParams, + T extends boolean, + R extends boolean, + TSource = Record, +> = { /** * The handler function to be called when the event is received */ - handler: BatchResolverHandler | ResolverHandler; + handler: BatchResolverHandler | ResolverHandler; /** * The field name of the event to be registered */ From 2457ee9b92d6ac1572fac8c60593d60d353efb44 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Fri, 25 Jul 2025 19:13:39 +0600 Subject: [PATCH 37/59] fix: rename parameter for clarity in BatchResolverAggregateHandlerFn and BatchResolverSyncAggregateHandlerFn --- packages/event-handler/src/types/appsync-graphql.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/event-handler/src/types/appsync-graphql.ts b/packages/event-handler/src/types/appsync-graphql.ts index a578363c4e..21429a3952 100644 --- a/packages/event-handler/src/types/appsync-graphql.ts +++ b/packages/event-handler/src/types/appsync-graphql.ts @@ -25,7 +25,7 @@ type BatchResolverAggregateHandlerFn< TParams = Record, TSource = Record, > = ( - event: AppSyncResolverEvent[], + events: AppSyncResolverEvent[], options: { event: AppSyncResolverEvent[]; context: Context; @@ -36,8 +36,9 @@ type BatchResolverSyncAggregateHandlerFn< TParams = Record, TSource = Record, > = ( - event: AppSyncResolverEvent[], + events: AppSyncResolverEvent[], options: { + event: AppSyncResolverEvent[]; context: Context; } ) => unknown; From 321b0e14c5087673b7f4460398b2ca0c3f993f45 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Fri, 25 Jul 2025 19:36:10 +0600 Subject: [PATCH 38/59] fix: enhance type safety in BatchResolverAggregateHandlerFn and BatchResolverSyncAggregateHandlerFn by allowing TSource to be null --- packages/event-handler/src/types/appsync-graphql.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/event-handler/src/types/appsync-graphql.ts b/packages/event-handler/src/types/appsync-graphql.ts index 21429a3952..44627cd71d 100644 --- a/packages/event-handler/src/types/appsync-graphql.ts +++ b/packages/event-handler/src/types/appsync-graphql.ts @@ -23,7 +23,7 @@ type BatchResolverHandlerFn> = ( type BatchResolverAggregateHandlerFn< TParams = Record, - TSource = Record, + TSource = Record | null, > = ( events: AppSyncResolverEvent[], options: { @@ -34,7 +34,7 @@ type BatchResolverAggregateHandlerFn< type BatchResolverSyncAggregateHandlerFn< TParams = Record, - TSource = Record, + TSource = Record | null, > = ( events: AppSyncResolverEvent[], options: { From 1fc0ce7b7f98b0abaccf19017c2802410b78896a Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 27 Jul 2025 10:43:52 +0600 Subject: [PATCH 39/59] fix: update error handling in #withErrorHandling to return formatted error response --- .../src/appsync-graphql/AppSyncGraphQLResolver.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts index 02c5a1f3f8..b6d8e10e43 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -185,20 +185,19 @@ class AppSyncGraphQLResolver extends Router { /** * Executes the provided asynchronous function with error handling. * If the function throws an error, it delegates error processing to `#handleError` - * and returns its result cast to the expected type. + * and returns the formatted error response. * - * @typeParam T - The return type of the asynchronous function. - * @param fn - A function returning a Promise of type `T` to be executed. + * @param fn - A function returning a Promise to be executed with error handling. * @param errorMessage - A custom error message to be used if an error occurs. */ - async #withErrorHandling( - fn: () => Promise, + async #withErrorHandling( + fn: () => Promise, errorMessage: string - ): Promise { + ): Promise { try { return await fn(); } catch (error) { - return this.#handleError(error, errorMessage) as T; + return this.#handleError(error, errorMessage); } } From 1f95054d5ef6a91729cea715a82700ca62f86343 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 27 Jul 2025 11:00:33 +0600 Subject: [PATCH 40/59] fix: enhance type safety in BatchResolverHandler and RouteHandlerOptions by allowing TSource to be null --- packages/event-handler/src/types/appsync-graphql.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/event-handler/src/types/appsync-graphql.ts b/packages/event-handler/src/types/appsync-graphql.ts index 44627cd71d..6928b4d4c1 100644 --- a/packages/event-handler/src/types/appsync-graphql.ts +++ b/packages/event-handler/src/types/appsync-graphql.ts @@ -45,7 +45,7 @@ type BatchResolverSyncAggregateHandlerFn< type BatchResolverHandler< TParams = Record, - TSource = Record, + TSource = Record | null, T extends boolean | undefined = undefined, > = T extends true ? @@ -100,7 +100,7 @@ type RouteHandlerOptions< TParams, T extends boolean, R extends boolean, - TSource = Record, + TSource = Record | null, > = { /** * The handler function to be called when the event is received From 2d03dd5b12c0a3e88978983615cd6aa9afde9307 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 27 Jul 2025 11:15:09 +0600 Subject: [PATCH 41/59] fix: enhance type safety in batch resolver methods by allowing TSource to be null --- .../src/appsync-graphql/Router.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/Router.ts b/packages/event-handler/src/appsync-graphql/Router.ts index c688e8ad45..e8d68e66c5 100644 --- a/packages/event-handler/src/appsync-graphql/Router.ts +++ b/packages/event-handler/src/appsync-graphql/Router.ts @@ -502,14 +502,14 @@ class Router { */ public batchResolver< TParams extends Record, - TSource = Record, + TSource = Record | null, >( handler: BatchResolverHandler, options: GraphQlBatchRouteOptions ): void; public batchResolver< TParams extends Record, - TSource = Record, + TSource = Record | null, >( handler: BatchResolverHandler, options: GraphQlBatchRouteOptions @@ -519,7 +519,7 @@ class Router { ): MethodDecorator; public batchResolver< TParams extends Record, - TSource = Record, + TSource = Record | null, T extends boolean = true, R extends boolean = false, >( @@ -685,7 +685,7 @@ class Router { */ public onBatchQuery< TParams extends Record, - TSource = Record, + TSource = Record | null, >( fieldName: string, handler: BatchResolverHandler, @@ -696,7 +696,7 @@ class Router { ): void; public onBatchQuery< TParams extends Record, - TSource = Record, + TSource = Record | null, >( fieldName: string, handler: BatchResolverHandler, @@ -721,7 +721,7 @@ class Router { ): MethodDecorator; public onBatchQuery< TParams extends Record, - TSource = Record, + TSource = Record | null, T extends boolean = true, R extends boolean = false, >( @@ -885,7 +885,7 @@ class Router { */ public onBatchMutation< TParams extends Record, - TSource = Record, + TSource = Record | null, >( fieldName: string, handler: BatchResolverHandler, @@ -896,7 +896,7 @@ class Router { ): void; public onBatchMutation< TParams extends Record, - TSource = Record, + TSource = Record | null, >( fieldName: string, handler: BatchResolverHandler, @@ -921,7 +921,7 @@ class Router { ): MethodDecorator; public onBatchMutation< TParams extends Record, - TSource = Record, + TSource = Record | null, T extends boolean = true, R extends boolean = false, >( From c198ed6fdfdd42558c0908d7cf0cffe1a5a98dea Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 27 Jul 2025 11:19:43 +0600 Subject: [PATCH 42/59] fix: enhance type safety in batch resolver examples by specifying generic parameters --- .../event-handler/src/appsync-graphql/Router.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/Router.ts b/packages/event-handler/src/appsync-graphql/Router.ts index e8d68e66c5..ea5f832a9f 100644 --- a/packages/event-handler/src/appsync-graphql/Router.ts +++ b/packages/event-handler/src/appsync-graphql/Router.ts @@ -63,7 +63,6 @@ class Router { * @example * ```ts * import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; - * import type { AppSyncResolverEvent } from 'aws-lambda'; * * const app = new AppSyncGraphQLResolver(); * @@ -85,7 +84,7 @@ class Router { * }); * * // Register a batch resolver - * app.batchResolver(async (events: AppSyncResolverEvent<{ id: number }>[]) => { + * app.batchResolver<{ id: number }>(async (events) => { * return events.map(event => ({ id: event.arguments.id, data: 'processed' })); * }, { * fieldName: 'getPosts', @@ -362,11 +361,10 @@ class Router { * @example * ```ts * import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; - * import type { AppSyncResolverEvent } from 'aws-lambda'; * * const app = new AppSyncGraphQLResolver(); * - * app.batchResolver(async (events: AppSyncResolverEvent<{id: number}>[]) => { + * app.batchResolver<{id: number}>(async (events) => { * // Process all events in batch * return events.map(event => ({ id: event.arguments.id, data: 'processed' })); * }, { @@ -564,11 +562,11 @@ class Router { * @example * ```ts * import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; - * import type { AppSyncResolverEvent } from 'aws-lambda'; + * * const app = new AppSyncGraphQLResolver(); * - * app.onBatchQuery('getPosts', async (events: AppSyncResolverEvent<{ id: number }>[]) => { + * app.onBatchQuery<{ id: number }>('getPosts', async (events) => { * // Process all events in batch * return events.map(event => ({ id: event.arguments.id, data: 'processed' })); * }); @@ -774,11 +772,10 @@ class Router { * @example * ```ts * import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; - * import type { AppSyncResolverEvent } from 'aws-lambda'; * * const app = new AppSyncGraphQLResolver(); * - * app.onBatchMutation('createPosts', async (events: AppSyncResolverEvent<{ id: number }>[]) => { + * app.onBatchMutation<{ id: number }>('createPosts', async (events) => { * // Process all events in batch * return events.map(event => ({ id: event.arguments.id, status: 'created' })); * }); From b02c5b6f57a29b81754ca3f2cdd671a1c5668f61 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 27 Jul 2025 12:02:35 +0600 Subject: [PATCH 43/59] fix: enhance type safety in batch resolver types by adding TSource parameter --- .../src/appsync-graphql/Router.ts | 2 +- .../event-handler/src/types/appsync-graphql.ts | 18 +++++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/Router.ts b/packages/event-handler/src/appsync-graphql/Router.ts index ea5f832a9f..9ddd8dbae1 100644 --- a/packages/event-handler/src/appsync-graphql/Router.ts +++ b/packages/event-handler/src/appsync-graphql/Router.ts @@ -540,7 +540,7 @@ class Router { } const batchResolverOptions = handler; - return (target, _propertyKey, descriptor: PropertyDescriptor) => { + return (_target, _propertyKey, descriptor: PropertyDescriptor) => { const { typeName = 'Query', fieldName } = batchResolverOptions; this.batchResolverRegistry.register({ fieldName, diff --git a/packages/event-handler/src/types/appsync-graphql.ts b/packages/event-handler/src/types/appsync-graphql.ts index 6928b4d4c1..029f3b30b7 100644 --- a/packages/event-handler/src/types/appsync-graphql.ts +++ b/packages/event-handler/src/types/appsync-graphql.ts @@ -5,18 +5,24 @@ import type { Router } from '../appsync-graphql/Router.js'; // #region BatchResolver fn -type BatchResolverSyncHandlerFn> = ( +type BatchResolverSyncHandlerFn< + TParams = Record, + TSource = Record | null, +> = ( args: TParams, options: { - event: AppSyncResolverEvent; + event: AppSyncResolverEvent; context: Context; } ) => unknown; -type BatchResolverHandlerFn> = ( +type BatchResolverHandlerFn< + TParams = Record, + TSource = Record | null, +> = ( args: TParams, options: { - event: AppSyncResolverEvent; + event: AppSyncResolverEvent; context: Context; } ) => Promise; @@ -51,7 +57,9 @@ type BatchResolverHandler< ? | BatchResolverAggregateHandlerFn | BatchResolverSyncAggregateHandlerFn - : BatchResolverHandlerFn | BatchResolverSyncHandlerFn; + : + | BatchResolverHandlerFn + | BatchResolverSyncHandlerFn; // #region Resolver fn From a6fb353205e4b8f57836db526aef7d6ed9268d00 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 27 Jul 2025 12:32:45 +0600 Subject: [PATCH 44/59] fix: enhance type safety in batchResolver by specifying generic parameters --- .../src/appsync-graphql/AppSyncGraphQLResolver.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts index b6d8e10e43..7693c27e93 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -71,11 +71,10 @@ class AppSyncGraphQLResolver extends Router { * @example * ```ts * import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; - * import type { AppSyncResolverEvent } from 'aws-lambda'; * * const app = new AppSyncGraphQLResolver(); * - * app.batchResolver(async (events: AppSyncResolverEvent<{ id: number }>[]) => { + * app.batchResolver<{ id: number }>(async (events) => { * // your business logic here * const ids = events.map((event) => event.arguments.id); * return ids.map((id) => ({ From 4c0135760c5dd5062d4e07044f027b81e167f6ca Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 27 Jul 2025 12:37:08 +0600 Subject: [PATCH 45/59] fix: simplify type guard for AppSync GraphQL event by removing redundant checks --- packages/event-handler/src/appsync-graphql/utils.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/utils.ts b/packages/event-handler/src/appsync-graphql/utils.ts index da8b4402a6..3acebb2b36 100644 --- a/packages/event-handler/src/appsync-graphql/utils.ts +++ b/packages/event-handler/src/appsync-graphql/utils.ts @@ -12,9 +12,7 @@ import type { AppSyncResolverEvent } from 'aws-lambda'; const isAppSyncGraphQLEvent = ( event: unknown ): event is AppSyncResolverEvent> => { - if (typeof event !== 'object' || event === null || !isRecord(event)) { - return false; - } + if (!isRecord(event)) return false; return ( isRecord(event.arguments) && 'identity' in event && From 22e79bc920cbce0b40512c0c203fe518f5116635 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 27 Jul 2025 12:49:36 +0600 Subject: [PATCH 46/59] fix: improve error handling by using event information in error messages --- .../src/appsync-graphql/AppSyncGraphQLResolver.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts index 7693c27e93..2ed447fad8 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -165,7 +165,7 @@ class AppSyncGraphQLResolver extends Router { } return this.#withErrorHandling( () => this.#executeBatchResolvers(event, context, options), - `An error occurred in handler ${event[0].info.fieldName}` + event[0] ); } if (!isAppSyncGraphQLEvent(event)) { @@ -177,7 +177,7 @@ class AppSyncGraphQLResolver extends Router { return this.#withErrorHandling( () => this.#executeSingleResolver(event, context, options), - `An error occurred in handler ${event.info.fieldName}` + event ); } @@ -187,15 +187,16 @@ class AppSyncGraphQLResolver extends Router { * and returns the formatted error response. * * @param fn - A function returning a Promise to be executed with error handling. - * @param errorMessage - A custom error message to be used if an error occurs. + * @param event - The AppSync event (single or first of batch). */ async #withErrorHandling( fn: () => Promise, - errorMessage: string + event: AppSyncResolverEvent> ): Promise { try { return await fn(); } catch (error) { + const errorMessage = `An error occurred in handler ${event.info.fieldName}`; return this.#handleError(error, errorMessage); } } From be731f02292978cd82437294c182dd2c3145dd00 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 27 Jul 2025 12:50:46 +0600 Subject: [PATCH 47/59] fix: improve error handling by formatting error messages for better readability --- .../src/appsync-graphql/AppSyncGraphQLResolver.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts index 2ed447fad8..f47c610679 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -196,8 +196,10 @@ class AppSyncGraphQLResolver extends Router { try { return await fn(); } catch (error) { - const errorMessage = `An error occurred in handler ${event.info.fieldName}`; - return this.#handleError(error, errorMessage); + return this.#handleError( + error, + `An error occurred in handler ${event.info.fieldName}` + ); } } From 2ab33b951c581caca70dc11b873420340b2db9bc Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 27 Jul 2025 13:01:20 +0600 Subject: [PATCH 48/59] fix: update type handling in AppSyncGraphQLResolver for improved clarity and consistency --- .../src/appsync-graphql/AppSyncGraphQLResolver.ts | 6 ++++-- packages/event-handler/src/types/appsync-graphql.ts | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts index f47c610679..d83851d61f 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -1,6 +1,8 @@ import type { AppSyncResolverEvent, Context } from 'aws-lambda'; import type { BatchResolverAggregateHandlerFn, + BatchResolverHandler, + BatchResolverHandlerFn, ResolverHandler, RouteHandlerOptions, } from '../types/appsync-graphql.js'; @@ -187,7 +189,7 @@ class AppSyncGraphQLResolver extends Router { * and returns the formatted error response. * * @param fn - A function returning a Promise to be executed with error handling. - * @param event - The AppSync event (single or first of batch). + * @param event - The AppSync resolver event (single or first of batch). */ async #withErrorHandling( fn: () => Promise, @@ -302,7 +304,7 @@ class AppSyncGraphQLResolver extends Router { return response; } - const handler = options.handler as ResolverHandler; + const handler = options.handler as BatchResolverHandlerFn; const results: unknown[] = []; if (raiseOnError) { diff --git a/packages/event-handler/src/types/appsync-graphql.ts b/packages/event-handler/src/types/appsync-graphql.ts index 029f3b30b7..b063dedbf5 100644 --- a/packages/event-handler/src/types/appsync-graphql.ts +++ b/packages/event-handler/src/types/appsync-graphql.ts @@ -186,5 +186,6 @@ export type { GraphQlBatchRouteOptions, ResolverHandler, BatchResolverHandler, + BatchResolverHandlerFn, BatchResolverAggregateHandlerFn, }; From d88a3b501a4cb9a689d10876cea4bf653c475a6a Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 27 Jul 2025 20:34:33 +0600 Subject: [PATCH 49/59] fix: clarify documentation for raiseOnError option in batch processing --- packages/event-handler/src/appsync-graphql/Router.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/Router.ts b/packages/event-handler/src/appsync-graphql/Router.ts index 9ddd8dbae1..215be9fc57 100644 --- a/packages/event-handler/src/appsync-graphql/Router.ts +++ b/packages/event-handler/src/appsync-graphql/Router.ts @@ -410,7 +410,7 @@ class Router { * * If you want stricter error handling when processing events individually, you can set the `raiseOnError` option * to `true`. In this case, if any event throws an error, the entire batch processing will stop and the error - * will be propagated. + * will be propagated. Note that `raiseOnError` can only be used when `aggregate` is set to `false`. * * @example * ```ts @@ -431,8 +431,6 @@ class Router { * app.resolve(event, context); * ``` * - * Note that `raiseOnError` can only be used when `aggregate` is set to `false`. - * * You can also specify the type of the arguments using generic type parameters for non-aggregated handlers: * * @example From 1f623361fded7776628e0d2ef3571d3e3be1adb7 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 27 Jul 2025 20:52:40 +0600 Subject: [PATCH 50/59] fix: update documentation for raiseOnError option to clarify usage with aggregate setting --- packages/event-handler/src/appsync-graphql/Router.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/Router.ts b/packages/event-handler/src/appsync-graphql/Router.ts index 215be9fc57..b60a64068c 100644 --- a/packages/event-handler/src/appsync-graphql/Router.ts +++ b/packages/event-handler/src/appsync-graphql/Router.ts @@ -615,7 +615,7 @@ class Router { * * If you want stricter error handling when processing events individually, you can set the `raiseOnError` option * to `true`. In this case, if any event throws an error, the entire batch processing will stop and the error - * will be propagated. + * will be propagated. Note that `raiseOnError` can only be used when `aggregate` is set to `false`. * * @example * ```ts @@ -632,8 +632,6 @@ class Router { * app.resolve(event, context); * ``` * - * Note that `raiseOnError` can only be used when `aggregate` is set to `false`. - * * As a decorator: * * @example @@ -814,7 +812,7 @@ class Router { * * If you want stricter error handling when processing events individually, you can set the `raiseOnError` option * to `true`. In this case, if any event throws an error, the entire batch processing will stop and the error - * will be propagated. + * will be propagated. Note that `raiseOnError` can only be used when `aggregate` is set to `false`. * * @example * ```ts @@ -831,8 +829,6 @@ class Router { * app.resolve(event, context); * ``` * - * Note that `raiseOnError` can only be used when `aggregate` is set to `false`. - * * As a decorator: * * @example From 9650d80ead6c4f1cd65f72886d8eeabd37051ff7 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 27 Jul 2025 21:02:52 +0600 Subject: [PATCH 51/59] test: enhance AppSyncGraphQLResolver tests by verifying handler call counts --- .../unit/appsync-graphql/AppSyncGraphQLResolver.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts index a87d5ef5ae..6d8c55df1c 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts @@ -522,6 +522,7 @@ describe('Class: AppSyncGraphQLResolver', () => { const result = await app.resolve(events, context); // Assess + expect(handler).toHaveBeenCalledTimes(1); expect(handler).toHaveBeenCalledWith(events, { event: events, context }); expect(result).toEqual([ { id: '1', value: 'A' }, @@ -613,8 +614,11 @@ describe('Class: AppSyncGraphQLResolver', () => { onGraphqlEventFactory('batchGet', 'Query', { id: '3' }), ]; - // Act && Assess + // Act const result = await app.resolve(events, context); + + // Assess + expect(handler).toHaveBeenCalledTimes(2); expect(result).toEqual({ error: 'Error - fail', }); From 1f908cdfca37e8bd96aa92af7487991bc9030199 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 27 Jul 2025 21:22:26 +0600 Subject: [PATCH 52/59] fix: add BatchResolverHandler type export to index --- packages/event-handler/src/types/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/event-handler/src/types/index.ts b/packages/event-handler/src/types/index.ts index c26f70b878..8a6189533e 100644 --- a/packages/event-handler/src/types/index.ts +++ b/packages/event-handler/src/types/index.ts @@ -11,6 +11,7 @@ export type { } from './appsync-events.js'; export type { + BatchResolverHandler, GraphQlRouteOptions, GraphQlRouterOptions, ResolverHandler, From 5d9d1c4abb86a7c9441b4de751722dc3ba3d32d0 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 27 Jul 2025 21:24:54 +0600 Subject: [PATCH 53/59] fix: remove unused BatchResolverHandler type import in AppSyncGraphQLResolver --- .../event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts index d83851d61f..02d77a7eb6 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -1,7 +1,6 @@ import type { AppSyncResolverEvent, Context } from 'aws-lambda'; import type { BatchResolverAggregateHandlerFn, - BatchResolverHandler, BatchResolverHandlerFn, ResolverHandler, RouteHandlerOptions, From 3a7516077b77dd13cb60fced799f3e5d66e00127 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 27 Jul 2025 21:26:53 +0600 Subject: [PATCH 54/59] fix: update batch resolver scope to include raiseOnError and aggregate flags --- .../appsync-graphql/AppSyncGraphQLResolver.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts index 6d8c55df1c..48de40b8ed 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts @@ -322,7 +322,7 @@ describe('Class: AppSyncGraphQLResolver', () => { public async handleBatchGet({ id }: { id: string }) { return { id, - scope: `${this.scope} id=${id}`, + scope: `${this.scope} id=${id} raiseOnError=true aggregate=false`, }; } @@ -344,8 +344,8 @@ describe('Class: AppSyncGraphQLResolver', () => { // Assess expect(result).toEqual([ - { id: 1, scope: 'scoped id=1' }, - { id: 2, scope: 'scoped id=2' }, + { id: 1, scope: 'scoped id=1 raiseOnError=true aggregate=false' }, + { id: 2, scope: 'scoped id=2 raiseOnError=true aggregate=false' }, ]); }); @@ -364,7 +364,7 @@ describe('Class: AppSyncGraphQLResolver', () => { public async handleBatchGet({ id }: { id: string }) { return { id, - scope: `${this.scope} id=${id}`, + scope: `${this.scope} id=${id} raiseOnError=false aggregate=false`, }; } @@ -386,8 +386,8 @@ describe('Class: AppSyncGraphQLResolver', () => { // Assess expect(result).toEqual([ - { id: 1, scope: 'scoped id=1' }, - { id: 2, scope: 'scoped id=2' }, + { id: 1, scope: 'scoped id=1 raiseOnError=false aggregate=false' }, + { id: 2, scope: 'scoped id=2 raiseOnError=false aggregate=false' }, ]); }); From 66719eb5622cb5a3357b09db2af4a68ce4b10d53 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 27 Jul 2025 21:31:34 +0600 Subject: [PATCH 55/59] fix: update location names in Router tests for clarity --- .../event-handler/tests/unit/appsync-graphql/Router.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/event-handler/tests/unit/appsync-graphql/Router.test.ts b/packages/event-handler/tests/unit/appsync-graphql/Router.test.ts index b46cc653ff..6dcd7dca26 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/Router.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/Router.test.ts @@ -193,8 +193,8 @@ describe('Class: Router', () => { public getLocations() { return [ { - name: 'Location 1', - description: 'Description 1', + name: 'Batch Location 1', + description: 'Batch Description 1', }, ]; } @@ -213,7 +213,7 @@ describe('Class: Router', () => { ); expect(response).toEqual([ - { name: 'Location 1', description: 'Description 1' }, + { name: 'Batch Location 1', description: 'Batch Description 1' }, ]); }); From 6275b516c13bcc6414c4accafe8d6d7cbc9b909c Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Mon, 28 Jul 2025 22:19:01 +0600 Subject: [PATCH 56/59] fix: rename raiseOnError option to throwOnError --- .../appsync-graphql/AppSyncGraphQLResolver.ts | 10 ++-- .../appsync-graphql/RouteHandlerRegistry.ts | 4 +- .../src/appsync-graphql/Router.ts | 46 +++++++++---------- .../src/types/appsync-graphql.ts | 10 ++-- .../AppSyncGraphQLResolver.test.ts | 28 +++++------ 5 files changed, 49 insertions(+), 49 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts index 02d77a7eb6..7e618c4ded 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -272,8 +272,8 @@ class AppSyncGraphQLResolver extends Router { * * @remarks * - If `aggregate` is true, invokes the handler once with the entire batch and expects an array response. - * - If `raiseOnError` is true, errors are propagated and will cause the function to throw. - * - If `raiseOnError` is false, errors are logged and `null` is appended for failed events, allowing graceful degradation. + * - If `throwOnError` is true, errors are propagated and will cause the function to throw. + * - If `throwOnError` is false, errors are logged and `null` is appended for failed events, allowing graceful degradation. */ async #callBatchResolver( events: AppSyncResolverEvent>[], @@ -281,9 +281,9 @@ class AppSyncGraphQLResolver extends Router { options: RouteHandlerOptions, boolean, boolean>, resolveOptions?: ResolveOptions ): Promise { - const { aggregate, raiseOnError } = options; + const { aggregate, throwOnError } = options; this.logger.debug( - `Aggregate flag aggregate=${aggregate} & Graceful error handling flag raiseOnError=${raiseOnError}` + `Aggregate flag aggregate=${aggregate} & Graceful error handling flag throwOnError=${throwOnError}` ); if (aggregate) { @@ -306,7 +306,7 @@ class AppSyncGraphQLResolver extends Router { const handler = options.handler as BatchResolverHandlerFn; const results: unknown[] = []; - if (raiseOnError) { + if (throwOnError) { for (const event of events) { const result = await handler.apply(resolveOptions?.scope ?? this, [ event.arguments, diff --git a/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts b/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts index 4e79a0c6a5..a33f7a62bf 100644 --- a/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts +++ b/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts @@ -40,7 +40,7 @@ class RouteHandlerRegistry { public register( options: RouteHandlerOptions, boolean, boolean> ): void { - const { fieldName, handler, typeName, raiseOnError, aggregate } = options; + const { fieldName, handler, typeName, throwOnError, aggregate } = options; this.#logger.debug(`Adding resolver for field ${typeName}.${fieldName}`); const cacheKey = this.#makeKey(typeName, fieldName); if (this.resolvers.has(cacheKey)) { @@ -52,7 +52,7 @@ class RouteHandlerRegistry { fieldName, handler, typeName, - raiseOnError, + throwOnError, aggregate, }); } diff --git a/packages/event-handler/src/appsync-graphql/Router.ts b/packages/event-handler/src/appsync-graphql/Router.ts index b60a64068c..ce70618726 100644 --- a/packages/event-handler/src/appsync-graphql/Router.ts +++ b/packages/event-handler/src/appsync-graphql/Router.ts @@ -408,9 +408,9 @@ class Router { * * **Strict error handling** * - * If you want stricter error handling when processing events individually, you can set the `raiseOnError` option + * If you want stricter error handling when processing events individually, you can set the `throwOnError` option * to `true`. In this case, if any event throws an error, the entire batch processing will stop and the error - * will be propagated. Note that `raiseOnError` can only be used when `aggregate` is set to `false`. + * will be propagated. Note that `throwOnError` can only be used when `aggregate` is set to `false`. * * @example * ```ts @@ -424,7 +424,7 @@ class Router { * }, { * fieldName: 'getPost', * aggregate: false, - * raiseOnError: true + * throwOnError: true * }); * * export const handler = async (event, context) => @@ -472,7 +472,7 @@ class Router { * return { id: args.id, data: 'processed' }; * } * - * ⁣@app.batchResolver({ fieldName: 'getPost', aggregate: false, raiseOnError: true }) + * ⁣@app.batchResolver({ fieldName: 'getPost', aggregate: false, throwOnError: true }) * async handleGetPostStrict(args, { event, context }) { * // Process individual request with strict error handling * return { id: args.id, data: 'processed' }; @@ -494,7 +494,7 @@ class Router { * @param options.fieldName - The name of the field to register the handler for. * @param options.typeName - The name of the GraphQL type to use for the resolver, defaults to `Query`. * @param options.aggregate - Whether to aggregate multiple requests into a single handler call, defaults to `true`. - * @param options.raiseOnError - Whether to raise errors when processing individual requests (only available when aggregate is false), defaults to `false`. + * @param options.throwOnError - Whether to raise errors when processing individual requests (only available when aggregate is false), defaults to `false`. */ public batchResolver< TParams extends Record, @@ -532,7 +532,7 @@ class Router { handler: handler as BatchResolverHandler, typeName, aggregate: batchResolverOptions?.aggregate ?? true, - raiseOnError: batchResolverOptions?.raiseOnError ?? false, + throwOnError: batchResolverOptions?.throwOnError ?? false, }); return; } @@ -545,7 +545,7 @@ class Router { handler: descriptor?.value, typeName, aggregate: batchResolverOptions?.aggregate ?? true, - raiseOnError: batchResolverOptions?.raiseOnError ?? false, + throwOnError: batchResolverOptions?.throwOnError ?? false, }); return descriptor; }; @@ -613,9 +613,9 @@ class Router { * * **Strict error handling** * - * If you want stricter error handling when processing events individually, you can set the `raiseOnError` option + * If you want stricter error handling when processing events individually, you can set the `throwOnError` option * to `true`. In this case, if any event throws an error, the entire batch processing will stop and the error - * will be propagated. Note that `raiseOnError` can only be used when `aggregate` is set to `false`. + * will be propagated. Note that `throwOnError` can only be used when `aggregate` is set to `false`. * * @example * ```ts @@ -626,7 +626,7 @@ class Router { * app.onBatchQuery('getPost', async (args, { event, context }) => { * // Process individual request * return { id: args.id, data: 'processed' }; - * }, { aggregate: false, raiseOnError: true }); + * }, { aggregate: false, throwOnError: true }); * * export const handler = async (event, context) => * app.resolve(event, context); @@ -654,7 +654,7 @@ class Router { * return { id: args.id, data: 'processed' }; * } * - * ⁣@app.onBatchQuery('getPost', { aggregate: false, raiseOnError: true }) + * ⁣@app.onBatchQuery('getPost', { aggregate: false, throwOnError: true }) * async handleGetPostStrict(args, { event, context }) { * // Process individual request with strict error handling * return { id: args.id, data: 'processed' }; @@ -673,9 +673,9 @@ class Router { * * @param fieldName - The name of the Query field to register the batch handler for. * @param handler - The batch handler function to be called when events are received. - * @param options - Optional batch configuration including aggregate and raiseOnError settings. + * @param options - Optional batch configuration including aggregate and throwOnError settings. * @param options.aggregate - Whether to aggregate multiple requests into a single handler call, defaults to `true`. - * @param options.raiseOnError - Whether to raise errors when processing individual requests (only available when aggregate is false), defaults to `false`. + * @param options.throwOnError - Whether to raise errors when processing individual requests (only available when aggregate is false), defaults to `false`. */ public onBatchQuery< TParams extends Record, @@ -731,7 +731,7 @@ class Router { handler: handlerOrOptions as BatchResolverHandler, typeName: 'Query', aggregate: options?.aggregate ?? true, - raiseOnError: options?.raiseOnError ?? false, + throwOnError: options?.throwOnError ?? false, }); return; @@ -743,7 +743,7 @@ class Router { handler: descriptor?.value, typeName: 'Query', aggregate: handlerOrOptions?.aggregate ?? true, - raiseOnError: handlerOrOptions?.raiseOnError ?? false, + throwOnError: handlerOrOptions?.throwOnError ?? false, }); return descriptor; @@ -810,9 +810,9 @@ class Router { * * **Strict error handling** * - * If you want stricter error handling when processing events individually, you can set the `raiseOnError` option + * If you want stricter error handling when processing events individually, you can set the `throwOnError` option * to `true`. In this case, if any event throws an error, the entire batch processing will stop and the error - * will be propagated. Note that `raiseOnError` can only be used when `aggregate` is set to `false`. + * will be propagated. Note that `throwOnError` can only be used when `aggregate` is set to `false`. * * @example * ```ts @@ -823,7 +823,7 @@ class Router { * app.onBatchMutation('createPost', async (args, { event, context }) => { * // Process individual request * return { id: args.id, status: 'created' }; - * }, { aggregate: false, raiseOnError: true }); + * }, { aggregate: false, throwOnError: true }); * * export const handler = async (event, context) => * app.resolve(event, context); @@ -851,7 +851,7 @@ class Router { * return { id: args.id, status: 'created' }; * } * - * ⁣@app.onBatchMutation('createPost', { aggregate: false, raiseOnError: true }) + * ⁣@app.onBatchMutation('createPost', { aggregate: false, throwOnError: true }) * async handleCreatePostStrict(args, { event, context }) { * // Process individual request with strict error handling * return { id: args.id, status: 'created' }; @@ -870,9 +870,9 @@ class Router { * * @param fieldName - The name of the Mutation field to register the batch handler for. * @param handler - The batch handler function to be called when events are received. - * @param options - Optional batch configuration including aggregate and raiseOnError settings. + * @param options - Optional batch configuration including aggregate and throwOnError settings. * @param options.aggregate - Whether to aggregate multiple requests into a single handler call, defaults to `true`. - * @param options.raiseOnError - Whether to raise errors when processing individual requests (only available when aggregate is false), defaults to `false`. + * @param options.throwOnError - Whether to raise errors when processing individual requests (only available when aggregate is false), defaults to `false`. */ public onBatchMutation< TParams extends Record, @@ -928,7 +928,7 @@ class Router { handler: handlerOrOptions as BatchResolverHandler, typeName: 'Mutation', aggregate: options?.aggregate ?? true, - raiseOnError: options?.raiseOnError ?? false, + throwOnError: options?.throwOnError ?? false, }); return; @@ -940,7 +940,7 @@ class Router { handler: descriptor?.value, typeName: 'Mutation', aggregate: handlerOrOptions?.aggregate ?? true, - raiseOnError: handlerOrOptions?.raiseOnError ?? false, + throwOnError: handlerOrOptions?.throwOnError ?? false, }); return descriptor; diff --git a/packages/event-handler/src/types/appsync-graphql.ts b/packages/event-handler/src/types/appsync-graphql.ts index b063dedbf5..91d81a4a13 100644 --- a/packages/event-handler/src/types/appsync-graphql.ts +++ b/packages/event-handler/src/types/appsync-graphql.ts @@ -131,7 +131,7 @@ type RouteHandlerOptions< * Whether to raise an error if the handler fails * @default false */ - raiseOnError?: R; + throwOnError?: R; }; // #region Router @@ -165,8 +165,8 @@ type GraphQlRouteOptions = { /** * Options for configuring a batch GraphQL route handler. * - * @template T - If `true`, the handler receives all events at once and `raiseOnError` cannot be specified. - * If `false`, the handler is called for each event individually and `raiseOnError` can be specified. + * @template T - If `true`, the handler receives all events at once and `throwOnError` cannot be specified. + * If `false`, the handler is called for each event individually and `throwOnError` can be specified. * Defaults to `true`. * @template R - If `true`, errors thrown by the handler will be raised. Defaults to `false`. */ @@ -175,8 +175,8 @@ type GraphQlBatchRouteOptions< R extends boolean | undefined = false, > = GraphQlRouteOptions & (T extends true - ? { aggregate?: T; raiseOnError?: never } - : { aggregate?: T; raiseOnError?: R }); + ? { aggregate?: T; throwOnError?: never } + : { aggregate?: T; throwOnError?: R }); export type { RouteHandlerRegistryOptions, diff --git a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts index 48de40b8ed..f81aa767f6 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts @@ -307,7 +307,7 @@ describe('Class: AppSyncGraphQLResolver', () => { ]); }); - it('preserves the scope when using `batchResolver` decorator when aggregate=false and raiseOnError=true', async () => { + it('preserves the scope when using `batchResolver` decorator when aggregate=false and throwOnError=true', async () => { // Prepare const app = new AppSyncGraphQLResolver({ logger: console }); @@ -316,13 +316,13 @@ describe('Class: AppSyncGraphQLResolver', () => { @app.batchResolver({ fieldName: 'batchGet', - raiseOnError: true, + throwOnError: true, aggregate: false, }) public async handleBatchGet({ id }: { id: string }) { return { id, - scope: `${this.scope} id=${id} raiseOnError=true aggregate=false`, + scope: `${this.scope} id=${id} throwOnError=true aggregate=false`, }; } @@ -344,12 +344,12 @@ describe('Class: AppSyncGraphQLResolver', () => { // Assess expect(result).toEqual([ - { id: 1, scope: 'scoped id=1 raiseOnError=true aggregate=false' }, - { id: 2, scope: 'scoped id=2 raiseOnError=true aggregate=false' }, + { id: 1, scope: 'scoped id=1 throwOnError=true aggregate=false' }, + { id: 2, scope: 'scoped id=2 throwOnError=true aggregate=false' }, ]); }); - it('preserves the scope when using `batchResolver` decorator when aggregate=false and raiseOnError=false', async () => { + it('preserves the scope when using `batchResolver` decorator when aggregate=false and throwOnError=false', async () => { // Prepare const app = new AppSyncGraphQLResolver({ logger: console }); @@ -358,13 +358,13 @@ describe('Class: AppSyncGraphQLResolver', () => { @app.batchResolver({ fieldName: 'batchGet', - raiseOnError: false, + throwOnError: false, aggregate: false, }) public async handleBatchGet({ id }: { id: string }) { return { id, - scope: `${this.scope} id=${id} raiseOnError=false aggregate=false`, + scope: `${this.scope} id=${id} throwOnError=false aggregate=false`, }; } @@ -386,8 +386,8 @@ describe('Class: AppSyncGraphQLResolver', () => { // Assess expect(result).toEqual([ - { id: 1, scope: 'scoped id=1 raiseOnError=false aggregate=false' }, - { id: 2, scope: 'scoped id=2 raiseOnError=false aggregate=false' }, + { id: 1, scope: 'scoped id=1 throwOnError=false aggregate=false' }, + { id: 2, scope: 'scoped id=2 throwOnError=false aggregate=false' }, ]); }); @@ -530,7 +530,7 @@ describe('Class: AppSyncGraphQLResolver', () => { ]); }); - it('registers a batch resolver via direct function call and invokes it (aggregate=false) and (raiseOnError=true)', async () => { + it('registers a batch resolver via direct function call and invokes it (aggregate=false) and (throwOnError=true)', async () => { // Prepare const app = new AppSyncGraphQLResolver({ logger: console }); const handler = vi @@ -541,7 +541,7 @@ describe('Class: AppSyncGraphQLResolver', () => { fieldName: 'batchGet', typeName: 'Query', aggregate: false, - raiseOnError: true, + throwOnError: true, }); const events = [ onGraphqlEventFactory('batchGet', 'Query', { id: '1' }), @@ -594,7 +594,7 @@ describe('Class: AppSyncGraphQLResolver', () => { ]); }); - it('stops on first error when aggregate=false and raiseOnError=true', async () => { + it('stops on first error when aggregate=false and throwOnError=true', async () => { // Prepare const app = new AppSyncGraphQLResolver({ logger: console }); const handler = vi @@ -606,7 +606,7 @@ describe('Class: AppSyncGraphQLResolver', () => { fieldName: 'batchGet', typeName: 'Query', aggregate: false, - raiseOnError: true, + throwOnError: true, }); const events = [ onGraphqlEventFactory('batchGet', 'Query', { id: '1' }), From 75684b7ac53cd43409c07b53de0a025f0b435104 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Mon, 28 Jul 2025 22:46:40 +0600 Subject: [PATCH 57/59] fix: update log message for graceful error handling flag --- .../event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts index 7e618c4ded..f6a0428e60 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -283,7 +283,7 @@ class AppSyncGraphQLResolver extends Router { ): Promise { const { aggregate, throwOnError } = options; this.logger.debug( - `Aggregate flag aggregate=${aggregate} & Graceful error handling flag throwOnError=${throwOnError}` + `Aggregate flag aggregate=${aggregate} & graceful error handling flag throwOnError=${throwOnError}` ); if (aggregate) { From 82701befa368fdaf79504fa388bcaeb451a3ced8 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Mon, 28 Jul 2025 22:49:03 +0600 Subject: [PATCH 58/59] fix: refactor batchResolver tests to use dynamic throwOnError flag --- .../AppSyncGraphQLResolver.test.ts | 130 +++++++----------- 1 file changed, 53 insertions(+), 77 deletions(-) diff --git a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts index f81aa767f6..5692587884 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts @@ -307,89 +307,65 @@ describe('Class: AppSyncGraphQLResolver', () => { ]); }); - it('preserves the scope when using `batchResolver` decorator when aggregate=false and throwOnError=true', async () => { - // Prepare - const app = new AppSyncGraphQLResolver({ logger: console }); - - class Lambda { - public scope = 'scoped'; + it.each([ + { + throwOnError: true, + description: 'throwOnError=true', + }, + { + throwOnError: false, + description: 'throwOnError=false', + }, + ])( + 'preserves the scope when using `batchResolver` decorator when aggregate=false and $description', + async ({ throwOnError }) => { + // Prepare + const app = new AppSyncGraphQLResolver({ logger: console }); - @app.batchResolver({ - fieldName: 'batchGet', - throwOnError: true, - aggregate: false, - }) - public async handleBatchGet({ id }: { id: string }) { - return { - id, - scope: `${this.scope} id=${id} throwOnError=true aggregate=false`, - }; - } + class Lambda { + public scope = 'scoped'; + + @app.batchResolver({ + fieldName: 'batchGet', + throwOnError, + aggregate: false, + }) + public async handleBatchGet({ id }: { id: string }) { + return { + id, + scope: `${this.scope} id=${id} throwOnError=${throwOnError} aggregate=false`, + }; + } - public async handler(event: unknown, context: Context) { - return app.resolve(event, context, { scope: this }); + public async handler(event: unknown, context: Context) { + return app.resolve(event, context, { scope: this }); + } } - } - const lambda = new Lambda(); - const handler = lambda.handler.bind(lambda); - - // Act - const result = await handler( - [ - onGraphqlEventFactory('batchGet', 'Query', { id: 1 }), - onGraphqlEventFactory('batchGet', 'Query', { id: 2 }), - ], - context - ); - - // Assess - expect(result).toEqual([ - { id: 1, scope: 'scoped id=1 throwOnError=true aggregate=false' }, - { id: 2, scope: 'scoped id=2 throwOnError=true aggregate=false' }, - ]); - }); + const lambda = new Lambda(); + const handler = lambda.handler.bind(lambda); - it('preserves the scope when using `batchResolver` decorator when aggregate=false and throwOnError=false', async () => { - // Prepare - const app = new AppSyncGraphQLResolver({ logger: console }); - - class Lambda { - public scope = 'scoped'; - - @app.batchResolver({ - fieldName: 'batchGet', - throwOnError: false, - aggregate: false, - }) - public async handleBatchGet({ id }: { id: string }) { - return { - id, - scope: `${this.scope} id=${id} throwOnError=false aggregate=false`, - }; - } + // Act + const result = await handler( + [ + onGraphqlEventFactory('batchGet', 'Query', { id: 1 }), + onGraphqlEventFactory('batchGet', 'Query', { id: 2 }), + ], + context + ); - public async handler(event: unknown, context: Context) { - return app.resolve(event, context, { scope: this }); - } + // Assess + expect(result).toEqual([ + { + id: 1, + scope: `scoped id=1 throwOnError=${throwOnError} aggregate=false`, + }, + { + id: 2, + scope: `scoped id=2 throwOnError=${throwOnError} aggregate=false`, + }, + ]); } - const lambda = new Lambda(); - const handler = lambda.handler.bind(lambda); - - // Act - const result = await handler( - [ - onGraphqlEventFactory('batchGet', 'Query', { id: 1 }), - onGraphqlEventFactory('batchGet', 'Query', { id: 2 }), - ], - context - ); - - // Assess - expect(result).toEqual([ - { id: 1, scope: 'scoped id=1 throwOnError=false aggregate=false' }, - { id: 2, scope: 'scoped id=2 throwOnError=false aggregate=false' }, - ]); - }); + ); it('emits debug message when AWS_LAMBDA_LOG_LEVEL is set to DEBUG', async () => { // Prepare From 13ebb912a90903d78a0ed9308b15ba0ec44edf3d Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Tue, 29 Jul 2025 10:21:53 +0600 Subject: [PATCH 59/59] fix: correct batch event wording and refactor batch resolver test cases for clarity --- .../AppSyncGraphQLResolver.test.ts | 124 ++++++++++-------- 1 file changed, 71 insertions(+), 53 deletions(-) diff --git a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts index 5692587884..27bc862e00 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts @@ -449,7 +449,7 @@ describe('Class: AppSyncGraphQLResolver', () => { } ); - it('logs a warning and returns early if one of the batch event is not compatible', async () => { + it('logs a warning and returns early if one of the batch events is not compatible', async () => { // Prepare const app = new AppSyncGraphQLResolver({ logger: console }); app.batchResolver(vi.fn(), { @@ -477,63 +477,81 @@ describe('Class: AppSyncGraphQLResolver', () => { expect(result).toBeUndefined(); }); - it('registers a batch resolver via direct function call and invokes it (aggregate=true)', async () => { - // Prepare - const app = new AppSyncGraphQLResolver({ logger: console }); - const handler = vi.fn().mockResolvedValue([ - { id: '1', value: 'A' }, - { id: '2', value: 'B' }, - ]); - app.batchResolver(handler, { - fieldName: 'batchGet', - typeName: 'Query', + it.each([ + { aggregate: true, - }); - const events = [ - onGraphqlEventFactory('batchGet', 'Query', { id: '1' }), - onGraphqlEventFactory('batchGet', 'Query', { id: '2' }), - ]; - - // Act - const result = await app.resolve(events, context); + description: 'aggregate=true', + setupHandler: (handler: ReturnType) => { + handler.mockResolvedValue([ + { id: '1', value: 'A' }, + { id: '2', value: 'B' }, + ]); + }, + }, + { + aggregate: false, + description: 'aggregate=false and throwOnError=true', + setupHandler: (handler: ReturnType) => { + handler + .mockResolvedValueOnce({ id: '1', value: 'A' }) + .mockResolvedValueOnce({ id: '2', value: 'B' }); + }, + }, + ])( + 'registers a batch resolver via direct function call and invokes it ($description)', + async ({ aggregate, setupHandler }) => { + // Prepare + const app = new AppSyncGraphQLResolver({ logger: console }); + const handler = vi.fn(); + setupHandler(handler); - // Assess - expect(handler).toHaveBeenCalledTimes(1); - expect(handler).toHaveBeenCalledWith(events, { event: events, context }); - expect(result).toEqual([ - { id: '1', value: 'A' }, - { id: '2', value: 'B' }, - ]); - }); + if (aggregate) { + app.batchResolver(handler, { + fieldName: 'batchGet', + typeName: 'Query', + aggregate: true, + }); + } else { + app.batchResolver(handler, { + fieldName: 'batchGet', + typeName: 'Query', + aggregate: false, + throwOnError: true, + }); + } - it('registers a batch resolver via direct function call and invokes it (aggregate=false) and (throwOnError=true)', async () => { - // Prepare - const app = new AppSyncGraphQLResolver({ logger: console }); - const handler = vi - .fn() - .mockResolvedValueOnce({ id: '1', value: 'A' }) - .mockResolvedValueOnce({ id: '2', value: 'B' }); - app.batchResolver(handler, { - fieldName: 'batchGet', - typeName: 'Query', - aggregate: false, - throwOnError: true, - }); - const events = [ - onGraphqlEventFactory('batchGet', 'Query', { id: '1' }), - onGraphqlEventFactory('batchGet', 'Query', { id: '2' }), - ]; + const events = [ + onGraphqlEventFactory('batchGet', 'Query', { id: '1' }), + onGraphqlEventFactory('batchGet', 'Query', { id: '2' }), + ]; - // Act - const result = await app.resolve(events, context); + // Act + const result = await app.resolve(events, context); - // Assess - expect(handler).toHaveBeenCalledTimes(2); - expect(result).toEqual([ - { id: '1', value: 'A' }, - { id: '2', value: 'B' }, - ]); - }); + // Assess + if (aggregate) { + expect(handler).toHaveBeenCalledTimes(1); + expect(handler).toHaveBeenCalledWith(events, { + event: events, + context, + }); + } else { + expect(handler).toHaveBeenCalledTimes(2); + expect(handler).toHaveBeenNthCalledWith(1, events[0].arguments, { + event: events[0], + context, + }); + expect(handler).toHaveBeenNthCalledWith(2, events[1].arguments, { + event: events[1], + context, + }); + } + expect(result).toEqual([ + { id: '1', value: 'A' }, + { id: '2', value: 'B' }, + ]); + } + ); it('returns null for failed records when aggregate=false', async () => { // Prepare