Skip to content

feat(event-handler): support onQuery and onMutation handlers #4111

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jul 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions packages/event-handler/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -21,7 +21,7 @@ class RouteHandlerRegistry {
/**
* A logger instance to be used for logging debug and warning messages.
*/
readonly #logger: GenericLogger;
readonly #logger: Pick<GenericLogger, 'debug' | 'warn' | 'error'>;
/**
* The event type stored in the registry.
*/
Expand Down
5 changes: 1 addition & 4 deletions packages/event-handler/src/appsync-events/Router.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
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,
RouterOptions,
} from '../types/appsync-events.js';
import { RouteHandlerRegistry } from './RouteHandlerRegistry.js';

// Simple global approach - store the last instance per router
const routerInstanceMap = new WeakMap<Router, unknown>();

/**
* Class for registering routes for the `onPublish` and `onSubscribe` events in AWS AppSync Events APIs.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -171,3 +171,5 @@ export class AppSyncGraphQLResolver extends Router {
};
}
}

export { AppSyncGraphQLResolver };
Original file line number Diff line number Diff line change
@@ -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 {
/**
Expand All @@ -18,7 +19,7 @@ class RouteHandlerRegistry {
/**
* A logger instance to be used for logging debug and warning messages.
*/
readonly #logger: GenericLogger;
readonly #logger: Pick<GenericLogger, 'debug' | 'warn' | 'error'>;

public constructor(options: RouteHandlerRegistryOptions) {
this.#logger = options.logger;
Expand Down
173 changes: 164 additions & 9 deletions packages/event-handler/src/appsync-graphql/Router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -126,38 +126,193 @@ 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<TParams extends Record<string, unknown>>(
handler: ResolverHandler<TParams>,
options: GraphQlRouteOptions
): void;
public resolver(options: GraphQlRouteOptions): MethodDecorator;
public resolver<TParams extends Record<string, unknown>>(
handler: ResolverHandler<TParams> | GraphQlRouteOptions,
handlerOrOptions: ResolverHandler<TParams> | 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<TParams extends Record<string, unknown>>(
fieldName: string,
handler: ResolverHandler<TParams>
): void;
public onQuery(fieldName: string): MethodDecorator;
public onQuery<TParams extends Record<string, unknown>>(
fieldName: string,
handlerOrFieldName?:
| ResolverHandler<TParams>
| Pick<GraphQlRouteOptions, 'fieldName'>
): 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<TParams extends Record<string, unknown>>(
fieldName: string,
handler: ResolverHandler<TParams>
): void;
public onMutation(fieldName: string): MethodDecorator;
public onMutation<TParams extends Record<string, unknown>>(
fieldName: string,
handlerOrFieldName?: ResolverHandler<TParams> | 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;
Expand Down
3 changes: 3 additions & 0 deletions packages/event-handler/src/appsync-graphql/errors.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
9 changes: 9 additions & 0 deletions packages/event-handler/src/appsync-graphql/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export { AppSyncGraphQLResolver } from './AppSyncGraphQLResolver.js';
export { ResolverNotFoundException } from './errors.js';
export {
makeId,
awsTimestamp,
awsDate,
awsTime,
awsDateTime,
} from './scalarTypesUtils.js';
Loading