Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 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
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
58 changes: 58 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,64 @@ 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"
```

#### Sharing contextual data

You can use `appendContext` when you want to share data between your App and Router instances. Any data you share will be available via the `sharedContext` parameter in your resolver handlers.

???+ warning
For safety, we clear the context after each invocation.

???+ tip
This can also be useful for injecting contextual information before a request is processed.

=== "app.ts"

```typescript hl_lines="10"
--8<-- "examples/snippets/event-handler/appsync-graphql/appendContext.ts"
```

=== "postRouter.ts"

```typescript hl_lines="5-8"
--8<-- "examples/snippets/event-handler/appsync-graphql/postRouterWithContext.ts"
```

=== "userRouter.ts"

```typescript hl_lines="5-8"
--8<-- "examples/snippets/event-handler/appsync-graphql/userRouterWithContext.ts"
```

### Nested mappings

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

const app = new AppSyncGraphQLResolver();

app.includeRouter([postRouter, userRouter]);

app.appendContext({ requestId: crypto.randomUUID() });

export const handler = async (event: unknown, context: Context) =>
app.resolve(event, context);
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 };
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Router } from '@aws-lambda-powertools/event-handler/appsync-graphql';

const postRouter = new Router();

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

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: '[email protected]' }];
});

export { userRouter };
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Router } from '@aws-lambda-powertools/event-handler/appsync-graphql';

const userRouter = new Router();

userRouter.onQuery('getUsers', async (args, { sharedContext }) => {
const requestId = sharedContext?.get('requestId');
return [{ id: 1, name: 'John Doe', email: '[email protected]', requestId }];
});

export { userRouter };
164 changes: 150 additions & 14 deletions packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { AppSyncResolverEvent, Context } from 'aws-lambda';
import type {
BatchResolverAggregateHandlerFn,
BatchResolverHandlerFn,
GraphQlRouterOptions,
ResolverHandler,
RouteHandlerOptions,
} from '../types/appsync-graphql.js';
Expand Down Expand Up @@ -42,6 +43,16 @@ import { isAppSyncGraphQLEvent } from './utils.js';
* ```
*/
class AppSyncGraphQLResolver extends Router {
/**
* A map to hold shared contextual data accessible to all resolver handlers.
*/
public readonly sharedContext: Map<string, unknown>;

public constructor(options?: GraphQlRouterOptions) {
super(options);
this.sharedContext = new Map<string, unknown>();
}

/**
* Resolve the response based on the provided event and route handlers configured.
*
Expand Down Expand Up @@ -160,11 +171,19 @@ class AppSyncGraphQLResolver extends Router {
);
return;
}
return this.#withErrorHandling(
() => this.#executeBatchResolvers(event, context, options),
event[0],
options
);

try {
return this.#withErrorHandling(
() => this.#executeBatchResolvers(event, context, options),
event[0],
options
);
} finally {
/**
* Clear shared context after batch processing for safety
*/
this.sharedContext.clear();
}
}
if (!isAppSyncGraphQLEvent(event)) {
this.logger.warn(
Expand All @@ -173,11 +192,96 @@ class AppSyncGraphQLResolver extends Router {
return;
}

return this.#withErrorHandling(
() => this.#executeSingleResolver(event, context, options),
event,
options
);
try {
return this.#withErrorHandling(
() => this.#executeSingleResolver(event, context, options),
event,
options
);
} finally {
/**
* Clear shared context after batch processing for safety
*/
this.sharedContext.clear();
}
}

/**
* 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.
*
* @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');
}

/**
* Appends contextual data to be shared with all resolver handlers.
*
* This method allows you to add key-value pairs to the shared context that will be
* accessible to all resolver handlers through the `sharedContext` parameter. The context
* is automatically cleared after each invocation for safety.
*
* @example
* ```ts
* import { AppSyncGraphQLResolver, Router } from '@aws-lambda-powertools/event-handler/appsync-graphql';
*
* const postRouter = new Router();
* postRouter.onQuery('getPosts', async ({ sharedContext }) => {
* const requestId = sharedContext?.get('requestId');
* return [{ id: 1, title: 'Post 1', requestId }];
* });
*
* const userRouter = new Router();
* userRouter.onQuery('getUsers', async ({ sharedContext }) => {
* const requestId = sharedContext?.get('requestId');
* return [{ id: 1, name: 'John Doe', requestId }];
* });
*
* const app = new AppSyncGraphQLResolver();
*
* app.includeRouter([userRouter, postRouter]);
* app.appendContext({ requestId: '12345' });
*
* export const handler = async (event, context) =>
* app.resolve(event, context);
* ```
*
* @param data - A record of key-value pairs to add to the shared context
*/
public appendContext(data: Record<string, unknown>): void {
for (const [key, value] of Object.entries(data)) {
this.sharedContext.set(key, value);
}
}

/**
Expand Down Expand Up @@ -315,7 +419,11 @@ class AppSyncGraphQLResolver extends Router {
options.handler as BatchResolverAggregateHandlerFn
).apply(resolveOptions?.scope ?? this, [
events,
{ event: events, context },
{
event: events,
context,
...this.#getSharedContextOnlyIfNotEmpty(),
},
]);

if (!Array.isArray(response)) {
Expand All @@ -334,7 +442,11 @@ class AppSyncGraphQLResolver extends Router {
for (const event of events) {
const result = await handler.apply(resolveOptions?.scope ?? this, [
event.arguments,
{ event, context },
{
event,
context,
...this.#getSharedContextOnlyIfNotEmpty(),
},
]);
results.push(result);
}
Expand All @@ -345,7 +457,11 @@ class AppSyncGraphQLResolver extends Router {
try {
const result = await handler.apply(resolveOptions?.scope ?? this, [
events[i].arguments,
{ event: events[i], context },
{
event: events[i],
context,
...this.#getSharedContextOnlyIfNotEmpty(),
},
]);
results.push(result);
} catch (error) {
Expand Down Expand Up @@ -387,7 +503,14 @@ class AppSyncGraphQLResolver extends Router {
if (resolverHandlerOptions) {
return (resolverHandlerOptions.handler as ResolverHandler).apply(
options?.scope ?? this,
[event.arguments, { event, context }]
[
event.arguments,
{
event,
context,
...this.#getSharedContextOnlyIfNotEmpty(),
},
]
);
}

Expand All @@ -411,6 +534,19 @@ class AppSyncGraphQLResolver extends Router {
error: 'An unknown error occurred',
};
}

/**
* Returns an object containing the shared context only if it has entries.
* This helps avoid passing an empty map to handlers.
*/
#getSharedContextOnlyIfNotEmpty(): {
sharedContext: Map<string, unknown> | undefined;
} {
return {
sharedContext:
this.sharedContext.size > 0 ? this.sharedContext : undefined,
};
}
}

export { AppSyncGraphQLResolver };
Loading