Skip to content

Commit 263db2d

Browse files
authored
feat(event-handler): support onQuery and onMutation handlers (#4111)
1 parent dfc31da commit 263db2d

23 files changed

+467
-240
lines changed

packages/event-handler/package.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,16 @@
3939
"default": "./lib/esm/appsync-events/index.js"
4040
}
4141
},
42+
"./appsync-graphql": {
43+
"require": {
44+
"types": "./lib/cjs/appsync-graphql/index.d.ts",
45+
"default": "./lib/cjs/appsync-graphql/index.js"
46+
},
47+
"import": {
48+
"types": "./lib/esm/appsync-graphql/index.d.ts",
49+
"default": "./lib/esm/appsync-graphql/index.js"
50+
}
51+
},
4252
"./bedrock-agent": {
4353
"require": {
4454
"types": "./lib/cjs/bedrock-agent/index.d.ts",
@@ -66,6 +76,10 @@
6676
"./lib/cjs/appsync-events/index.d.ts",
6777
"./lib/esm/appsync-events/index.d.ts"
6878
],
79+
"appsync-graphql": [
80+
"./lib/cjs/appsync-graphql/index.d.ts",
81+
"./lib/esm/appsync-graphql/index.d.ts"
82+
],
6983
"bedrock-agent": [
7084
"./lib/cjs/bedrock-agent/index.d.ts",
7185
"./lib/esm/bedrock-agent/index.d.ts"

packages/event-handler/src/appsync-events/AppSyncEventsResolver.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import type {
55
OnPublishHandlerAggregateFn,
66
OnPublishHandlerFn,
77
OnSubscribeHandler,
8-
ResolveOptions,
98
} from '../types/appsync-events.js';
9+
import type { ResolveOptions } from '../types/common.js';
1010
import { Router } from './Router.js';
1111
import { UnauthorizedException } from './errors.js';
1212
import { isAppSyncEventsEvent, isAppSyncEventsPublishEvent } from './utils.js';

packages/event-handler/src/appsync-events/RouteHandlerRegistry.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
import type { GenericLogger } from '@aws-lambda-powertools/commons/types';
12
import { LRUCache } from '@aws-lambda-powertools/commons/utils/lru-cache';
23
import type {
3-
GenericLogger,
44
RouteHandlerOptions,
55
RouteHandlerRegistryOptions,
66
} from '../types/appsync-events.js';
@@ -21,7 +21,7 @@ class RouteHandlerRegistry {
2121
/**
2222
* A logger instance to be used for logging debug and warning messages.
2323
*/
24-
readonly #logger: GenericLogger;
24+
readonly #logger: Pick<GenericLogger, 'debug' | 'warn' | 'error'>;
2525
/**
2626
* The event type stored in the registry.
2727
*/

packages/event-handler/src/appsync-events/Router.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
11
import { EnvironmentVariablesService } from '@aws-lambda-powertools/commons';
2+
import type { GenericLogger } from '@aws-lambda-powertools/commons/types';
23
import { isRecord } from '@aws-lambda-powertools/commons/typeutils';
34
import type {
4-
GenericLogger,
55
OnPublishHandler,
66
OnSubscribeHandler,
77
RouteOptions,
88
RouterOptions,
99
} from '../types/appsync-events.js';
1010
import { RouteHandlerRegistry } from './RouteHandlerRegistry.js';
1111

12-
// Simple global approach - store the last instance per router
13-
const routerInstanceMap = new WeakMap<Router, unknown>();
14-
1512
/**
1613
* Class for registering routes for the `onPublish` and `onSubscribe` events in AWS AppSync Events APIs.
1714
*/

packages/event-handler/src/appsync-graphql/AppSyncGraphQLResolver.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { AppSyncResolverEvent, Context } from 'aws-lambda';
2-
import type { ResolveOptions } from '../types/appsync-graphql.js';
2+
import type { ResolveOptions } from '../types/common.js';
33
import { Router } from './Router.js';
44
import { ResolverNotFoundException } from './errors.js';
55
import { isAppSyncGraphQLEvent } from './utils.js';
@@ -32,7 +32,7 @@ import { isAppSyncGraphQLEvent } from './utils.js';
3232
* app.resolve(event, context);
3333
* ```
3434
*/
35-
export class AppSyncGraphQLResolver extends Router {
35+
class AppSyncGraphQLResolver extends Router {
3636
/**
3737
* Resolve the response based on the provided event and route handlers configured.
3838
*
@@ -89,7 +89,7 @@ export class AppSyncGraphQLResolver extends Router {
8989
* ```
9090
*
9191
* @param event - The incoming event, which may be an AppSync GraphQL event or an array of events.
92-
* @param context - The Lambda execution context.
92+
* @param context - The AWS Lambda context object.
9393
* @param options - Optional parameters for the resolver, such as the scope of the handler.
9494
*/
9595
public async resolve(
@@ -171,3 +171,5 @@ export class AppSyncGraphQLResolver extends Router {
171171
};
172172
}
173173
}
174+
175+
export { AppSyncGraphQLResolver };

packages/event-handler/src/appsync-graphql/RouteHandlerRegistry.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1+
import type { GenericLogger } from '@aws-lambda-powertools/commons/types';
12
import type {
2-
GenericLogger,
33
RouteHandlerOptions,
44
RouteHandlerRegistryOptions,
55
} from '../types/appsync-graphql.js';
6+
import type { AppSyncGraphQLResolver } from './AppSyncGraphQLResolver.js';
67

78
/**
89
* Registry for storing route handlers for GraphQL resolvers in AWS AppSync GraphQL API's.
910
*
1011
* This class should not be used directly unless you are implementing a custom router.
11-
* Instead, use the {@link Router} class, which is the recommended way to register routes.
12+
* Instead, use the {@link AppSyncGraphQLResolver | `AppSyncGraphQLResolver`} class, which is the recommended way to register routes.
1213
*/
1314
class RouteHandlerRegistry {
1415
/**
@@ -18,7 +19,7 @@ class RouteHandlerRegistry {
1819
/**
1920
* A logger instance to be used for logging debug and warning messages.
2021
*/
21-
readonly #logger: GenericLogger;
22+
readonly #logger: Pick<GenericLogger, 'debug' | 'warn' | 'error'>;
2223

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

packages/event-handler/src/appsync-graphql/Router.ts

Lines changed: 164 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ class Router {
106106
* const app = new AppSyncGraphQLResolver();
107107
*
108108
* class Lambda {
109-
* @app.resolver({ fieldName: 'getPost' })
109+
* @app.resolver({ fieldName: 'getPost' })
110110
* async handleGetPost(payload) {
111111
* // your business logic here
112112
* return payload;
@@ -126,38 +126,193 @@ class Router {
126126
* @param handler - The handler function to be called when the event is received.
127127
* @param options - Route options including the required fieldName and optional typeName.
128128
* @param options.fieldName - The name of the field to register the handler for.
129-
* @param options.typeName - The name of the GraphQL type to use for the resolver (defaults to 'Query').
129+
* @param options.typeName - The name of the GraphQL type to use for the resolver, defaults to `Query`.
130130
*/
131131
public resolver<TParams extends Record<string, unknown>>(
132132
handler: ResolverHandler<TParams>,
133133
options: GraphQlRouteOptions
134134
): void;
135135
public resolver(options: GraphQlRouteOptions): MethodDecorator;
136136
public resolver<TParams extends Record<string, unknown>>(
137-
handler: ResolverHandler<TParams> | GraphQlRouteOptions,
137+
handlerOrOptions: ResolverHandler<TParams> | GraphQlRouteOptions,
138138
options?: GraphQlRouteOptions
139139
): MethodDecorator | undefined {
140-
if (typeof handler === 'function') {
140+
if (typeof handlerOrOptions === 'function') {
141141
const resolverOptions = options as GraphQlRouteOptions;
142142
const { typeName = 'Query', fieldName } = resolverOptions;
143143

144144
this.resolverRegistry.register({
145145
fieldName,
146-
handler: handler as ResolverHandler,
146+
handler: handlerOrOptions as ResolverHandler,
147147
typeName,
148148
});
149149

150150
return;
151151
}
152152

153-
const resolverOptions = handler;
154-
return (target, _propertyKey, descriptor: PropertyDescriptor) => {
155-
const { typeName = 'Query', fieldName } = resolverOptions;
153+
return (_target, _propertyKey, descriptor: PropertyDescriptor) => {
154+
this.resolverRegistry.register({
155+
fieldName: handlerOrOptions.fieldName,
156+
handler: descriptor?.value,
157+
typeName: handlerOrOptions.typeName ?? 'Query',
158+
});
159+
160+
return descriptor;
161+
};
162+
}
156163

164+
/**
165+
* Register a handler function for the `query` event.
166+
167+
* Registers a handler for a specific GraphQL Query field. The handler will be invoked when a request is made
168+
* for the specified field in the Query type.
169+
*
170+
* @example
171+
* ```ts
172+
* import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql';
173+
*
174+
* const app = new AppSyncGraphQLResolver();
175+
*
176+
* app.onQuery('getPost', async (payload) => {
177+
* // your business logic here
178+
* return payload;
179+
* });
180+
181+
* export const handler = async (event, context) =>
182+
* app.resolve(event, context);
183+
* ```
184+
*
185+
* As a decorator:
186+
*
187+
* @example
188+
* ```ts
189+
* import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql';
190+
*
191+
* const app = new AppSyncGraphQLResolver();
192+
*
193+
* class Lambda {
194+
* ⁣@app.onQuery('getPost')
195+
* async handleGetPost(payload) {
196+
* // your business logic here
197+
* return payload;
198+
* }
199+
*
200+
* async handler(event, context) {
201+
* return app.resolve(event, context);
202+
* }
203+
* }
204+
*
205+
* const lambda = new Lambda();
206+
* export const handler = lambda.handler.bind(lambda);
207+
* ```
208+
*
209+
* @param fieldName - The name of the Query field to register the handler for.
210+
* @param handler - The handler function to be called when the event is received.
211+
*/
212+
public onQuery<TParams extends Record<string, unknown>>(
213+
fieldName: string,
214+
handler: ResolverHandler<TParams>
215+
): void;
216+
public onQuery(fieldName: string): MethodDecorator;
217+
public onQuery<TParams extends Record<string, unknown>>(
218+
fieldName: string,
219+
handlerOrFieldName?:
220+
| ResolverHandler<TParams>
221+
| Pick<GraphQlRouteOptions, 'fieldName'>
222+
): MethodDecorator | undefined {
223+
if (typeof handlerOrFieldName === 'function') {
224+
this.resolverRegistry.register({
225+
fieldName: fieldName,
226+
handler: handlerOrFieldName as ResolverHandler,
227+
typeName: 'Query',
228+
});
229+
230+
return;
231+
}
232+
233+
return (_target, _propertyKey, descriptor: PropertyDescriptor) => {
234+
this.resolverRegistry.register({
235+
fieldName: fieldName,
236+
handler: descriptor?.value,
237+
typeName: 'Query',
238+
});
239+
240+
return descriptor;
241+
};
242+
}
243+
244+
/**
245+
* Register a handler function for the `mutation` event.
246+
*
247+
* Registers a handler for a specific GraphQL Mutation field. The handler will be invoked when a request is made
248+
* for the specified field in the Mutation type.
249+
*
250+
* @example
251+
* ```ts
252+
* import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql';
253+
*
254+
* const app = new AppSyncGraphQLResolver();
255+
*
256+
* app.onMutation('createPost', async (payload) => {
257+
* // your business logic here
258+
* return payload;
259+
* });
260+
*
261+
* export const handler = async (event, context) =>
262+
* app.resolve(event, context);
263+
* ```
264+
*
265+
* As a decorator:
266+
*
267+
* @example
268+
* ```ts
269+
* import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql';
270+
*
271+
* const app = new AppSyncGraphQLResolver();
272+
*
273+
* class Lambda {
274+
* ⁣@app.onMutation('createPost')
275+
* async handleCreatePost(payload) {
276+
* // your business logic here
277+
* return payload;
278+
* }
279+
*
280+
* async handler(event, context) {
281+
* return app.resolve(event, context);
282+
* }
283+
* }
284+
*
285+
* const lambda = new Lambda();
286+
* export const handler = lambda.handler.bind(lambda);
287+
* ```
288+
*
289+
* @param fieldName - The name of the Mutation field to register the handler for.
290+
* @param handler - The handler function to be called when the event is received.
291+
*/
292+
public onMutation<TParams extends Record<string, unknown>>(
293+
fieldName: string,
294+
handler: ResolverHandler<TParams>
295+
): void;
296+
public onMutation(fieldName: string): MethodDecorator;
297+
public onMutation<TParams extends Record<string, unknown>>(
298+
fieldName: string,
299+
handlerOrFieldName?: ResolverHandler<TParams> | string
300+
): MethodDecorator | undefined {
301+
if (typeof handlerOrFieldName === 'function') {
302+
this.resolverRegistry.register({
303+
fieldName,
304+
handler: handlerOrFieldName as ResolverHandler,
305+
typeName: 'Mutation',
306+
});
307+
308+
return;
309+
}
310+
311+
return (_target, _propertyKey, descriptor: PropertyDescriptor) => {
157312
this.resolverRegistry.register({
158313
fieldName,
159314
handler: descriptor?.value,
160-
typeName,
315+
typeName: 'Mutation',
161316
});
162317

163318
return descriptor;

packages/event-handler/src/appsync-graphql/errors.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
/**
2+
* Error thrown when a resolver is not found for a given field and type name in AppSync GraphQL.
3+
*/
14
class ResolverNotFoundException extends Error {
25
constructor(message: string, options?: ErrorOptions) {
36
super(message, options);
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export { AppSyncGraphQLResolver } from './AppSyncGraphQLResolver.js';
2+
export { ResolverNotFoundException } from './errors.js';
3+
export {
4+
makeId,
5+
awsTimestamp,
6+
awsDate,
7+
awsTime,
8+
awsDateTime,
9+
} from './scalarTypesUtils.js';

0 commit comments

Comments
 (0)