Skip to content

feat(event-handler): add single resolver functionality for AppSync GraphQL API #3999

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

Open
wants to merge 30 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
951dba3
feat: implement `RouteHandlerRegistry` for managing GraphQL route han…
arnabrahman May 25, 2025
7d9a27e
feat: add type guard for AppSync GraphQL event validation
arnabrahman May 25, 2025
69c6210
refactor: simplify handler function signatures and update type defini…
arnabrahman May 25, 2025
e5762f3
refactor: remove wrong `result` property check from AppSync GraphQL e…
arnabrahman May 25, 2025
7b9d1f9
feat: implement Router class for managing `Query` events for appsync …
arnabrahman May 28, 2025
707dc1c
feat: add `onMutation` method for handling GraphQL Mutation events in…
arnabrahman May 28, 2025
a71aead
feat: implement AppSyncGraphQLResolver class to handle onQuery and on…
arnabrahman May 28, 2025
c4f6b8d
doc: `#executeSingleResolver` function
arnabrahman May 28, 2025
387f5cf
feat: add warning for unimplemented batch resolvers in AppSyncGraphQL…
arnabrahman May 28, 2025
70b9921
feat: enhance `RouteHandlerRegistry` to log handler registration and …
arnabrahman May 29, 2025
286096b
feat: add `onQueryEventFactory` and `onMutationEventFactory` to creat…
arnabrahman May 29, 2025
58e3b26
feat: add unit tests for `AppSyncGraphQLResolver` class to validate e…
arnabrahman May 29, 2025
d821b3d
feat: add unit tests for `RouteHandlerRegistry` to validate handler r…
arnabrahman May 29, 2025
3076c1c
feat: add unit tests for `Router` class to validate resolver registra…
arnabrahman May 29, 2025
ff85c24
feat: add test for nested resolvers registration using the decorator …
arnabrahman May 29, 2025
0d5c893
feat: enhance documentation for `resolve` method in `AppSyncGraphQLRe…
arnabrahman Jun 1, 2025
700c779
chore: warning message for batch resolver
arnabrahman Jun 1, 2025
5580923
fix: return query handler if found
arnabrahman Jun 1, 2025
7e8aa10
fix: correct warning message for batch resolver in AppSyncGraphQLReso…
arnabrahman Jun 1, 2025
80a11ff
fix: update debug messages to reflect resolver registration format in…
arnabrahman Jun 1, 2025
e8b0db7
fix: update resolver not found messages for consistency in AppSyncGra…
arnabrahman Jun 1, 2025
65c8ee2
fix: doc for Router
arnabrahman Jun 1, 2025
f1b545e
refactor: remove unused cache and warning set from `RouteHandlerRegis…
arnabrahman Jun 1, 2025
11a0443
fix: update documentation for resolve method in RouteHandlerRegistry
arnabrahman Jun 1, 2025
0e9d2ca
refactor: remove redundant test for cached route handler evaluation i…
arnabrahman Jun 1, 2025
99531aa
fix: update import path for Router in Router.test.ts
arnabrahman Jun 1, 2025
1342ca4
fix: update debug messages to include event type in RouteHandlerRegis…
arnabrahman Jun 1, 2025
e5454c9
fix: update terminology from "handler" to "resolver" in RouteHandlerR…
arnabrahman Jun 1, 2025
c855e53
Merge branch 'main' into 1166-graphql-resolver
arnabrahman Jun 1, 2025
881138f
fix: refactor logger initialization and import structure in Router an…
arnabrahman Jun 5, 2025
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
161 changes: 161 additions & 0 deletions packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
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 {
/**
* 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<unknown> {
if (Array.isArray(event)) {
this.logger.warn('Batch resolver is not implemented yet');
return;
}
if (!isAppSyncGraphQLEvent(event)) {
this.logger.warn(
'Received an event that is not compatible with this resolver'
);
return;
}
try {
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);
}
}

/**
* 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<unknown> {
const { fieldName, parentTypeName: typeName } = event.info;
const queryHandlerOptions = this.onQueryRegistry.resolve(
typeName,
fieldName
);
if (queryHandlerOptions) {
return await queryHandlerOptions.handler.apply(this, [event.arguments]);
}

const mutationHandlerOptions = this.onMutationRegistry.resolve(
typeName,
fieldName
);
if (mutationHandlerOptions) {
return await mutationHandlerOptions.handler.apply(this, [
event.arguments,
]);
}

throw new ResolverNotFoundException(
`No resolver found for ${typeName}-${fieldName}`
);
}

/**
* 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',
};
}
}
86 changes: 86 additions & 0 deletions packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
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<string, RouteHandlerOptions> = 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';

public constructor(options: RouteHandlerRegistryOptions) {
this.#logger = options.logger;
this.#eventType = options.eventType ?? 'onQuery';
}

/**
* 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
* @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(
`Adding ${this.#eventType} resolver for field ${typeName}.${fieldName}`
);
const cacheKey = this.#makeKey(typeName, fieldName);
if (this.resolvers.has(cacheKey)) {
this.#logger.warn(
`A resolver for field '${fieldName}' is already registered for '${typeName}'. The previous resolver will be replaced.`
);
}
this.resolvers.set(cacheKey, {
fieldName,
handler,
typeName,
});
}

/**
* Resolves the handler for a specific GraphQL API event.
*
* @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,
fieldName: string
): RouteHandlerOptions | undefined {
this.#logger.debug(
`Looking for ${this.#eventType} resolver for type=${typeName}, field=${fieldName}`
);
return this.resolvers.get(this.#makeKey(typeName, fieldName));
}

/**
* 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 };
Loading
Loading