From 951dba32957012cc3b2b5c98e96cbbae2dc9441d Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 25 May 2025 15:09:50 +0600 Subject: [PATCH 01/29] feat: implement `RouteHandlerRegistry` for managing GraphQL route handlers --- .../appsync-graphql/RouteHandlerRegistry.ts | 108 +++++++++++++ .../src/types/appsync-graphql.ts | 153 ++++++++++++++++++ 2 files changed, 261 insertions(+) create mode 100644 packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts create mode 100644 packages/event-handler/src/types/appsync-graphql.ts diff --git a/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts b/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts new file mode 100644 index 000000000..946507839 --- /dev/null +++ b/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts @@ -0,0 +1,108 @@ +import { LRUCache } from '@aws-lambda-powertools/commons/utils/lru-cache'; +import type { + GenericLogger, + RouteHandlerOptions, + RouteHandlerRegistryOptions, +} from '../types/appsync-graphql.js'; + +/** + * Registry for storing route handlers for the `query` and `mutation` events in AWS AppSync GraphQL API's. + * + * This class should not be used directly unless you are implementing a custom router. + * Instead, use the {@link Router} class, which is the recommended way to register routes. + */ +class RouteHandlerRegistry { + /** + * A map of registered route handlers, keyed by their type & field name. + */ + protected readonly resolvers: Map = new Map(); + /** + * A logger instance to be used for logging debug and warning messages. + */ + readonly #logger: GenericLogger; + /** + * The event type stored in the registry. + */ + readonly #eventType: 'onQuery' | 'onMutation'; + /** + * A cache for storing the resolved route handlers. + */ + readonly #resolverCache: LRUCache = new LRUCache( + { + maxSize: 100, + } + ); + /** + * A set of warning messages to avoid duplicate warnings. + */ + readonly #warningSet: Set = new Set(); + + public constructor(options: RouteHandlerRegistryOptions) { + this.#logger = options.logger; + this.#eventType = options.eventType ?? 'onQuery'; + } + + /** + * Registers a new GraphQL route handler for a specific type and field. + * + * @param options - The options for registering the route handler, including the GraphQL type name, field name, and the handler function. + * + * @remarks + * This method logs the registration and stores the handler in the internal resolver map. + */ + public register(options: RouteHandlerOptions): void { + const { fieldName, handler, typeName } = options; + this.#logger.debug( + `Registering ${typeName} api handler for field '${fieldName}'` + ); + this.resolvers.set(this.#makeKey(typeName, fieldName), { + fieldName, + handler, + typeName, + }); + } + + /** + * Resolves the handler for a specific GraphQL API event. + * + * This method first checks an internal cache for the handler. If not found, it attempts to retrieve + * the handler from the registered resolvers. If the handler is still not found, a warning is logged + * (only once per missing handler), and `undefined` is returned. + * + * @param typeName - The name of the GraphQL type. + * @param fieldName - The name of the field within the GraphQL type. + */ + public resolve( + typeName: string, + fieldName: string + ): RouteHandlerOptions | undefined { + const cacheKey = this.#makeKey(typeName, fieldName); + if (this.#resolverCache.has(cacheKey)) { + return this.#resolverCache.get(cacheKey); + } + const handler = this.resolvers.get(cacheKey); + if (handler === undefined) { + if (!this.#warningSet.has(cacheKey)) { + this.#logger.warn( + `No route handler found for field '${fieldName}' registered for ${this.#eventType}.` + ); + this.#warningSet.add(cacheKey); + } + return undefined; + } + this.#resolverCache.add(cacheKey, handler); + return handler; + } + + /** + * Generates a unique key by combining the provided GraphQL type name and field name. + * + * @param typeName - The name of the GraphQL type. + * @param fieldName - The name of the GraphQL field. + */ + #makeKey(typeName: string, fieldName: string): string { + return `${typeName}.${fieldName}`; + } +} + +export { RouteHandlerRegistry }; diff --git a/packages/event-handler/src/types/appsync-graphql.ts b/packages/event-handler/src/types/appsync-graphql.ts new file mode 100644 index 000000000..101763ec9 --- /dev/null +++ b/packages/event-handler/src/types/appsync-graphql.ts @@ -0,0 +1,153 @@ +import type { Context } from 'aws-lambda'; +import type { RouteHandlerRegistry } from '../appsync-graphql/RouteHandlerRegistry.js'; + +// biome-ignore lint/suspicious/noExplicitAny: We intentionally use `any` here to represent any type of data and keep the logger is as flexible as possible. +type Anything = any; + +/** + * Interface for a generic logger object. + */ +type GenericLogger = { + trace?: (...content: Anything[]) => void; + debug: (...content: Anything[]) => void; + info?: (...content: Anything[]) => void; + warn: (...content: Anything[]) => void; + error: (...content: Anything[]) => void; +}; + +// #region OnQuery fn + +type OnQuerySyncHandlerFn = ( + event: AppSyncGraphQLEvent, + context: Context +) => unknown; + +type OnQueryHandlerFn = ( + event: AppSyncGraphQLEvent, + context: Context +) => Promise; + +type OnQueryHandler = OnQuerySyncHandlerFn | OnQueryHandlerFn; + +// #region OnMutation fn + +type OnMutationSyncHandlerFn = ( + event: AppSyncGraphQLEvent, + context: Context +) => unknown; + +type OnMutationHandlerFn = ( + event: AppSyncGraphQLEvent, + context: Context +) => Promise; + +type OnMutationHandler = OnMutationSyncHandlerFn | OnMutationHandlerFn; + +// #region Resolver registry + +/** + * Options for the {@link RouteHandlerRegistry} class + */ +type RouteHandlerRegistryOptions = { + /** + * A logger instance to be used for logging debug, warning, and error messages. + * + * When no logger is provided, we'll only log warnings and errors using the global `console` object. + */ + logger: GenericLogger; + /** + * Event type stored in the registry + * @default 'onQuery' + */ + eventType?: 'onQuery' | 'onMutation'; +}; + +/** + * Options for registering a resolver event + * + * @property handler - The handler function to be called when the event is received + * @property fieldName - The name of the field to be registered + * @property typeName - The name of the type to be registered + */ +type RouteHandlerOptions = { + /** + * The handler function to be called when the event is received + */ + handler: OnQueryHandler | OnMutationHandler; + /** + * The field name of the event to be registered + */ + fieldName: string; + /** + * The type name of the event to be registered + */ + typeName: string; +}; + +// #region Router + +/** + * Options for the {@link Router} class + */ +type RouterOptions = { + /** + * A logger instance to be used for logging debug, warning, and error messages. + * + * When no logger is provided, we'll only log warnings and errors using the global `console` object. + */ + logger?: GenericLogger; +}; + +/** + * Options for registering a route + */ +type RouteOptions = { + /** + * The type name of the event to be registered + */ + typeName?: string; +}; + +// #region Events + +/** + * Event type for AppSync GraphQL. + * + * https://docs.aws.amazon.com/appsync/latest/devguide/resolver-context-reference.html + * + * For strongly typed validation and parsing at runtime, check out the `@aws-lambda-powertools/parser` package. + */ +type AppSyncGraphQLEvent = { + arguments: Record; + /** + * The `identity` field varies based on the authentication type used for the AppSync API. + * When using an API key, it will be `null`. When using IAM, it will contain the AWS credentials of the user. When using Cognito, + * it will contain the Cognito user pool information. When using a Lambda authorizer, it will contain the information returned + * by the authorizer. + */ + identity: null | Record; + source: null | Record; + result: null; + request: { + headers: Record; + domainName: null; + }; + prev: null; + info: { + fieldName: string; + selectionSetList: string[]; + parentTypeName: string; + }; + stash: Record; +}; + +export type { + GenericLogger, + RouteHandlerRegistryOptions, + RouteHandlerOptions, + RouterOptions, + RouteOptions, + AppSyncGraphQLEvent, + OnQueryHandler, + OnMutationHandler, +}; From 7d9a27e1a8236039a4f6b9a944ae8c1609d28b27 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 25 May 2025 15:17:07 +0600 Subject: [PATCH 02/29] feat: add type guard for AppSync GraphQL event validation --- .../src/appsync-graphql/utils.ts | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 packages/event-handler/src/appsync-graphql/utils.ts diff --git a/packages/event-handler/src/appsync-graphql/utils.ts b/packages/event-handler/src/appsync-graphql/utils.ts new file mode 100644 index 000000000..86f4fb920 --- /dev/null +++ b/packages/event-handler/src/appsync-graphql/utils.ts @@ -0,0 +1,45 @@ +import { isRecord, isString } from '@aws-lambda-powertools/commons/typeutils'; +import type { AppSyncGraphQLEvent } from '../types/appsync-graphql.js'; + +/** + * Type guard to check if the provided event is an AppSync GraphQL event. + * + * We use this function to ensure that the event is an object and has the required properties + * without adding any dependency. + * + * @param event - The incoming event to check + */ +const isAppSyncGraphQLEvent = ( + event: unknown +): event is AppSyncGraphQLEvent => { + if (typeof event !== 'object' || event === null || !isRecord(event)) { + return false; + } + return ( + 'arguments' in event && + isRecord(event.arguments) && + 'identity' in event && + 'source' in event && + 'result' in event && + isRecord(event.request) && + isRecord(event.request.headers) && + 'domainName' in event.request && + 'prev' in event && + isRecord(event.info) && + 'fieldName' in event.info && + isString(event.info.fieldName) && + 'parentTypeName' in event.info && + isString(event.info.parentTypeName) && + 'variables' in event.info && + isRecord(event.info.variables) && + 'selectionSetList' in event.info && + Array.isArray(event.info.selectionSetList) && + event.info.selectionSetList.every((item) => isString(item)) && + 'parentTypeName' in event.info && + isString(event.info.parentTypeName) && + 'stash' in event && + isRecord(event.stash) + ); +}; + +export { isAppSyncGraphQLEvent }; From 69c6210a087d815190f3cb5fc1dea5b7d5473a05 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 25 May 2025 17:54:12 +0600 Subject: [PATCH 03/29] refactor: simplify handler function signatures and update type definitions for GraphQL events --- .../src/types/appsync-graphql.ts | 30 +++++-------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/packages/event-handler/src/types/appsync-graphql.ts b/packages/event-handler/src/types/appsync-graphql.ts index 101763ec9..331d5d424 100644 --- a/packages/event-handler/src/types/appsync-graphql.ts +++ b/packages/event-handler/src/types/appsync-graphql.ts @@ -1,4 +1,3 @@ -import type { Context } from 'aws-lambda'; import type { RouteHandlerRegistry } from '../appsync-graphql/RouteHandlerRegistry.js'; // biome-ignore lint/suspicious/noExplicitAny: We intentionally use `any` here to represent any type of data and keep the logger is as flexible as possible. @@ -17,29 +16,17 @@ type GenericLogger = { // #region OnQuery fn -type OnQuerySyncHandlerFn = ( - event: AppSyncGraphQLEvent, - context: Context -) => unknown; +type OnQuerySyncHandlerFn = ({ ...args }: Anything) => unknown; -type OnQueryHandlerFn = ( - event: AppSyncGraphQLEvent, - context: Context -) => Promise; +type OnQueryHandlerFn = ({ ...args }: Anything) => Promise; type OnQueryHandler = OnQuerySyncHandlerFn | OnQueryHandlerFn; // #region OnMutation fn -type OnMutationSyncHandlerFn = ( - event: AppSyncGraphQLEvent, - context: Context -) => unknown; +type OnMutationSyncHandlerFn = ({ ...args }: Anything) => unknown; -type OnMutationHandlerFn = ( - event: AppSyncGraphQLEvent, - context: Context -) => Promise; +type OnMutationHandlerFn = ({ ...args }: Anything) => Promise; type OnMutationHandler = OnMutationSyncHandlerFn | OnMutationHandlerFn; @@ -89,7 +76,7 @@ type RouteHandlerOptions = { /** * Options for the {@link Router} class */ -type RouterOptions = { +type GraphQlRouterOptions = { /** * A logger instance to be used for logging debug, warning, and error messages. * @@ -101,7 +88,7 @@ type RouterOptions = { /** * Options for registering a route */ -type RouteOptions = { +type GraphQlRouteOptions = { /** * The type name of the event to be registered */ @@ -127,7 +114,6 @@ type AppSyncGraphQLEvent = { */ identity: null | Record; source: null | Record; - result: null; request: { headers: Record; domainName: null; @@ -145,8 +131,8 @@ export type { GenericLogger, RouteHandlerRegistryOptions, RouteHandlerOptions, - RouterOptions, - RouteOptions, + GraphQlRouterOptions, + GraphQlRouteOptions, AppSyncGraphQLEvent, OnQueryHandler, OnMutationHandler, From e5762f3f2ed913f977fb20529d9e9c3ecdac7e91 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 25 May 2025 17:54:38 +0600 Subject: [PATCH 04/29] refactor: remove wrong `result` property check from AppSync GraphQL event type guard --- packages/event-handler/src/appsync-graphql/utils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/event-handler/src/appsync-graphql/utils.ts b/packages/event-handler/src/appsync-graphql/utils.ts index 86f4fb920..e925d6b32 100644 --- a/packages/event-handler/src/appsync-graphql/utils.ts +++ b/packages/event-handler/src/appsync-graphql/utils.ts @@ -20,7 +20,6 @@ const isAppSyncGraphQLEvent = ( isRecord(event.arguments) && 'identity' in event && 'source' in event && - 'result' in event && isRecord(event.request) && isRecord(event.request.headers) && 'domainName' in event.request && From 7b9d1f964e4effdb500fa92e241deb03943f7899 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Wed, 28 May 2025 10:11:30 +0600 Subject: [PATCH 05/29] feat: implement Router class for managing `Query` events for appsync graphql --- .../src/appsync-graphql/Router.ts | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 packages/event-handler/src/appsync-graphql/Router.ts diff --git a/packages/event-handler/src/appsync-graphql/Router.ts b/packages/event-handler/src/appsync-graphql/Router.ts new file mode 100644 index 000000000..b759131ad --- /dev/null +++ b/packages/event-handler/src/appsync-graphql/Router.ts @@ -0,0 +1,145 @@ +import { + EnvironmentVariablesService, + isRecord, +} from '@aws-lambda-powertools/commons'; +import type { + GenericLogger, + GraphQlRouteOptions, + GraphQlRouterOptions, + OnQueryHandler, +} from '../types/appsync-graphql.js'; +import { RouteHandlerRegistry } from './RouteHandlerRegistry.js'; + +/** + * Class for registering routes for the `onQuery` and `onMutation` events in AWS AppSync Events APIs. + */ +class Router { + /** + * A map of registered routes for the `onQuery` event, keyed by their fieldNames. + */ + protected readonly onQueryRegistry: RouteHandlerRegistry; + /** + * A map of registered routes for the `onMutation` event, keyed by their fieldNames. + */ + protected readonly onMutationRegistry: RouteHandlerRegistry; + /** + * A logger instance to be used for logging debug, warning, and error messages. + * + * When no logger is provided, we'll only log warnings and errors using the global `console` object. + */ + protected readonly logger: Pick; + /** + * Whether the router is running in development mode. + */ + protected readonly isDev: boolean = false; + /** + * The environment variables service instance. + */ + protected readonly envService: EnvironmentVariablesService; + + public constructor(options?: GraphQlRouterOptions) { + this.envService = new EnvironmentVariablesService(); + const alcLogLevel = this.envService.get('AWS_LAMBDA_LOG_LEVEL'); + this.logger = options?.logger ?? { + debug: alcLogLevel === 'DEBUG' ? console.debug : () => undefined, + error: console.error, + warn: console.warn, + }; + this.onQueryRegistry = new RouteHandlerRegistry({ + logger: this.logger, + eventType: 'onQuery', + }); + this.onMutationRegistry = new RouteHandlerRegistry({ + logger: this.logger, + eventType: 'onMutation', + }); + this.isDev = this.envService.isDevMode(); + } + /** + * Register a handler function for the `onQuery` event. + * + * Registers a handler for a specific GraphQL Query field. The handler will be invoked when a request is made + * for the specified field in the Query type. + * + * This method can be used as a direct function call or as a method decorator. + * + * @example + * ```ts + * import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; + * + * const app = new AppSyncGraphQLResolver(); + * + * app.onQuery('getPost', async (payload) => { + * // your business logic here + * return payload; + * }); + * + * export const handler = async (event, context) => + * app.resolve(event, context); + * ``` + * + * As a decorator: + * + * @example + * ```ts + * import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; + * + * const app = new AppSyncGraphQLResolver(); + * + * class Lambda { + * @app.onQuery('getPost') + * async handleGetPost(payload) { + * // your business logic here + * return payload; + * } + * + * async handler(event, context) { + * return app.resolve(event, context); + * } + * } + * + * const lambda = new Lambda(); + * export const handler = lambda.handler.bind(lambda); + * ``` + * + * @param fieldName - The name of the Query field to register the handler for. + * @param handler - The function to handle the Query. Receives the payload as the first argument. + * @param options - Optional route options. + * @param options.typeName - The name of the GraphQL type to use for the resolver (defaults to 'Query'). + */ + public onQuery( + fieldName: string, + handler: OnQueryHandler, + options?: GraphQlRouteOptions + ): void; + public onQuery( + fieldName: string, + options?: GraphQlRouteOptions + ): MethodDecorator; + public onQuery( + fieldName: string, + handler?: OnQueryHandler | GraphQlRouteOptions, + options?: GraphQlRouteOptions + ): MethodDecorator | undefined { + if (handler && typeof handler === 'function') { + this.onQueryRegistry.register({ + fieldName, + handler, + typeName: options?.typeName ?? 'Query', + }); + return; + } + + return (_target, _propertyKey, descriptor: PropertyDescriptor) => { + const routeOptions = isRecord(handler) ? handler : options; + this.onQueryRegistry.register({ + fieldName, + handler: descriptor.value, + typeName: routeOptions?.typeName ?? 'Query', + }); + return descriptor; + }; + } +} + +export { Router }; From 707dc1c364aa1ff15ca080164dfa263038b490e8 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Wed, 28 May 2025 10:15:37 +0600 Subject: [PATCH 06/29] feat: add `onMutation` method for handling GraphQL Mutation events in Router class --- .../src/appsync-graphql/Router.ts | 96 +++++++++++++++++-- 1 file changed, 90 insertions(+), 6 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/Router.ts b/packages/event-handler/src/appsync-graphql/Router.ts index b759131ad..548b3d1e5 100644 --- a/packages/event-handler/src/appsync-graphql/Router.ts +++ b/packages/event-handler/src/appsync-graphql/Router.ts @@ -6,6 +6,7 @@ import type { GenericLogger, GraphQlRouteOptions, GraphQlRouterOptions, + OnMutationHandler, OnQueryHandler, } from '../types/appsync-graphql.js'; import { RouteHandlerRegistry } from './RouteHandlerRegistry.js'; @@ -55,14 +56,13 @@ class Router { }); this.isDev = this.envService.isDevMode(); } + /** * Register a handler function for the `onQuery` event. - * + * Registers a handler for a specific GraphQL Query field. The handler will be invoked when a request is made * for the specified field in the Query type. - * - * This method can be used as a direct function call or as a method decorator. - * + * * @example * ```ts * import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; @@ -73,7 +73,7 @@ class Router { * // your business logic here * return payload; * }); - * + * export const handler = async (event, context) => * app.resolve(event, context); * ``` @@ -103,7 +103,7 @@ class Router { * ``` * * @param fieldName - The name of the Query field to register the handler for. - * @param handler - The function to handle the Query. Receives the payload as the first argument. + * @param handler - The handler function to be called when the event is received. * @param options - Optional route options. * @param options.typeName - The name of the GraphQL type to use for the resolver (defaults to 'Query'). */ @@ -140,6 +140,90 @@ class Router { return descriptor; }; } + + /** + * Register a handler function for the `onMutation` event. + * + * Registers a handler for a specific GraphQL Mutation field. The handler will be invoked when a request is made + * for the specified field in the Mutation type. + * + * @example + * ```ts + * import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; + * + * const app = new AppSyncGraphQLResolver(); + * + * app.onMutation('createPost', async (payload) => { + * // your business logic here + * return payload; + * }); + * + * export const handler = async (event, context) => + * app.resolve(event, context); + * ``` + * + * As a decorator: + * + * @example + * ```ts + * import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; + * + * const app = new AppSyncGraphQLResolver(); + * + * class Lambda { + * @app.onMutation('createPost') + * async handleCreatePost(payload) { + * // your business logic here + * return payload; + * } + * + * async handler(event, context) { + * return app.resolve(event, context); + * } + * } + * + * const lambda = new Lambda(); + * export const handler = lambda.handler.bind(lambda); + * ``` + * + * @param fieldName - The name of the Mutation field to register the handler for. + * @param handler - The handler function to be called when the event is received. + * @param options - Optional route options. + * @param options.typeName - The name of the GraphQL type to use for the resolver (defaults to 'Mutation'). + */ + public onMutation( + fieldName: string, + handler: OnMutationHandler, + options?: GraphQlRouteOptions + ): void; + public onMutation( + fieldName: string, + options?: GraphQlRouteOptions + ): MethodDecorator; + public onMutation( + fieldName: string, + handler?: OnMutationHandler | GraphQlRouteOptions, + options?: GraphQlRouteOptions + ): MethodDecorator | undefined { + if (handler && typeof handler === 'function') { + this.onMutationRegistry.register({ + fieldName, + handler, + typeName: options?.typeName ?? 'Mutation', + }); + return; + } + + return (_target, _propertyKey, descriptor: PropertyDescriptor) => { + const routeOptions = isRecord(handler) ? handler : options; + this.onMutationRegistry.register({ + fieldName, + handler: descriptor.value, + typeName: routeOptions?.typeName ?? 'Mutation', + }); + return descriptor; + }; + } } export { Router }; From a71aead414afd2ba4cfbfe04f9321c0783c9556e Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Wed, 28 May 2025 10:41:01 +0600 Subject: [PATCH 07/29] feat: implement AppSyncGraphQLResolver class to handle onQuery and onMutation events --- .../appsync-graphql/AppSyncGraphQLResolver.ts | 98 +++++++++++++++++++ .../src/appsync-graphql/errors.ts | 8 ++ 2 files changed, 106 insertions(+) create mode 100644 packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts create mode 100644 packages/event-handler/src/appsync-graphql/errors.ts diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts new file mode 100644 index 000000000..c38490b5e --- /dev/null +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -0,0 +1,98 @@ +import type { Context } from 'aws-lambda'; +import type { AppSyncGraphQLEvent } from '../types/appsync-graphql.js'; +import { Router } from './Router.js'; +import { ResolverNotFoundException } from './errors.js'; +import { isAppSyncGraphQLEvent } from './utils.js'; + +/** + * Resolver for AWS AppSync GraphQL APIs. + * + * This resolver is designed to handle the `onQuery` and `onMutation` events + * from AWS AppSync GraphQL APIs. It allows you to register handlers for these events + * and route them to the appropriate functions based on the event's field & type. + * + * @example + * ```ts + * import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; + * + * const app = new AppSyncGraphQLResolver(); + * + * app.onQuery('getPost', async ({ id }) => { + * // your business logic here + * return { + * id, + * title: 'Post Title', + * content: 'Post Content', + * }; + * }); + * + * export const handler = async (event, context) => + * app.resolve(event, context); + * ``` + */ +export class AppSyncGraphQLResolver extends Router { + public async resolve(event: unknown, context: Context): Promise { + if (!isAppSyncGraphQLEvent(event)) { + this.logger.warn( + 'Received an event that is not compatible with this resolver' + ); + return; + } + + try { + if (Array.isArray(event)) { + this.logger.warn('Batch resolvers not implemented yet'); + } else { + return await this.#executeSingleResolver(event); + } + } catch (error) { + this.logger.error( + `An error occurred in handler ${event.info.fieldName}`, + error + ); + if (error instanceof ResolverNotFoundException) throw error; + return this.#formatErrorResponse(error); + } + } + + async #executeSingleResolver(event: AppSyncGraphQLEvent): Promise { + const { fieldName, parentTypeName: typeName } = event.info; + const queryHandlerOptions = this.onQueryRegistry.resolve( + typeName, + fieldName + ); + const mutationHandlerOptions = this.onMutationRegistry.resolve( + typeName, + fieldName + ); + + if (queryHandlerOptions) { + return await queryHandlerOptions.handler.apply(this, [event.arguments]); + } + if (mutationHandlerOptions) { + return await mutationHandlerOptions.handler.apply(this, [ + event.arguments, + ]); + } + + throw new ResolverNotFoundException( + `No resolver found for the event ${fieldName}-${typeName}.` + ); + } + + /** + * Format the error response to be returned to the client. + * + * @param error - The error object + */ + #formatErrorResponse(error: unknown) { + if (error instanceof Error) { + return { + error: `${error.name} - ${error.message}`, + }; + } + return { + error: 'An unknown error occurred', + }; + } +} diff --git a/packages/event-handler/src/appsync-graphql/errors.ts b/packages/event-handler/src/appsync-graphql/errors.ts new file mode 100644 index 000000000..b3f3c15b9 --- /dev/null +++ b/packages/event-handler/src/appsync-graphql/errors.ts @@ -0,0 +1,8 @@ +class ResolverNotFoundException extends Error { + constructor(message: string, options?: ErrorOptions) { + super(message, options); + this.name = 'ResolverNotFoundException'; + } +} + +export { ResolverNotFoundException }; From c4f6b8d5b5088c81e9beef1f7cc52ec345852456 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Wed, 28 May 2025 10:42:01 +0600 Subject: [PATCH 08/29] doc: `#executeSingleResolver` function --- .../src/appsync-graphql/AppSyncGraphQLResolver.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts index c38490b5e..c388ba445 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -55,6 +55,17 @@ export class AppSyncGraphQLResolver extends Router { } } + /** + * Executes the appropriate resolver (query or mutation) for a given AppSync GraphQL event. + * + * This method attempts to resolve the handler for the specified field and type name + * from the query and mutation registries. If a matching handler is found, it invokes + * the handler with the event arguments. If no handler is found, it throws a + * `ResolverNotFoundException`. + * + * @param event - The AppSync GraphQL event containing resolver information. + * @throws {ResolverNotFoundException} If no resolver is registered for the given field and type. + */ async #executeSingleResolver(event: AppSyncGraphQLEvent): Promise { const { fieldName, parentTypeName: typeName } = event.info; const queryHandlerOptions = this.onQueryRegistry.resolve( From 387f5cfd8aeea3dba970a54ff92a59de56cbf889 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Wed, 28 May 2025 10:54:48 +0600 Subject: [PATCH 09/29] feat: add warning for unimplemented batch resolvers in AppSyncGraphQLResolver --- .../src/appsync-graphql/AppSyncGraphQLResolver.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts index c388ba445..503c3fb48 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -32,19 +32,18 @@ import { isAppSyncGraphQLEvent } from './utils.js'; */ export class AppSyncGraphQLResolver extends Router { public async resolve(event: unknown, context: Context): Promise { + if (Array.isArray(event)) { + this.logger.warn('Batch resolvers are not implemented yet'); + return; + } if (!isAppSyncGraphQLEvent(event)) { this.logger.warn( 'Received an event that is not compatible with this resolver' ); return; } - try { - if (Array.isArray(event)) { - this.logger.warn('Batch resolvers not implemented yet'); - } else { - return await this.#executeSingleResolver(event); - } + return await this.#executeSingleResolver(event); } catch (error) { this.logger.error( `An error occurred in handler ${event.info.fieldName}`, From 70b9921ac7da1f33cf8798ca89d493fca391e9ee Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Thu, 29 May 2025 17:52:40 +0600 Subject: [PATCH 10/29] feat: enhance `RouteHandlerRegistry` to log handler registration and resolution details --- .../appsync-graphql/RouteHandlerRegistry.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts b/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts index 946507839..4f484e7b6 100644 --- a/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts +++ b/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts @@ -46,16 +46,23 @@ class RouteHandlerRegistry { * Registers a new GraphQL route handler for a specific type and field. * * @param options - The options for registering the route handler, including the GraphQL type name, field name, and the handler function. + * @param options.fieldName - The field name of the GraphQL type to be registered + * @param options.handler - The handler function to be called when the event is received + * @param options.typeName - The name of the GraphQL type to be registered * - * @remarks - * This method logs the registration and stores the handler in the internal resolver map. */ public register(options: RouteHandlerOptions): void { const { fieldName, handler, typeName } = options; this.#logger.debug( - `Registering ${typeName} api handler for field '${fieldName}'` + `Registering ${this.#eventType} route handler for field '${fieldName}' with type '${typeName}'` ); - this.resolvers.set(this.#makeKey(typeName, fieldName), { + const cacheKey = this.#makeKey(typeName, fieldName); + if (this.resolvers.has(cacheKey)) { + this.#logger.warn( + `A route handler for field '${fieldName}' is already registered for '${typeName}'. The previous handler will be replaced.` + ); + } + this.resolvers.set(cacheKey, { fieldName, handler, typeName, @@ -80,6 +87,9 @@ class RouteHandlerRegistry { if (this.#resolverCache.has(cacheKey)) { return this.#resolverCache.get(cacheKey); } + this.#logger.debug( + `Resolving handler '${fieldName}' for type '${typeName}'` + ); const handler = this.resolvers.get(cacheKey); if (handler === undefined) { if (!this.#warningSet.has(cacheKey)) { From 286096b69fb9e6ffcc6ea325a44d15dec32085cc Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Thu, 29 May 2025 17:53:16 +0600 Subject: [PATCH 11/29] feat: add `onQueryEventFactory` and `onMutationEventFactory` to create event objects for GraphQL operations --- .../event-handler/tests/helpers/factories.ts | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/packages/event-handler/tests/helpers/factories.ts b/packages/event-handler/tests/helpers/factories.ts index a439da81a..149f08e9e 100644 --- a/packages/event-handler/tests/helpers/factories.ts +++ b/packages/event-handler/tests/helpers/factories.ts @@ -74,4 +74,45 @@ const onSubscribeEventFactory = ( events: null, }); -export { onPublishEventFactory, onSubscribeEventFactory }; +const createEventFactory = ( + fieldName: string, + args: Record, + parentTypeName: string +) => ({ + arguments: { ...args }, + identity: null, + source: null, + request: { + headers: { + key: 'value', + }, + domainName: null, + }, + info: { + fieldName, + parentTypeName, + selectionSetList: [], + variables: {}, + }, + prev: null, + stash: {}, +}); + +const onQueryEventFactory = ( + fieldName = 'getPost', + args = {}, + typeName = 'Query' +) => createEventFactory(fieldName, args, typeName); + +const onMutationEventFactory = ( + fieldName = 'addPost', + args = {}, + typeName = 'Mutation' +) => createEventFactory(fieldName, args, typeName); + +export { + onPublishEventFactory, + onSubscribeEventFactory, + onQueryEventFactory, + onMutationEventFactory, +}; From 58e3b26910dec8d8493a93c7e543618d51ec67dd Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Thu, 29 May 2025 17:53:33 +0600 Subject: [PATCH 12/29] feat: add unit tests for `AppSyncGraphQLResolver` class to validate event handling and error formatting --- .../AppSyncGraphQLResolver.test.ts | 166 ++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts diff --git a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts new file mode 100644 index 000000000..abfed9eec --- /dev/null +++ b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts @@ -0,0 +1,166 @@ +import context from '@aws-lambda-powertools/testing-utils/context'; +import { + onMutationEventFactory, + onQueryEventFactory, +} from 'tests/helpers/factories.js'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { AppSyncGraphQLResolver } from '../../../src/appsync-graphql/AppSyncGraphQLResolver.js'; +import { ResolverNotFoundException } from '../../../src/appsync-graphql/errors.js'; + +describe('Class: AppSyncGraphQLResolver', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('logs a warning and returns early if the event is batched', async () => { + // Prepare + const app = new AppSyncGraphQLResolver({ logger: console }); + + // Act + const result = await app.resolve([onQueryEventFactory()], context); + + // Assess + expect(console.warn).toHaveBeenCalledWith( + 'Batch resolvers are not implemented yet' + ); + expect(result).toBeUndefined(); + }); + + it('logs a warning and returns early if the event is not compatible', async () => { + // Prepare + const app = new AppSyncGraphQLResolver({ logger: console }); + + // Act + const result = await app.resolve(null, context); + + // Assess + expect(console.warn).toHaveBeenCalledWith( + 'Received an event that is not compatible with this resolver' + ); + expect(result).toBeUndefined(); + }); + + it('throw error if there are no onQuery handlers', async () => { + // Prepare + const app = new AppSyncGraphQLResolver({ logger: console }); + + // Act && Assess + await expect( + app.resolve(onQueryEventFactory('getPost'), context) + ).rejects.toThrow( + new ResolverNotFoundException( + 'No resolver found for the event getPost-Query.' + ) + ); + expect(console.error).toHaveBeenCalled(); + }); + + it('throw error if there are no onMutation handlers', async () => { + // Prepare + const app = new AppSyncGraphQLResolver({ logger: console }); + + // Act && Assess + await expect( + app.resolve(onMutationEventFactory('addPost'), context) + ).rejects.toThrow( + new ResolverNotFoundException( + 'No resolver found for the event addPost-Mutation.' + ) + ); + expect(console.error).toHaveBeenCalled(); + }); + + it('returns the response of the onQuery handler', async () => { + // Prepare + const app = new AppSyncGraphQLResolver({ logger: console }); + app.onQuery('getPost', async ({ id }) => { + return { + id, + title: 'Post Title', + content: 'Post Content', + }; + }); + + // Act + const result = await app.resolve( + onQueryEventFactory('getPost', { id: '123' }), + context + ); + + // Assess + expect(result).toEqual({ + id: '123', + title: 'Post Title', + content: 'Post Content', + }); + }); + + it('returns the response of the onMutation handler', async () => { + // Prepare + const app = new AppSyncGraphQLResolver({ logger: console }); + app.onMutation('addPost', async ({ title, content }) => { + return { + id: '123', + title, + content, + }; + }); + + // Act + const result = await app.resolve( + onMutationEventFactory('addPost', { + title: 'Post Title', + content: 'Post Content', + }), + context + ); + + // Assess + expect(result).toEqual({ + id: '123', + title: 'Post Title', + content: 'Post Content', + }); + }); + + it.each([ + { + type: 'base error', + error: new Error('Error in handler'), + message: 'Error - Error in handler', + }, + { + type: 'syntax error', + error: new SyntaxError('Syntax error in handler'), + message: 'SyntaxError - Syntax error in handler', + }, + { + type: 'unknown error', + error: 'foo', + message: 'An unknown error occurred', + }, + ])( + 'formats the error thrown by the onSubscribe handler $type', + async ({ error, message }) => { + // Prepare + const app = new AppSyncGraphQLResolver({ logger: console }); + app.onMutation('addPost', async () => { + throw error; + }); + + // Act + const result = await app.resolve( + onMutationEventFactory('addPost', { + title: 'Post Title', + content: 'Post Content', + }), + context + ); + + // Assess + expect(result).toEqual({ + error: message, + }); + } + ); +}); From d821b3d0a841c3aafb896e5039d534d2c9dc3bc4 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Thu, 29 May 2025 17:53:40 +0600 Subject: [PATCH 13/29] feat: add unit tests for `RouteHandlerRegistry` to validate handler registration and resolution behavior --- .../RouteHandlerRegistry.test.ts | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 packages/event-handler/tests/unit/appsync-graphql/RouteHandlerRegistry.test.ts diff --git a/packages/event-handler/tests/unit/appsync-graphql/RouteHandlerRegistry.test.ts b/packages/event-handler/tests/unit/appsync-graphql/RouteHandlerRegistry.test.ts new file mode 100644 index 000000000..0bc956e40 --- /dev/null +++ b/packages/event-handler/tests/unit/appsync-graphql/RouteHandlerRegistry.test.ts @@ -0,0 +1,118 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { RouteHandlerRegistry } from '../../../src/appsync-graphql/RouteHandlerRegistry.js'; +import type { RouteHandlerOptions } from '../../../src/types/appsync-graphql.js'; +describe('Class: RouteHandlerRegistry', () => { + class MockRouteHandlerRegistry extends RouteHandlerRegistry { + public declare resolvers: Map; + } + + const getRegistry = () => new MockRouteHandlerRegistry({ logger: console }); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it.each([ + { fieldName: 'getPost', typeName: 'Query' }, + { fieldName: 'addPost', typeName: 'Mutation' }, + ])( + 'registers a route handler for a field $fieldName', + ({ fieldName, typeName }) => { + // Prepare + const registry = getRegistry(); + + // Act + registry.register({ + fieldName, + typeName, + handler: vi.fn(), + }); + + // Assess + expect(registry.resolvers.size).toBe(1); + expect(registry.resolvers.get(`${typeName}.${fieldName}`)).toBeDefined(); + } + ); + + it('logs a warning and replaces the previous handler if the field & type is already registered', () => { + // Prepare + const registry = getRegistry(); + const originalHandler = vi.fn(); + const otherHandler = vi.fn(); + + // Act + registry.register({ + fieldName: 'getPost', + typeName: 'Query', + handler: originalHandler, + }); + registry.register({ + fieldName: 'getPost', + typeName: 'Query', + handler: otherHandler, + }); + + // Assess + expect(registry.resolvers.size).toBe(1); + expect(registry.resolvers.get('Query.getPost')).toEqual({ + fieldName: 'getPost', + typeName: 'Query', + handler: otherHandler, + }); + expect(console.warn).toHaveBeenCalledWith( + "A route handler for field 'getPost' is already registered for 'Query'. The previous handler will be replaced." + ); + }); + + it('will not replace the handler if the event type is different', () => { + // Prepare + const registry = getRegistry(); + const originalHandler = vi.fn(); + const otherHandler = vi.fn(); + + // Act + registry.register({ + fieldName: 'getPost', + typeName: 'Query', + handler: originalHandler, + }); + registry.register({ + fieldName: 'getPost', + typeName: 'Mutation', // Different type + handler: otherHandler, + }); + + // Assess + expect(registry.resolvers.size).toBe(2); + expect(registry.resolvers.get('Query.getPost')).toEqual({ + fieldName: 'getPost', + typeName: 'Query', + handler: originalHandler, + }); + expect(registry.resolvers.get('Mutation.getPost')).toEqual({ + fieldName: 'getPost', + typeName: 'Mutation', + handler: otherHandler, + }); + }); + + it('returns the cached route handler if already evaluated', () => { + // Prepare + const registry = getRegistry(); + registry.register({ + fieldName: 'getPost', + handler: vi.fn(), + typeName: 'Query', + }); + + // Act + registry.resolve('Query', 'getPost'); + registry.resolve('Query', 'getPost'); + + // Assess + expect(console.debug).toHaveBeenCalledTimes(2); // once for registration, once for resolution + expect(console.debug).toHaveBeenLastCalledWith( + "Resolving handler 'getPost' for type 'Query'" + ); + }); +}); From 3076c1cabc9362cde67c93ad4e294ffafc94d5b0 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Thu, 29 May 2025 17:53:46 +0600 Subject: [PATCH 14/29] feat: add unit tests for `Router` class to validate resolver registration and logging behavior --- .../tests/unit/appsync-graphql/Router.test.ts | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 packages/event-handler/tests/unit/appsync-graphql/Router.test.ts diff --git a/packages/event-handler/tests/unit/appsync-graphql/Router.test.ts b/packages/event-handler/tests/unit/appsync-graphql/Router.test.ts new file mode 100644 index 000000000..c94698115 --- /dev/null +++ b/packages/event-handler/tests/unit/appsync-graphql/Router.test.ts @@ -0,0 +1,112 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { Router } from '../../../src/appsync-graphql/index.js'; + +describe('Class: Router', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('registers resolvers using the functional approach', () => { + // Prepare + const router = new Router({ logger: console }); + const getPost = vi.fn(() => [true]); + const addPost = vi.fn(async () => true); + + // Act + router.onQuery('getPost', getPost, { typeName: 'Query' }); + router.onMutation('addPost', addPost, { typeName: 'Mutation' }); + + // Assess + expect(console.debug).toHaveBeenNthCalledWith( + 1, + `Registering onQuery route handler for field 'getPost' with type 'Query'` + ); + expect(console.debug).toHaveBeenNthCalledWith( + 2, + `Registering onMutation route handler for field 'addPost' with type 'Mutation'` + ); + }); + + it('registers resolvers using the decorator pattern', () => { + // Prepare + const router = new Router({ logger: console }); + + // Act + class Lambda { + readonly prop = 'value'; + + @router.onQuery('getPost') + public getPost() { + return `${this.prop} foo`; + } + + @router.onQuery('getAuthor', { typeName: 'Query' }) + public getAuthor() { + return `${this.prop} bar`; + } + + @router.onMutation('addPost') + public addPost() { + return `${this.prop} bar`; + } + + @router.onMutation('updatePost', { typeName: 'Mutation' }) + public updatePost() { + return `${this.prop} baz`; + } + } + const lambda = new Lambda(); + const res1 = lambda.getPost(); + const res2 = lambda.getAuthor(); + const res3 = lambda.addPost(); + const res4 = lambda.updatePost(); + + // Assess + expect(console.debug).toHaveBeenNthCalledWith( + 1, + `Registering onQuery route handler for field 'getPost' with type 'Query'` + ); + expect(console.debug).toHaveBeenNthCalledWith( + 2, + `Registering onQuery route handler for field 'getAuthor' with type 'Query'` + ); + expect(console.debug).toHaveBeenNthCalledWith( + 3, + `Registering onMutation route handler for field 'addPost' with type 'Mutation'` + ); + expect(console.debug).toHaveBeenNthCalledWith( + 4, + `Registering onMutation route handler for field 'updatePost' with type 'Mutation'` + ); + + // verify that class scope is preserved after decorating + expect(res1).toBe('value foo'); + expect(res2).toBe('value bar'); + expect(res3).toBe('value bar'); + expect(res4).toBe('value baz'); + }); + + it('uses a default logger with only warnings if none is provided', () => { + // Prepare + const router = new Router(); + + // Act + router.onQuery('getPost', vi.fn()); + + // Assess + expect(console.debug).not.toHaveBeenCalled(); + }); + + it('emits debug messages when ALC_LOG_LEVEL is set to DEBUG', () => { + // Prepare + process.env.AWS_LAMBDA_LOG_LEVEL = 'DEBUG'; + const router = new Router(); + + // Act + router.onQuery('getPost', vi.fn()); + + // Assess + expect(console.debug).toHaveBeenCalled(); + process.env.AWS_LAMBDA_LOG_LEVEL = undefined; + }); +}); From ff85c2431f12e3d35dab0b7751b5e8c3415bf833 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Thu, 29 May 2025 18:21:51 +0600 Subject: [PATCH 15/29] feat: add test for nested resolvers registration using the decorator pattern in `Router` class --- .../tests/unit/appsync-graphql/Router.test.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/packages/event-handler/tests/unit/appsync-graphql/Router.test.ts b/packages/event-handler/tests/unit/appsync-graphql/Router.test.ts index c94698115..888b7f2da 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/Router.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/Router.test.ts @@ -86,6 +86,38 @@ describe('Class: Router', () => { expect(res4).toBe('value baz'); }); + it('registers nested resolvers using the decorator pattern', () => { + // Prepare + const router = new Router({ logger: console }); + + // Act + class Lambda { + readonly prop = 'value'; + + @router.onQuery('listLocations') + @router.onQuery('locations') + public getLocations() { + return [{ name: 'Location 1', description: 'Description 1' }]; + } + } + const lambda = new Lambda(); + const response = lambda.getLocations(); + + // Assess + expect(console.debug).toHaveBeenNthCalledWith( + 1, + `Registering onQuery route handler for field 'locations' with type 'Query'` + ); + expect(console.debug).toHaveBeenNthCalledWith( + 2, + `Registering onQuery route handler for field 'listLocations' with type 'Query'` + ); + + expect(response).toEqual([ + { name: 'Location 1', description: 'Description 1' }, + ]); + }); + it('uses a default logger with only warnings if none is provided', () => { // Prepare const router = new Router(); From 0d5c893a6ac085750d5dff520109b852d5a1e988 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 1 Jun 2025 11:37:14 +0600 Subject: [PATCH 16/29] feat: enhance documentation for `resolve` method in `AppSyncGraphQLResolver` class with examples and parameter descriptions --- .../appsync-graphql/AppSyncGraphQLResolver.ts | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts index 503c3fb48..b9c31a457 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -31,6 +31,59 @@ import { isAppSyncGraphQLEvent } from './utils.js'; * ``` */ export class AppSyncGraphQLResolver extends Router { + /** + * Resolve the response based on the provided event and route handlers configured. + * + * @example + * ```ts + * import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; + * + * const app = new AppSyncGraphQLResolver(); + * + * app.onQuery('getPost', async ({ id }) => { + * // your business logic here + * return { + * id, + * title: 'Post Title', + * content: 'Post Content', + * }; + * }); + * + * export const handler = async (event, context) => + * app.resolve(event, context); + * ``` + * + * The method works also as class method decorator, so you can use it like this: + * + * @example + * ```ts + * import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; + * + * const app = new AppSyncGraphQLResolver(); + * + * class Lambda { + * ⁣@app.onQuery('getPost') + * async handleGetPost({ id }) { + * // your business logic here + * return { + * id, + * title: 'Post Title', + * content: 'Post Content', + * }; + * } + * + * async handler(event, context) { + * return app.resolve(event, context); + * } + * } + * + * const lambda = new Lambda(); + * export const handler = lambda.handler.bind(lambda); + * ``` + * + * @param event - The incoming event, which may be an AppSync GraphQL event or an array of events. + * @param context - The Lambda execution context. + */ public async resolve(event: unknown, context: Context): Promise { if (Array.isArray(event)) { this.logger.warn('Batch resolvers are not implemented yet'); From 700c779f02c7695674eeb71b03ead9ea5f0a6ce9 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 1 Jun 2025 11:38:16 +0600 Subject: [PATCH 17/29] chore: warning message for batch resolver --- .../event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts index b9c31a457..b3b028c82 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -86,7 +86,7 @@ export class AppSyncGraphQLResolver extends Router { */ public async resolve(event: unknown, context: Context): Promise { if (Array.isArray(event)) { - this.logger.warn('Batch resolvers are not implemented yet'); + this.logger.warn('Batch resolver is not implemented yet'); return; } if (!isAppSyncGraphQLEvent(event)) { From 55809235e9765d6bb9d391b45a1f2b426c9bafbd Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 1 Jun 2025 11:42:19 +0600 Subject: [PATCH 18/29] fix: return query handler if found --- .../src/appsync-graphql/AppSyncGraphQLResolver.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts index b3b028c82..00a959dd3 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -124,14 +124,14 @@ export class AppSyncGraphQLResolver extends Router { typeName, fieldName ); + if (queryHandlerOptions) { + return await queryHandlerOptions.handler.apply(this, [event.arguments]); + } + const mutationHandlerOptions = this.onMutationRegistry.resolve( typeName, fieldName ); - - if (queryHandlerOptions) { - return await queryHandlerOptions.handler.apply(this, [event.arguments]); - } if (mutationHandlerOptions) { return await mutationHandlerOptions.handler.apply(this, [ event.arguments, From 7e8aa10160390ff77b3e22db71bcb42293e0fde2 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 1 Jun 2025 11:46:44 +0600 Subject: [PATCH 19/29] fix: correct warning message for batch resolver in AppSyncGraphQLResolver tests --- .../tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts index abfed9eec..817c703b0 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts @@ -21,7 +21,7 @@ describe('Class: AppSyncGraphQLResolver', () => { // Assess expect(console.warn).toHaveBeenCalledWith( - 'Batch resolvers are not implemented yet' + 'Batch resolver is not implemented yet' ); expect(result).toBeUndefined(); }); From 80a11ff7e16f02e736461f20771294a97f7f43c8 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 1 Jun 2025 12:01:03 +0600 Subject: [PATCH 20/29] fix: update debug messages to reflect resolver registration format in RouteHandlerRegistry and Router tests --- .../src/appsync-graphql/RouteHandlerRegistry.ts | 4 ++-- .../tests/unit/appsync-graphql/Router.test.ts | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts b/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts index 4f484e7b6..394b7d898 100644 --- a/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts +++ b/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts @@ -47,14 +47,14 @@ class RouteHandlerRegistry { * * @param options - The options for registering the route handler, including the GraphQL type name, field name, and the handler function. * @param options.fieldName - The field name of the GraphQL type to be registered - * @param options.handler - The handler function to be called when the event is received + * @param options.handler - The handler function to be called when the GraphQL event is received * @param options.typeName - The name of the GraphQL type to be registered * */ public register(options: RouteHandlerOptions): void { const { fieldName, handler, typeName } = options; this.#logger.debug( - `Registering ${this.#eventType} route handler for field '${fieldName}' with type '${typeName}'` + `Adding resolver ${handler.name} for field ${typeName}.${fieldName}` ); const cacheKey = this.#makeKey(typeName, fieldName); if (this.resolvers.has(cacheKey)) { diff --git a/packages/event-handler/tests/unit/appsync-graphql/Router.test.ts b/packages/event-handler/tests/unit/appsync-graphql/Router.test.ts index 888b7f2da..e8e3738ce 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/Router.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/Router.test.ts @@ -19,11 +19,11 @@ describe('Class: Router', () => { // Assess expect(console.debug).toHaveBeenNthCalledWith( 1, - `Registering onQuery route handler for field 'getPost' with type 'Query'` + `Adding resolver ${getPost.name} for field Query.getPost` ); expect(console.debug).toHaveBeenNthCalledWith( 2, - `Registering onMutation route handler for field 'addPost' with type 'Mutation'` + `Adding resolver ${addPost.name} for field Mutation.addPost` ); }); @@ -64,19 +64,19 @@ describe('Class: Router', () => { // Assess expect(console.debug).toHaveBeenNthCalledWith( 1, - `Registering onQuery route handler for field 'getPost' with type 'Query'` + 'Adding resolver getPost for field Query.getPost' ); expect(console.debug).toHaveBeenNthCalledWith( 2, - `Registering onQuery route handler for field 'getAuthor' with type 'Query'` + 'Adding resolver getAuthor for field Query.getAuthor' ); expect(console.debug).toHaveBeenNthCalledWith( 3, - `Registering onMutation route handler for field 'addPost' with type 'Mutation'` + 'Adding resolver addPost for field Mutation.addPost' ); expect(console.debug).toHaveBeenNthCalledWith( 4, - `Registering onMutation route handler for field 'updatePost' with type 'Mutation'` + 'Adding resolver updatePost for field Mutation.updatePost' ); // verify that class scope is preserved after decorating @@ -106,11 +106,11 @@ describe('Class: Router', () => { // Assess expect(console.debug).toHaveBeenNthCalledWith( 1, - `Registering onQuery route handler for field 'locations' with type 'Query'` + 'Adding resolver getLocations for field Query.locations' ); expect(console.debug).toHaveBeenNthCalledWith( 2, - `Registering onQuery route handler for field 'listLocations' with type 'Query'` + 'Adding resolver getLocations for field Query.listLocations' ); expect(response).toEqual([ From e8b0db76a47fbdadd978a8d48970d4731665191c Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 1 Jun 2025 12:33:48 +0600 Subject: [PATCH 21/29] fix: update resolver not found messages for consistency in AppSyncGraphQLResolver and RouteHandlerRegistry --- .../src/appsync-graphql/AppSyncGraphQLResolver.ts | 2 +- .../src/appsync-graphql/RouteHandlerRegistry.ts | 9 +++------ .../unit/appsync-graphql/AppSyncGraphQLResolver.test.ts | 8 ++------ .../unit/appsync-graphql/RouteHandlerRegistry.test.ts | 2 +- 4 files changed, 7 insertions(+), 14 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts index 00a959dd3..86f38ef20 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -139,7 +139,7 @@ export class AppSyncGraphQLResolver extends Router { } throw new ResolverNotFoundException( - `No resolver found for the event ${fieldName}-${typeName}.` + `No resolver found for ${typeName}-${fieldName}` ); } diff --git a/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts b/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts index 394b7d898..37bb5d58f 100644 --- a/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts +++ b/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts @@ -84,18 +84,15 @@ class RouteHandlerRegistry { fieldName: string ): RouteHandlerOptions | undefined { const cacheKey = this.#makeKey(typeName, fieldName); - if (this.#resolverCache.has(cacheKey)) { + if (this.#resolverCache.has(cacheKey)) return this.#resolverCache.get(cacheKey); - } this.#logger.debug( - `Resolving handler '${fieldName}' for type '${typeName}'` + `Looking for resolver for type=${typeName}, field=${fieldName}` ); const handler = this.resolvers.get(cacheKey); if (handler === undefined) { if (!this.#warningSet.has(cacheKey)) { - this.#logger.warn( - `No route handler found for field '${fieldName}' registered for ${this.#eventType}.` - ); + this.#logger.warn(`No resolver found for ${typeName}-${fieldName}`); this.#warningSet.add(cacheKey); } return undefined; 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 817c703b0..c707a1cba 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts @@ -48,9 +48,7 @@ describe('Class: AppSyncGraphQLResolver', () => { await expect( app.resolve(onQueryEventFactory('getPost'), context) ).rejects.toThrow( - new ResolverNotFoundException( - 'No resolver found for the event getPost-Query.' - ) + new ResolverNotFoundException('No resolver found for Query-getPost') ); expect(console.error).toHaveBeenCalled(); }); @@ -63,9 +61,7 @@ describe('Class: AppSyncGraphQLResolver', () => { await expect( app.resolve(onMutationEventFactory('addPost'), context) ).rejects.toThrow( - new ResolverNotFoundException( - 'No resolver found for the event addPost-Mutation.' - ) + new ResolverNotFoundException('No resolver found for Mutation-addPost') ); expect(console.error).toHaveBeenCalled(); }); diff --git a/packages/event-handler/tests/unit/appsync-graphql/RouteHandlerRegistry.test.ts b/packages/event-handler/tests/unit/appsync-graphql/RouteHandlerRegistry.test.ts index 0bc956e40..3cf40f2e5 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/RouteHandlerRegistry.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/RouteHandlerRegistry.test.ts @@ -112,7 +112,7 @@ describe('Class: RouteHandlerRegistry', () => { // Assess expect(console.debug).toHaveBeenCalledTimes(2); // once for registration, once for resolution expect(console.debug).toHaveBeenLastCalledWith( - "Resolving handler 'getPost' for type 'Query'" + 'Looking for resolver for type=Query, field=getPost' ); }); }); From 65c8ee2f30b78bb2eea77d54e2e801e932ab6b99 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 1 Jun 2025 12:41:31 +0600 Subject: [PATCH 22/29] fix: doc for Router --- packages/event-handler/src/appsync-graphql/Router.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/Router.ts b/packages/event-handler/src/appsync-graphql/Router.ts index 548b3d1e5..71e99a990 100644 --- a/packages/event-handler/src/appsync-graphql/Router.ts +++ b/packages/event-handler/src/appsync-graphql/Router.ts @@ -12,15 +12,15 @@ import type { import { RouteHandlerRegistry } from './RouteHandlerRegistry.js'; /** - * Class for registering routes for the `onQuery` and `onMutation` events in AWS AppSync Events APIs. + * Class for registering routes for the `query` and `mutation` events in AWS AppSync GraphQL APIs. */ class Router { /** - * A map of registered routes for the `onQuery` event, keyed by their fieldNames. + * A map of registered routes for the `query` event, keyed by their fieldNames. */ protected readonly onQueryRegistry: RouteHandlerRegistry; /** - * A map of registered routes for the `onMutation` event, keyed by their fieldNames. + * A map of registered routes for the `mutation` event, keyed by their fieldNames. */ protected readonly onMutationRegistry: RouteHandlerRegistry; /** @@ -58,7 +58,7 @@ class Router { } /** - * Register a handler function for the `onQuery` event. + * Register a handler function for the `query` event. * Registers a handler for a specific GraphQL Query field. The handler will be invoked when a request is made * for the specified field in the Query type. @@ -142,7 +142,7 @@ class Router { } /** - * Register a handler function for the `onMutation` event. + * Register a handler function for the `mutation` event. * * Registers a handler for a specific GraphQL Mutation field. The handler will be invoked when a request is made * for the specified field in the Mutation type. From f1b545e210ebef335b07d8923de7a27dcec8c45e Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 1 Jun 2025 13:48:04 +0600 Subject: [PATCH 23/29] refactor: remove unused cache and warning set from `RouteHandlerRegistry` --- .../appsync-graphql/RouteHandlerRegistry.ts | 27 +------------------ .../RouteHandlerRegistry.test.ts | 15 ++++++++--- 2 files changed, 13 insertions(+), 29 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts b/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts index 37bb5d58f..f5449a497 100644 --- a/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts +++ b/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts @@ -1,4 +1,3 @@ -import { LRUCache } from '@aws-lambda-powertools/commons/utils/lru-cache'; import type { GenericLogger, RouteHandlerOptions, @@ -24,18 +23,6 @@ class RouteHandlerRegistry { * The event type stored in the registry. */ readonly #eventType: 'onQuery' | 'onMutation'; - /** - * A cache for storing the resolved route handlers. - */ - readonly #resolverCache: LRUCache = new LRUCache( - { - maxSize: 100, - } - ); - /** - * A set of warning messages to avoid duplicate warnings. - */ - readonly #warningSet: Set = new Set(); public constructor(options: RouteHandlerRegistryOptions) { this.#logger = options.logger; @@ -83,22 +70,10 @@ class RouteHandlerRegistry { typeName: string, fieldName: string ): RouteHandlerOptions | undefined { - const cacheKey = this.#makeKey(typeName, fieldName); - if (this.#resolverCache.has(cacheKey)) - return this.#resolverCache.get(cacheKey); this.#logger.debug( `Looking for resolver for type=${typeName}, field=${fieldName}` ); - const handler = this.resolvers.get(cacheKey); - if (handler === undefined) { - if (!this.#warningSet.has(cacheKey)) { - this.#logger.warn(`No resolver found for ${typeName}-${fieldName}`); - this.#warningSet.add(cacheKey); - } - return undefined; - } - this.#resolverCache.add(cacheKey, handler); - return handler; + return this.resolvers.get(this.#makeKey(typeName, fieldName)); } /** diff --git a/packages/event-handler/tests/unit/appsync-graphql/RouteHandlerRegistry.test.ts b/packages/event-handler/tests/unit/appsync-graphql/RouteHandlerRegistry.test.ts index 3cf40f2e5..a0fcff6f7 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/RouteHandlerRegistry.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/RouteHandlerRegistry.test.ts @@ -99,9 +99,10 @@ describe('Class: RouteHandlerRegistry', () => { it('returns the cached route handler if already evaluated', () => { // Prepare const registry = getRegistry(); + const handler = vi.fn(); registry.register({ fieldName: 'getPost', - handler: vi.fn(), + handler, typeName: 'Query', }); @@ -110,8 +111,16 @@ describe('Class: RouteHandlerRegistry', () => { registry.resolve('Query', 'getPost'); // Assess - expect(console.debug).toHaveBeenCalledTimes(2); // once for registration, once for resolution - expect(console.debug).toHaveBeenLastCalledWith( + expect(console.debug).toHaveBeenNthCalledWith( + 1, + `Adding resolver ${handler.name} for field Query.getPost` + ); + expect(console.debug).toHaveBeenNthCalledWith( + 2, + 'Looking for resolver for type=Query, field=getPost' + ); + expect(console.debug).toHaveBeenNthCalledWith( + 3, 'Looking for resolver for type=Query, field=getPost' ); }); From 11a04432ffd696db15da828d61d6d2a72fff1e57 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 1 Jun 2025 13:52:27 +0600 Subject: [PATCH 24/29] fix: update documentation for resolve method in RouteHandlerRegistry --- .../src/appsync-graphql/RouteHandlerRegistry.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts b/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts index f5449a497..53e94354c 100644 --- a/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts +++ b/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts @@ -59,12 +59,8 @@ class RouteHandlerRegistry { /** * Resolves the handler for a specific GraphQL API event. * - * This method first checks an internal cache for the handler. If not found, it attempts to retrieve - * the handler from the registered resolvers. If the handler is still not found, a warning is logged - * (only once per missing handler), and `undefined` is returned. - * - * @param typeName - The name of the GraphQL type. - * @param fieldName - The name of the field within the GraphQL type. + * @param typeName - The name of the GraphQL type (e.g., "Query", "Mutation", or a custom type). + * @param fieldName - The name of the field within the specified type. */ public resolve( typeName: string, From 0e9d2ca3ea1e301dd9fa56d84a7e2b82be093eb1 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 1 Jun 2025 13:53:38 +0600 Subject: [PATCH 25/29] refactor: remove redundant test for cached route handler evaluation in RouteHandlerRegistry --- .../RouteHandlerRegistry.test.ts | 29 ------------------- 1 file changed, 29 deletions(-) diff --git a/packages/event-handler/tests/unit/appsync-graphql/RouteHandlerRegistry.test.ts b/packages/event-handler/tests/unit/appsync-graphql/RouteHandlerRegistry.test.ts index a0fcff6f7..134f90cdd 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/RouteHandlerRegistry.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/RouteHandlerRegistry.test.ts @@ -95,33 +95,4 @@ describe('Class: RouteHandlerRegistry', () => { handler: otherHandler, }); }); - - it('returns the cached route handler if already evaluated', () => { - // Prepare - const registry = getRegistry(); - const handler = vi.fn(); - registry.register({ - fieldName: 'getPost', - handler, - typeName: 'Query', - }); - - // Act - registry.resolve('Query', 'getPost'); - registry.resolve('Query', 'getPost'); - - // Assess - expect(console.debug).toHaveBeenNthCalledWith( - 1, - `Adding resolver ${handler.name} for field Query.getPost` - ); - expect(console.debug).toHaveBeenNthCalledWith( - 2, - 'Looking for resolver for type=Query, field=getPost' - ); - expect(console.debug).toHaveBeenNthCalledWith( - 3, - 'Looking for resolver for type=Query, field=getPost' - ); - }); }); From 99531aaef9502bd466097e78712d3986f0f1399e Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 1 Jun 2025 13:58:27 +0600 Subject: [PATCH 26/29] fix: update import path for Router in Router.test.ts --- .../event-handler/tests/unit/appsync-graphql/Router.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/event-handler/tests/unit/appsync-graphql/Router.test.ts b/packages/event-handler/tests/unit/appsync-graphql/Router.test.ts index e8e3738ce..e9db3b2e4 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/Router.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/Router.test.ts @@ -1,5 +1,5 @@ +import { Router } from 'src/appsync-graphql/Router.js'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { Router } from '../../../src/appsync-graphql/index.js'; describe('Class: Router', () => { beforeEach(() => { From 1342ca4690e00116f376dad558c135dff6fccc9a Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 1 Jun 2025 14:10:01 +0600 Subject: [PATCH 27/29] fix: update debug messages to include event type in RouteHandlerRegistry and Router --- .../src/appsync-graphql/RouteHandlerRegistry.ts | 4 ++-- .../AppSyncGraphQLResolver.test.ts | 12 ++++++++++++ .../tests/unit/appsync-graphql/Router.test.ts | 16 ++++++++-------- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts b/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts index 53e94354c..f47a6627e 100644 --- a/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts +++ b/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts @@ -41,7 +41,7 @@ class RouteHandlerRegistry { public register(options: RouteHandlerOptions): void { const { fieldName, handler, typeName } = options; this.#logger.debug( - `Adding resolver ${handler.name} for field ${typeName}.${fieldName}` + `Adding ${this.#eventType} resolver for field ${typeName}.${fieldName}` ); const cacheKey = this.#makeKey(typeName, fieldName); if (this.resolvers.has(cacheKey)) { @@ -67,7 +67,7 @@ class RouteHandlerRegistry { fieldName: string ): RouteHandlerOptions | undefined { this.#logger.debug( - `Looking for resolver for type=${typeName}, field=${fieldName}` + `Looking for ${this.#eventType} resolver for type=${typeName}, field=${fieldName}` ); return this.resolvers.get(this.#makeKey(typeName, fieldName)); } 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 c707a1cba..cd781224e 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts @@ -112,6 +112,18 @@ describe('Class: AppSyncGraphQLResolver', () => { ); // Assess + expect(console.debug).toHaveBeenNthCalledWith( + 1, + 'Adding onMutation resolver for field Mutation.addPost' + ); + expect(console.debug).toHaveBeenNthCalledWith( + 2, + 'Looking for onQuery resolver for type=Mutation, field=addPost' + ); + expect(console.debug).toHaveBeenNthCalledWith( + 3, + 'Looking for onMutation resolver for type=Mutation, field=addPost' + ); expect(result).toEqual({ id: '123', title: 'Post Title', diff --git a/packages/event-handler/tests/unit/appsync-graphql/Router.test.ts b/packages/event-handler/tests/unit/appsync-graphql/Router.test.ts index e9db3b2e4..86878bebf 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/Router.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/Router.test.ts @@ -19,11 +19,11 @@ describe('Class: Router', () => { // Assess expect(console.debug).toHaveBeenNthCalledWith( 1, - `Adding resolver ${getPost.name} for field Query.getPost` + 'Adding onQuery resolver for field Query.getPost' ); expect(console.debug).toHaveBeenNthCalledWith( 2, - `Adding resolver ${addPost.name} for field Mutation.addPost` + 'Adding onMutation resolver for field Mutation.addPost' ); }); @@ -64,19 +64,19 @@ describe('Class: Router', () => { // Assess expect(console.debug).toHaveBeenNthCalledWith( 1, - 'Adding resolver getPost for field Query.getPost' + 'Adding onQuery resolver for field Query.getPost' ); expect(console.debug).toHaveBeenNthCalledWith( 2, - 'Adding resolver getAuthor for field Query.getAuthor' + 'Adding onQuery resolver for field Query.getAuthor' ); expect(console.debug).toHaveBeenNthCalledWith( 3, - 'Adding resolver addPost for field Mutation.addPost' + 'Adding onMutation resolver for field Mutation.addPost' ); expect(console.debug).toHaveBeenNthCalledWith( 4, - 'Adding resolver updatePost for field Mutation.updatePost' + 'Adding onMutation resolver for field Mutation.updatePost' ); // verify that class scope is preserved after decorating @@ -106,11 +106,11 @@ describe('Class: Router', () => { // Assess expect(console.debug).toHaveBeenNthCalledWith( 1, - 'Adding resolver getLocations for field Query.locations' + 'Adding onQuery resolver for field Query.locations' ); expect(console.debug).toHaveBeenNthCalledWith( 2, - 'Adding resolver getLocations for field Query.listLocations' + 'Adding onQuery resolver for field Query.listLocations' ); expect(response).toEqual([ From e5454c928b6548766d00fd3c1fa380b29a5866cb Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 1 Jun 2025 14:19:39 +0600 Subject: [PATCH 28/29] fix: update terminology from "handler" to "resolver" in RouteHandlerRegistry and related tests --- .../src/appsync-graphql/RouteHandlerRegistry.ts | 4 ++-- .../tests/unit/appsync-graphql/RouteHandlerRegistry.test.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts b/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts index f47a6627e..78dcf9264 100644 --- a/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts +++ b/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts @@ -30,7 +30,7 @@ class RouteHandlerRegistry { } /** - * Registers a new GraphQL route handler for a specific type and field. + * Registers a new GraphQL route resolver for a specific type and field. * * @param options - The options for registering the route handler, including the GraphQL type name, field name, and the handler function. * @param options.fieldName - The field name of the GraphQL type to be registered @@ -46,7 +46,7 @@ class RouteHandlerRegistry { const cacheKey = this.#makeKey(typeName, fieldName); if (this.resolvers.has(cacheKey)) { this.#logger.warn( - `A route handler for field '${fieldName}' is already registered for '${typeName}'. The previous handler will be replaced.` + `A resolver for field '${fieldName}' is already registered for '${typeName}'. The previous resolver will be replaced.` ); } this.resolvers.set(cacheKey, { diff --git a/packages/event-handler/tests/unit/appsync-graphql/RouteHandlerRegistry.test.ts b/packages/event-handler/tests/unit/appsync-graphql/RouteHandlerRegistry.test.ts index 134f90cdd..b333b91d4 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/RouteHandlerRegistry.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/RouteHandlerRegistry.test.ts @@ -34,7 +34,7 @@ describe('Class: RouteHandlerRegistry', () => { } ); - it('logs a warning and replaces the previous handler if the field & type is already registered', () => { + it('logs a warning and replaces the previous resolver if the field & type is already registered', () => { // Prepare const registry = getRegistry(); const originalHandler = vi.fn(); @@ -60,11 +60,11 @@ describe('Class: RouteHandlerRegistry', () => { handler: otherHandler, }); expect(console.warn).toHaveBeenCalledWith( - "A route handler for field 'getPost' is already registered for 'Query'. The previous handler will be replaced." + "A resolver for field 'getPost' is already registered for 'Query'. The previous resolver will be replaced." ); }); - it('will not replace the handler if the event type is different', () => { + it('will not replace the resolver if the event type is different', () => { // Prepare const registry = getRegistry(); const originalHandler = vi.fn(); From 881138fb0cdb9cb84862d31f30da401c45e3383f Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Thu, 5 Jun 2025 12:04:16 +0600 Subject: [PATCH 29/29] fix: refactor logger initialization and import structure in Router and related types --- .../event-handler/src/appsync-graphql/Router.ts | 8 ++++++-- .../event-handler/src/types/appsync-graphql.ts | 15 +-------------- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/packages/event-handler/src/appsync-graphql/Router.ts b/packages/event-handler/src/appsync-graphql/Router.ts index 71e99a990..8bf30b5d6 100644 --- a/packages/event-handler/src/appsync-graphql/Router.ts +++ b/packages/event-handler/src/appsync-graphql/Router.ts @@ -2,8 +2,9 @@ import { EnvironmentVariablesService, isRecord, } from '@aws-lambda-powertools/commons'; +import type { GenericLogger } from '@aws-lambda-powertools/commons/types'; +import { getStringFromEnv } from '@aws-lambda-powertools/commons/utils/env'; import type { - GenericLogger, GraphQlRouteOptions, GraphQlRouterOptions, OnMutationHandler, @@ -40,7 +41,10 @@ class Router { public constructor(options?: GraphQlRouterOptions) { this.envService = new EnvironmentVariablesService(); - const alcLogLevel = this.envService.get('AWS_LAMBDA_LOG_LEVEL'); + const alcLogLevel = getStringFromEnv({ + key: 'AWS_LAMBDA_LOG_LEVEL', + defaultValue: '', + }); this.logger = options?.logger ?? { debug: alcLogLevel === 'DEBUG' ? console.debug : () => undefined, error: console.error, diff --git a/packages/event-handler/src/types/appsync-graphql.ts b/packages/event-handler/src/types/appsync-graphql.ts index 331d5d424..ad9b81d42 100644 --- a/packages/event-handler/src/types/appsync-graphql.ts +++ b/packages/event-handler/src/types/appsync-graphql.ts @@ -1,18 +1,5 @@ import type { RouteHandlerRegistry } from '../appsync-graphql/RouteHandlerRegistry.js'; - -// biome-ignore lint/suspicious/noExplicitAny: We intentionally use `any` here to represent any type of data and keep the logger is as flexible as possible. -type Anything = any; - -/** - * Interface for a generic logger object. - */ -type GenericLogger = { - trace?: (...content: Anything[]) => void; - debug: (...content: Anything[]) => void; - info?: (...content: Anything[]) => void; - warn: (...content: Anything[]) => void; - error: (...content: Anything[]) => void; -}; +import type { Anything, GenericLogger } from './common.js'; // #region OnQuery fn