diff --git a/packages/event-handler/package.json b/packages/event-handler/package.json index 10a440799a..19fea06316 100644 --- a/packages/event-handler/package.json +++ b/packages/event-handler/package.json @@ -39,6 +39,16 @@ "default": "./lib/esm/appsync-events/index.js" } }, + "./appsync-graphql": { + "require": { + "types": "./lib/cjs/appsync-graphql/index.d.ts", + "default": "./lib/cjs/appsync-graphql/index.js" + }, + "import": { + "types": "./lib/esm/appsync-graphql/index.d.ts", + "default": "./lib/esm/appsync-graphql/index.js" + } + }, "./bedrock-agent": { "require": { "types": "./lib/cjs/bedrock-agent/index.d.ts", @@ -66,6 +76,10 @@ "./lib/cjs/appsync-events/index.d.ts", "./lib/esm/appsync-events/index.d.ts" ], + "appsync-graphql": [ + "./lib/cjs/appsync-graphql/index.d.ts", + "./lib/esm/appsync-graphql/index.d.ts" + ], "bedrock-agent": [ "./lib/cjs/bedrock-agent/index.d.ts", "./lib/esm/bedrock-agent/index.d.ts" diff --git a/packages/event-handler/src/appsync-events/AppSyncEventsResolver.ts b/packages/event-handler/src/appsync-events/AppSyncEventsResolver.ts index d889361104..4e1e06b1c5 100644 --- a/packages/event-handler/src/appsync-events/AppSyncEventsResolver.ts +++ b/packages/event-handler/src/appsync-events/AppSyncEventsResolver.ts @@ -5,8 +5,8 @@ import type { OnPublishHandlerAggregateFn, OnPublishHandlerFn, OnSubscribeHandler, - ResolveOptions, } from '../types/appsync-events.js'; +import type { ResolveOptions } from '../types/common.js'; import { Router } from './Router.js'; import { UnauthorizedException } from './errors.js'; import { isAppSyncEventsEvent, isAppSyncEventsPublishEvent } from './utils.js'; diff --git a/packages/event-handler/src/appsync-events/RouteHandlerRegistry.ts b/packages/event-handler/src/appsync-events/RouteHandlerRegistry.ts index 7632563d4d..f6cbf9ebe2 100644 --- a/packages/event-handler/src/appsync-events/RouteHandlerRegistry.ts +++ b/packages/event-handler/src/appsync-events/RouteHandlerRegistry.ts @@ -1,6 +1,6 @@ +import type { GenericLogger } from '@aws-lambda-powertools/commons/types'; import { LRUCache } from '@aws-lambda-powertools/commons/utils/lru-cache'; import type { - GenericLogger, RouteHandlerOptions, RouteHandlerRegistryOptions, } from '../types/appsync-events.js'; @@ -21,7 +21,7 @@ class RouteHandlerRegistry { /** * A logger instance to be used for logging debug and warning messages. */ - readonly #logger: GenericLogger; + readonly #logger: Pick; /** * The event type stored in the registry. */ diff --git a/packages/event-handler/src/appsync-events/Router.ts b/packages/event-handler/src/appsync-events/Router.ts index 7326fc1319..feb70526ef 100644 --- a/packages/event-handler/src/appsync-events/Router.ts +++ b/packages/event-handler/src/appsync-events/Router.ts @@ -1,7 +1,7 @@ import { EnvironmentVariablesService } from '@aws-lambda-powertools/commons'; +import type { GenericLogger } from '@aws-lambda-powertools/commons/types'; import { isRecord } from '@aws-lambda-powertools/commons/typeutils'; import type { - GenericLogger, OnPublishHandler, OnSubscribeHandler, RouteOptions, @@ -9,9 +9,6 @@ import type { } from '../types/appsync-events.js'; import { RouteHandlerRegistry } from './RouteHandlerRegistry.js'; -// Simple global approach - store the last instance per router -const routerInstanceMap = new WeakMap(); - /** * Class for registering routes for the `onPublish` and `onSubscribe` events in AWS AppSync Events APIs. */ diff --git a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts index 6e6cab9fac..1abdbf18c2 100644 --- a/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts +++ b/packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts @@ -1,5 +1,5 @@ import type { AppSyncResolverEvent, Context } from 'aws-lambda'; -import type { ResolveOptions } from '../types/appsync-graphql.js'; +import type { ResolveOptions } from '../types/common.js'; import { Router } from './Router.js'; import { ResolverNotFoundException } from './errors.js'; import { isAppSyncGraphQLEvent } from './utils.js'; @@ -32,7 +32,7 @@ import { isAppSyncGraphQLEvent } from './utils.js'; * app.resolve(event, context); * ``` */ -export class AppSyncGraphQLResolver extends Router { +class AppSyncGraphQLResolver extends Router { /** * Resolve the response based on the provided event and route handlers configured. * @@ -89,7 +89,7 @@ export class AppSyncGraphQLResolver extends Router { * ``` * * @param event - The incoming event, which may be an AppSync GraphQL event or an array of events. - * @param context - The Lambda execution context. + * @param context - The AWS Lambda context object. * @param options - Optional parameters for the resolver, such as the scope of the handler. */ public async resolve( @@ -171,3 +171,5 @@ export class AppSyncGraphQLResolver extends Router { }; } } + +export { AppSyncGraphQLResolver }; diff --git a/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts b/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts index fde0969b66..7113ccfd80 100644 --- a/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts +++ b/packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts @@ -1,14 +1,15 @@ +import type { GenericLogger } from '@aws-lambda-powertools/commons/types'; import type { - GenericLogger, RouteHandlerOptions, RouteHandlerRegistryOptions, } from '../types/appsync-graphql.js'; +import type { AppSyncGraphQLResolver } from './AppSyncGraphQLResolver.js'; /** * Registry for storing route handlers for GraphQL resolvers 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. + * Instead, use the {@link AppSyncGraphQLResolver | `AppSyncGraphQLResolver`} class, which is the recommended way to register routes. */ class RouteHandlerRegistry { /** @@ -18,7 +19,7 @@ class RouteHandlerRegistry { /** * A logger instance to be used for logging debug and warning messages. */ - readonly #logger: GenericLogger; + readonly #logger: Pick; public constructor(options: RouteHandlerRegistryOptions) { this.#logger = options.logger; diff --git a/packages/event-handler/src/appsync-graphql/Router.ts b/packages/event-handler/src/appsync-graphql/Router.ts index 00f02aa3f5..b1d0feb4e6 100644 --- a/packages/event-handler/src/appsync-graphql/Router.ts +++ b/packages/event-handler/src/appsync-graphql/Router.ts @@ -106,7 +106,7 @@ class Router { * const app = new AppSyncGraphQLResolver(); * * class Lambda { - * @app.resolver({ fieldName: 'getPost' }) + * ⁣@app.resolver({ fieldName: 'getPost' }) * async handleGetPost(payload) { * // your business logic here * return payload; @@ -126,7 +126,7 @@ class Router { * @param handler - The handler function to be called when the event is received. * @param options - Route options including the required fieldName and optional typeName. * @param options.fieldName - The name of the field to register the handler for. - * @param options.typeName - The name of the GraphQL type to use for the resolver (defaults to 'Query'). + * @param options.typeName - The name of the GraphQL type to use for the resolver, defaults to `Query`. */ public resolver>( handler: ResolverHandler, @@ -134,30 +134,185 @@ class Router { ): void; public resolver(options: GraphQlRouteOptions): MethodDecorator; public resolver>( - handler: ResolverHandler | GraphQlRouteOptions, + handlerOrOptions: ResolverHandler | GraphQlRouteOptions, options?: GraphQlRouteOptions ): MethodDecorator | undefined { - if (typeof handler === 'function') { + if (typeof handlerOrOptions === 'function') { const resolverOptions = options as GraphQlRouteOptions; const { typeName = 'Query', fieldName } = resolverOptions; this.resolverRegistry.register({ fieldName, - handler: handler as ResolverHandler, + handler: handlerOrOptions as ResolverHandler, typeName, }); return; } - const resolverOptions = handler; - return (target, _propertyKey, descriptor: PropertyDescriptor) => { - const { typeName = 'Query', fieldName } = resolverOptions; + return (_target, _propertyKey, descriptor: PropertyDescriptor) => { + this.resolverRegistry.register({ + fieldName: handlerOrOptions.fieldName, + handler: descriptor?.value, + typeName: handlerOrOptions.typeName ?? 'Query', + }); + + return descriptor; + }; + } + /** + * 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. + * + * @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 handler function to be called when the event is received. + */ + public onQuery>( + fieldName: string, + handler: ResolverHandler + ): void; + public onQuery(fieldName: string): MethodDecorator; + public onQuery>( + fieldName: string, + handlerOrFieldName?: + | ResolverHandler + | Pick + ): MethodDecorator | undefined { + if (typeof handlerOrFieldName === 'function') { + this.resolverRegistry.register({ + fieldName: fieldName, + handler: handlerOrFieldName as ResolverHandler, + typeName: 'Query', + }); + + return; + } + + return (_target, _propertyKey, descriptor: PropertyDescriptor) => { + this.resolverRegistry.register({ + fieldName: fieldName, + handler: descriptor?.value, + typeName: 'Query', + }); + + return descriptor; + }; + } + + /** + * 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. + * + * @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. + */ + public onMutation>( + fieldName: string, + handler: ResolverHandler + ): void; + public onMutation(fieldName: string): MethodDecorator; + public onMutation>( + fieldName: string, + handlerOrFieldName?: ResolverHandler | string + ): MethodDecorator | undefined { + if (typeof handlerOrFieldName === 'function') { + this.resolverRegistry.register({ + fieldName, + handler: handlerOrFieldName as ResolverHandler, + typeName: 'Mutation', + }); + + return; + } + + return (_target, _propertyKey, descriptor: PropertyDescriptor) => { this.resolverRegistry.register({ fieldName, handler: descriptor?.value, - typeName, + typeName: 'Mutation', }); return descriptor; diff --git a/packages/event-handler/src/appsync-graphql/errors.ts b/packages/event-handler/src/appsync-graphql/errors.ts index b3f3c15b97..73825cf2b7 100644 --- a/packages/event-handler/src/appsync-graphql/errors.ts +++ b/packages/event-handler/src/appsync-graphql/errors.ts @@ -1,3 +1,6 @@ +/** + * Error thrown when a resolver is not found for a given field and type name in AppSync GraphQL. + */ class ResolverNotFoundException extends Error { constructor(message: string, options?: ErrorOptions) { super(message, options); diff --git a/packages/event-handler/src/appsync-graphql/index.ts b/packages/event-handler/src/appsync-graphql/index.ts new file mode 100644 index 0000000000..78092fbd57 --- /dev/null +++ b/packages/event-handler/src/appsync-graphql/index.ts @@ -0,0 +1,9 @@ +export { AppSyncGraphQLResolver } from './AppSyncGraphQLResolver.js'; +export { ResolverNotFoundException } from './errors.js'; +export { + makeId, + awsTimestamp, + awsDate, + awsTime, + awsDateTime, +} from './scalarTypesUtils.js'; diff --git a/packages/event-handler/src/appsync-graphql/scalarTypesUtils.ts b/packages/event-handler/src/appsync-graphql/scalarTypesUtils.ts index 1b7cce0a1b..8d3f02334c 100644 --- a/packages/event-handler/src/appsync-graphql/scalarTypesUtils.ts +++ b/packages/event-handler/src/appsync-graphql/scalarTypesUtils.ts @@ -1,42 +1,5 @@ import { randomUUID } from 'node:crypto'; -/** - * ID - A unique identifier for an object. This scalar is serialized like a String - * but isn't meant to be human-readable. - */ -export const makeId = () => randomUUID(); - -/** - * AWSTimestamp - An integer value representing the number of seconds - * before or after 1970-01-01-T00:00Z. - */ -export const awsTimestamp = () => Math.floor(Date.now() / 1000); - -/** - * AWSDate - An extended ISO 8601 date string in the format YYYY-MM-DD. - * - * @param timezoneOffset - Timezone offset in hours, defaults to 0 - */ -export const awsDate = (timezoneOffset = 0) => - formattedTime(new Date(), '%Y-%m-%d', timezoneOffset); - -/** - * AWSTime - An extended ISO 8601 time string in the format hh:mm:ss.sss. - * - * @param timezoneOffset - Timezone offset in hours, defaults to 0 - */ -export const awsTime = (timezoneOffset = 0) => - formattedTime(new Date(), '%H:%M:%S.%f', timezoneOffset); - -/** - * AWSDateTime - An extended ISO 8601 date and time string in the format - * YYYY-MM-DDThh:mm:ss.sssZ. - * - * @param timezoneOffset - Timezone offset in hours, defaults to 0 - */ -export const awsDateTime = (timezoneOffset = 0) => - formattedTime(new Date(), '%Y-%m-%dT%H:%M:%S.%f', timezoneOffset); - /** * String formatted time with optional timezone offset * @@ -87,3 +50,51 @@ const formattedTime = ( return `${dateTimeStr}${postfix}`; }; + +/** + * Generate a unique identifier of type `ID` for use in GraphQL. + * + * This scalar is serialized like a String + * but isn't meant to be human-readable. + */ +const makeId = () => randomUUID(); + +/** + * Generate an `AWSTimestamp` value for use in GraphQL. + * + * An integer value representing the number of seconds before or after `1970-01-01-T00:00Z`. + */ +const awsTimestamp = () => Math.floor(Date.now() / 1000); + +/** + * Generate an `AWSDate` value for use in GraphQL. + * + * An extended ISO 8601 date string in the format `YYYY-MM-DD`. + * + * @param timezoneOffset - Timezone offset in hours, defaults to 0 + */ +const awsDate = (timezoneOffset = 0) => + formattedTime(new Date(), '%Y-%m-%d', timezoneOffset); + +/** + * Generate an `AWSTime` value for use in GraphQL. + * + * An extended ISO 8601 time string in the format `hh:mm:ss.sss`. + * + * @param timezoneOffset - Timezone offset in hours, defaults to 0 + */ +const awsTime = (timezoneOffset = 0) => + formattedTime(new Date(), '%H:%M:%S.%f', timezoneOffset); + +/** + * Generate an `AWSDateTime` value for use in GraphQL. + * + * An extended ISO 8601 date and time string in the format + * `YYYY-MM-DDThh:mm:ss.sssZ`. + * + * @param timezoneOffset - Timezone offset in hours, defaults to `0` + */ +const awsDateTime = (timezoneOffset = 0) => + formattedTime(new Date(), '%Y-%m-%dT%H:%M:%S.%f', timezoneOffset); + +export { makeId, awsTimestamp, awsDate, awsTime, awsDateTime }; diff --git a/packages/event-handler/src/appsync-graphql/utils.ts b/packages/event-handler/src/appsync-graphql/utils.ts index b748d8dfab..da8b4402a6 100644 --- a/packages/event-handler/src/appsync-graphql/utils.ts +++ b/packages/event-handler/src/appsync-graphql/utils.ts @@ -27,8 +27,6 @@ const isAppSyncGraphQLEvent = ( isString(event.info.fieldName) && isString(event.info.parentTypeName) && isRecord(event.info.variables) && - Array.isArray(event.info.selectionSetList) && - event.info.selectionSetList.every((item) => isString(item)) && isString(event.info.parentTypeName) && isRecord(event.stash) ); diff --git a/packages/event-handler/src/bedrock-agent/BedrockAgentFunctionResolver.ts b/packages/event-handler/src/bedrock-agent/BedrockAgentFunctionResolver.ts index 01ec210743..5211ddade3 100644 --- a/packages/event-handler/src/bedrock-agent/BedrockAgentFunctionResolver.ts +++ b/packages/event-handler/src/bedrock-agent/BedrockAgentFunctionResolver.ts @@ -1,3 +1,4 @@ +import type { GenericLogger } from '@aws-lambda-powertools/commons/types'; import { isNullOrUndefined } from '@aws-lambda-powertools/commons/typeutils'; import { getStringFromEnv } from '@aws-lambda-powertools/commons/utils/env'; import type { Context } from 'aws-lambda'; @@ -9,7 +10,6 @@ import type { Tool, ToolFunction, } from '../types/bedrock-agent.js'; -import type { GenericLogger } from '../types/common.js'; import { BedrockFunctionResponse } from './BedrockFunctionResponse.js'; import { assertBedrockAgentFunctionEvent } from './utils.js'; diff --git a/packages/event-handler/src/bedrock-agent/BedrockFunctionResponse.ts b/packages/event-handler/src/bedrock-agent/BedrockFunctionResponse.ts index 5656a6d6dd..25609c9ee9 100644 --- a/packages/event-handler/src/bedrock-agent/BedrockFunctionResponse.ts +++ b/packages/event-handler/src/bedrock-agent/BedrockFunctionResponse.ts @@ -3,6 +3,7 @@ import type { ResponseState, } from '../types/bedrock-agent.js'; import type { BedrockAgentFunctionResolver } from './BedrockAgentFunctionResolver.js'; + /** * Class representing a response from a Bedrock agent function. * @@ -11,7 +12,7 @@ import type { BedrockAgentFunctionResolver } from './BedrockAgentFunctionResolve * - prompt session attributes * - response state (`FAILURE` or `REPROMPT`) * - * When working with the {@link BedrockAgentFunctionResolver} class, this is built automatically + * When working with the {@link BedrockAgentFunctionResolver | `BedrockAgentFunctionResolver`} class, this is built automatically * when you return anything from your function handler other than an instance of this class. */ class BedrockFunctionResponse { diff --git a/packages/event-handler/src/types/appsync-events.ts b/packages/event-handler/src/types/appsync-events.ts index a225446190..6b7466b151 100644 --- a/packages/event-handler/src/types/appsync-events.ts +++ b/packages/event-handler/src/types/appsync-events.ts @@ -1,46 +1,8 @@ +import type { GenericLogger } from '@aws-lambda-powertools/commons/types'; import type { Context } from 'aws-lambda'; -import type { AppSyncEventsResolver } from '../appsync-events/AppSyncEventsResolver.js'; import type { RouteHandlerRegistry } from '../appsync-events/RouteHandlerRegistry.js'; import type { Router } from '../appsync-events/Router.js'; -import type { Anything, GenericLogger } from './common.js'; - -// #region resolve options - -/** - * Optional object to pass to the {@link AppSyncEventsResolver.resolve | `AppSyncEventsResolver.resolve()`} method. - */ -type ResolveOptions = { - /** - * Reference to `this` instance of the class that is calling the `resolve` method. - * - * This parameter should be used only when using {@link AppSyncEventsResolver.onPublish | `AppSyncEventsResolver.onPublish()`} - * and {@link AppSyncEventsResolver.onSubscribe | `AppSyncEventsResolver.onSubscribe()`} as class method decorators, and - * it's used to bind the decorated methods to your class instance. - * - * @example - * ```ts - * import { AppSyncEventsResolver } from '@aws-lambda-powertools/event-handler/appsync-events'; - * - * const app = new AppSyncEventsResolver(); - * - * class Lambda { - * public scope = 'scoped'; - * - * ⁣@app.onPublish('/foo') - * public async handleFoo(payload: string) { - * return `${this.scope} ${payload}`; - * } - * - * public async handler(event: unknown, context: Context) { - * return app.resolve(event, context, { scope: this }); - * } - * } - * const lambda = new Lambda(); - * const handler = lambda.handler.bind(lambda); - * ``` - */ - scope?: unknown; -}; +import type { Anything } from './common.js'; // #region OnPublish fn @@ -129,7 +91,7 @@ type RouteHandlerRegistryOptions = { * * When no logger is provided, we'll only log warnings and errors using the global `console` object. */ - logger: GenericLogger; + logger: Pick; /** * Event type stored in the registry * @default 'onPublish' @@ -322,7 +284,6 @@ type AppSyncEventsSubscribeEvent = AppSyncEventsEvent & { }; export type { - GenericLogger, RouteHandlerRegistryOptions, RouteHandlerOptions, RouterOptions, @@ -340,5 +301,4 @@ export type { OnPublishAggregateOutput, OnPublishEventPayload, OnPublishOutput, - ResolveOptions, }; diff --git a/packages/event-handler/src/types/appsync-graphql.ts b/packages/event-handler/src/types/appsync-graphql.ts index a6546a8ce6..b7f405ee3f 100644 --- a/packages/event-handler/src/types/appsync-graphql.ts +++ b/packages/event-handler/src/types/appsync-graphql.ts @@ -1,48 +1,7 @@ +import type { GenericLogger } from '@aws-lambda-powertools/commons/types'; import type { AppSyncResolverEvent, Context } from 'aws-lambda'; -import type { AppSyncGraphQLResolver } from '../appsync-graphql/AppSyncGraphQLResolver.js'; import type { RouteHandlerRegistry } from '../appsync-graphql/RouteHandlerRegistry.js'; -import type { GenericLogger } from './common.js'; - -// #region resolve options - -/** - * Optional object to pass to the {@link AppSyncGraphQLResolver.resolve | `AppSyncGraphQLResolver.resolve()`} method. - */ -type ResolveOptions = { - /** - * Reference to `this` instance of the class that is calling the `resolve` method. - * - * This parameter should be used when using {@link AppSyncGraphQLResolver.resolver | `AppSyncGraphQLResolver.resolver()`} - * as class method decorators, and it's used to bind the decorated methods to your class instance. - * @example - * ```ts - * import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; - * - * const app = new AppSyncGraphQLResolver(); - * - * class Lambda { - * public scope = 'scoped'; - * - * @app.resolver({ fieldName: 'getPost', typeName: 'Query' }) - * public async handleGetPost({ id }) { - * // your business logic here - * return { - * id, - * title: `${this.scope} Post Title`, - * }; - * } - * - * public async handler(event, context) { - * return app.resolve(event, context, { scope: this }); - * } - * } - * - * const lambda = new Lambda(); - * export const handler = lambda.handler.bind(lambda); - * ``` - */ - scope?: unknown; -}; +import type { Router } from '../appsync-graphql/Router.js'; // #region Resolver fn @@ -65,7 +24,7 @@ type ResolverHandler> = // #region Resolver registry /** - * Options for the {@link RouteHandlerRegistry} class + * Options for the {@link RouteHandlerRegistry | `RouteHandlerRegistry`} class */ type RouteHandlerRegistryOptions = { /** @@ -73,7 +32,7 @@ type RouteHandlerRegistryOptions = { * * When no logger is provided, we'll only log warnings and errors using the global `console` object. */ - logger: GenericLogger; + logger: Pick; }; /** @@ -101,7 +60,7 @@ type RouteHandlerOptions> = { // #region Router /** - * Options for the {@link Router} class + * Options for the {@link Router | `Router`} class */ type GraphQlRouterOptions = { /** @@ -121,17 +80,15 @@ type GraphQlRouteOptions = { */ fieldName: string; /** - * The type name of the event to be registered + * The type name of the event to be registered, i.e. `Query`, `Mutation`, or a custom type */ typeName?: string; }; export type { - GenericLogger, RouteHandlerRegistryOptions, RouteHandlerOptions, GraphQlRouterOptions, GraphQlRouteOptions, ResolverHandler, - ResolveOptions, }; diff --git a/packages/event-handler/src/types/bedrock-agent.ts b/packages/event-handler/src/types/bedrock-agent.ts index 6659dea22a..c649e1200d 100644 --- a/packages/event-handler/src/types/bedrock-agent.ts +++ b/packages/event-handler/src/types/bedrock-agent.ts @@ -1,8 +1,10 @@ -import type { JSONValue } from '@aws-lambda-powertools/commons/types'; +import type { + GenericLogger, + JSONValue, +} from '@aws-lambda-powertools/commons/types'; import type { Context } from 'aws-lambda'; import type { BedrockAgentFunctionResolver } from '../bedrock-agent/BedrockAgentFunctionResolver.js'; import type { BedrockFunctionResponse } from '../bedrock-agent/BedrockFunctionResponse.js'; -import type { GenericLogger } from '../types/common.js'; /** * Configuration for a tool in the Bedrock Agent Function Resolver. diff --git a/packages/event-handler/src/types/common.ts b/packages/event-handler/src/types/common.ts index a3f71b397b..8efc28bf51 100644 --- a/packages/event-handler/src/types/common.ts +++ b/packages/event-handler/src/types/common.ts @@ -1,15 +1,45 @@ +import type { AppSyncEventsResolver } from '../appsync-events/AppSyncEventsResolver.js'; +import type { AppSyncGraphQLResolver } from '../appsync-graphql/AppSyncGraphQLResolver.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; +// #region resolve options + /** - * Interface for a generic logger object. + * Optional object to pass to the {@link AppSyncEventsResolver.resolve | `AppSyncEventsResolver.resolve()`} or {@link AppSyncGraphQLResolver.resolve | `AppSyncGraphQLResolver.resolve()`} methods. */ -type GenericLogger = { - trace?: (...content: Anything[]) => void; - debug: (...content: Anything[]) => void; - info?: (...content: Anything[]) => void; - warn: (...content: Anything[]) => void; - error: (...content: Anything[]) => void; +type ResolveOptions = { + /** + * Reference to `this` instance of the class that is calling the `resolve` method. + * + * This parameter should be used only when using {@link AppSyncEventsResolver.onPublish | `AppSyncEventsResolver.onPublish()`}, + * {@link AppSyncEventsResolver.onSubscribe | `AppSyncEventsResolver.onSubscribe()`}, and {@link AppSyncGraphQLResolver.resolve | `AppSyncGraphQLResolver.resolve()`} as class method decorators, and + * it's used to bind the decorated methods to your class instance. + * + * @example + * ```ts + * import { AppSyncEventsResolver } from '@aws-lambda-powertools/event-handler/appsync-events'; + * + * const app = new AppSyncEventsResolver(); + * + * class Lambda { + * public scope = 'scoped'; + * + * ⁣@app.onPublish('/foo') + * public async handleFoo(payload: string) { + * return `${this.scope} ${payload}`; + * } + * + * public async handler(event: unknown, context: Context) { + * return app.resolve(event, context, { scope: this }); + * } + * } + * const lambda = new Lambda(); + * const handler = lambda.handler.bind(lambda); + * ``` + */ + scope?: unknown; }; -export type { Anything, GenericLogger }; +export type { Anything, ResolveOptions }; diff --git a/packages/event-handler/src/types/index.ts b/packages/event-handler/src/types/index.ts index f43b4c8de3..3a5d286377 100644 --- a/packages/event-handler/src/types/index.ts +++ b/packages/event-handler/src/types/index.ts @@ -8,9 +8,15 @@ export type { OnPublishOutput, RouteOptions, RouterOptions, - ResolveOptions, } from './appsync-events.js'; +export type { + ResolverHandler, + RouteHandlerOptions, + GraphQlRouterOptions, + GraphQlRouteOptions, +} from './appsync-graphql.js'; + export type { BedrockAgentFunctionEvent, BedrockAgentFunctionResponse, @@ -20,6 +26,6 @@ export type { } from './bedrock-agent.js'; export type { - GenericLogger, + ResolveOptions, Anything, } from './common.js'; 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 ed3d06502c..c814301a09 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/AppSyncGraphQLResolver.test.ts @@ -1,9 +1,9 @@ import context from '@aws-lambda-powertools/testing-utils/context'; import type { Context } from 'aws-lambda'; -import { onGraphqlEventFactory } 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'; +import { ResolverNotFoundException } from '../../../src/appsync-graphql/index.js'; +import { onGraphqlEventFactory } from '../../helpers/factories.js'; describe('Class: AppSyncGraphQLResolver', () => { beforeEach(() => { @@ -142,17 +142,14 @@ describe('Class: AppSyncGraphQLResolver', () => { it('logs only warnings and errors using global console object if no logger supplied', async () => { // Prepare const app = new AppSyncGraphQLResolver(); - app.resolver<{ title: string; content: string }>( + app.onMutation<{ title: string; content: string }>( + 'addPost', async ({ title, content }) => { return { id: '123', title, content, }; - }, - { - fieldName: 'addPost', - typeName: 'Mutation', } ); @@ -177,18 +174,13 @@ describe('Class: AppSyncGraphQLResolver', () => { it('resolver function has access to event and context', async () => { // Prepare const app = new AppSyncGraphQLResolver({ logger: console }); - app.resolver<{ id: string }>( - async ({ id }, event, context) => { - return { - id, - event, - context, - }; - }, - { - fieldName: 'getPost', - } - ); + app.onQuery<{ id: string }>('getPost', async ({ id }, event, context) => { + return { + id, + event, + context, + }; + }); // Act const event = onGraphqlEventFactory('getPost', 'Query', { id: '123' }); @@ -209,7 +201,7 @@ describe('Class: AppSyncGraphQLResolver', () => { class Lambda { public scope = 'scoped'; - @app.resolver({ fieldName: 'getPost', typeName: 'Query' }) + @app.onQuery('getPost') public async handleGetPost({ id }: { id: string }) { return { id, @@ -217,6 +209,19 @@ describe('Class: AppSyncGraphQLResolver', () => { }; } + @app.onMutation('addPost') + public async handleAddPost({ + title, + content, + }: { title: string; content: string }) { + return { + id: '123', + title, + content, + scope: this.scope, + }; + } + public async handler(event: unknown, context: Context) { return this.stuff(event, context); } @@ -229,16 +234,29 @@ describe('Class: AppSyncGraphQLResolver', () => { const handler = lambda.handler.bind(lambda); // Act - const result = await handler( + const resultQuery = await handler( onGraphqlEventFactory('getPost', 'Query', { id: '123' }), context ); + const resultMutation = await handler( + onGraphqlEventFactory('addPost', 'Mutation', { + title: 'Post Title', + content: 'Post Content', + }), + context + ); // Assess - expect(result).toEqual({ + expect(resultQuery).toEqual({ id: '123', scope: 'scoped id=123', }); + expect(resultMutation).toEqual({ + id: '123', + title: 'Post Title', + content: 'Post Content', + scope: 'scoped', + }); }); it('emits debug message when AWS_LAMBDA_LOG_LEVEL is set to DEBUG', async () => { @@ -246,23 +264,26 @@ describe('Class: AppSyncGraphQLResolver', () => { vi.stubEnv('AWS_LAMBDA_LOG_LEVEL', 'DEBUG'); const app = new AppSyncGraphQLResolver(); - app.resolver<{ title: string; content: string }>( - async ({ title, content }) => { + class Lambda { + @app.resolver({ fieldName: 'getPost' }) + public async handleGetPost({ id }: { id: string }) { return { - id: '123', - title, - content, + id, + title: 'Post Title', + content: 'Post Content', }; - }, - { - fieldName: 'addPost', - typeName: 'Mutation', } - ); + + public async handler(event: unknown, context: Context) { + return app.resolve(event, context, { scope: this }); + } + } + const lambda = new Lambda(); + const handler = lambda.handler.bind(lambda); // Act - await app.resolve( - onGraphqlEventFactory('addPost', 'Mutation', { + await handler( + onGraphqlEventFactory('getPost', 'Query', { title: 'Post Title', content: 'Post Content', }), 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 b333b91d4b..215a05445d 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/RouteHandlerRegistry.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/RouteHandlerRegistry.test.ts @@ -1,6 +1,7 @@ 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; 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 b515e299b6..29c6a02aae 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/Router.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/Router.test.ts @@ -8,13 +8,13 @@ describe('Class: Router', () => { it('registers resolvers using the functional approach', () => { // Prepare - const router = new Router({ logger: console }); + const app = new Router({ logger: console }); const getPost = vi.fn(() => [true]); const addPost = vi.fn(async () => true); // Act - router.resolver(getPost, { typeName: 'Query', fieldName: 'getPost' }); - router.resolver(addPost, { typeName: 'Mutation', fieldName: 'addPost' }); + app.onQuery('getPost', getPost); + app.onMutation('addPost', addPost); // Assess expect(console.debug).toHaveBeenNthCalledWith( @@ -29,28 +29,28 @@ describe('Class: Router', () => { it('registers resolvers using the decorator pattern', () => { // Prepare - const router = new Router({ logger: console }); + const app = new Router({ logger: console }); // Act class Lambda { readonly prop = 'value'; - @router.resolver({ fieldName: 'getPost' }) + @app.resolver({ fieldName: 'getPost' }) public getPost() { return `${this.prop} foo`; } - @router.resolver({ fieldName: 'getAuthor', typeName: 'Query' }) + @app.resolver({ fieldName: 'getAuthor' }) public getAuthor() { return `${this.prop} bar`; } - @router.resolver({ fieldName: 'addPost', typeName: 'Mutation' }) + @app.onMutation('addPost') public addPost() { return `${this.prop} bar`; } - @router.resolver({ fieldName: 'updatePost', typeName: 'Mutation' }) + @app.resolver({ fieldName: 'updatePost', typeName: 'Mutation' }) public updatePost() { return `${this.prop} baz`; } @@ -88,18 +88,22 @@ describe('Class: Router', () => { it('registers nested resolvers using the decorator pattern', () => { // Prepare - const router = new Router({ logger: console }); + const app = new Router({ logger: console }); // Act class Lambda { - readonly prop = 'value'; - - @router.resolver({ fieldName: 'listLocations' }) - @router.resolver({ fieldName: 'locations' }) + @app.onQuery('listLocations') + @app.onQuery('locations') public getLocations() { - return [{ name: 'Location 1', description: 'Description 1' }]; + return [ + { + name: 'Location 1', + description: 'Description 1', + }, + ]; } } + const lambda = new Lambda(); const response = lambda.getLocations(); @@ -120,10 +124,10 @@ describe('Class: Router', () => { it('uses a default logger with only warnings if none is provided', () => { // Prepare - const router = new Router(); + const app = new Router(); // Act - router.resolver(vi.fn(), { fieldName: 'getPost' }); + app.resolver(vi.fn(), { fieldName: 'getPost' }); // Assess expect(console.debug).not.toHaveBeenCalled(); diff --git a/packages/event-handler/tests/unit/appsync-graphql/scalarTypesUtils.test.ts b/packages/event-handler/tests/unit/appsync-graphql/scalarTypesUtils.test.ts index 0671fa492b..d3d28e999b 100644 --- a/packages/event-handler/tests/unit/appsync-graphql/scalarTypesUtils.test.ts +++ b/packages/event-handler/tests/unit/appsync-graphql/scalarTypesUtils.test.ts @@ -5,9 +5,10 @@ import { awsTime, awsTimestamp, makeId, -} from '../../../src/appsync-graphql/scalarTypesUtils.js'; +} from '../../../src/appsync-graphql/index.js'; const mockDate = new Date('2025-06-15T10:30:45.123Z'); + describe('Scalar Types Utils', () => { beforeAll(() => { vi.useFakeTimers().setSystemTime(mockDate); @@ -33,111 +34,164 @@ describe('Scalar Types Utils', () => { }); describe('awsDate', () => { - it('should return a date in YYYY-MM-DD format with Z timezone', () => { + it('returns a date in YYYY-MM-DD format with Z timezone', () => { + // Act const result = awsDate(); + + // Assess expect(result).toBe('2025-06-15Z'); }); - it('should handle positive timezone offset', () => { + it('handles a positive timezone offset', () => { + // Act const result = awsDate(5); + + // Assess expect(result).toBe('2025-06-15+05:00:00'); }); - it('should handle negative timezone offset', () => { + it('handles a negative timezone offset', () => { + // Act const result = awsDate(-8); + + // Assess expect(result).toBe('2025-06-15-08:00:00'); }); - it('should handle date change with timezone offset', () => { + it('handle a date change with timezone offset', () => { + // Act const result = awsDate(-11); + + // Assess expect(result).toBe('2025-06-14-11:00:00'); }); - it('should handle fractional timezone offset', () => { + it('handles a fractional timezone offset', () => { + // Act const result = awsDate(5.5); + + // Assess expect(result).toBe('2025-06-15+05:30:00'); }); - it('should handle negative fractional timezone offset', () => { + it('handles a negative fractional timezone offset', () => { + // Act const result = awsDate(-9.5); + + // Assess expect(result).toBe('2025-06-15-09:30:00'); }); - it('should throw RangeError for invalid timezone offset', () => { + it('throws a RangeError for invalid timezone offset', () => { + // Act & Assess expect(() => awsDate(15)).toThrow(RangeError); expect(() => awsDate(-13)).toThrow(RangeError); }); }); describe('awsTime', () => { - it('should return a time in HH:MM:SS.sss format with Z timezone', () => { + it('returns a time in HH:MM:SS.sss format with Z timezone', () => { + // Act const result = awsTime(); + + // Assess expect(result).toBe('10:30:45.123Z'); }); - it('should handle positive timezone offset', () => { + it('handles a positive timezone offset', () => { + // Act const result = awsTime(3); + + // Assess expect(result).toBe('13:30:45.123+03:00:00'); }); - it('should handle negative timezone offset', () => { + it('handles a negative timezone offset', () => { + // Act const result = awsTime(-5); + + // Assess expect(result).toBe('05:30:45.123-05:00:00'); }); - it('should handle fractional timezone offset', () => { + it('handles a fractional timezone offset', () => { + // Act const result = awsTime(5.5); + + // Assess expect(result).toBe('16:00:45.123+05:30:00'); }); - it('should throw RangeError for invalid timezone offset', () => { + it('throws a RangeError for invalid timezone offset', () => { + // Act & Assess expect(() => awsTime(15)).toThrow(RangeError); expect(() => awsTime(-13)).toThrow(RangeError); }); }); describe('awsDateTime', () => { - it('should return a datetime in ISO 8601 format with Z timezone', () => { + it('return a datetime in ISO 8601 format with Z timezone', () => { + // Act const result = awsDateTime(); + + // Assess expect(result).toBe('2025-06-15T10:30:45.123Z'); }); - it('should handle positive timezone offset', () => { + it('handles a positive timezone offset', () => { + // Act const result = awsDateTime(2); + + // Assess expect(result).toBe('2025-06-15T12:30:45.123+02:00:00'); }); - it('should handle negative timezone offset', () => { + it('handles a negative timezone offset', () => { + // Act const result = awsDateTime(-7); + + // Assess expect(result).toBe('2025-06-15T03:30:45.123-07:00:00'); }); - it('should handle date/time change with timezone offset', () => { + it('handles date/time change with timezone offset', () => { + // Act const result = awsDateTime(-11); + + // Assess expect(result).toBe('2025-06-14T23:30:45.123-11:00:00'); }); - it('should handle fractional timezone offset', () => { + it('handles a fractional timezone offset', () => { + // Act const result = awsDateTime(5.5); + + // Assess expect(result).toBe('2025-06-15T16:00:45.123+05:30:00'); }); - it('should handle negative fractional timezone offset', () => { + it('handles a negative fractional timezone offset', () => { + // Act const result = awsDateTime(-9.5); + + // Assess expect(result).toBe('2025-06-15T01:00:45.123-09:30:00'); }); - it('should throw RangeError for invalid timezone offset', () => { + it('throws a RangeError for invalid timezone offset', () => { + // Act & Assess expect(() => awsDateTime(15)).toThrow(RangeError); expect(() => awsDateTime(-13)).toThrow(RangeError); }); }); describe('awsTimestamp', () => { - it('should return current time as Unix timestamp in seconds', () => { + it('returns the current time as Unix timestamp in seconds', () => { + // Act const result = awsTimestamp(); - const expected = Math.floor(mockDate.getTime() / 1000); - expect(result).toBe(expected); + + // Assess + expect(result).toBe(Math.floor(mockDate.getTime() / 1000)); }); }); }); diff --git a/packages/event-handler/typedoc.json b/packages/event-handler/typedoc.json index 35095bca88..fce95e37d4 100644 --- a/packages/event-handler/typedoc.json +++ b/packages/event-handler/typedoc.json @@ -4,6 +4,7 @@ ], "entryPoints": [ "./src/appsync-events/index.ts", + "./src/appsync-graphql/index.ts", "./src/bedrock-agent/index.ts", "./src/types/index.ts", ],