Skip to content

feat(event-handler): add support for AppSync GraphQL batch resolvers #4218

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 59 commits into
base: main
Choose a base branch
from

Conversation

arnabrahman
Copy link
Contributor

Summary

This PR will add the ability to register batch resolvers for AppSync GraphQL API.

Changes

  • batchResolver method is introduced to register handler for batch events

    import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql';
    
    const app = new AppSyncGraphQLResolver();
    
    app.batchResolver<{id: number}>(async (events) => {
      // Process all events in batch
      return events.map(event => ({ id: event.arguments.id, data: 'processed' }));
    }, {
      fieldName: 'getPosts'
    });
    
    export const handler = async (event, context) =>
      app.resolve(event, context);
  • Process events individually

    If you want to process each event individually instead of receiving all events at once, you can set the
    aggregate option to false. In this case, the handler will be called once for each event in the batch,
    similar to regular resolvers.

    import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql';
    
    const app = new AppSyncGraphQLResolver();
    
    app.batchResolver(async (args, { event, context }) => {
      // Process individual request
      return { id: args.id, data: 'processed' };
    }, {
      fieldName: 'getPost',
      aggregate: false
    });
    
    export const handler = async (event, context) =>
      app.resolve(event, context);
  • Strict error handling

    If you want stricter error handling when processing events individually, you can set the raiseOnError option
    to true. In this case, if any event throws an error, the entire batch processing will stop and the error
    will be propagated. Note that raiseOnError can only be used when aggregate is set to false.

    import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql';
    
    const app = new AppSyncGraphQLResolver();
    
    app.batchResolver(async (args, { event, context }) => {
      // Process individual request
      return { id: args.id, data: 'processed' };
    }, {
      fieldName: 'getPost',
      aggregate: false,
      raiseOnError: true
    });
    
    export const handler = async (event, context) =>
      app.resolve(event, context);

    You can also specify the type of the arguments using generic type parameters for non-aggregated handlers:

    import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql';
    
    const app = new AppSyncGraphQLResolver()
    
    app.batchResolver<{ postId: string }>(async (args, { event, context }) => {
      // args is typed as { postId: string }
      return { id: args.postId };
    }, {
      fieldName: 'getPost',
      aggregate: false
    });
    
    export const handler = async (event, context) =>
      app.resolve(event, context);

    As a decorator:

    import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql';
    
    const app = new AppSyncGraphQLResolver();
    
    class Lambda {
      @app.batchResolver({ fieldName: 'getPosts' })
      async handleGetPosts(events) {
        // Process batch of events
        return events.map(event => ({ id: event.arguments.id, data: 'processed' }));
      }
    
      @app.batchResolver({ fieldName: 'getPost', aggregate: false })
      async handleGetPost(args, { event, context }) {
        // Process individual request
        return { id: args.id, data: 'processed' };
      }
    
      @app.batchResolver({ fieldName: 'getPost', aggregate: false, raiseOnError: true })
      async handleGetPostStrict(args, { event, context }) {
        // Process individual request with strict error handling
        return { id: args.id, data: 'processed' };
      }
    
      async handler(event, context) {
        return app.resolve(event, context, {
          scope: this, // bind decorated methods to the class instance
        });
      }
    }
    
    const lambda = new Lambda();
    export const handler = lambda.handler.bind(lambda);
  • Also, onBatchQuery & onBatchMutation methods are defined for registering batch Query & Mutation events. aggregate & raiseOnError options are also available for these two methods.

    import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql';
    
    
    const app = new AppSyncGraphQLResolver();
    
    app.onBatchQuery<{ id: number }>('getPosts', async (events) => {
    // Process all events in batch
    return events.map(event => ({ id: event.arguments.id, data: 'processed' }));
    });
    
    export const handler = async (event, context) =>
    app.resolve(event, context);
      import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql';
      
      const app = new AppSyncGraphQLResolver();
      
      app.onBatchMutation<{ id: number }>('createPosts', async (events) => {
        // Process all events in batch
        return events.map(event => ({ id: event.arguments.id, status: 'created' }));
      });
      
      export const handler = async (event, context) =>
        app.resolve(event, context);

Issue number: #4100


By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

Disclaimer: We value your time and bandwidth. As such, any pull requests created on non-triaged issues might not be successful.

@pull-request-size pull-request-size bot added the size/XXL PRs with 1K+ LOC, largely documentation related label Jul 27, 2025
@boring-cyborg boring-cyborg bot added event-handler This item relates to the Event Handler Utility tests PRs that add or change tests labels Jul 27, 2025
@arnabrahman arnabrahman marked this pull request as ready for review July 27, 2025 15:33
@dreamorosi dreamorosi requested review from svozza and sdangol July 27, 2025 16:37
const results: unknown[] = [];

if (raiseOnError) {
for (const event of events) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should give users the option to do these in parallel with Promise.all.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When raiseOnError is true, we stop on the first error. But if we use Promise.all, I believe we won’t be able to stop at the first error, since the promises will run in parallel.

However, it should be possible when raiseOnError is false. Also, do we want to give the user the option for this or just do parallel processing by default, similar to what has been done in AppSyncEventResolver

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way Promise.all works is that the first error causes all in-flight promises to be rejected so I think that's still OK. But we would need to use Promise.allSettled if raiseOnError was false. Good point about following what's done in AppSyncEventResolver, I think we should do that.

Small nit about the raiseOnError variable name, would throwOnError not be more appropriate. The word raise is more a Python thing. to my mind.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good callout about throwOn..., we use this in several other places in this version of Powertools for AWS:

As well as probably others, so +1 on changing it to this.


Regarding the Promise.all, if I remember correctly we didn't use it in the other Event Handler because even though the first rejected promises causes the "overall" promise to also be rejected, it doesn't abort the other in-flight promises, for example:

const promise1 = new Promise((resolve, reject) => {
  setTimeout(resolve, 2000, 'resolve1');
}).then((a) => {
  console.log('then1');
  return a;
});

const promise2 = new Promise((resolve, reject) => {
  setTimeout(reject, 1000, 'reject2');
}).then((a) => {
  console.log('then2');
  return a;
});

const promise3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 3000, 'resolve3');
}).then((a) => {
  console.log('then3');
  return a;
});

Promise.all([promise1, promise2, promise3])
  .then((values) => {
    console.log('then', values);
  })
  .catch((err) => {
    console.log('catch', err);
  });

results in:

catch reject2
then1
then3

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, that makes sense!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renamed raiseOnError to throwOnError

Copy link

@arnabrahman arnabrahman requested a review from svozza July 29, 2025 04:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
event-handler This item relates to the Event Handler Utility size/XXL PRs with 1K+ LOC, largely documentation related tests PRs that add or change tests
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants