11import type { AppSyncResolverEvent , Context } from 'aws-lambda' ;
2+ import type {
3+ BatchResolverAggregateHandlerFn ,
4+ BatchResolverHandlerFn ,
5+ ResolverHandler ,
6+ RouteHandlerOptions ,
7+ } from '../types/appsync-graphql.js' ;
28import type { ResolveOptions } from '../types/common.js' ;
3- import { ResolverNotFoundException } from './errors.js' ;
9+ import {
10+ InvalidBatchResponseException ,
11+ ResolverNotFoundException ,
12+ } from './errors.js' ;
413import { Router } from './Router.js' ;
514import { isAppSyncGraphQLEvent } from './utils.js' ;
615
@@ -58,6 +67,28 @@ class AppSyncGraphQLResolver extends Router {
5867 * app.resolve(event, context);
5968 * ```
6069 *
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+ *
6192 * The method works also as class method decorator, so you can use it like this:
6293 *
6394 * @example
@@ -88,6 +119,35 @@ class AppSyncGraphQLResolver extends Router {
88119 * export const handler = lambda.handler.bind(lambda);
89120 * ```
90121 *
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+ * ```
91151 * @param event - The incoming event, which may be an AppSync GraphQL event or an array of events.
92152 * @param context - The AWS Lambda context object.
93153 * @param options - Optional parameters for the resolver, such as the scope of the handler.
@@ -98,27 +158,185 @@ class AppSyncGraphQLResolver extends Router {
98158 options ?: ResolveOptions
99159 ) : Promise < unknown > {
100160 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+ ) ;
103171 }
104172 if ( ! isAppSyncGraphQLEvent ( event ) ) {
105173 this . logger . warn (
106174 'Received an event that is not compatible with this resolver'
107175 ) ;
108176 return ;
109177 }
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 > {
110197 try {
111- return await this . #executeSingleResolver ( event , context , options ) ;
198+ return await fn ( ) ;
112199 } 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 } `
116203 ) ;
117- if ( error instanceof ResolverNotFoundException ) throw error ;
118- return this . #formatErrorResponse( error ) ;
119204 }
120205 }
121206
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+
122340 /**
123341 * Executes the appropriate resolver for a given AppSync GraphQL event.
124342 *
@@ -143,10 +361,10 @@ class AppSyncGraphQLResolver extends Router {
143361 fieldName
144362 ) ;
145363 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+ ) ;
150368 }
151369
152370 throw new ResolverNotFoundException (
0 commit comments