1
1
import type { AppSyncResolverEvent , Context } from 'aws-lambda' ;
2
+ import type {
3
+ BatchResolverAggregateHandlerFn ,
4
+ BatchResolverHandlerFn ,
5
+ ResolverHandler ,
6
+ RouteHandlerOptions ,
7
+ } from '../types/appsync-graphql.js' ;
2
8
import type { ResolveOptions } from '../types/common.js' ;
3
- import { ResolverNotFoundException } from './errors.js' ;
9
+ import {
10
+ InvalidBatchResponseException ,
11
+ ResolverNotFoundException ,
12
+ } from './errors.js' ;
4
13
import { Router } from './Router.js' ;
5
14
import { isAppSyncGraphQLEvent } from './utils.js' ;
6
15
@@ -58,6 +67,28 @@ class AppSyncGraphQLResolver extends Router {
58
67
* app.resolve(event, context);
59
68
* ```
60
69
*
70
+ * Resolves the response based on the provided batch event and route handlers configured.
71
+ *
72
+ * @example
73
+ * ```ts
74
+ * import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql';
75
+ *
76
+ * const app = new AppSyncGraphQLResolver();
77
+ *
78
+ * app.batchResolver<{ id: number }>(async (events) => {
79
+ * // your business logic here
80
+ * const ids = events.map((event) => event.arguments.id);
81
+ * return ids.map((id) => ({
82
+ * id,
83
+ * title: 'Post Title',
84
+ * content: 'Post Content',
85
+ * }));
86
+ * });
87
+ *
88
+ * export const handler = async (event, context) =>
89
+ * app.resolve(event, context);
90
+ * ```
91
+ *
61
92
* The method works also as class method decorator, so you can use it like this:
62
93
*
63
94
* @example
@@ -88,6 +119,35 @@ class AppSyncGraphQLResolver extends Router {
88
119
* export const handler = lambda.handler.bind(lambda);
89
120
* ```
90
121
*
122
+ * @example
123
+ * ```ts
124
+ * import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql';
125
+ * import type { AppSyncResolverEvent } from 'aws-lambda';
126
+ *
127
+ * const app = new AppSyncGraphQLResolver();
128
+ *
129
+ * class Lambda {
130
+ * @app .batchResolver({ fieldName: 'getPosts', typeName: 'Query' })
131
+ * async getPosts(events: AppSyncResolverEvent<{ id: number }>[]) {
132
+ * // your business logic here
133
+ * const ids = events.map((event) => event.arguments.id);
134
+ * return ids.map((id) => ({
135
+ * id,
136
+ * title: 'Post Title',
137
+ * content: 'Post Content',
138
+ * }));
139
+ * }
140
+ *
141
+ * async handler(event, context) {
142
+ * return app.resolve(event, context, {
143
+ * scope: this, // bind decorated methods to the class instance
144
+ * });
145
+ * }
146
+ * }
147
+ *
148
+ * const lambda = new Lambda();
149
+ * export const handler = lambda.handler.bind(lambda);
150
+ * ```
91
151
* @param event - The incoming event, which may be an AppSync GraphQL event or an array of events.
92
152
* @param context - The AWS Lambda context object.
93
153
* @param options - Optional parameters for the resolver, such as the scope of the handler.
@@ -98,27 +158,185 @@ class AppSyncGraphQLResolver extends Router {
98
158
options ?: ResolveOptions
99
159
) : Promise < unknown > {
100
160
if ( Array . isArray ( event ) ) {
101
- this . logger . warn ( 'Batch resolver is not implemented yet' ) ;
102
- return ;
161
+ if ( event . some ( ( e ) => ! isAppSyncGraphQLEvent ( e ) ) ) {
162
+ this . logger . warn (
163
+ 'Received a batch event that is not compatible with this resolver'
164
+ ) ;
165
+ return ;
166
+ }
167
+ return this . #withErrorHandling(
168
+ ( ) => this . #executeBatchResolvers( event , context , options ) ,
169
+ event [ 0 ]
170
+ ) ;
103
171
}
104
172
if ( ! isAppSyncGraphQLEvent ( event ) ) {
105
173
this . logger . warn (
106
174
'Received an event that is not compatible with this resolver'
107
175
) ;
108
176
return ;
109
177
}
178
+
179
+ return this . #withErrorHandling(
180
+ ( ) => this . #executeSingleResolver( event , context , options ) ,
181
+ event
182
+ ) ;
183
+ }
184
+
185
+ /**
186
+ * Executes the provided asynchronous function with error handling.
187
+ * If the function throws an error, it delegates error processing to `#handleError`
188
+ * and returns the formatted error response.
189
+ *
190
+ * @param fn - A function returning a Promise to be executed with error handling.
191
+ * @param event - The AppSync resolver event (single or first of batch).
192
+ */
193
+ async #withErrorHandling(
194
+ fn : ( ) => Promise < unknown > ,
195
+ event : AppSyncResolverEvent < Record < string , unknown > >
196
+ ) : Promise < unknown > {
110
197
try {
111
- return await this . #executeSingleResolver ( event , context , options ) ;
198
+ return await fn ( ) ;
112
199
} catch ( error ) {
113
- this . logger . error (
114
- `An error occurred in handler ${ event . info . fieldName } ` ,
115
- error
200
+ return this . #handleError (
201
+ error ,
202
+ `An error occurred in handler ${ event . info . fieldName } `
116
203
) ;
117
- if ( error instanceof ResolverNotFoundException ) throw error ;
118
- return this . #formatErrorResponse( error ) ;
119
204
}
120
205
}
121
206
207
+ /**
208
+ * Handles errors encountered during resolver execution.
209
+ *
210
+ * Logs the provided error message and error object. If the error is an instance of
211
+ * `InvalidBatchResponseException` or `ResolverNotFoundException`, it is re-thrown.
212
+ * Otherwise, the error is formatted into a response using `#formatErrorResponse`.
213
+ *
214
+ * @param error - The error object to handle.
215
+ * @param errorMessage - A descriptive message to log alongside the error.
216
+ * @throws InvalidBatchResponseException | ResolverNotFoundException
217
+ */
218
+ #handleError( error : unknown , errorMessage : string ) {
219
+ this . logger . error ( errorMessage , error ) ;
220
+ if ( error instanceof InvalidBatchResponseException ) throw error ;
221
+ if ( error instanceof ResolverNotFoundException ) throw error ;
222
+ return this . #formatErrorResponse( error ) ;
223
+ }
224
+
225
+ /**
226
+ * Executes batch resolvers for multiple AppSync GraphQL events.
227
+ *
228
+ * This method processes an array of AppSync resolver events as a batch operation.
229
+ * It looks up the appropriate batch resolver from the registry using the field name
230
+ * and parent type name from the first event, then delegates to the batch resolver
231
+ * if found.
232
+ *
233
+ * @param events - Array of AppSync resolver events to process as a batch
234
+ * @param context - AWS Lambda context object
235
+ * @param options - Optional resolve options for customizing resolver behavior
236
+ * @throws {ResolverNotFoundException } When no batch resolver is registered for the given type and field combination
237
+ */
238
+ async #executeBatchResolvers(
239
+ events : AppSyncResolverEvent < Record < string , unknown > > [ ] ,
240
+ context : Context ,
241
+ options ?: ResolveOptions
242
+ ) : Promise < unknown [ ] > {
243
+ const { fieldName, parentTypeName : typeName } = events [ 0 ] . info ;
244
+ const batchHandlerOptions = this . batchResolverRegistry . resolve (
245
+ typeName ,
246
+ fieldName
247
+ ) ;
248
+
249
+ if ( batchHandlerOptions ) {
250
+ return await this . #callBatchResolver(
251
+ events ,
252
+ context ,
253
+ batchHandlerOptions ,
254
+ options
255
+ ) ;
256
+ }
257
+
258
+ throw new ResolverNotFoundException (
259
+ `No batch resolver found for ${ typeName } -${ fieldName } `
260
+ ) ;
261
+ }
262
+
263
+ /**
264
+ * Handles batch invocation of AppSync GraphQL resolvers with support for aggregation and error handling.
265
+ *
266
+ * @param events - An array of AppSyncResolverEvent objects representing the batch of incoming events.
267
+ * @param context - The Lambda context object.
268
+ * @param options - Route handler options, including the handler function, aggregation, and error handling flags.
269
+ * @param resolveOptions - Optional resolve options, such as custom scope for handler invocation.
270
+ *
271
+ * @throws {InvalidBatchResponseException } If the aggregate handler does not return an array.
272
+ *
273
+ * @remarks
274
+ * - If `aggregate` is true, invokes the handler once with the entire batch and expects an array response.
275
+ * - If `throwOnError` is true, errors are propagated and will cause the function to throw.
276
+ * - If `throwOnError` is false, errors are logged and `null` is appended for failed events, allowing graceful degradation.
277
+ */
278
+ async #callBatchResolver(
279
+ events : AppSyncResolverEvent < Record < string , unknown > > [ ] ,
280
+ context : Context ,
281
+ options : RouteHandlerOptions < Record < string , unknown > , boolean , boolean > ,
282
+ resolveOptions ?: ResolveOptions
283
+ ) : Promise < unknown [ ] > {
284
+ const { aggregate, throwOnError } = options ;
285
+ this . logger . debug (
286
+ `Aggregate flag aggregate=${ aggregate } & graceful error handling flag throwOnError=${ throwOnError } `
287
+ ) ;
288
+
289
+ if ( aggregate ) {
290
+ const response = await (
291
+ options . handler as BatchResolverAggregateHandlerFn
292
+ ) . apply ( resolveOptions ?. scope ?? this , [
293
+ events ,
294
+ { event : events , context } ,
295
+ ] ) ;
296
+
297
+ if ( ! Array . isArray ( response ) ) {
298
+ throw new InvalidBatchResponseException (
299
+ 'The response must be an array when using batch resolvers'
300
+ ) ;
301
+ }
302
+
303
+ return response ;
304
+ }
305
+
306
+ const handler = options . handler as BatchResolverHandlerFn ;
307
+ const results : unknown [ ] = [ ] ;
308
+
309
+ if ( throwOnError ) {
310
+ for ( const event of events ) {
311
+ const result = await handler . apply ( resolveOptions ?. scope ?? this , [
312
+ event . arguments ,
313
+ { event, context } ,
314
+ ] ) ;
315
+ results . push ( result ) ;
316
+ }
317
+ return results ;
318
+ }
319
+
320
+ for ( let i = 0 ; i < events . length ; i ++ ) {
321
+ try {
322
+ const result = await handler . apply ( resolveOptions ?. scope ?? this , [
323
+ events [ i ] . arguments ,
324
+ { event : events [ i ] , context } ,
325
+ ] ) ;
326
+ results . push ( result ) ;
327
+ } catch ( error ) {
328
+ this . logger . error ( error ) ;
329
+ this . logger . debug (
330
+ `Failed to process event #${ i + 1 } from field '${ events [ i ] . info . fieldName } '`
331
+ ) ;
332
+ // By default, we gracefully append `null` for any records that failed processing
333
+ results . push ( null ) ;
334
+ }
335
+ }
336
+
337
+ return results ;
338
+ }
339
+
122
340
/**
123
341
* Executes the appropriate resolver for a given AppSync GraphQL event.
124
342
*
@@ -143,10 +361,10 @@ class AppSyncGraphQLResolver extends Router {
143
361
fieldName
144
362
) ;
145
363
if ( resolverHandlerOptions ) {
146
- return resolverHandlerOptions . handler . apply ( options ?. scope ?? this , [
147
- event . arguments ,
148
- { event, context } ,
149
- ] ) ;
364
+ return ( resolverHandlerOptions . handler as ResolverHandler ) . apply (
365
+ options ?. scope ?? this ,
366
+ [ event . arguments , { event, context } ]
367
+ ) ;
150
368
}
151
369
152
370
throw new ResolverNotFoundException (
0 commit comments