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

Merged
merged 50 commits into from
Jun 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
8c91a44
feat: implement `RouteHandlerRegistry` for managing GraphQL route han…
arnabrahman May 25, 2025
34e5548
feat: add type guard for AppSync GraphQL event validation
arnabrahman May 25, 2025
d98d507
refactor: simplify handler function signatures and update type defini…
arnabrahman May 25, 2025
a2d4c7a
refactor: remove wrong `result` property check from AppSync GraphQL e…
arnabrahman May 25, 2025
0134b3d
feat: implement Router class for managing `Query` events for appsync …
arnabrahman May 28, 2025
84d55d4
feat: add `onMutation` method for handling GraphQL Mutation events in…
arnabrahman May 28, 2025
febd4ce
feat: implement AppSyncGraphQLResolver class to handle onQuery and on…
arnabrahman May 28, 2025
3e5d504
doc: `#executeSingleResolver` function
arnabrahman May 28, 2025
17a0c55
feat: add warning for unimplemented batch resolvers in AppSyncGraphQL…
arnabrahman May 28, 2025
34d8c13
feat: enhance `RouteHandlerRegistry` to log handler registration and …
arnabrahman May 29, 2025
72db338
feat: add `onQueryEventFactory` and `onMutationEventFactory` to creat…
arnabrahman May 29, 2025
1989c83
feat: add unit tests for `AppSyncGraphQLResolver` class to validate e…
arnabrahman May 29, 2025
88723eb
feat: add unit tests for `RouteHandlerRegistry` to validate handler r…
arnabrahman May 29, 2025
a1a0d28
feat: add unit tests for `Router` class to validate resolver registra…
arnabrahman May 29, 2025
376bfae
feat: add test for nested resolvers registration using the decorator …
arnabrahman May 29, 2025
0681850
feat: enhance documentation for `resolve` method in `AppSyncGraphQLRe…
arnabrahman Jun 1, 2025
c763cbc
chore: warning message for batch resolver
arnabrahman Jun 1, 2025
1387c38
fix: return query handler if found
arnabrahman Jun 1, 2025
41c2945
fix: correct warning message for batch resolver in AppSyncGraphQLReso…
arnabrahman Jun 1, 2025
a0354f3
fix: update debug messages to reflect resolver registration format in…
arnabrahman Jun 1, 2025
59f02db
fix: update resolver not found messages for consistency in AppSyncGra…
arnabrahman Jun 1, 2025
2c78894
fix: doc for Router
arnabrahman Jun 1, 2025
4a5ef0a
refactor: remove unused cache and warning set from `RouteHandlerRegis…
arnabrahman Jun 1, 2025
2d9ec9a
fix: update documentation for resolve method in RouteHandlerRegistry
arnabrahman Jun 1, 2025
ff97740
refactor: remove redundant test for cached route handler evaluation i…
arnabrahman Jun 1, 2025
86ea83f
fix: update import path for Router in Router.test.ts
arnabrahman Jun 1, 2025
b7c4a9b
fix: update debug messages to include event type in RouteHandlerRegis…
arnabrahman Jun 1, 2025
6587a7e
fix: update terminology from "handler" to "resolver" in RouteHandlerR…
arnabrahman Jun 1, 2025
decddbe
fix: refactor logger initialization and import structure in Router an…
arnabrahman Jun 5, 2025
cc94ba2
fix: enhance type safety for onQuery and onMutation handlers with gen…
arnabrahman Jun 18, 2025
4df541a
refactor: unify resolver registration by replacing onQuery and onMuta…
arnabrahman Jun 18, 2025
a1dbfb8
test: replace onQuery and onMutation methods with a unified resolver …
arnabrahman Jun 18, 2025
4f927d8
fix: enhance type parameters for resolver handlers and route options
arnabrahman Jun 18, 2025
33ab42c
refactor: resolve PR feedbacks and replace onQuery and onMutation eve…
arnabrahman Jun 23, 2025
0802bbd
refactor: consolidate onQuery and onMutation event factories into a s…
arnabrahman Jun 23, 2025
3996555
feat: add scalar types utility functions and corresponding tests
arnabrahman Jun 23, 2025
b693f57
fix: remove unnecessary checks in isAppSyncGraphQLEvent type guard
arnabrahman Jun 23, 2025
67ab911
refactor: update AppSyncGraphQLResolver to use unified resolver metho…
arnabrahman Jun 23, 2025
50c40b2
refactor: update test descriptions for clarity on Query and Mutation …
arnabrahman Jun 23, 2025
45d18ee
feat: enhance AppSyncGraphQLResolver to pass context to resolver hand…
arnabrahman Jun 23, 2025
0817c6b
refactor: remove unused imports in AppSyncGraphQLResolver test file
arnabrahman Jun 23, 2025
8796757
fix: remove duplicate assertion for console.debug in AppSyncGraphQLRe…
arnabrahman Jun 26, 2025
cb9242e
refactor: replace AppSyncGraphQLEvent with AppSyncResolverEvent for i…
arnabrahman Jun 26, 2025
0d1c061
refactor: simplify development mode check by using isDevMode utility …
arnabrahman Jun 26, 2025
1e1c1ac
fix: update event type in resolver handler functions for improved typ…
arnabrahman Jun 26, 2025
f12cb21
feat: add optional parameters to resolve method for enhanced flexibility
arnabrahman Jun 26, 2025
a4860cc
doc: bind decorated methods to class instance in resolver handler
arnabrahman Jun 29, 2025
cf0b699
test: add scope preservation test for resolver decorator in AppSyncGr…
arnabrahman Jun 29, 2025
0871132
fix: handle optional descriptor value in resolver registration for im…
arnabrahman Jun 29, 2025
b8107d4
Merge branch 'main' into 1166-graphql-resolver
dreamorosi Jun 30, 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
173 changes: 173 additions & 0 deletions packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import type { AppSyncResolverEvent, Context } from 'aws-lambda';
import type { ResolveOptions } 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 GraphQL 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.resolver(async ({ id }) => {
* // your business logic here
* return {
* id,
* title: 'Post Title',
* content: 'Post Content',
* };
* }, {
* fieldName: 'getPost',
* typeName: 'Query'
* });
*
* 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.resolver(async ({ id }) => {
* // your business logic here
* return {
* id,
* title: 'Post Title',
* content: 'Post Content',
* };
* }, {
* fieldName: 'getPost',
* typeName: 'Query'
* });
*
* 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.resolver({ fieldName: 'getPost', typeName: 'Query' })
* async handleGetPost({ id }) {
* // your business logic here
* return {
* id,
* title: 'Post Title',
* content: 'Post Content',
* };
* }
*
* async handler(event, context) {
* return app.resolve(event, context, {
* scope: this, // bind decorated methods to the class instance
* });
* }
* }
*
* 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.
* @param options - Optional parameters for the resolver, such as the scope of the handler.
*/
public async resolve(
event: unknown,
context: Context,
options?: ResolveOptions
): 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, context, options);
} 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 for a given AppSync GraphQL event.
*
* This method attempts to resolve the handler for the specified field and type name
* from the resolver registry. 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 resolver event containing the necessary information.
* @param context - The Lambda execution context.
* @param options - Optional parameters for the resolver, such as the scope of the handler.
* @throws {ResolverNotFoundException} If no resolver is registered for the given field and type.
*/
async #executeSingleResolver(
event: AppSyncResolverEvent<Record<string, unknown>>,
context: Context,
options?: ResolveOptions
): Promise<unknown> {
const { fieldName, parentTypeName: typeName } = event.info;

const resolverHandlerOptions = this.resolverRegistry.resolve(
typeName,
fieldName
);
if (resolverHandlerOptions) {
return resolverHandlerOptions.handler.apply(options?.scope ?? this, [
event.arguments,
event,
context,
]);
}

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',
};
}
}
79 changes: 79 additions & 0 deletions packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import type {
GenericLogger,
RouteHandlerOptions,
RouteHandlerRegistryOptions,
} from '../types/appsync-graphql.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.
*/
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;

public constructor(options: RouteHandlerRegistryOptions) {
this.#logger = options.logger;
}

/**
* 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 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 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