From d4e4232aa5f30a92e5772ae620393d4601a1e509 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 31 Aug 2025 19:52:23 +0600 Subject: [PATCH 01/27] feat: enhance error handling with detailed logging and handler merging --- .../ExceptionHandlerRegistry.ts | 71 +++++++++++++------ 1 file changed, 50 insertions(+), 21 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/ExceptionHandlerRegistry.ts b/packages/event-handler/src/appsync-graphql/ExceptionHandlerRegistry.ts index 261c426b8d..1e03a2deb6 100644 --- a/packages/event-handler/src/appsync-graphql/ExceptionHandlerRegistry.ts +++ b/packages/event-handler/src/appsync-graphql/ExceptionHandlerRegistry.ts @@ -37,7 +37,45 @@ class ExceptionHandlerRegistry { const errors = Array.isArray(error) ? error : [error]; for (const err of errors) { - this.registerErrorHandler(err, handler); + this.#registerErrorHandler(err, handler); + } + } + + /** + * Resolves and returns the appropriate exception handler for a given error instance. + * + * This method attempts to find a registered exception handler based on the error class name. + * If a matching handler is found, it is returned; otherwise, `null` is returned. + * + * @param error - The error instance for which to resolve an exception handler. + */ + public resolve(error: Error): ExceptionHandler | null { + const errorName = error.name; + this.#logger.debug(`Looking for exception handler for error: ${errorName}`); + + const handlerOptions = this.handlers.get(errorName); + if (handlerOptions) { + this.#logger.debug(`Found exact match for error class: ${errorName}`); + return handlerOptions.handler; + } + + this.#logger.debug(`No exception handler found for error: ${errorName}`); + return null; + } + + /** + * Merges handlers from another ExceptionHandlerRegistry into this registry. + * Existing handlers for the same error class will be replaced and a warning will be logged. + * + * @param otherRegistry - The registry to merge handlers from. + */ + public merge(otherRegistry: ExceptionHandlerRegistry): void { + this.#logger.debug('Merging exception handler registries'); + for (const [errorName, handlerOptions] of otherRegistry.handlers) { + if (this.handlers.has(errorName)) { + this.#warnHandlerOverriding(errorName); + } + this.handlers.set(errorName, handlerOptions); } } @@ -47,7 +85,7 @@ class ExceptionHandlerRegistry { * @param errorClass - The error class to register the handler for. * @param handler - The exception handler function. */ - private registerErrorHandler( + #registerErrorHandler( errorClass: ErrorClass, handler: ExceptionHandler ): void { @@ -56,9 +94,7 @@ class ExceptionHandlerRegistry { this.#logger.debug(`Adding exception handler for error class ${errorName}`); if (this.handlers.has(errorName)) { - this.#logger.warn( - `An exception handler for error class '${errorName}' is already registered. The previous handler will be replaced.` - ); + this.#warnHandlerOverriding(errorName); } this.handlers.set(errorName, { @@ -68,25 +104,18 @@ class ExceptionHandlerRegistry { } /** - * Resolves and returns the appropriate exception handler for a given error instance. + * Logs a warning message when an exception handler is being overridden. * - * This method attempts to find a registered exception handler based on the error class name. - * If a matching handler is found, it is returned; otherwise, `null` is returned. + * This method is called internally when registering a new exception handler + * for an error class that already has a handler registered. It warns the user + * that the previous handler will be replaced with the new one. * - * @param error - The error instance for which to resolve an exception handler. + * @param errorName - The name of the error class for which a handler is being overridden */ - public resolve(error: Error): ExceptionHandler | null { - const errorName = error.name; - this.#logger.debug(`Looking for exception handler for error: ${errorName}`); - - const handlerOptions = this.handlers.get(errorName); - if (handlerOptions) { - this.#logger.debug(`Found exact match for error class: ${errorName}`); - return handlerOptions.handler; - } - - this.#logger.debug(`No exception handler found for error: ${errorName}`); - return null; + #warnHandlerOverriding(errorName: string): void { + this.#logger.warn( + `An exception handler for error class '${errorName}' is already registered. The previous handler will be replaced.` + ); } } From 725b67dd28957a27cd64ddeaa94ee34941bb12b5 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 31 Aug 2025 19:52:59 +0600 Subject: [PATCH 02/27] feat: improve resolver registration with dedicated warning method and enhanced merge functionality --- .../appsync-graphql/RouteHandlerRegistry.ts | 33 +++++++++++++++++-- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts b/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts index ac6c2ff2aa..31ace05fef 100644 --- a/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts +++ b/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts @@ -50,9 +50,7 @@ class RouteHandlerRegistry { this.#logger.debug(`Adding resolver for field ${typeName}.${fieldName}`); const cacheKey = this.#makeKey(typeName, fieldName); if (this.resolvers.has(cacheKey)) { - this.#logger.warn( - `A resolver for field '${fieldName}' is already registered for '${typeName}'. The previous resolver will be replaced.` - ); + this.#warnResolverOverriding(fieldName, typeName); } this.resolvers.set(cacheKey, { fieldName, @@ -81,6 +79,22 @@ class RouteHandlerRegistry { return this.resolvers.get(this.#makeKey(typeName, fieldName)); } + /** + * Merges handlers from another RouteHandlerRegistry into this registry. + * Existing handlers with the same key will be replaced and a warning will be logged. + * + * @param otherRegistry - The registry to merge handlers from. + */ + public merge(otherRegistry: RouteHandlerRegistry): void { + this.#logger.debug('Merging route handler registries'); + for (const [key, handler] of otherRegistry.resolvers) { + if (this.resolvers.has(key)) { + this.#warnResolverOverriding(handler.fieldName, handler.typeName); + } + this.resolvers.set(key, handler); + } + } + /** * Generates a unique key by combining the provided GraphQL type name and field name. * @@ -90,6 +104,19 @@ class RouteHandlerRegistry { #makeKey(typeName: string, fieldName: string): string { return `${typeName}.${fieldName}`; } + + /** + * Logs a warning message indicating that a resolver for the specified field and type + * is already registered and will be replaced by a new resolver. + * + * @param fieldName - The name of the field for which the resolver is being overridden. + * @param typeName - The name of the type associated with the field. + */ + #warnResolverOverriding(fieldName: string, typeName: string): void { + this.#logger.warn( + `A resolver for field '${fieldName}' is already registered for '${typeName}'. The previous resolver will be replaced.` + ); + } } export { RouteHandlerRegistry }; From dcbc5320ab7a4fc5bcccdb64dc46c86fb0bd9448 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 31 Aug 2025 19:54:14 +0600 Subject: [PATCH 03/27] feat: add method to merge resolver registries from another router --- .../event-handler/src/appsync-graphql/Router.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/event-handler/src/appsync-graphql/Router.ts b/packages/event-handler/src/appsync-graphql/Router.ts index 29745e2663..b84a268fba 100644 --- a/packages/event-handler/src/appsync-graphql/Router.ts +++ b/packages/event-handler/src/appsync-graphql/Router.ts @@ -64,6 +64,20 @@ class Router { this.isDev = isDevMode(); } + /** + * Merges resolver registries from another router into this router. + * + * This method combines the resolver registry, batch resolver registry, and exception handler registry + * from the provided router with the current router's registries. + * + * @param router - The source router whose registries will be merged into this router + */ + protected mergeRegistriesFrom(router: Router): void { + this.resolverRegistry.merge(router.resolverRegistry); + this.batchResolverRegistry.merge(router.batchResolverRegistry); + this.exceptionHandlerRegistry.merge(router.exceptionHandlerRegistry); + } + /** * Register a resolver function for any GraphQL event. * From c79e4a3415cad17040b652b4b24b9c1f8ff294b3 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 31 Aug 2025 19:54:26 +0600 Subject: [PATCH 04/27] feat: add `includeRouter` method to merge route registries into AppSync resolver --- .../src/appsync-graphql/AppSyncGraphQLResolver.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts index 6aede4c554..3d87031bd8 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -180,6 +180,20 @@ class AppSyncGraphQLResolver extends Router { ); } + /** + * Includes a router and merges its registries into the current resolver. + * + * This method allows you to compose multiple routers by merging their + * route registries into the current AppSync GraphQL resolver instance. + * + * @param router - The router instance whose registries will be merged + */ + public includeRouter(router: Router): void { + this.logger.debug('Including router'); + this.mergeRegistriesFrom(router); + this.logger.debug('Router included successfully'); + } + /** * Executes the provided asynchronous function with error handling. * If the function throws an error, it delegates error processing to `#handleError` From 32d713b3ea27daa92d52e8a3a93c8e93c1fb0329 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 31 Aug 2025 20:22:53 +0600 Subject: [PATCH 05/27] test: `includeRouter` function tests --- .../AppSyncGraphQLResolver.test.ts | 237 ++++++++++++++++++ 1 file changed, 237 insertions(+) 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 1100a41005..81b9f7df3a 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts @@ -5,7 +5,9 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { AppSyncGraphQLResolver } from '../../../src/appsync-graphql/AppSyncGraphQLResolver.js'; import { InvalidBatchResponseException, + makeId, ResolverNotFoundException, + Router, } from '../../../src/appsync-graphql/index.js'; import type { ErrorClass } from '../../../src/types/appsync-graphql.js'; import { onGraphqlEventFactory } from '../../helpers/factories.js'; @@ -1327,4 +1329,239 @@ describe('Class: AppSyncGraphQLResolver', () => { }); // #endregion Exception handling + + // #region includeRouter + + it('handles multiple routers and resolves their handlers correctly', async () => { + // Prepare + const userRouter = new Router(); + userRouter.onQuery<{ id: string }>('getUser', async ({ id }) => ({ + id, + name: 'John Doe', + })); + + userRouter.onMutation<{ name: string; email: string }>( + 'createUser', + async ({ name, email }) => ({ + id: makeId(), + name, + email, + }) + ); + + const todoRouter = new Router(); + todoRouter.onQuery<{ id: string }>('getTodo', async ({ id }) => ({ + id, + title: 'Sample Todo', + completed: false, + })); + + const app = new AppSyncGraphQLResolver({ logger: console }); + app.includeRouter(userRouter); + app.includeRouter(todoRouter); + + // Act + const getUserResult = await app.resolve( + onGraphqlEventFactory('getUser', 'Query', { id: '123' }), + context + ); + const createUserResult = await app.resolve( + onGraphqlEventFactory('createUser', 'Mutation', { + name: 'Jane Doe', + email: 'jane.doe@example.com', + }), + context + ); + const todoResult = await app.resolve( + onGraphqlEventFactory('getTodo', 'Query', { id: '456' }), + context + ); + + // Assess + expect(getUserResult).toEqual({ id: '123', name: 'John Doe' }); + expect(createUserResult).toEqual({ + id: expect.any(String), + name: 'Jane Doe', + email: 'jane.doe@example.com', + }); + expect(todoResult).toEqual({ + id: '456', + title: 'Sample Todo', + completed: false, + }); + }); + + it('handles multiple routers with batch resolvers and resolves their handlers correctly', async () => { + // Prepare + const postRouter = new Router(); + postRouter.onBatchQuery('getPosts', async (events) => + events.map((event) => ({ + id: event.arguments.id, + title: `Post ${event.arguments.id}`, + })) + ); + + const todoRouter = new Router(); + todoRouter.onBatchQuery('getTodos', async (events) => + events.map((event) => ({ + id: event.arguments.id, + title: `Todo ${event.arguments.id}`, + })) + ); + + const app = new AppSyncGraphQLResolver({ logger: console }); + app.includeRouter(postRouter); + app.includeRouter(todoRouter); + + // Act + const postResults = await app.resolve( + [ + onGraphqlEventFactory('getPosts', 'Query', { id: '1' }), + onGraphqlEventFactory('getPosts', 'Query', { id: '2' }), + ], + context + ); + const todoResults = await app.resolve( + [ + onGraphqlEventFactory('getTodos', 'Query', { id: '1' }), + onGraphqlEventFactory('getTodos', 'Query', { id: '2' }), + ], + context + ); + + // Assess + expect(postResults).toEqual([ + { id: '1', title: 'Post 1' }, + { id: '2', title: 'Post 2' }, + ]); + expect(todoResults).toEqual([ + { id: '1', title: 'Todo 1' }, + { id: '2', title: 'Todo 2' }, + ]); + }); + + it('handles multiple routers with exception handlers', async () => { + // Prepare + const firstRouter = new Router(); + firstRouter.exceptionHandler(ValidationError, async (error) => ({ + error: `Handled: ${error.message}`, + type: 'validation', + })); + firstRouter.resolver( + async () => { + throw new ValidationError('Test validation error'); + }, + { fieldName: 'firstHandler' } + ); + + const secondRouter = new Router(); + secondRouter.exceptionHandler(EvalError, async (error) => ({ + error: `Handled: ${error.message}`, + type: 'evaluation', + })); + secondRouter.resolver( + async () => { + throw new EvalError('Test evaluation error'); + }, + { fieldName: 'secondHandler' } + ); + + const app = new AppSyncGraphQLResolver({ logger: console }); + app.includeRouter(firstRouter); + app.includeRouter(secondRouter); + + // Act + const firstResult = await app.resolve( + onGraphqlEventFactory('firstHandler', 'Query', { shouldThrow: true }), + context + ); + const secondResult = await app.resolve( + onGraphqlEventFactory('secondHandler', 'Query', { shouldThrow: true }), + context + ); + + // Assess + expect(firstResult).toEqual({ + error: 'Handled: Test validation error', + type: 'validation', + }); + expect(secondResult).toEqual({ + error: 'Handled: Test evaluation error', + type: 'evaluation', + }); + }); + + it('handles conflicts when including multiple routers with same resolver', async () => { + // Prepare + const firstRouter = new Router(); + firstRouter.onQuery('getTest', () => ({ + source: 'first', + })); + + const secondRouter = new Router(); + secondRouter.onQuery('getTest', () => ({ + source: 'second', + })); + + const app = new AppSyncGraphQLResolver({ logger: console }); + app.includeRouter(firstRouter); + app.includeRouter(secondRouter); + + // Act + const result = await app.resolve( + onGraphqlEventFactory('getTest', 'Query', {}), + context + ); + + // Assess + expect(result).toEqual({ source: 'second' }); + expect(console.warn).toHaveBeenCalledWith( + "A resolver for field 'getTest' is already registered for 'Query'. The previous resolver will be replaced." + ); + }); + + it('handles conflicts when including multiple routers with same exception handler', async () => { + // Prepare + const firstRouter = new Router(); + firstRouter.exceptionHandler(ValidationError, async (error) => ({ + source: 'first', + message: error.message, + type: 'first_validation', + })); + firstRouter.onQuery('testError', async () => { + throw new ValidationError('Test validation error'); + }); + + const secondRouter = new Router(); + secondRouter.exceptionHandler(ValidationError, async (error) => ({ + source: 'second', + message: error.message, + type: 'second_validation', + })); + secondRouter.onQuery('testError', async () => { + throw new ValidationError('Test validation error'); + }); + + const app = new AppSyncGraphQLResolver({ logger: console }); + app.includeRouter(firstRouter); + app.includeRouter(secondRouter); + + // Act + const result = await app.resolve( + onGraphqlEventFactory('testError', 'Query', {}), + context + ); + + // Assess + expect(result).toEqual({ + source: 'second', + message: 'Test validation error', + type: 'second_validation', + }); + expect(console.warn).toHaveBeenCalledWith( + "An exception handler for error class 'ValidationError' is already registered. The previous handler will be replaced." + ); + }); + + // #endregion includeRouters }); From 1dcfb6913d53d3a9372c793b8c0fa30d5a31e2c4 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 31 Aug 2025 20:24:37 +0600 Subject: [PATCH 06/27] feat: export `Router` from Router.js in AppSync GraphQL index --- packages/event-handler/src/appsync-graphql/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/event-handler/src/appsync-graphql/index.ts b/packages/event-handler/src/appsync-graphql/index.ts index bffc35bad1..9e38af73d6 100644 --- a/packages/event-handler/src/appsync-graphql/index.ts +++ b/packages/event-handler/src/appsync-graphql/index.ts @@ -3,6 +3,7 @@ export { InvalidBatchResponseException, ResolverNotFoundException, } from './errors.js'; +export { Router } from './Router.js'; export { awsDate, awsDateTime, From 4d858fdb10441e31b8f4fb630d7d8ad40e09ce3a Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Tue, 9 Sep 2025 09:54:51 +0600 Subject: [PATCH 07/27] feat: add shared context support to AppSync resolver and handler types --- .../appsync-graphql/AppSyncGraphQLResolver.ts | 113 +++++++++++++++--- .../src/types/appsync-graphql.ts | 6 + 2 files changed, 105 insertions(+), 14 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts index 3d87031bd8..08cb7fb649 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -2,6 +2,7 @@ import type { AppSyncResolverEvent, Context } from 'aws-lambda'; import type { BatchResolverAggregateHandlerFn, BatchResolverHandlerFn, + GraphQlRouterOptions, ResolverHandler, RouteHandlerOptions, } from '../types/appsync-graphql.js'; @@ -42,6 +43,16 @@ import { isAppSyncGraphQLEvent } from './utils.js'; * ``` */ class AppSyncGraphQLResolver extends Router { + /** + * A map to hold contextual data that can be shared across all resolver handlers. + */ + public readonly context: Map; + + public constructor(options?: GraphQlRouterOptions) { + super(options); + this.context = new Map(); + } + /** * Resolve the response based on the provided event and route handlers configured. * @@ -160,11 +171,16 @@ class AppSyncGraphQLResolver extends Router { ); return; } - return this.#withErrorHandling( - () => this.#executeBatchResolvers(event, context, options), - event[0], - options - ); + + try { + return await this.#withErrorHandling( + () => this.#executeBatchResolvers(event, context, options), + event[0], + options + ); + } finally { + this.context.clear(); + } } if (!isAppSyncGraphQLEvent(event)) { this.logger.warn( @@ -173,11 +189,15 @@ class AppSyncGraphQLResolver extends Router { return; } - return this.#withErrorHandling( - () => this.#executeSingleResolver(event, context, options), - event, - options - ); + try { + return await this.#withErrorHandling( + () => this.#executeSingleResolver(event, context, options), + event, + options + ); + } finally { + this.context.clear(); + } } /** @@ -194,6 +214,52 @@ class AppSyncGraphQLResolver extends Router { this.logger.debug('Router included successfully'); } + /** + * Appends contextual data to be shared with all resolver handlers. + * + * This method allows you to add key-value pairs to the shared context that will be + * accessible to all resolver handlers through the `sharedContext` parameter. The context + * is automatically cleared after each invocation for safety. + * + * @example + * ```ts + * import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; + * import type { Context } from 'aws-lambda'; + * + * const app = new AppSyncGraphQLResolver(); + * + * export const handler = async (event: unknown, context: Context) => { + * // Share context between main app and routers + * app.appendContext({ + * isAdmin: true, + * requestId: context.awsRequestId, + * timestamp: Date.now() + * }); + * + * return app.resolve(event, context); + * }; + * + * // Resolver handlers can access shared context + * app.onQuery('getUser', async ({ id }, { sharedContext }) => { + * const isAdmin = sharedContext?.get('isAdmin'); + * const requestId = sharedContext?.get('requestId'); + * + * return { + * id, + * name: 'John Doe', + * email: isAdmin ? 'john@example.com' : 'hidden' + * }; + * }); + * ``` + * + * @param data - A record of key-value pairs to add to the shared context + */ + public appendContext(data: Record): void { + Object.entries(data).forEach(([key, value]) => { + this.context.set(key, value); + }); + } + /** * Executes the provided asynchronous function with error handling. * If the function throws an error, it delegates error processing to `#handleError` @@ -329,7 +395,11 @@ class AppSyncGraphQLResolver extends Router { options.handler as BatchResolverAggregateHandlerFn ).apply(resolveOptions?.scope ?? this, [ events, - { event: events, context }, + { + event: events, + context, + ...(this.context.size > 0 && { sharedContext: this.context }), + }, ]); if (!Array.isArray(response)) { @@ -348,7 +418,11 @@ class AppSyncGraphQLResolver extends Router { for (const event of events) { const result = await handler.apply(resolveOptions?.scope ?? this, [ event.arguments, - { event, context }, + { + event, + context, + ...(this.context.size > 0 && { sharedContext: this.context }), + }, ]); results.push(result); } @@ -359,7 +433,11 @@ class AppSyncGraphQLResolver extends Router { try { const result = await handler.apply(resolveOptions?.scope ?? this, [ events[i].arguments, - { event: events[i], context }, + { + event: events[i], + context, + ...(this.context.size > 0 && { sharedContext: this.context }), + }, ]); results.push(result); } catch (error) { @@ -401,7 +479,14 @@ class AppSyncGraphQLResolver extends Router { if (resolverHandlerOptions) { return (resolverHandlerOptions.handler as ResolverHandler).apply( options?.scope ?? this, - [event.arguments, { event, context }] + [ + event.arguments, + { + event, + context, + ...(this.context.size > 0 && { sharedContext: this.context }), + }, + ] ); } diff --git a/packages/event-handler/src/types/appsync-graphql.ts b/packages/event-handler/src/types/appsync-graphql.ts index cd4f8685ed..aeb893ab29 100644 --- a/packages/event-handler/src/types/appsync-graphql.ts +++ b/packages/event-handler/src/types/appsync-graphql.ts @@ -14,6 +14,7 @@ type BatchResolverSyncHandlerFn< options: { event: AppSyncResolverEvent; context: Context; + sharedContext?: Map; } ) => unknown; @@ -25,6 +26,7 @@ type BatchResolverHandlerFn< options: { event: AppSyncResolverEvent; context: Context; + sharedContext?: Map; } ) => Promise; @@ -36,6 +38,7 @@ type BatchResolverAggregateHandlerFn< options: { event: AppSyncResolverEvent[]; context: Context; + sharedContext?: Map; } ) => Promise; @@ -47,6 +50,7 @@ type BatchResolverSyncAggregateHandlerFn< options: { event: AppSyncResolverEvent[]; context: Context; + sharedContext?: Map; } ) => unknown; @@ -70,6 +74,7 @@ type ResolverSyncHandlerFn> = ( options: { event: AppSyncResolverEvent; context: Context; + sharedContext?: Map; } ) => unknown; @@ -78,6 +83,7 @@ type ResolverHandlerFn> = ( options: { event: AppSyncResolverEvent; context: Context; + sharedContext?: Map; } ) => Promise; From 7a96f8654b58c394734f273a141c8724d52d16c3 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Tue, 9 Sep 2025 10:03:44 +0600 Subject: [PATCH 08/27] test: tests for `includeRouter` method and context sharing in AppSyncGraphQLResolver --- .../AppSyncGraphQLResolver.test.ts | 354 ++++++++++++++++++ 1 file changed, 354 insertions(+) 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 81b9f7df3a..770b79e2af 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts @@ -1563,5 +1563,359 @@ describe('Class: AppSyncGraphQLResolver', () => { ); }); + it('works as a method decorator for `includeRouter`', async () => { + // Prepare + const app = new AppSyncGraphQLResolver(); + + const userRouter = new Router(); + const todoRouter = new Router(); + + class Lambda { + public scope = 'scoped'; + + @userRouter.onQuery('getUser') + async getUserById({ id }: { id: string }) { + if (id.length === 0) + throw new ValidationError('User ID cannot be empty'); + return { id, name: 'John Doe', scope: this.scope }; + } + + @userRouter.onMutation('createUser') + async createUser({ name, email }: { name: string; email: string }) { + return { id: makeId(), name, email, scope: this.scope }; + } + + @userRouter.exceptionHandler(ValidationError) + async handleValidationError(error: ValidationError) { + return { + message: 'UserRouter validation error', + details: error.message, + type: 'user_validation_error', + scope: this.scope, + }; + } + + @todoRouter.onQuery('getTodo') + async getTodoById({ id }: { id: string }) { + if (id === 'eval-error') { + throw new EvalError('Todo evaluation error'); + } + return { + id, + title: 'Sample Todo', + completed: false, + scope: this.scope, + }; + } + + @todoRouter.exceptionHandler(EvalError) + async handleEvalError(error: EvalError) { + return { + message: 'TodoRouter evaluation error', + details: error.message, + type: 'todo_evaluation_error', + scope: this.scope, + }; + } + async handler(event: unknown, context: Context) { + app.includeRouter(userRouter); + app.includeRouter(todoRouter); + return app.resolve(event, context, { + scope: this, + }); + } + } + + const lambda = new Lambda(); + const handler = lambda.handler.bind(lambda); + + // Act + const getUserResult = await handler( + onGraphqlEventFactory('getUser', 'Query', { id: '123' }), + context + ); + const createUserResult = await handler( + onGraphqlEventFactory('createUser', 'Mutation', { + name: 'Jane Doe', + email: 'jane.doe@example.com', + }), + context + ); + const userValidationError = await handler( + onGraphqlEventFactory('getUser', 'Query', { id: '' }), + context + ); + + const getTodoResult = await handler( + onGraphqlEventFactory('getTodo', 'Query', { id: '456' }), + context + ); + const todoEvalError = await handler( + onGraphqlEventFactory('getTodo', 'Query', { id: 'eval-error' }), + context + ); + + // Assess + expect(getUserResult).toEqual({ + id: '123', + name: 'John Doe', + scope: 'scoped', + }); + expect(createUserResult).toEqual({ + id: expect.any(String), + name: 'Jane Doe', + email: 'jane.doe@example.com', + scope: 'scoped', + }); + expect(getTodoResult).toEqual({ + id: '456', + title: 'Sample Todo', + completed: false, + scope: 'scoped', + }); + expect(userValidationError).toEqual({ + message: 'UserRouter validation error', + details: 'User ID cannot be empty', + type: 'user_validation_error', + scope: 'scoped', + }); + expect(todoEvalError).toEqual({ + details: 'Todo evaluation error', + message: 'TodoRouter evaluation error', + type: 'todo_evaluation_error', + scope: 'scoped', + }); + }); + // #endregion includeRouters + + // #region appendContext + + it('allows sharing context data with resolver handlers', async () => { + // Prepare + const app = new AppSyncGraphQLResolver(); + + app.onQuery<{ id: string }>( + 'getUser', + async ({ id }, { sharedContext }) => { + const isAdmin = sharedContext?.get('isAdmin'); + const requestId = sharedContext?.get('requestId'); + + return { + id, + name: 'John Doe', + email: isAdmin ? 'john@example.com' : 'hidden', + requestId, + }; + } + ); + + // Act + app.appendContext({ + isAdmin: true, + requestId: 'test-request-123', + timestamp: Date.now(), + }); + + const result = await app.resolve( + onGraphqlEventFactory('getUser', 'Query', { id: '1' }), + context + ); + + // Assess + expect(result).toEqual({ + id: '1', + name: 'John Doe', + email: 'john@example.com', + requestId: 'test-request-123', + }); + }); + + it('allows context sharing with included routers', async () => { + // Prepare + const userRouter = new Router(); + userRouter.onQuery<{ id: string }>( + 'getUser', + async ({ id }, { sharedContext }) => { + const isAdmin = sharedContext?.get('isAdmin'); + const requestId = sharedContext?.get('requestId'); + + return { + id, + name: 'John Doe', + role: isAdmin ? 'admin' : 'user', + requestId, + }; + } + ); + + const todoRouter = new Router(); + todoRouter.onQuery<{ id: string }>( + 'getTodo', + async ({ id }, { sharedContext }) => { + const isAdmin = sharedContext?.get('isAdmin'); + const requestId = sharedContext?.get('requestId'); + + return { + id, + title: 'Sample Todo', + completed: false, + role: isAdmin ? 'admin' : 'user', + requestId, + }; + } + ); + + const app = new AppSyncGraphQLResolver(); + app.includeRouter(userRouter); + app.includeRouter(todoRouter); + app.appendContext({ + isAdmin: false, + requestId: 'router-test-456', + }); + + // Act + const userResult = await app.resolve( + onGraphqlEventFactory('getUser', 'Query', { id: '2' }), + context + ); + + // Assess + expect(userResult).toEqual({ + id: '2', + name: 'John Doe', + role: 'user', + requestId: 'router-test-456', + }); + }); + + it('clears context after each invocation for single events', async () => { + // Prepare + const app = new AppSyncGraphQLResolver(); + + app.onQuery<{ id: string }>( + 'getUser', + async ({ id }, { sharedContext }) => { + const requestId = sharedContext?.get('requestId'); + + return { + id, + requestId: requestId || 'no-request-id', + }; + } + ); + + // Act + app.appendContext({ requestId: 'first-request' }); + const firstResult = await app.resolve( + onGraphqlEventFactory('getUser', 'Query', { id: '1' }), + context + ); + + // Assess + expect(firstResult).toEqual({ + id: '1', + requestId: 'first-request', + }); + + // Act + const secondResult = await app.resolve( + onGraphqlEventFactory('getUser', 'Query', { id: '2' }), + context + ); + + // Assess + expect(secondResult).toEqual({ + id: '2', + requestId: 'no-request-id', + }); + }); + + it('clears context after each invocation for batch events', async () => { + // Prepare + const app = new AppSyncGraphQLResolver(); + + app.batchResolver<{ id: string }>( + async (events, { sharedContext }) => { + const requestId = sharedContext?.get('requestId'); + + return events.map((event) => ({ + id: event.arguments.id, + requestId: requestId || 'no-request-id', + })); + }, + { + fieldName: 'getUsers', + } + ); + + // Act + app.appendContext({ requestId: 'batch-request' }); + const firstResult = await app.resolve( + [ + onGraphqlEventFactory('getUsers', 'Query', { id: '1' }), + onGraphqlEventFactory('getUsers', 'Query', { id: '2' }), + ], + context + ); + + // Assess + expect(firstResult).toEqual([ + { id: '1', requestId: 'batch-request' }, + { id: '2', requestId: 'batch-request' }, + ]); + + // Act + const secondResult = await app.resolve( + [ + onGraphqlEventFactory('getUsers', 'Query', { id: '3' }), + onGraphqlEventFactory('getUsers', 'Query', { id: '4' }), + ], + context + ); + + // Assess + expect(secondResult).toEqual([ + { id: '3', requestId: 'no-request-id' }, + { id: '4', requestId: 'no-request-id' }, + ]); + }); + + it('allows updating context data multiple times before invocation', async () => { + // Prepare + const app = new AppSyncGraphQLResolver(); + + app.onQuery<{ id: string }>( + 'getUser', + async ({ id }, { sharedContext }) => { + const role = sharedContext?.get('role'); + const permissions = sharedContext?.get('permissions'); + + return { + id, + role, + permissions, + }; + } + ); + + // Act + app.appendContext({ role: 'user' }); + app.appendContext({ permissions: ['read'] }); + app.appendContext({ role: 'admin' }); + + const result = await app.resolve( + onGraphqlEventFactory('getUser', 'Query', { id: '1' }), + context + ); + + // Assess + expect(result).toEqual({ + id: '1', + role: 'admin', + permissions: ['read'], + }); + }); + + // #endregion appendContext }); From 607ed4e5afd94eabad1268fa863984ef90d180ba Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Tue, 9 Sep 2025 10:22:28 +0600 Subject: [PATCH 09/27] test: add tests for sharedContext handling in batch resolvers --- .../AppSyncGraphQLResolver.test.ts | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) 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 770b79e2af..ffee1a546e 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts @@ -1917,5 +1917,116 @@ describe('Class: AppSyncGraphQLResolver', () => { }); }); + it('does not include sharedContext when context is empty for batch resolvers with throwOnError=true', async () => { + // Prepare + const app = new AppSyncGraphQLResolver(); + const handlerSpy = vi.fn().mockResolvedValue({ id: '1', processed: true }); + + app.batchResolver(handlerSpy, { + fieldName: 'batchProcess', + aggregate: false, + throwOnError: true, + }); + + // Act + await app.resolve( + [onGraphqlEventFactory('batchProcess', 'Query', { id: '1' })], + context + ); + + // Assess + expect(handlerSpy).toHaveBeenCalledWith( + { id: '1' }, + { + event: expect.any(Object), + context, + } + ); + }); + + it('does not include sharedContext when context is empty for batch resolvers with throwOnError=false', async () => { + // Prepare + const app = new AppSyncGraphQLResolver(); + const handlerSpy = vi.fn().mockResolvedValue({ id: '1', processed: true }); + + app.batchResolver(handlerSpy, { + fieldName: 'batchProcess', + aggregate: false, + throwOnError: false, + }); + + // Act + await app.resolve( + [onGraphqlEventFactory('batchProcess', 'Query', { id: '1' })], + context + ); + + // Assess + expect(handlerSpy).toHaveBeenCalledWith( + { id: '1' }, + { + event: expect.any(Object), + context, + } + ); + }); + + it('includes sharedContext when context has data for batch resolvers with throwOnError=true', async () => { + // Prepare + const app = new AppSyncGraphQLResolver(); + const handlerSpy = vi.fn().mockResolvedValue({ id: '1', processed: true }); + app.batchResolver(handlerSpy, { + fieldName: 'batchProcess', + aggregate: false, + throwOnError: true, + }); + + // Act + app.appendContext({ requestId: 'test-123' }); + + await app.resolve( + [onGraphqlEventFactory('batchProcess', 'Query', { id: '1' })], + context + ); + + // Assess + expect(handlerSpy).toHaveBeenCalledWith( + { id: '1' }, + expect.objectContaining({ + event: expect.any(Object), + context, + sharedContext: expect.any(Map), + }) + ); + }); + + it('includes sharedContext when context has data for batch resolvers with throwOnError=false', async () => { + // Prepare + const app = new AppSyncGraphQLResolver(); + const handlerSpy = vi.fn().mockResolvedValue({ id: '1', processed: true }); + app.batchResolver(handlerSpy, { + fieldName: 'batchProcess', + aggregate: false, + throwOnError: false, + }); + + // Act + app.appendContext({ requestId: 'test-456' }); + await app.resolve( + [onGraphqlEventFactory('batchProcess', 'Query', { id: '1' })], + context + ); + + // Assess + expect(handlerSpy).toHaveBeenCalledWith( + { id: '1' }, + expect.objectContaining({ + event: expect.any(Object), + context, + sharedContext: expect.any(Map), + }) + ); + }); + // #endregion appendContext }); From 7227f948545282ad1afb1db63e0b16f41b5790d6 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Tue, 9 Sep 2025 10:28:35 +0600 Subject: [PATCH 10/27] refactor: update includeRouter method to accept multiple routers and adjust tests accordingly --- .../appsync-graphql/AppSyncGraphQLResolver.ts | 40 ++++++++++++++++--- .../AppSyncGraphQLResolver.test.ts | 3 +- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts index 08cb7fb649..c4f0ad40ee 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -201,17 +201,45 @@ class AppSyncGraphQLResolver extends Router { } /** - * Includes a router and merges its registries into the current resolver. + * Includes one or more routers and merges their registries into the current resolver. * * This method allows you to compose multiple routers by merging their * route registries into the current AppSync GraphQL resolver instance. + * All resolver handlers, batch resolver handlers, and exception handlers + * from the included routers will be available in the current resolver. * - * @param router - The router instance whose registries will be merged + * @example + * ```ts + * import { AppSyncGraphQLResolver, Router } from '@aws-lambda-powertools/event-handler/appsync-graphql'; + * + * const postsRouter = new Router(); + * postsRouter.onQuery('getPosts', async () => { + * return [{ id: 1, title: 'Post 1' }]; + * }); + * + * const usersRouter = new Router(); + * usersRouter.onQuery('getUsers', async () => { + * return [{ id: 1, name: 'John Doe' }]; + * }); + * + * const app = new AppSyncGraphQLResolver(); + * + * app.includeRouter([usersRouter, postsRouter]); + * + * export const handler = async (event, context) => + * app.resolve(event, context); + * ``` + * + * @param router - The router instance or array of router instances whose registries will be merged */ - public includeRouter(router: Router): void { - this.logger.debug('Including router'); - this.mergeRegistriesFrom(router); - this.logger.debug('Router included successfully'); + public includeRouter(router: Router | Router[]): void { + const routers = Array.isArray(router) ? router : [router]; + + routers.forEach((router) => { + this.logger.debug('Including router'); + this.mergeRegistriesFrom(router); + this.logger.debug('Router included successfully'); + }); } /** 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 ffee1a546e..5f50197f8d 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts @@ -1357,8 +1357,7 @@ describe('Class: AppSyncGraphQLResolver', () => { })); const app = new AppSyncGraphQLResolver({ logger: console }); - app.includeRouter(userRouter); - app.includeRouter(todoRouter); + app.includeRouter([userRouter, todoRouter]); // Act const getUserResult = await app.resolve( From da0368b973b318da6934b14bbfda445e51171d1b Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Tue, 9 Sep 2025 20:20:48 +0600 Subject: [PATCH 11/27] refactor: streamline logging in includeRouter method for clarity --- .../src/appsync-graphql/AppSyncGraphQLResolver.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts index c4f0ad40ee..1c9ca6a4a7 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -234,12 +234,11 @@ class AppSyncGraphQLResolver extends Router { */ public includeRouter(router: Router | Router[]): void { const routers = Array.isArray(router) ? router : [router]; - + this.logger.debug('Including router'); routers.forEach((router) => { - this.logger.debug('Including router'); this.mergeRegistriesFrom(router); - this.logger.debug('Router included successfully'); }); + this.logger.debug('Router included successfully'); } /** From 1f2bb0c2cd18d6a9d26f482a1373cdb4b5565be0 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Tue, 9 Sep 2025 20:27:59 +0600 Subject: [PATCH 12/27] refactor: rename context to sharedContext for clarity and consistency --- .../appsync-graphql/AppSyncGraphQLResolver.ts | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts index 1c9ca6a4a7..d371d29311 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -46,11 +46,11 @@ class AppSyncGraphQLResolver extends Router { /** * A map to hold contextual data that can be shared across all resolver handlers. */ - public readonly context: Map; + public readonly sharedContext: Map; public constructor(options?: GraphQlRouterOptions) { super(options); - this.context = new Map(); + this.sharedContext = new Map(); } /** @@ -179,7 +179,7 @@ class AppSyncGraphQLResolver extends Router { options ); } finally { - this.context.clear(); + this.sharedContext.clear(); } } if (!isAppSyncGraphQLEvent(event)) { @@ -196,7 +196,7 @@ class AppSyncGraphQLResolver extends Router { options ); } finally { - this.context.clear(); + this.sharedContext.clear(); } } @@ -283,7 +283,7 @@ class AppSyncGraphQLResolver extends Router { */ public appendContext(data: Record): void { Object.entries(data).forEach(([key, value]) => { - this.context.set(key, value); + this.sharedContext.set(key, value); }); } @@ -425,7 +425,9 @@ class AppSyncGraphQLResolver extends Router { { event: events, context, - ...(this.context.size > 0 && { sharedContext: this.context }), + ...(this.sharedContext.size > 0 && { + sharedContext: this.sharedContext, + }), }, ]); @@ -448,7 +450,9 @@ class AppSyncGraphQLResolver extends Router { { event, context, - ...(this.context.size > 0 && { sharedContext: this.context }), + ...(this.sharedContext.size > 0 && { + sharedContext: this.sharedContext, + }), }, ]); results.push(result); @@ -463,7 +467,9 @@ class AppSyncGraphQLResolver extends Router { { event: events[i], context, - ...(this.context.size > 0 && { sharedContext: this.context }), + ...(this.sharedContext.size > 0 && { + sharedContext: this.sharedContext, + }), }, ]); results.push(result); @@ -511,7 +517,9 @@ class AppSyncGraphQLResolver extends Router { { event, context, - ...(this.context.size > 0 && { sharedContext: this.context }), + ...(this.sharedContext.size > 0 && { + sharedContext: this.sharedContext, + }), }, ] ); From 46bb22053d41577468b50b69a30b115048035af3 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Tue, 9 Sep 2025 20:28:27 +0600 Subject: [PATCH 13/27] doc: enhance sharedContext documentation and update example usage in AppSyncGraphQLResolver --- .../appsync-graphql/AppSyncGraphQLResolver.ts | 41 ++++++++----------- 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts index d371d29311..21b72c2e51 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -44,7 +44,7 @@ import { isAppSyncGraphQLEvent } from './utils.js'; */ class AppSyncGraphQLResolver extends Router { /** - * A map to hold contextual data that can be shared across all resolver handlers. + * A map to hold shared contextual data accessible to all resolver handlers. */ public readonly sharedContext: Map; @@ -250,33 +250,26 @@ class AppSyncGraphQLResolver extends Router { * * @example * ```ts - * import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; - * import type { Context } from 'aws-lambda'; - * - * const app = new AppSyncGraphQLResolver(); - * - * export const handler = async (event: unknown, context: Context) => { - * // Share context between main app and routers - * app.appendContext({ - * isAdmin: true, - * requestId: context.awsRequestId, - * timestamp: Date.now() - * }); - * - * return app.resolve(event, context); - * }; + * import { AppSyncGraphQLResolver, Router } from '@aws-lambda-powertools/event-handler/appsync-graphql'; * - * // Resolver handlers can access shared context - * app.onQuery('getUser', async ({ id }, { sharedContext }) => { - * const isAdmin = sharedContext?.get('isAdmin'); + * const postsRouter = new Router(); + * postsRouter.onQuery('getPosts', async ({ sharedContext }) => { * const requestId = sharedContext?.get('requestId'); + * return [{ id: 1, title: 'Post 1', requestId }]; + * }); * - * return { - * id, - * name: 'John Doe', - * email: isAdmin ? 'john@example.com' : 'hidden' - * }; + * const usersRouter = new Router(); + * usersRouter.onQuery('getUsers', async ({ sharedContext }) => { + * const requestId = sharedContext?.get('requestId'); + * return [{ id: 1, name: 'John Doe', requestId }]; * }); + * + * const app = new AppSyncGraphQLResolver(); + * app.includeRouter([usersRouter, postsRouter]); + * app.appendContext({ requestId: '12345' }); + * + * export const handler = async (event, context) => + * app.resolve(event, context); * ``` * * @param data - A record of key-value pairs to add to the shared context From 14f601f9f7bf7844cdd34ebdcb546f6922f569f2 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Tue, 9 Sep 2025 20:35:00 +0600 Subject: [PATCH 14/27] refactor: clear shared context after processing to prevent data leakage --- .../src/appsync-graphql/AppSyncGraphQLResolver.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts index 21b72c2e51..d18d6f48e8 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -179,6 +179,9 @@ class AppSyncGraphQLResolver extends Router { options ); } finally { + /** + * Clear shared context after batch processing to avoid data leakage between invocations. + */ this.sharedContext.clear(); } } @@ -196,6 +199,9 @@ class AppSyncGraphQLResolver extends Router { options ); } finally { + /** + * Clear shared context after single event processing to avoid data leakage between invocations. + */ this.sharedContext.clear(); } } @@ -265,6 +271,7 @@ class AppSyncGraphQLResolver extends Router { * }); * * const app = new AppSyncGraphQLResolver(); + * * app.includeRouter([usersRouter, postsRouter]); * app.appendContext({ requestId: '12345' }); * From fcd384705720d21fa76db15c5553e8df203b14c3 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Tue, 9 Sep 2025 20:44:38 +0600 Subject: [PATCH 15/27] refactor: remove debug logging during registry merging for cleaner output --- .../src/appsync-graphql/ExceptionHandlerRegistry.ts | 1 - .../src/appsync-graphql/RouteHandlerRegistry.ts | 1 - packages/event-handler/src/appsync-graphql/Router.ts | 5 +++++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/ExceptionHandlerRegistry.ts b/packages/event-handler/src/appsync-graphql/ExceptionHandlerRegistry.ts index 1e03a2deb6..e247f910ca 100644 --- a/packages/event-handler/src/appsync-graphql/ExceptionHandlerRegistry.ts +++ b/packages/event-handler/src/appsync-graphql/ExceptionHandlerRegistry.ts @@ -70,7 +70,6 @@ class ExceptionHandlerRegistry { * @param otherRegistry - The registry to merge handlers from. */ public merge(otherRegistry: ExceptionHandlerRegistry): void { - this.#logger.debug('Merging exception handler registries'); for (const [errorName, handlerOptions] of otherRegistry.handlers) { if (this.handlers.has(errorName)) { this.#warnHandlerOverriding(errorName); diff --git a/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts b/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts index 31ace05fef..14614dbcef 100644 --- a/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts +++ b/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts @@ -86,7 +86,6 @@ class RouteHandlerRegistry { * @param otherRegistry - The registry to merge handlers from. */ public merge(otherRegistry: RouteHandlerRegistry): void { - this.#logger.debug('Merging route handler registries'); for (const [key, handler] of otherRegistry.resolvers) { if (this.resolvers.has(key)) { this.#warnResolverOverriding(handler.fieldName, handler.typeName); diff --git a/packages/event-handler/src/appsync-graphql/Router.ts b/packages/event-handler/src/appsync-graphql/Router.ts index b84a268fba..45ccbee918 100644 --- a/packages/event-handler/src/appsync-graphql/Router.ts +++ b/packages/event-handler/src/appsync-graphql/Router.ts @@ -73,8 +73,13 @@ class Router { * @param router - The source router whose registries will be merged into this router */ protected mergeRegistriesFrom(router: Router): void { + this.logger.debug('Merging route handler registries'); this.resolverRegistry.merge(router.resolverRegistry); + + this.logger.debug('Merging batch route handler registries'); this.batchResolverRegistry.merge(router.batchResolverRegistry); + + this.logger.debug('Merging exception handler registries'); this.exceptionHandlerRegistry.merge(router.exceptionHandlerRegistry); } From ecc1f1d31183324d1d6ebb471f47a9ff3964d154 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Tue, 9 Sep 2025 22:22:15 +0600 Subject: [PATCH 16/27] doc: `includeRouter` & `appendContext` method doc --- .../features/event-handler/appsync-graphql.md | 58 +++++++++++++++++++ .../appsync-graphql/appendContext.ts | 11 ++++ .../appsync-graphql/postRouter.ts | 18 ++++++ .../appsync-graphql/postRouterWithContext.ts | 10 ++++ .../appsync-graphql/splitRouter.ts | 9 +++ .../appsync-graphql/userRouter.ts | 9 +++ .../appsync-graphql/userRouterWithContext.ts | 10 ++++ 7 files changed, 125 insertions(+) create mode 100644 examples/snippets/event-handler/appsync-graphql/appendContext.ts create mode 100644 examples/snippets/event-handler/appsync-graphql/postRouter.ts create mode 100644 examples/snippets/event-handler/appsync-graphql/postRouterWithContext.ts create mode 100644 examples/snippets/event-handler/appsync-graphql/splitRouter.ts create mode 100644 examples/snippets/event-handler/appsync-graphql/userRouter.ts create mode 100644 examples/snippets/event-handler/appsync-graphql/userRouterWithContext.ts diff --git a/docs/features/event-handler/appsync-graphql.md b/docs/features/event-handler/appsync-graphql.md index 9112da2c01..d7b9f82eaa 100644 --- a/docs/features/event-handler/appsync-graphql.md +++ b/docs/features/event-handler/appsync-graphql.md @@ -114,6 +114,64 @@ Here's a table with their related scalar as a quick reference: ## Advanced +### Split operations with Router + +As you grow the number of related GraphQL operations a given Lambda function should handle, it is natural to split them into separate files to ease maintenance - That's when the `Router` feature comes handy. + +Let's assume you have `app.ts` as your Lambda function entrypoint and routes in `postRouter.ts` and `userRouter.ts`. This is how you'd use the `Router` feature. + +=== "postRouter.ts" + + We import **Router** instead of **AppSyncGraphQLResolver**; syntax wise is exactly the same. + + ```typescript hl_lines="1 3" + --8<-- "examples/snippets/event-handler/appsync-graphql/postRouter.ts" + ``` + +=== "userRouter.ts" + + We import **Router** instead of **AppSyncGraphQLResolver**; syntax wise is exactly the same. + + ```typescript hl_lines="1 3" + --8<-- "examples/snippets/event-handler/appsync-graphql/userRouter.ts" + ``` + +=== "app.ts" + + We use `includeRouter` method and include all operations registered in the router instances. + + ```typescript hl_lines="2-3 7" + --8<-- "examples/snippets/event-handler/appsync-graphql/splitRouter.ts" + ``` + +#### Sharing contextual data + +You can use `appendContext` when you want to share data between your App and Router instances. Any data you share will be available via the `sharedContext` parameter in your resolver handlers. + +???+ warning + For safety, we clear the context after each invocation. + +???+ tip + This can also be useful for injecting contextual information before a request is processed. + +=== "app.ts" + + ```typescript hl_lines="9" + --8<-- "examples/snippets/event-handler/appsync-graphql/appendContext.ts" + ``` + +=== "postRouter.ts" + + ```typescript hl_lines="5-8" + --8<-- "examples/snippets/event-handler/appsync-graphql/postRouterWithContext.ts" + ``` + +=== "userRouter.ts" + + ```typescript hl_lines="5-8" + --8<-- "examples/snippets/event-handler/appsync-graphql/userRouterWithContext.ts" + ``` + ### Nested mappings !!! note diff --git a/examples/snippets/event-handler/appsync-graphql/appendContext.ts b/examples/snippets/event-handler/appsync-graphql/appendContext.ts new file mode 100644 index 0000000000..007699e83f --- /dev/null +++ b/examples/snippets/event-handler/appsync-graphql/appendContext.ts @@ -0,0 +1,11 @@ +import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; +import { postRouter } from './postRouter'; +import { usersRouter } from './userRouter'; + +const app = new AppSyncGraphQLResolver(); + +app.includeRouter([postRouter, usersRouter]); + +app.appendContext({ requestId: crypto.randomUUID() }); + +export const handler = async (event, context) => app.resolve(event, context); diff --git a/examples/snippets/event-handler/appsync-graphql/postRouter.ts b/examples/snippets/event-handler/appsync-graphql/postRouter.ts new file mode 100644 index 0000000000..34bebb4e4c --- /dev/null +++ b/examples/snippets/event-handler/appsync-graphql/postRouter.ts @@ -0,0 +1,18 @@ +import { Router } from '@aws-lambda-powertools/event-handler/appsync-graphql'; + +const postRouter = new Router(); + +postRouter.onQuery('getPosts', async () => { + return [{ id: 1, title: 'First post', content: 'Hello world!' }]; +}); + +postRouter.onMutation('createPost', async ({ title, content }) => { + return { + id: Date.now(), + title, + content, + createdAt: new Date().toISOString(), + }; +}); + +export { postRouter }; diff --git a/examples/snippets/event-handler/appsync-graphql/postRouterWithContext.ts b/examples/snippets/event-handler/appsync-graphql/postRouterWithContext.ts new file mode 100644 index 0000000000..7c33932e38 --- /dev/null +++ b/examples/snippets/event-handler/appsync-graphql/postRouterWithContext.ts @@ -0,0 +1,10 @@ +import { Router } from '@aws-lambda-powertools/event-handler/appsync-graphql'; + +const postRouter = new Router(); + +postRouter.onQuery('getPosts', async (args, { sharedContext }) => { + const requestId = sharedContext?.get('requestId'); + return [{ id: 1, title: 'First post', content: 'Hello world!', requestId }]; +}); + +export { postRouter }; diff --git a/examples/snippets/event-handler/appsync-graphql/splitRouter.ts b/examples/snippets/event-handler/appsync-graphql/splitRouter.ts new file mode 100644 index 0000000000..829048ec69 --- /dev/null +++ b/examples/snippets/event-handler/appsync-graphql/splitRouter.ts @@ -0,0 +1,9 @@ +import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; +import { postRouter } from './postRouter'; +import { userRouter } from './userRouter'; + +const app = new AppSyncGraphQLResolver(); + +app.includeRouter([postRouter, userRouter]); + +export const handler = async (event, context) => app.resolve(event, context); diff --git a/examples/snippets/event-handler/appsync-graphql/userRouter.ts b/examples/snippets/event-handler/appsync-graphql/userRouter.ts new file mode 100644 index 0000000000..51e35b82d4 --- /dev/null +++ b/examples/snippets/event-handler/appsync-graphql/userRouter.ts @@ -0,0 +1,9 @@ +import { Router } from '@aws-lambda-powertools/event-handler/appsync-graphql'; + +const userRouter = new Router(); + +userRouter.onQuery('getUsers', async () => { + return [{ id: 1, name: 'John Doe', email: 'john@example.com' }]; +}); + +export { userRouter }; diff --git a/examples/snippets/event-handler/appsync-graphql/userRouterWithContext.ts b/examples/snippets/event-handler/appsync-graphql/userRouterWithContext.ts new file mode 100644 index 0000000000..83efd183e3 --- /dev/null +++ b/examples/snippets/event-handler/appsync-graphql/userRouterWithContext.ts @@ -0,0 +1,10 @@ +import { Router } from '@aws-lambda-powertools/event-handler/appsync-graphql'; + +const usersRouter = new Router(); + +usersRouter.onQuery('getUsers', async (args, { sharedContext }) => { + const requestId = sharedContext?.get('requestId'); + return [{ id: 1, name: 'John Doe', email: 'john@example.com', requestId }]; +}); + +export { usersRouter }; From 450ed663dcd47553567aff2c5b608022f5eb3b83 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Wed, 10 Sep 2025 08:24:01 +0600 Subject: [PATCH 17/27] refactor: standardize router naming and improve type annotations in examples --- docs/features/event-handler/appsync-graphql.md | 4 ++-- .../event-handler/appsync-graphql/appendContext.ts | 8 +++++--- .../snippets/event-handler/appsync-graphql/splitRouter.ts | 4 +++- .../appsync-graphql/userRouterWithContext.ts | 6 +++--- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/docs/features/event-handler/appsync-graphql.md b/docs/features/event-handler/appsync-graphql.md index d7b9f82eaa..ba4565ed5d 100644 --- a/docs/features/event-handler/appsync-graphql.md +++ b/docs/features/event-handler/appsync-graphql.md @@ -140,7 +140,7 @@ Let's assume you have `app.ts` as your Lambda function entrypoint and routes in We use `includeRouter` method and include all operations registered in the router instances. - ```typescript hl_lines="2-3 7" + ```typescript hl_lines="3-4 8" --8<-- "examples/snippets/event-handler/appsync-graphql/splitRouter.ts" ``` @@ -156,7 +156,7 @@ You can use `appendContext` when you want to share data between your App and Rou === "app.ts" - ```typescript hl_lines="9" + ```typescript hl_lines="10" --8<-- "examples/snippets/event-handler/appsync-graphql/appendContext.ts" ``` diff --git a/examples/snippets/event-handler/appsync-graphql/appendContext.ts b/examples/snippets/event-handler/appsync-graphql/appendContext.ts index 007699e83f..7301ae58f1 100644 --- a/examples/snippets/event-handler/appsync-graphql/appendContext.ts +++ b/examples/snippets/event-handler/appsync-graphql/appendContext.ts @@ -1,11 +1,13 @@ import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; +import type { Context } from 'aws-lambda/handler'; import { postRouter } from './postRouter'; -import { usersRouter } from './userRouter'; +import { userRouter } from './userRouter'; const app = new AppSyncGraphQLResolver(); -app.includeRouter([postRouter, usersRouter]); +app.includeRouter([postRouter, userRouter]); app.appendContext({ requestId: crypto.randomUUID() }); -export const handler = async (event, context) => app.resolve(event, context); +export const handler = async (event: unknown, context: Context) => + app.resolve(event, context); diff --git a/examples/snippets/event-handler/appsync-graphql/splitRouter.ts b/examples/snippets/event-handler/appsync-graphql/splitRouter.ts index 829048ec69..2f4948a9d3 100644 --- a/examples/snippets/event-handler/appsync-graphql/splitRouter.ts +++ b/examples/snippets/event-handler/appsync-graphql/splitRouter.ts @@ -1,4 +1,5 @@ import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; +import type { Context } from 'aws-lambda'; import { postRouter } from './postRouter'; import { userRouter } from './userRouter'; @@ -6,4 +7,5 @@ const app = new AppSyncGraphQLResolver(); app.includeRouter([postRouter, userRouter]); -export const handler = async (event, context) => app.resolve(event, context); +export const handler = async (event: unknown, context: Context) => + app.resolve(event, context); diff --git a/examples/snippets/event-handler/appsync-graphql/userRouterWithContext.ts b/examples/snippets/event-handler/appsync-graphql/userRouterWithContext.ts index 83efd183e3..9c09ea6d3a 100644 --- a/examples/snippets/event-handler/appsync-graphql/userRouterWithContext.ts +++ b/examples/snippets/event-handler/appsync-graphql/userRouterWithContext.ts @@ -1,10 +1,10 @@ import { Router } from '@aws-lambda-powertools/event-handler/appsync-graphql'; -const usersRouter = new Router(); +const userRouter = new Router(); -usersRouter.onQuery('getUsers', async (args, { sharedContext }) => { +userRouter.onQuery('getUsers', async (args, { sharedContext }) => { const requestId = sharedContext?.get('requestId'); return [{ id: 1, name: 'John Doe', email: 'john@example.com', requestId }]; }); -export { usersRouter }; +export { userRouter }; From 28645ca597db0453faa916c573785aa87fc19c2f Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Wed, 10 Sep 2025 08:52:06 +0600 Subject: [PATCH 18/27] refactor: remove debug logging from registry merging for cleaner output --- packages/event-handler/src/appsync-graphql/Router.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/Router.ts b/packages/event-handler/src/appsync-graphql/Router.ts index 45ccbee918..b84a268fba 100644 --- a/packages/event-handler/src/appsync-graphql/Router.ts +++ b/packages/event-handler/src/appsync-graphql/Router.ts @@ -73,13 +73,8 @@ class Router { * @param router - The source router whose registries will be merged into this router */ protected mergeRegistriesFrom(router: Router): void { - this.logger.debug('Merging route handler registries'); this.resolverRegistry.merge(router.resolverRegistry); - - this.logger.debug('Merging batch route handler registries'); this.batchResolverRegistry.merge(router.batchResolverRegistry); - - this.logger.debug('Merging exception handler registries'); this.exceptionHandlerRegistry.merge(router.exceptionHandlerRegistry); } From 3147d3e0ec3f0f9c2e0ad6d9f54d5d921bd39838 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Wed, 10 Sep 2025 08:52:23 +0600 Subject: [PATCH 19/27] refactor: improve clarity in comments and streamline router example code --- .../appsync-graphql/AppSyncGraphQLResolver.ts | 39 +++++++++---------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts index d18d6f48e8..63eebca757 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -180,7 +180,7 @@ class AppSyncGraphQLResolver extends Router { ); } finally { /** - * Clear shared context after batch processing to avoid data leakage between invocations. + * Clear shared context after batch processing for safety */ this.sharedContext.clear(); } @@ -200,7 +200,7 @@ class AppSyncGraphQLResolver extends Router { ); } finally { /** - * Clear shared context after single event processing to avoid data leakage between invocations. + * Clear shared context after batch processing for safety */ this.sharedContext.clear(); } @@ -218,19 +218,15 @@ class AppSyncGraphQLResolver extends Router { * ```ts * import { AppSyncGraphQLResolver, Router } from '@aws-lambda-powertools/event-handler/appsync-graphql'; * - * const postsRouter = new Router(); - * postsRouter.onQuery('getPosts', async () => { - * return [{ id: 1, title: 'Post 1' }]; - * }); + * const postRouter = new Router(); + * postRouter.onQuery('getPosts', async () => [{ id: 1, title: 'Post 1' }]); * - * const usersRouter = new Router(); - * usersRouter.onQuery('getUsers', async () => { - * return [{ id: 1, name: 'John Doe' }]; - * }); + * const userRouter = new Router(); + * userRouter.onQuery('getUsers', async () => [{ id: 1, name: 'John Doe' }]); * * const app = new AppSyncGraphQLResolver(); * - * app.includeRouter([usersRouter, postsRouter]); + * app.includeRouter([userRouter, postRouter]); * * export const handler = async (event, context) => * app.resolve(event, context); @@ -240,10 +236,11 @@ class AppSyncGraphQLResolver extends Router { */ public includeRouter(router: Router | Router[]): void { const routers = Array.isArray(router) ? router : [router]; + this.logger.debug('Including router'); - routers.forEach((router) => { - this.mergeRegistriesFrom(router); - }); + for (const routerToBeIncluded of routers) { + this.mergeRegistriesFrom(routerToBeIncluded); + } this.logger.debug('Router included successfully'); } @@ -258,21 +255,21 @@ class AppSyncGraphQLResolver extends Router { * ```ts * import { AppSyncGraphQLResolver, Router } from '@aws-lambda-powertools/event-handler/appsync-graphql'; * - * const postsRouter = new Router(); - * postsRouter.onQuery('getPosts', async ({ sharedContext }) => { + * const postRouter = new Router(); + * postRouter.onQuery('getPosts', async ({ sharedContext }) => { * const requestId = sharedContext?.get('requestId'); * return [{ id: 1, title: 'Post 1', requestId }]; * }); * - * const usersRouter = new Router(); - * usersRouter.onQuery('getUsers', async ({ sharedContext }) => { + * const userRouter = new Router(); + * userRouter.onQuery('getUsers', async ({ sharedContext }) => { * const requestId = sharedContext?.get('requestId'); * return [{ id: 1, name: 'John Doe', requestId }]; * }); * * const app = new AppSyncGraphQLResolver(); * - * app.includeRouter([usersRouter, postsRouter]); + * app.includeRouter([userRouter, postRouter]); * app.appendContext({ requestId: '12345' }); * * export const handler = async (event, context) => @@ -282,9 +279,9 @@ class AppSyncGraphQLResolver extends Router { * @param data - A record of key-value pairs to add to the shared context */ public appendContext(data: Record): void { - Object.entries(data).forEach(([key, value]) => { + for (const [key, value] of Object.entries(data)) { this.sharedContext.set(key, value); - }); + } } /** From 364facaab7b6e84d0e8c2be9ada569bc7162aad4 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Wed, 10 Sep 2025 09:07:49 +0600 Subject: [PATCH 20/27] refactor: extract sharedContext empty check in a method --- .../appsync-graphql/AppSyncGraphQLResolver.ts | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts index 63eebca757..b9bcb1a64a 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -422,9 +422,7 @@ class AppSyncGraphQLResolver extends Router { { event: events, context, - ...(this.sharedContext.size > 0 && { - sharedContext: this.sharedContext, - }), + ...this.#getSharedContextOnlyIfNotEmpty(), }, ]); @@ -447,9 +445,7 @@ class AppSyncGraphQLResolver extends Router { { event, context, - ...(this.sharedContext.size > 0 && { - sharedContext: this.sharedContext, - }), + ...this.#getSharedContextOnlyIfNotEmpty(), }, ]); results.push(result); @@ -464,9 +460,7 @@ class AppSyncGraphQLResolver extends Router { { event: events[i], context, - ...(this.sharedContext.size > 0 && { - sharedContext: this.sharedContext, - }), + ...this.#getSharedContextOnlyIfNotEmpty(), }, ]); results.push(result); @@ -514,9 +508,7 @@ class AppSyncGraphQLResolver extends Router { { event, context, - ...(this.sharedContext.size > 0 && { - sharedContext: this.sharedContext, - }), + ...this.#getSharedContextOnlyIfNotEmpty(), }, ] ); @@ -542,6 +534,19 @@ class AppSyncGraphQLResolver extends Router { error: 'An unknown error occurred', }; } + + /** + * Returns an object containing the shared context only if it has entries. + * This helps avoid passing an empty map to handlers. + */ + #getSharedContextOnlyIfNotEmpty(): { + sharedContext: Map | undefined; + } { + return { + sharedContext: + this.sharedContext.size > 0 ? this.sharedContext : undefined, + }; + } } export { AppSyncGraphQLResolver }; From f7b84922245dc83e330557dce221a9c0e6afda73 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Wed, 10 Sep 2025 09:36:17 +0600 Subject: [PATCH 21/27] test: update sharedContext in AppSyncGraphQLResolver test for consistency --- .../tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 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 5f50197f8d..8f3c280cc2 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts @@ -1994,7 +1994,7 @@ describe('Class: AppSyncGraphQLResolver', () => { expect.objectContaining({ event: expect.any(Object), context, - sharedContext: expect.any(Map), + sharedContext: new Map([['requestId', 'test-123']]), }) ); }); @@ -2022,7 +2022,7 @@ describe('Class: AppSyncGraphQLResolver', () => { expect.objectContaining({ event: expect.any(Object), context, - sharedContext: expect.any(Map), + sharedContext: new Map([['requestId', 'test-456']]), }) ); }); From 2b4099707997d9f3304e0ddcc94f17280d246ed6 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Mon, 6 Oct 2025 18:19:23 +0600 Subject: [PATCH 22/27] refactor: remove unnecessary await from error handling in resolver methods --- .../src/appsync-graphql/AppSyncGraphQLResolver.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts index b9bcb1a64a..2c773fb045 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -173,7 +173,7 @@ class AppSyncGraphQLResolver extends Router { } try { - return await this.#withErrorHandling( + return this.#withErrorHandling( () => this.#executeBatchResolvers(event, context, options), event[0], options @@ -193,7 +193,7 @@ class AppSyncGraphQLResolver extends Router { } try { - return await this.#withErrorHandling( + return this.#withErrorHandling( () => this.#executeSingleResolver(event, context, options), event, options From aef50b9c87fd3c5c541876a7a4ac47bf66e61a5f Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 12 Oct 2025 10:03:59 +0600 Subject: [PATCH 23/27] refactor: remove sharedContext from AppSyncGraphQLResolver and related types --- .../appsync-graphql/AppSyncGraphQLResolver.ts | 102 +---- .../src/types/appsync-graphql.ts | 6 - .../AppSyncGraphQLResolver.test.ts | 361 +----------------- 3 files changed, 20 insertions(+), 449 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts index 2c773fb045..80d93977e7 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -2,7 +2,6 @@ import type { AppSyncResolverEvent, Context } from 'aws-lambda'; import type { BatchResolverAggregateHandlerFn, BatchResolverHandlerFn, - GraphQlRouterOptions, ResolverHandler, RouteHandlerOptions, } from '../types/appsync-graphql.js'; @@ -43,16 +42,6 @@ import { isAppSyncGraphQLEvent } from './utils.js'; * ``` */ class AppSyncGraphQLResolver extends Router { - /** - * A map to hold shared contextual data accessible to all resolver handlers. - */ - public readonly sharedContext: Map; - - public constructor(options?: GraphQlRouterOptions) { - super(options); - this.sharedContext = new Map(); - } - /** * Resolve the response based on the provided event and route handlers configured. * @@ -172,18 +161,11 @@ class AppSyncGraphQLResolver extends Router { return; } - try { - return this.#withErrorHandling( - () => this.#executeBatchResolvers(event, context, options), - event[0], - options - ); - } finally { - /** - * Clear shared context after batch processing for safety - */ - this.sharedContext.clear(); - } + return this.#withErrorHandling( + () => this.#executeBatchResolvers(event, context, options), + event[0], + options + ); } if (!isAppSyncGraphQLEvent(event)) { this.logger.warn( @@ -192,18 +174,11 @@ class AppSyncGraphQLResolver extends Router { return; } - try { - return this.#withErrorHandling( - () => this.#executeSingleResolver(event, context, options), - event, - options - ); - } finally { - /** - * Clear shared context after batch processing for safety - */ - this.sharedContext.clear(); - } + return this.#withErrorHandling( + () => this.#executeSingleResolver(event, context, options), + event, + options + ); } /** @@ -244,46 +219,6 @@ class AppSyncGraphQLResolver extends Router { this.logger.debug('Router included successfully'); } - /** - * Appends contextual data to be shared with all resolver handlers. - * - * This method allows you to add key-value pairs to the shared context that will be - * accessible to all resolver handlers through the `sharedContext` parameter. The context - * is automatically cleared after each invocation for safety. - * - * @example - * ```ts - * import { AppSyncGraphQLResolver, Router } from '@aws-lambda-powertools/event-handler/appsync-graphql'; - * - * const postRouter = new Router(); - * postRouter.onQuery('getPosts', async ({ sharedContext }) => { - * const requestId = sharedContext?.get('requestId'); - * return [{ id: 1, title: 'Post 1', requestId }]; - * }); - * - * const userRouter = new Router(); - * userRouter.onQuery('getUsers', async ({ sharedContext }) => { - * const requestId = sharedContext?.get('requestId'); - * return [{ id: 1, name: 'John Doe', requestId }]; - * }); - * - * const app = new AppSyncGraphQLResolver(); - * - * app.includeRouter([userRouter, postRouter]); - * app.appendContext({ requestId: '12345' }); - * - * export const handler = async (event, context) => - * app.resolve(event, context); - * ``` - * - * @param data - A record of key-value pairs to add to the shared context - */ - public appendContext(data: Record): void { - for (const [key, value] of Object.entries(data)) { - this.sharedContext.set(key, value); - } - } - /** * Executes the provided asynchronous function with error handling. * If the function throws an error, it delegates error processing to `#handleError` @@ -422,7 +357,6 @@ class AppSyncGraphQLResolver extends Router { { event: events, context, - ...this.#getSharedContextOnlyIfNotEmpty(), }, ]); @@ -445,7 +379,6 @@ class AppSyncGraphQLResolver extends Router { { event, context, - ...this.#getSharedContextOnlyIfNotEmpty(), }, ]); results.push(result); @@ -460,7 +393,6 @@ class AppSyncGraphQLResolver extends Router { { event: events[i], context, - ...this.#getSharedContextOnlyIfNotEmpty(), }, ]); results.push(result); @@ -508,7 +440,6 @@ class AppSyncGraphQLResolver extends Router { { event, context, - ...this.#getSharedContextOnlyIfNotEmpty(), }, ] ); @@ -534,19 +465,6 @@ class AppSyncGraphQLResolver extends Router { error: 'An unknown error occurred', }; } - - /** - * Returns an object containing the shared context only if it has entries. - * This helps avoid passing an empty map to handlers. - */ - #getSharedContextOnlyIfNotEmpty(): { - sharedContext: Map | undefined; - } { - return { - sharedContext: - this.sharedContext.size > 0 ? this.sharedContext : undefined, - }; - } } export { AppSyncGraphQLResolver }; diff --git a/packages/event-handler/src/types/appsync-graphql.ts b/packages/event-handler/src/types/appsync-graphql.ts index aeb893ab29..cd4f8685ed 100644 --- a/packages/event-handler/src/types/appsync-graphql.ts +++ b/packages/event-handler/src/types/appsync-graphql.ts @@ -14,7 +14,6 @@ type BatchResolverSyncHandlerFn< options: { event: AppSyncResolverEvent; context: Context; - sharedContext?: Map; } ) => unknown; @@ -26,7 +25,6 @@ type BatchResolverHandlerFn< options: { event: AppSyncResolverEvent; context: Context; - sharedContext?: Map; } ) => Promise; @@ -38,7 +36,6 @@ type BatchResolverAggregateHandlerFn< options: { event: AppSyncResolverEvent[]; context: Context; - sharedContext?: Map; } ) => Promise; @@ -50,7 +47,6 @@ type BatchResolverSyncAggregateHandlerFn< options: { event: AppSyncResolverEvent[]; context: Context; - sharedContext?: Map; } ) => unknown; @@ -74,7 +70,6 @@ type ResolverSyncHandlerFn> = ( options: { event: AppSyncResolverEvent; context: Context; - sharedContext?: Map; } ) => unknown; @@ -83,7 +78,6 @@ type ResolverHandlerFn> = ( options: { event: AppSyncResolverEvent; context: Context; - sharedContext?: Map; } ) => Promise; 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 8f3c280cc2..15b363a22a 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts @@ -1447,7 +1447,7 @@ describe('Class: AppSyncGraphQLResolver', () => { type: 'validation', })); firstRouter.resolver( - async () => { + () => { throw new ValidationError('Test validation error'); }, { fieldName: 'firstHandler' } @@ -1459,7 +1459,7 @@ describe('Class: AppSyncGraphQLResolver', () => { type: 'evaluation', })); secondRouter.resolver( - async () => { + () => { throw new EvalError('Test evaluation error'); }, { fieldName: 'secondHandler' } @@ -1527,7 +1527,7 @@ describe('Class: AppSyncGraphQLResolver', () => { message: error.message, type: 'first_validation', })); - firstRouter.onQuery('testError', async () => { + firstRouter.onQuery('testError', () => { throw new ValidationError('Test validation error'); }); @@ -1537,7 +1537,7 @@ describe('Class: AppSyncGraphQLResolver', () => { message: error.message, type: 'second_validation', })); - secondRouter.onQuery('testError', async () => { + secondRouter.onQuery('testError', () => { throw new ValidationError('Test validation error'); }); @@ -1573,19 +1573,19 @@ describe('Class: AppSyncGraphQLResolver', () => { public scope = 'scoped'; @userRouter.onQuery('getUser') - async getUserById({ id }: { id: string }) { + getUserById({ id }: { id: string }) { if (id.length === 0) throw new ValidationError('User ID cannot be empty'); return { id, name: 'John Doe', scope: this.scope }; } @userRouter.onMutation('createUser') - async createUser({ name, email }: { name: string; email: string }) { + createUser({ name, email }: { name: string; email: string }) { return { id: makeId(), name, email, scope: this.scope }; } @userRouter.exceptionHandler(ValidationError) - async handleValidationError(error: ValidationError) { + handleValidationError(error: ValidationError) { return { message: 'UserRouter validation error', details: error.message, @@ -1595,7 +1595,7 @@ describe('Class: AppSyncGraphQLResolver', () => { } @todoRouter.onQuery('getTodo') - async getTodoById({ id }: { id: string }) { + getTodoById({ id }: { id: string }) { if (id === 'eval-error') { throw new EvalError('Todo evaluation error'); } @@ -1608,7 +1608,7 @@ describe('Class: AppSyncGraphQLResolver', () => { } @todoRouter.exceptionHandler(EvalError) - async handleEvalError(error: EvalError) { + handleEvalError(error: EvalError) { return { message: 'TodoRouter evaluation error', details: error.message, @@ -1616,7 +1616,7 @@ describe('Class: AppSyncGraphQLResolver', () => { scope: this.scope, }; } - async handler(event: unknown, context: Context) { + handler(event: unknown, context: Context) { app.includeRouter(userRouter); app.includeRouter(todoRouter); return app.resolve(event, context, { @@ -1687,345 +1687,4 @@ describe('Class: AppSyncGraphQLResolver', () => { }); // #endregion includeRouters - - // #region appendContext - - it('allows sharing context data with resolver handlers', async () => { - // Prepare - const app = new AppSyncGraphQLResolver(); - - app.onQuery<{ id: string }>( - 'getUser', - async ({ id }, { sharedContext }) => { - const isAdmin = sharedContext?.get('isAdmin'); - const requestId = sharedContext?.get('requestId'); - - return { - id, - name: 'John Doe', - email: isAdmin ? 'john@example.com' : 'hidden', - requestId, - }; - } - ); - - // Act - app.appendContext({ - isAdmin: true, - requestId: 'test-request-123', - timestamp: Date.now(), - }); - - const result = await app.resolve( - onGraphqlEventFactory('getUser', 'Query', { id: '1' }), - context - ); - - // Assess - expect(result).toEqual({ - id: '1', - name: 'John Doe', - email: 'john@example.com', - requestId: 'test-request-123', - }); - }); - - it('allows context sharing with included routers', async () => { - // Prepare - const userRouter = new Router(); - userRouter.onQuery<{ id: string }>( - 'getUser', - async ({ id }, { sharedContext }) => { - const isAdmin = sharedContext?.get('isAdmin'); - const requestId = sharedContext?.get('requestId'); - - return { - id, - name: 'John Doe', - role: isAdmin ? 'admin' : 'user', - requestId, - }; - } - ); - - const todoRouter = new Router(); - todoRouter.onQuery<{ id: string }>( - 'getTodo', - async ({ id }, { sharedContext }) => { - const isAdmin = sharedContext?.get('isAdmin'); - const requestId = sharedContext?.get('requestId'); - - return { - id, - title: 'Sample Todo', - completed: false, - role: isAdmin ? 'admin' : 'user', - requestId, - }; - } - ); - - const app = new AppSyncGraphQLResolver(); - app.includeRouter(userRouter); - app.includeRouter(todoRouter); - app.appendContext({ - isAdmin: false, - requestId: 'router-test-456', - }); - - // Act - const userResult = await app.resolve( - onGraphqlEventFactory('getUser', 'Query', { id: '2' }), - context - ); - - // Assess - expect(userResult).toEqual({ - id: '2', - name: 'John Doe', - role: 'user', - requestId: 'router-test-456', - }); - }); - - it('clears context after each invocation for single events', async () => { - // Prepare - const app = new AppSyncGraphQLResolver(); - - app.onQuery<{ id: string }>( - 'getUser', - async ({ id }, { sharedContext }) => { - const requestId = sharedContext?.get('requestId'); - - return { - id, - requestId: requestId || 'no-request-id', - }; - } - ); - - // Act - app.appendContext({ requestId: 'first-request' }); - const firstResult = await app.resolve( - onGraphqlEventFactory('getUser', 'Query', { id: '1' }), - context - ); - - // Assess - expect(firstResult).toEqual({ - id: '1', - requestId: 'first-request', - }); - - // Act - const secondResult = await app.resolve( - onGraphqlEventFactory('getUser', 'Query', { id: '2' }), - context - ); - - // Assess - expect(secondResult).toEqual({ - id: '2', - requestId: 'no-request-id', - }); - }); - - it('clears context after each invocation for batch events', async () => { - // Prepare - const app = new AppSyncGraphQLResolver(); - - app.batchResolver<{ id: string }>( - async (events, { sharedContext }) => { - const requestId = sharedContext?.get('requestId'); - - return events.map((event) => ({ - id: event.arguments.id, - requestId: requestId || 'no-request-id', - })); - }, - { - fieldName: 'getUsers', - } - ); - - // Act - app.appendContext({ requestId: 'batch-request' }); - const firstResult = await app.resolve( - [ - onGraphqlEventFactory('getUsers', 'Query', { id: '1' }), - onGraphqlEventFactory('getUsers', 'Query', { id: '2' }), - ], - context - ); - - // Assess - expect(firstResult).toEqual([ - { id: '1', requestId: 'batch-request' }, - { id: '2', requestId: 'batch-request' }, - ]); - - // Act - const secondResult = await app.resolve( - [ - onGraphqlEventFactory('getUsers', 'Query', { id: '3' }), - onGraphqlEventFactory('getUsers', 'Query', { id: '4' }), - ], - context - ); - - // Assess - expect(secondResult).toEqual([ - { id: '3', requestId: 'no-request-id' }, - { id: '4', requestId: 'no-request-id' }, - ]); - }); - - it('allows updating context data multiple times before invocation', async () => { - // Prepare - const app = new AppSyncGraphQLResolver(); - - app.onQuery<{ id: string }>( - 'getUser', - async ({ id }, { sharedContext }) => { - const role = sharedContext?.get('role'); - const permissions = sharedContext?.get('permissions'); - - return { - id, - role, - permissions, - }; - } - ); - - // Act - app.appendContext({ role: 'user' }); - app.appendContext({ permissions: ['read'] }); - app.appendContext({ role: 'admin' }); - - const result = await app.resolve( - onGraphqlEventFactory('getUser', 'Query', { id: '1' }), - context - ); - - // Assess - expect(result).toEqual({ - id: '1', - role: 'admin', - permissions: ['read'], - }); - }); - - it('does not include sharedContext when context is empty for batch resolvers with throwOnError=true', async () => { - // Prepare - const app = new AppSyncGraphQLResolver(); - const handlerSpy = vi.fn().mockResolvedValue({ id: '1', processed: true }); - - app.batchResolver(handlerSpy, { - fieldName: 'batchProcess', - aggregate: false, - throwOnError: true, - }); - - // Act - await app.resolve( - [onGraphqlEventFactory('batchProcess', 'Query', { id: '1' })], - context - ); - - // Assess - expect(handlerSpy).toHaveBeenCalledWith( - { id: '1' }, - { - event: expect.any(Object), - context, - } - ); - }); - - it('does not include sharedContext when context is empty for batch resolvers with throwOnError=false', async () => { - // Prepare - const app = new AppSyncGraphQLResolver(); - const handlerSpy = vi.fn().mockResolvedValue({ id: '1', processed: true }); - - app.batchResolver(handlerSpy, { - fieldName: 'batchProcess', - aggregate: false, - throwOnError: false, - }); - - // Act - await app.resolve( - [onGraphqlEventFactory('batchProcess', 'Query', { id: '1' })], - context - ); - - // Assess - expect(handlerSpy).toHaveBeenCalledWith( - { id: '1' }, - { - event: expect.any(Object), - context, - } - ); - }); - - it('includes sharedContext when context has data for batch resolvers with throwOnError=true', async () => { - // Prepare - const app = new AppSyncGraphQLResolver(); - const handlerSpy = vi.fn().mockResolvedValue({ id: '1', processed: true }); - app.batchResolver(handlerSpy, { - fieldName: 'batchProcess', - aggregate: false, - throwOnError: true, - }); - - // Act - app.appendContext({ requestId: 'test-123' }); - - await app.resolve( - [onGraphqlEventFactory('batchProcess', 'Query', { id: '1' })], - context - ); - - // Assess - expect(handlerSpy).toHaveBeenCalledWith( - { id: '1' }, - expect.objectContaining({ - event: expect.any(Object), - context, - sharedContext: new Map([['requestId', 'test-123']]), - }) - ); - }); - - it('includes sharedContext when context has data for batch resolvers with throwOnError=false', async () => { - // Prepare - const app = new AppSyncGraphQLResolver(); - const handlerSpy = vi.fn().mockResolvedValue({ id: '1', processed: true }); - app.batchResolver(handlerSpy, { - fieldName: 'batchProcess', - aggregate: false, - throwOnError: false, - }); - - // Act - app.appendContext({ requestId: 'test-456' }); - await app.resolve( - [onGraphqlEventFactory('batchProcess', 'Query', { id: '1' })], - context - ); - - // Assess - expect(handlerSpy).toHaveBeenCalledWith( - { id: '1' }, - expect.objectContaining({ - event: expect.any(Object), - context, - sharedContext: new Map([['requestId', 'test-456']]), - }) - ); - }); - - // #endregion appendContext }); From b2bbf20b1d77707fce861ad9f85f33a18001d62e Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 12 Oct 2025 10:05:35 +0600 Subject: [PATCH 24/27] doc: remove appendContext and related router examples for clarity --- .../features/event-handler/appsync-graphql.md | 28 ------------------- .../appsync-graphql/appendContext.ts | 13 --------- .../appsync-graphql/postRouterWithContext.ts | 10 ------- .../appsync-graphql/userRouterWithContext.ts | 10 ------- 4 files changed, 61 deletions(-) delete mode 100644 examples/snippets/event-handler/appsync-graphql/appendContext.ts delete mode 100644 examples/snippets/event-handler/appsync-graphql/postRouterWithContext.ts delete mode 100644 examples/snippets/event-handler/appsync-graphql/userRouterWithContext.ts diff --git a/docs/features/event-handler/appsync-graphql.md b/docs/features/event-handler/appsync-graphql.md index ba4565ed5d..fc97428b55 100644 --- a/docs/features/event-handler/appsync-graphql.md +++ b/docs/features/event-handler/appsync-graphql.md @@ -144,34 +144,6 @@ Let's assume you have `app.ts` as your Lambda function entrypoint and routes in --8<-- "examples/snippets/event-handler/appsync-graphql/splitRouter.ts" ``` -#### Sharing contextual data - -You can use `appendContext` when you want to share data between your App and Router instances. Any data you share will be available via the `sharedContext` parameter in your resolver handlers. - -???+ warning - For safety, we clear the context after each invocation. - -???+ tip - This can also be useful for injecting contextual information before a request is processed. - -=== "app.ts" - - ```typescript hl_lines="10" - --8<-- "examples/snippets/event-handler/appsync-graphql/appendContext.ts" - ``` - -=== "postRouter.ts" - - ```typescript hl_lines="5-8" - --8<-- "examples/snippets/event-handler/appsync-graphql/postRouterWithContext.ts" - ``` - -=== "userRouter.ts" - - ```typescript hl_lines="5-8" - --8<-- "examples/snippets/event-handler/appsync-graphql/userRouterWithContext.ts" - ``` - ### Nested mappings !!! note diff --git a/examples/snippets/event-handler/appsync-graphql/appendContext.ts b/examples/snippets/event-handler/appsync-graphql/appendContext.ts deleted file mode 100644 index 7301ae58f1..0000000000 --- a/examples/snippets/event-handler/appsync-graphql/appendContext.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; -import type { Context } from 'aws-lambda/handler'; -import { postRouter } from './postRouter'; -import { userRouter } from './userRouter'; - -const app = new AppSyncGraphQLResolver(); - -app.includeRouter([postRouter, userRouter]); - -app.appendContext({ requestId: crypto.randomUUID() }); - -export const handler = async (event: unknown, context: Context) => - app.resolve(event, context); diff --git a/examples/snippets/event-handler/appsync-graphql/postRouterWithContext.ts b/examples/snippets/event-handler/appsync-graphql/postRouterWithContext.ts deleted file mode 100644 index 7c33932e38..0000000000 --- a/examples/snippets/event-handler/appsync-graphql/postRouterWithContext.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Router } from '@aws-lambda-powertools/event-handler/appsync-graphql'; - -const postRouter = new Router(); - -postRouter.onQuery('getPosts', async (args, { sharedContext }) => { - const requestId = sharedContext?.get('requestId'); - return [{ id: 1, title: 'First post', content: 'Hello world!', requestId }]; -}); - -export { postRouter }; diff --git a/examples/snippets/event-handler/appsync-graphql/userRouterWithContext.ts b/examples/snippets/event-handler/appsync-graphql/userRouterWithContext.ts deleted file mode 100644 index 9c09ea6d3a..0000000000 --- a/examples/snippets/event-handler/appsync-graphql/userRouterWithContext.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Router } from '@aws-lambda-powertools/event-handler/appsync-graphql'; - -const userRouter = new Router(); - -userRouter.onQuery('getUsers', async (args, { sharedContext }) => { - const requestId = sharedContext?.get('requestId'); - return [{ id: 1, name: 'John Doe', email: 'john@example.com', requestId }]; -}); - -export { userRouter }; From 55c125dc647bb84a4f29f090cc8be07a3e1bb7b7 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 12 Oct 2025 10:10:59 +0600 Subject: [PATCH 25/27] refactor: simplify event and context handling in resolver methods --- .../appsync-graphql/AppSyncGraphQLResolver.ts | 23 ++++--------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts index 80d93977e7..696e8b7009 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -354,10 +354,7 @@ class AppSyncGraphQLResolver extends Router { options.handler as BatchResolverAggregateHandlerFn ).apply(resolveOptions?.scope ?? this, [ events, - { - event: events, - context, - }, + { event: events, context }, ]); if (!Array.isArray(response)) { @@ -376,10 +373,7 @@ class AppSyncGraphQLResolver extends Router { for (const event of events) { const result = await handler.apply(resolveOptions?.scope ?? this, [ event.arguments, - { - event, - context, - }, + { event, context }, ]); results.push(result); } @@ -390,10 +384,7 @@ class AppSyncGraphQLResolver extends Router { try { const result = await handler.apply(resolveOptions?.scope ?? this, [ events[i].arguments, - { - event: events[i], - context, - }, + { event: events[i], context }, ]); results.push(result); } catch (error) { @@ -435,13 +426,7 @@ class AppSyncGraphQLResolver extends Router { if (resolverHandlerOptions) { return (resolverHandlerOptions.handler as ResolverHandler).apply( options?.scope ?? this, - [ - event.arguments, - { - event, - context, - }, - ] + [event.arguments, { event, context }] ); } From 326587e520b075b1eaa0f5e7dad39067ec1ed879 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 12 Oct 2025 10:14:33 +0600 Subject: [PATCH 26/27] refactor: remove unnecessary blank line 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 696e8b7009..32ffbbb4ea 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -160,7 +160,6 @@ class AppSyncGraphQLResolver extends Router { ); return; } - return this.#withErrorHandling( () => this.#executeBatchResolvers(event, context, options), event[0], From 0a20b2e549a2a0446ce247578590186cc79a8653 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 12 Oct 2025 18:19:03 +0600 Subject: [PATCH 27/27] doc: add note about handler precedence and conflict warnings for `includeRouter` method --- .../src/appsync-graphql/AppSyncGraphQLResolver.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts index 32ffbbb4ea..cb3c9a616c 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -188,6 +188,12 @@ class AppSyncGraphQLResolver extends Router { * All resolver handlers, batch resolver handlers, and exception handlers * from the included routers will be available in the current resolver. * + * **Note:** When multiple routers register handlers for the same type and field combination + * (e.g., both `userRouter` and `postRouter` define `Query.getPost`), the handler from the + * last included router takes precedence and will override earlier registrations. + * This behavior also applies to exception handlers registered for the same error class. + * A warning is logged to help you identify potential conflicts when handlers are overridden. + * * @example * ```ts * import { AppSyncGraphQLResolver, Router } from '@aws-lambda-powertools/event-handler/appsync-graphql';