Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
d4e4232
feat: enhance error handling with detailed logging and handler merging
arnabrahman Aug 31, 2025
725b67d
feat: improve resolver registration with dedicated warning method and…
arnabrahman Aug 31, 2025
dcbc532
feat: add method to merge resolver registries from another router
arnabrahman Aug 31, 2025
c79e4a3
feat: add `includeRouter` method to merge route registries into AppSy…
arnabrahman Aug 31, 2025
32d713b
test: `includeRouter` function tests
arnabrahman Aug 31, 2025
1dcfb69
feat: export `Router` from Router.js in AppSync GraphQL index
arnabrahman Aug 31, 2025
4d858fd
feat: add shared context support to AppSync resolver and handler types
arnabrahman Sep 9, 2025
7a96f86
test: tests for `includeRouter` method and context sharing in AppSync…
arnabrahman Sep 9, 2025
607ed4e
test: add tests for sharedContext handling in batch resolvers
arnabrahman Sep 9, 2025
7227f94
refactor: update includeRouter method to accept multiple routers and …
arnabrahman Sep 9, 2025
da0368b
refactor: streamline logging in includeRouter method for clarity
arnabrahman Sep 9, 2025
1f2bb0c
refactor: rename context to sharedContext for clarity and consistency
arnabrahman Sep 9, 2025
46bb220
doc: enhance sharedContext documentation and update example usage in …
arnabrahman Sep 9, 2025
14f601f
refactor: clear shared context after processing to prevent data leakage
arnabrahman Sep 9, 2025
fcd3847
refactor: remove debug logging during registry merging for cleaner ou…
arnabrahman Sep 9, 2025
ecc1f1d
doc: `includeRouter` & `appendContext` method doc
arnabrahman Sep 9, 2025
450ed66
refactor: standardize router naming and improve type annotations in e…
arnabrahman Sep 10, 2025
28645ca
refactor: remove debug logging from registry merging for cleaner output
arnabrahman Sep 10, 2025
3147d3e
refactor: improve clarity in comments and streamline router example code
arnabrahman Sep 10, 2025
364faca
refactor: extract sharedContext empty check in a method
arnabrahman Sep 10, 2025
f7b8492
test: update sharedContext in AppSyncGraphQLResolver test for consist…
arnabrahman Sep 10, 2025
2b40997
refactor: remove unnecessary await from error handling in resolver me…
arnabrahman Oct 6, 2025
aef50b9
refactor: remove sharedContext from AppSyncGraphQLResolver and relate…
arnabrahman Oct 12, 2025
b2bbf20
doc: remove appendContext and related router examples for clarity
arnabrahman Oct 12, 2025
55c125d
refactor: simplify event and context handling in resolver methods
arnabrahman Oct 12, 2025
326587e
refactor: remove unnecessary blank line in AppSyncGraphQLResolver
arnabrahman Oct 12, 2025
0a20b2e
doc: add note about handler precedence and conflict warnings for `inc…
arnabrahman Oct 12, 2025
f911c81
Merge branch 'main' into 4131-graphql-includerouter
svozza Oct 13, 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
30 changes: 30 additions & 0 deletions docs/features/event-handler/appsync-graphql.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,36 @@ Here's a table with their related scalar as a quick reference:

## Advanced

### Split operations with Router

As you grow the number of related GraphQL operations a given Lambda function should handle, it is natural to split them into separate files to ease maintenance - That's when the `Router` feature comes handy.

Let's assume you have `app.ts` as your Lambda function entrypoint and routes in `postRouter.ts` and `userRouter.ts`. This is how you'd use the `Router` feature.

=== "postRouter.ts"

We import **Router** instead of **AppSyncGraphQLResolver**; syntax wise is exactly the same.

```typescript hl_lines="1 3"
--8<-- "examples/snippets/event-handler/appsync-graphql/postRouter.ts"
```

=== "userRouter.ts"

We import **Router** instead of **AppSyncGraphQLResolver**; syntax wise is exactly the same.

```typescript hl_lines="1 3"
--8<-- "examples/snippets/event-handler/appsync-graphql/userRouter.ts"
```

=== "app.ts"

We use `includeRouter` method and include all operations registered in the router instances.

```typescript hl_lines="3-4 8"
--8<-- "examples/snippets/event-handler/appsync-graphql/splitRouter.ts"
```

### Nested mappings

!!! note
Expand Down
18 changes: 18 additions & 0 deletions examples/snippets/event-handler/appsync-graphql/postRouter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Router } from '@aws-lambda-powertools/event-handler/appsync-graphql';

const postRouter = new Router();

postRouter.onQuery('getPosts', async () => {
return [{ id: 1, title: 'First post', content: 'Hello world!' }];
});

postRouter.onMutation('createPost', async ({ title, content }) => {
return {
id: Date.now(),
title,
content,
createdAt: new Date().toISOString(),
};
});

export { postRouter };
11 changes: 11 additions & 0 deletions examples/snippets/event-handler/appsync-graphql/splitRouter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql';
import type { Context } from 'aws-lambda';
import { postRouter } from './postRouter';
import { userRouter } from './userRouter';

const app = new AppSyncGraphQLResolver();

app.includeRouter([postRouter, userRouter]);

export const handler = async (event: unknown, context: Context) =>
app.resolve(event, context);
9 changes: 9 additions & 0 deletions examples/snippets/event-handler/appsync-graphql/userRouter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Router } from '@aws-lambda-powertools/event-handler/appsync-graphql';

const userRouter = new Router();

userRouter.onQuery('getUsers', async () => {
return [{ id: 1, name: 'John Doe', email: 'john@example.com' }];
});

export { userRouter };
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,50 @@ class AppSyncGraphQLResolver extends Router {
);
}

/**
* Includes one or more routers and merges their registries into the current resolver.
*
* This method allows you to compose multiple routers by merging their
* route registries into the current AppSync GraphQL resolver instance.
* All resolver handlers, batch resolver handlers, and exception handlers
* from the included routers will be available in the current resolver.
*
* **Note:** When multiple routers register handlers for the same type and field combination
* (e.g., both `userRouter` and `postRouter` define `Query.getPost`), the handler from the
* last included router takes precedence and will override earlier registrations.
* This behavior also applies to exception handlers registered for the same error class.
* A warning is logged to help you identify potential conflicts when handlers are overridden.
*
* @example
* ```ts
* import { AppSyncGraphQLResolver, Router } from '@aws-lambda-powertools/event-handler/appsync-graphql';
*
* const postRouter = new Router();
* postRouter.onQuery('getPosts', async () => [{ id: 1, title: 'Post 1' }]);
*
* const userRouter = new Router();
* userRouter.onQuery('getUsers', async () => [{ id: 1, name: 'John Doe' }]);
*
* const app = new AppSyncGraphQLResolver();
*
* app.includeRouter([userRouter, postRouter]);
*
* export const handler = async (event, context) =>
* app.resolve(event, context);
* ```
*
* @param router - The router instance or array of router instances whose registries will be merged
*/
public includeRouter(router: Router | Router[]): void {
const routers = Array.isArray(router) ? router : [router];

this.logger.debug('Including router');
for (const routerToBeIncluded of routers) {
this.mergeRegistriesFrom(routerToBeIncluded);
}
this.logger.debug('Router included successfully');
}

/**
* Executes the provided asynchronous function with error handling.
* If the function throws an error, it delegates error processing to `#handleError`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,44 @@ class ExceptionHandlerRegistry {
const errors = Array.isArray(error) ? error : [error];

for (const err of errors) {
this.registerErrorHandler(err, handler);
this.#registerErrorHandler(err, handler);
}
}

/**
* Resolves and returns the appropriate exception handler for a given error instance.
*
* This method attempts to find a registered exception handler based on the error class name.
* If a matching handler is found, it is returned; otherwise, `null` is returned.
*
* @param error - The error instance for which to resolve an exception handler.
*/
public resolve(error: Error): ExceptionHandler | null {
const errorName = error.name;
this.#logger.debug(`Looking for exception handler for error: ${errorName}`);

const handlerOptions = this.handlers.get(errorName);
if (handlerOptions) {
this.#logger.debug(`Found exact match for error class: ${errorName}`);
return handlerOptions.handler;
}

this.#logger.debug(`No exception handler found for error: ${errorName}`);
return null;
}

/**
* Merges handlers from another ExceptionHandlerRegistry into this registry.
* Existing handlers for the same error class will be replaced and a warning will be logged.
*
* @param otherRegistry - The registry to merge handlers from.
*/
public merge(otherRegistry: ExceptionHandlerRegistry): void {
for (const [errorName, handlerOptions] of otherRegistry.handlers) {
if (this.handlers.has(errorName)) {
this.#warnHandlerOverriding(errorName);
}
this.handlers.set(errorName, handlerOptions);
}
}

Expand All @@ -47,7 +84,7 @@ class ExceptionHandlerRegistry {
* @param errorClass - The error class to register the handler for.
* @param handler - The exception handler function.
*/
private registerErrorHandler(
#registerErrorHandler(
errorClass: ErrorClass<Error>,
handler: ExceptionHandler
): void {
Expand All @@ -56,9 +93,7 @@ class ExceptionHandlerRegistry {
this.#logger.debug(`Adding exception handler for error class ${errorName}`);

if (this.handlers.has(errorName)) {
this.#logger.warn(
`An exception handler for error class '${errorName}' is already registered. The previous handler will be replaced.`
);
this.#warnHandlerOverriding(errorName);
}

this.handlers.set(errorName, {
Expand All @@ -68,25 +103,18 @@ class ExceptionHandlerRegistry {
}

/**
* Resolves and returns the appropriate exception handler for a given error instance.
* Logs a warning message when an exception handler is being overridden.
*
* This method attempts to find a registered exception handler based on the error class name.
* If a matching handler is found, it is returned; otherwise, `null` is returned.
* This method is called internally when registering a new exception handler
* for an error class that already has a handler registered. It warns the user
* that the previous handler will be replaced with the new one.
*
* @param error - The error instance for which to resolve an exception handler.
* @param errorName - The name of the error class for which a handler is being overridden
*/
public resolve(error: Error): ExceptionHandler | null {
const errorName = error.name;
this.#logger.debug(`Looking for exception handler for error: ${errorName}`);

const handlerOptions = this.handlers.get(errorName);
if (handlerOptions) {
this.#logger.debug(`Found exact match for error class: ${errorName}`);
return handlerOptions.handler;
}

this.#logger.debug(`No exception handler found for error: ${errorName}`);
return null;
#warnHandlerOverriding(errorName: string): void {
this.#logger.warn(
`An exception handler for error class '${errorName}' is already registered. The previous handler will be replaced.`
);
}
}

Expand Down
32 changes: 29 additions & 3 deletions packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,7 @@ class RouteHandlerRegistry {
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.#warnResolverOverriding(fieldName, typeName);
}
this.resolvers.set(cacheKey, {
fieldName,
Expand Down Expand Up @@ -81,6 +79,21 @@ class RouteHandlerRegistry {
return this.resolvers.get(this.#makeKey(typeName, fieldName));
}

/**
* Merges handlers from another RouteHandlerRegistry into this registry.
* Existing handlers with the same key will be replaced and a warning will be logged.
*
* @param otherRegistry - The registry to merge handlers from.
*/
public merge(otherRegistry: RouteHandlerRegistry): void {
for (const [key, handler] of otherRegistry.resolvers) {
if (this.resolvers.has(key)) {
this.#warnResolverOverriding(handler.fieldName, handler.typeName);
}
this.resolvers.set(key, handler);
}
}

/**
* Generates a unique key by combining the provided GraphQL type name and field name.
*
Expand All @@ -90,6 +103,19 @@ class RouteHandlerRegistry {
#makeKey(typeName: string, fieldName: string): string {
return `${typeName}.${fieldName}`;
}

/**
* Logs a warning message indicating that a resolver for the specified field and type
* is already registered and will be replaced by a new resolver.
*
* @param fieldName - The name of the field for which the resolver is being overridden.
* @param typeName - The name of the type associated with the field.
*/
#warnResolverOverriding(fieldName: string, typeName: string): void {
this.#logger.warn(
`A resolver for field '${fieldName}' is already registered for '${typeName}'. The previous resolver will be replaced.`
);
}
}

export { RouteHandlerRegistry };
14 changes: 14 additions & 0 deletions packages/event-handler/src/appsync-graphql/Router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,20 @@ class Router {
this.isDev = isDevMode();
}

/**
* Merges resolver registries from another router into this router.
*
* This method combines the resolver registry, batch resolver registry, and exception handler registry
* from the provided router with the current router's registries.
*
* @param router - The source router whose registries will be merged into this router
*/
protected mergeRegistriesFrom(router: Router): void {
this.resolverRegistry.merge(router.resolverRegistry);
this.batchResolverRegistry.merge(router.batchResolverRegistry);
this.exceptionHandlerRegistry.merge(router.exceptionHandlerRegistry);
}

/**
* Register a resolver function for any GraphQL event.
*
Expand Down
1 change: 1 addition & 0 deletions packages/event-handler/src/appsync-graphql/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export {
InvalidBatchResponseException,
ResolverNotFoundException,
} from './errors.js';
export { Router } from './Router.js';
export {
awsDate,
awsDateTime,
Expand Down
Loading
Loading