diff --git a/docs/features/event-handler/appsync-events.md b/docs/features/event-handler/appsync-events.md index 8d220ce921..58defee58e 100644 --- a/docs/features/event-handler/appsync-events.md +++ b/docs/features/event-handler/appsync-events.md @@ -1,7 +1,6 @@ --- title: AppSync Events description: Event Handler for AWS AppSync real-time events -status: new --- Event Handler for AWS AppSync real-time events. diff --git a/docs/features/event-handler/appsync-graphql.md b/docs/features/event-handler/appsync-graphql.md new file mode 100644 index 0000000000..e782593fba --- /dev/null +++ b/docs/features/event-handler/appsync-graphql.md @@ -0,0 +1,181 @@ +--- +title: AppSync GraphQL +description: Event Handler for AppSync GraphQL APIs +status: new +--- + +Event Handler for AWS AppSync GraphQL APIs simplifies routing and processing of events in AWS Lambda functions. It allows you to define resolvers for GraphQL types and fields, making it easier to handle GraphQL requests without the need for complex VTL or JavaScript templates. + +```mermaid +--8<-- "examples/snippets/event-handler/appsync-graphql/diagrams/intro.mermaid" +``` + +## Key Features + +- Route events based on GraphQL type and field keys +- Automatically parse API arguments to function parameters +- Handle GraphQL responses and errors in the expected format + +## Terminology + +**[Direct Lambda Resolver](https://docs.aws.amazon.com/appsync/latest/devguide/direct-lambda-reference.html){target="_blank"}**. A custom AppSync Resolver that bypasses Apache Velocity Template (VTL) and JavaScript templates, and automatically maps your function's response to a GraphQL field. + +**[Batching resolvers](https://docs.aws.amazon.com/appsync/latest/devguide/tutorial-lambda-resolvers.html#advanced-use-case-batching){target="_blank"}**. A technique that allows you to batch multiple GraphQL requests into a single Lambda function invocation, reducing the number of calls and improving performance. + +## Getting started + +???+ tip "Tip: Designing GraphQL Schemas for the first time?" + Visit [AWS AppSync schema documentation](https://docs.aws.amazon.com/appsync/latest/devguide/designing-your-schema.html){target="_blank"} to understand how to define types, nesting, and pagination. + +### Required resources + +You must have an existing AppSync GraphQL API and IAM permissions to invoke your Lambda function. That said, there is no additional permissions to use Event Handler as routing requires no dependency (_standard library_). + +This is the sample infrastructure we will be using for the initial examples with an AppSync Direct Lambda Resolver. + +=== "gettingStartedSchema.graphql" + + ```typescript + --8<-- "examples/snippets/event-handler/appsync-graphql/templates/gettingStartedSchema.graphql" + ``` + +=== "template.yaml" + + ```yaml hl_lines="59-60 71-72 94-95 104-105 112-113" + --8<-- "examples/snippets/event-handler/appsync-graphql/templates/gettingStartedSam.yaml" + ``` + +### Registering a resolver + +You can register functions to match GraphQL types and fields with one of three methods: + +- `onQuery()` - Register a function to handle a GraphQL Query type. +- `onMutation()` - Register a function to handle a GraphQL Mutation type. +- `resolver()` - Register a function to handle a GraphQL type and field. + +!!! question "What is a type and field?" + A type would be a top-level **GraphQL Type** like `Query`, `Mutation`, `Todo`. A **GraphQL Field** would be `listTodos` under `Query`, `createTodo` under `Mutation`, etc. + +The function receives the parsed arguments from the GraphQL request as its first parameter. We also take care of parsing the response or catching errors and returning them in the expected format. + +#### Query resolver + +When registering a resolver for a `Query` type, you can use the `onQuery()` method. This method allows you to define a function that will be invoked when a GraphQL Query is made. + +```typescript hl_lines="2 8 10 21" title="Registering a resolver for a Query type" +--8<-- "examples/snippets/event-handler/appsync-graphql/gettingStartedOnQuery.ts" +``` + +#### Mutation resolver + +Similarly, you can register a resolver for a `Mutation` type using the `onMutation()` method. This method allows you to define a function that will be invoked when a GraphQL Mutation is made. + +```typescript hl_lines="2-5 11 13 25" title="Registering a resolver for a Mutation type" +--8<-- "examples/snippets/event-handler/appsync-graphql/gettingStartedOnMutation.ts" +``` + +#### Generic resolver + +When you want to have more control over the type and field, you can use the `resolver()` method. This method allows you to register a function for a specific GraphQL type and field including custom types. + +```typescript hl_lines="2 8 10 27-30" title="Registering a resolver for a type and field" +--8<-- "examples/snippets/event-handler/appsync-graphql/gettingStartedResolver.ts" +``` + +#### Using decorators + +If you prefer to use the decorator syntax, you can instead use the same methods on a class method to register your handlers. Learn more about how Powertools for TypeScript supports [decorators](../../getting-started/usage-patterns.md). + +```typescript hl_lines="3-6 12 15 27 38 60" title="Using decorators to register a resolver" +--8<-- "examples/snippets/event-handler/appsync-graphql/gettingStartedDecorators.ts" +``` + +1. It's recommended to pass a refernce of `this` to ensure the correct class scope is propageted to the route handler functions. + +### Scalar functions + +When working with [AWS AppSync Scalar types](https://docs.aws.amazon.com/appsync/latest/devguide/scalars.html){target="_blank"}, you might want to generate the same values for data validation purposes. + +For convenience, the most commonly used values are available as helper functions within the module. + +```typescript hl_lines="2-6" title="Creating key scalar values" +--8<-- "examples/snippets/event-handler/appsync-graphql/gettingStartedScalarFunctions.ts" +``` + +Here's a table with their related scalar as a quick reference: + +| Scalar type | Scalar function | Sample value | +| ---------------- | --------------- | -------------------------------------- | +| **ID** | `makeId` | `e916c84d-48b6-484c-bef3-cee3e4d86ebf` | +| **AWSDate** | `awsDate` | `2022-07-08Z` | +| **AWSTime** | `awsTime` | `15:11:00.189Z` | +| **AWSDateTime** | `awsDateTime` | `2022-07-08T15:11:00.189Z` | +| **AWSTimestamp** | `awsTimestamp` | `1657293060` | + +## Advanced + +### Nested mappings + +!!! note + + The following examples use a more advanced schema. These schemas differ from the [initial sample infrastructure we used earlier](#required-resources). + +You can register the same route handler multiple times to resolve fields with the same return value. + +=== "Nested Mappings Example" + + ```typescript hl_lines="8 33-39" + --8<-- "examples/snippets/event-handler/appsync-graphql/advancedNestedMappings.ts" + ``` + + 1. If omitted, the `typeName` defaults to `Query`. + +=== "Nested Mappings Schema" + + ```graphql hl_lines="6 20" + --8<-- "examples/snippets/event-handler/appsync-graphql/templates/advancedNestedMappingsSchema.graphql" + ``` + +### Accessing Lambda context and event + +You can access the original Lambda event or context for additional information. These are passed to the handler function as optional arguments. + +=== "Access event and context" + + ```typescript hl_lines="10" + --8<-- "examples/snippets/event-handler/appsync-graphql/advancedAccessEventAndContext.ts" + ``` + + 1. The `event` parameter contains the original AppSync event and has type `AppSyncResolverEvent` from the `@types/aws-lambda`. + +### Logging + +By default, the utility uses the global `console` logger and emits only warnings and errors. + +You can change this behavior by passing a custom logger instance to the `AppSyncGraphQLResolver` or `Router` and setting the log level for it, or by enabling [Lambda Advanced Logging Controls](https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs-advanced.html) and setting the log level to `DEBUG`. + +When debug logging is enabled, the resolver will emit logs that show the underlying handler resolution process. This is useful for understanding how your handlers are being resolved and invoked and can help you troubleshoot issues with your event processing. + +For example, when using the [Powertools for AWS Lambda logger](../logger.md), you can set the `LOG_LEVEL` to `DEBUG` in your environment variables or at the logger level and pass the logger instance to the constructor to enable debug logging. + +=== "Debug logging" + + ```typescript hl_lines="11" + --8<-- "examples/snippets/event-handler/appsync-graphql/advancedDebugLogging.ts" + ``` + +=== "Logs output" + + ```json + --8<-- "examples/snippets/event-handler/appsync-graphql/samples/debugLogExcerpt.json" + ``` + +## Testing your code + +You can test your resolvers by passing an event with the shape expected by the AppSync GraphQL API resolver. + +Here's an example of how you can test your resolvers that uses a factory function to create the event shape: + +```typescript +--8<-- "examples/snippets/event-handler/appsync-graphql/advancedTestYourCode.ts" +``` diff --git a/docs/features/event-handler/index.md b/docs/features/event-handler/index.md new file mode 100644 index 0000000000..6ee9a38397 --- /dev/null +++ b/docs/features/event-handler/index.md @@ -0,0 +1,34 @@ +--- +title: Event Handler +description: Simplify routing and processing of events in AWS Lambda functions +--- + + + +
+ +- __AppSync Events API__ + + --- + + Event Handler for AWS AppSync real-time events, easily handle publish and subscribe events with dedicated handler methods. + + [:octicons-arrow-right-24: Read more](./appsync-events.md) + +- __AppSync GraphQL API__ + + --- + + Event Handler for AWS AppSync GraphQL APIs, it allows you to define resolvers for GraphQL types and fields, making it easier to handle requests without the need for complex VTL or JavaScript templates. + + [:octicons-arrow-right-24: Read more](./appsync-graphql.md) + +- __Bedrock Agents__ + + --- + + Create [Amazon Bedrock Agents](https://docs.aws.amazon.com/bedrock/latest/userguide/agents.html#agents-how) and focus on building your agent's logic without worrying about parsing and routing requests. + + [:octicons-arrow-right-24: Read more](./bedrock-agents.md) + +
diff --git a/docs/features/index.md b/docs/features/index.md index 43653c06dc..3460132579 100644 --- a/docs/features/index.md +++ b/docs/features/index.md @@ -31,11 +31,11 @@ description: Features of Powertools for AWS Lambda [:octicons-arrow-right-24: Read more](./metrics.md) -- __Event Handler - AppSync Events__ +- __Event Handler__ --- - Event Handler for AWS AppSync real-time events + Event Handler for AWS AppSync real-time events, AppSync GraphQL APIs, Bedrock Agents, and more. It simplifies routing and processing of events in AWS Lambda functions. [:octicons-arrow-right-24: Read more](./event-handler/appsync-events.md) diff --git a/docs/index.md b/docs/index.md index 1595509d5d..5ce7348fc6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -47,7 +47,7 @@ Powertools for AWS Lambda (TypeScript) is built as a modular toolkit, so you can | [Tracer](./features/tracer.md) | Decorators and utilities to trace Lambda function handlers, and both synchronous and asynchronous functions | | [Logger](./features/logger.md) | Structured logging made easier, and a middleware to enrich structured logging with key Lambda context details | | [Metrics](./features/metrics.md) | Custom Metrics created asynchronously via CloudWatch Embedded Metric Format (EMF) | -| [Event Handler - AppSync Events](./features/event-handler/appsync-events.md) | Event Handler for AWS AppSync real-time events | +| [Event Handler](./features/event-handler/appsync-events.md) | Event Handler for AWS AppSync real-time events, AppSync GraphQL APIs, Bedrock Agents, and more. | | [Parameters](./features/parameters.md) | High-level functions to retrieve one or more parameters from AWS SSM Parameter Store, AWS Secrets Manager, AWS AppConfig, and Amazon DynamoDB | | [Idempotency](./features/idempotency.md) | Class method decorator, Middy middleware, and function wrapper to make your Lambda functions idempotent and prevent duplicate execution based on payload content. | | [Batch Processing](./features/batch.md) | Utility to handle partial failures when processing batches from Amazon SQS, Amazon Kinesis Data Streams, and Amazon DynamoDB Streams. | diff --git a/examples/snippets/event-handler/appsync-events/gettingStartedOnPublishDecorator.ts b/examples/snippets/event-handler/appsync-events/gettingStartedOnPublishDecorator.ts index b577acc29d..53e2da432b 100644 --- a/examples/snippets/event-handler/appsync-events/gettingStartedOnPublishDecorator.ts +++ b/examples/snippets/event-handler/appsync-events/gettingStartedOnPublishDecorator.ts @@ -14,7 +14,7 @@ class Lambda { } async handler(event: unknown, context: Context) { - return app.resolve(event, context); + return app.resolve(event, context, { scope: this }); } } diff --git a/examples/snippets/event-handler/appsync-events/gettingStartedOnSubscribeDecorator.ts b/examples/snippets/event-handler/appsync-events/gettingStartedOnSubscribeDecorator.ts index 43ba8e551d..bbcc0b81bd 100644 --- a/examples/snippets/event-handler/appsync-events/gettingStartedOnSubscribeDecorator.ts +++ b/examples/snippets/event-handler/appsync-events/gettingStartedOnSubscribeDecorator.ts @@ -18,7 +18,7 @@ class Lambda { } async handler(event: unknown, context: Context) { - return app.resolve(event, context); + return app.resolve(event, context, { scope: this }); } } diff --git a/examples/snippets/event-handler/appsync-graphql/advancedAccessEventAndContext.ts b/examples/snippets/event-handler/appsync-graphql/advancedAccessEventAndContext.ts new file mode 100644 index 0000000000..fbc9d699c7 --- /dev/null +++ b/examples/snippets/event-handler/appsync-graphql/advancedAccessEventAndContext.ts @@ -0,0 +1,23 @@ +import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; +import { Logger } from '@aws-lambda-powertools/logger'; +import type { Context } from 'aws-lambda'; + +const logger = new Logger({ + serviceName: 'TodoManager', +}); +const app = new AppSyncGraphQLResolver({ logger }); + +app.onQuery<{ id: string }>('getTodo', async ({ id }, { event, context }) => { + const { headers } = event.request; // (1)! + const { awsRequestId } = context; + logger.info('headers', { headers, awsRequestId }); + + return { + id, + title: 'Todo Title', + completed: false, + }; +}); + +export const handler = async (event: unknown, context: Context) => + app.resolve(event, context); diff --git a/examples/snippets/event-handler/appsync-graphql/advancedDebugLogging.ts b/examples/snippets/event-handler/appsync-graphql/advancedDebugLogging.ts new file mode 100644 index 0000000000..cbfbc7d3fb --- /dev/null +++ b/examples/snippets/event-handler/appsync-graphql/advancedDebugLogging.ts @@ -0,0 +1,29 @@ +import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; +import { Logger } from '@aws-lambda-powertools/logger'; +import { + correlationPaths, + search, +} from '@aws-lambda-powertools/logger/correlationId'; +import type { Context } from 'aws-lambda'; + +const logger = new Logger({ + serviceName: 'TodoManager', + logLevel: 'DEBUG', + correlationIdSearchFn: search, +}); +const app = new AppSyncGraphQLResolver({ logger }); + +app.onQuery<{ id: string }>('getTodo', async ({ id }) => { + logger.debug('Resolving todo', { id }); + // Simulate fetching a todo from a database or external service + return { + id, + title: 'Todo Title', + completed: false, + }; +}); + +export const handler = async (event: unknown, context: Context) => { + logger.setCorrelationId(event, correlationPaths.APPSYNC_RESOLVER); + return app.resolve(event, context); +}; diff --git a/examples/snippets/event-handler/appsync-graphql/advancedNestedMappings.ts b/examples/snippets/event-handler/appsync-graphql/advancedNestedMappings.ts new file mode 100644 index 0000000000..7eea465eb7 --- /dev/null +++ b/examples/snippets/event-handler/appsync-graphql/advancedNestedMappings.ts @@ -0,0 +1,42 @@ +import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; +import { Logger } from '@aws-lambda-powertools/logger'; +import type { Context } from 'aws-lambda'; + +const logger = new Logger({ + serviceName: 'TodoManager', +}); +const app = new AppSyncGraphQLResolver({ logger }); + +type Location = { + id: string; + name: string; + description?: string; +}; + +const locationsResolver = async (): Promise => { + logger.debug('Resolving locations'); + // Simulate fetching locations from a database or external service + return [ + { + id: 'loc1', + name: 'Location One', + description: 'First location description', + }, + { + id: 'loc2', + name: 'Location Two', + description: 'Second location description', + }, + ]; +}; + +app.resolver(locationsResolver, { + fieldName: 'locations', + typeName: 'Merchant', +}); +app.resolver(locationsResolver, { + fieldName: 'listLocations', // (1)! +}); + +export const handler = async (event: unknown, context: Context) => + app.resolve(event, context); diff --git a/examples/snippets/event-handler/appsync-graphql/advancedTestYourCode.ts b/examples/snippets/event-handler/appsync-graphql/advancedTestYourCode.ts new file mode 100644 index 0000000000..adf69ab2ee --- /dev/null +++ b/examples/snippets/event-handler/appsync-graphql/advancedTestYourCode.ts @@ -0,0 +1,56 @@ +import type { Context } from 'aws-lambda'; +import { describe, expect, it } from 'vitest'; +import { handler } from './advancedNestedMappings.js'; + +const createEventFactory = ( + fieldName: string, + args: Record, + parentTypeName: string +) => ({ + arguments: { ...args }, + identity: null, + source: null, + request: { + headers: { + key: 'value', + }, + domainName: null, + }, + info: { + fieldName, + parentTypeName, + selectionSetList: [], + variables: {}, + }, + prev: null, + stash: {}, +}); + +const onGraphqlEventFactory = ( + fieldName: string, + typeName: 'Query' | 'Mutation', + args: Record = {} +) => createEventFactory(fieldName, args, typeName); + +describe('Unit test for AppSync GraphQL Resolver', () => { + it('returns the location', async () => { + // Prepare + const event = onGraphqlEventFactory('listLocations', 'Query'); + + // Act + const result = (await handler(event, {} as Context)) as Promise; + + // Assess + expect(result).toHaveLength(2); + expect(result[0]).toEqual({ + id: 'loc1', + name: 'Location One', + description: 'First location description', + }); + expect(result[1]).toEqual({ + id: 'loc2', + name: 'Location Two', + description: 'Second location description', + }); + }); +}); diff --git a/examples/snippets/event-handler/appsync-graphql/diagrams/intro.mermaid b/examples/snippets/event-handler/appsync-graphql/diagrams/intro.mermaid new file mode 100644 index 0000000000..d825cddacd --- /dev/null +++ b/examples/snippets/event-handler/appsync-graphql/diagrams/intro.mermaid @@ -0,0 +1,27 @@ +stateDiagram-v2 + direction LR + EventSource: AWS Lambda Event Sources + EventHandlerResolvers: AWS AppSync invocation + LambdaInit: Lambda invocation + EventHandler: Event Handler + EventHandlerResolver: Route event based on GraphQL type/field keys + YourLogic: Run your registered resolver function + EventHandlerResolverBuilder: Adapts response to Event Source contract + LambdaResponse: Lambda response + + state EventSource { + EventHandlerResolvers + } + + EventHandlerResolvers --> LambdaInit + + LambdaInit --> EventHandler + EventHandler --> EventHandlerResolver + + state EventHandler { + [*] --> EventHandlerResolver: app.resolve(event, context) + EventHandlerResolver --> YourLogic + YourLogic --> EventHandlerResolverBuilder + } + + EventHandler --> LambdaResponse \ No newline at end of file diff --git a/examples/snippets/event-handler/appsync-graphql/gettingStartedDecorators.ts b/examples/snippets/event-handler/appsync-graphql/gettingStartedDecorators.ts new file mode 100644 index 0000000000..6e876bdeab --- /dev/null +++ b/examples/snippets/event-handler/appsync-graphql/gettingStartedDecorators.ts @@ -0,0 +1,65 @@ +import type { LambdaInterface } from '@aws-lambda-powertools/commons/types'; +import { + AppSyncGraphQLResolver, + makeId, +} from '@aws-lambda-powertools/event-handler/appsync-graphql'; +import { Logger } from '@aws-lambda-powertools/logger'; +import type { Context } from 'aws-lambda'; + +const logger = new Logger({ + serviceName: 'TodoManager', +}); +const app = new AppSyncGraphQLResolver({ logger }); + +class Lambda implements LambdaInterface { + @app.onMutation('createTodo') + public async createTodo({ title }: { title: string }) { + logger.debug('Creating todo', { title }); + const todoId = makeId(); + // Simulate creating a todo in a database or external service + return { + id: todoId, + title, + completed: false, + }; + } + + @app.onQuery('getTodo') + public async getTodo({ id }: { id: string }) { + logger.debug('Resolving todo', { id }); + // Simulate fetching a todo from a database or external service + return { + id, + title: 'Todo Title', + completed: false, + }; + } + + @app.resolver({ + fieldName: 'listTodos', + typeName: 'Query', + }) + public async listTodos() { + logger.debug('Resolving todos'); + // Simulate fetching a todo from a database or external service + return [ + { + id: 'todo-id', + title: 'Todo Title', + completed: false, + }, + { + id: 'todo-id-2', + title: 'Todo Title 2', + completed: true, + }, + ]; + } + + async handler(event: unknown, context: Context) { + return app.resolve(event, context, { scope: this }); // (1)! + } +} + +const lambda = new Lambda(); +export const handler = lambda.handler.bind(lambda); diff --git a/examples/snippets/event-handler/appsync-graphql/gettingStartedOnMutation.ts b/examples/snippets/event-handler/appsync-graphql/gettingStartedOnMutation.ts new file mode 100644 index 0000000000..746ccc98ac --- /dev/null +++ b/examples/snippets/event-handler/appsync-graphql/gettingStartedOnMutation.ts @@ -0,0 +1,25 @@ +import { + AppSyncGraphQLResolver, + makeId, +} from '@aws-lambda-powertools/event-handler/appsync-graphql'; +import { Logger } from '@aws-lambda-powertools/logger'; +import type { Context } from 'aws-lambda'; + +const logger = new Logger({ + serviceName: 'TodoManager', +}); +const app = new AppSyncGraphQLResolver({ logger }); + +app.onMutation<{ title: string }>('createTodo', async ({ title }) => { + logger.debug('Creating todo', { title }); + const todoId = makeId(); + // Simulate creating a todo in a database or external service + return { + id: todoId, + title, + completed: false, + }; +}); + +export const handler = async (event: unknown, context: Context) => + app.resolve(event, context); diff --git a/examples/snippets/event-handler/appsync-graphql/gettingStartedOnQuery.ts b/examples/snippets/event-handler/appsync-graphql/gettingStartedOnQuery.ts new file mode 100644 index 0000000000..8afb93f58b --- /dev/null +++ b/examples/snippets/event-handler/appsync-graphql/gettingStartedOnQuery.ts @@ -0,0 +1,21 @@ +import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; +import { Logger } from '@aws-lambda-powertools/logger'; +import type { Context } from 'aws-lambda'; + +const logger = new Logger({ + serviceName: 'TodoManager', +}); +const app = new AppSyncGraphQLResolver({ logger }); + +app.onQuery<{ id: string }>('getTodo', async ({ id }) => { + logger.debug('Resolving todo', { id }); + // Simulate fetching a todo from a database or external service + return { + id, + title: 'Todo Title', + completed: false, + }; +}); + +export const handler = async (event: unknown, context: Context) => + app.resolve(event, context); diff --git a/examples/snippets/event-handler/appsync-graphql/gettingStartedResolver.ts b/examples/snippets/event-handler/appsync-graphql/gettingStartedResolver.ts new file mode 100644 index 0000000000..e1b797da3d --- /dev/null +++ b/examples/snippets/event-handler/appsync-graphql/gettingStartedResolver.ts @@ -0,0 +1,34 @@ +import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; +import { Logger } from '@aws-lambda-powertools/logger'; +import type { Context } from 'aws-lambda'; + +const logger = new Logger({ + serviceName: 'TodoManager', +}); +const app = new AppSyncGraphQLResolver({ logger }); + +app.resolver( + async () => { + logger.debug('Resolving todos'); + // Simulate fetching a todo from a database or external service + return [ + { + id: 'todo-id', + title: 'Todo Title', + completed: false, + }, + { + id: 'todo-id-2', + title: 'Todo Title 2', + completed: true, + }, + ]; + }, + { + fieldName: 'listTodos', + typeName: 'Query', + } +); + +export const handler = async (event: unknown, context: Context) => + app.resolve(event, context); diff --git a/examples/snippets/event-handler/appsync-graphql/gettingStartedScalarFunctions.ts b/examples/snippets/event-handler/appsync-graphql/gettingStartedScalarFunctions.ts new file mode 100644 index 0000000000..f6c2fe912b --- /dev/null +++ b/examples/snippets/event-handler/appsync-graphql/gettingStartedScalarFunctions.ts @@ -0,0 +1,34 @@ +import { + AppSyncGraphQLResolver, + awsDate, + awsDateTime, + awsTime, + awsTimestamp, + makeId, +} from '@aws-lambda-powertools/event-handler/appsync-graphql'; +import type { Context } from 'aws-lambda'; + +const app = new AppSyncGraphQLResolver(); + +app.resolver( + async ({ title, content }) => { + // your business logic here + return { + title, + content, + id: makeId(), + createdAt: awsDateTime(), + updatedAt: awsDateTime(), + timestamp: awsTimestamp(), + time: awsTime(), + date: awsDate(), + }; + }, + { + fieldName: 'createTodo', + typeName: 'Mutation', + } +); + +export const handler = async (event: unknown, context: Context) => + app.resolve(event, context); diff --git a/examples/snippets/event-handler/appsync-graphql/samples/debugLogExcerpt.json b/examples/snippets/event-handler/appsync-graphql/samples/debugLogExcerpt.json new file mode 100644 index 0000000000..79a5d8ae06 --- /dev/null +++ b/examples/snippets/event-handler/appsync-graphql/samples/debugLogExcerpt.json @@ -0,0 +1,28 @@ +[ + { + "level": "DEBUG", + "message": "Adding resolver for field Query.getTodo", + "timestamp": "2025-07-02T13:39:36.017Z", + "service": "service_undefined", + "sampling_rate": 0 + }, + { + "level": "DEBUG", + "message": "Looking for resolver for type=Query, field=getTodo", + "timestamp": "2025-07-02T13:39:36.033Z", + "service": "service_undefined", + "sampling_rate": 0, + "xray_trace_id": "1-68653697-0f1223120d19409c38812f01", + "correlation_id": "Root=1-68653697-3623822a02e171272e2ecfe4" + }, + { + "level": "DEBUG", + "message": "Resolving todo", + "timestamp": "2025-07-02T13:39:36.033Z", + "service": "service_undefined", + "sampling_rate": 0, + "xray_trace_id": "1-68653697-0f1223120d19409c38812f01", + "correlation_id": "Root=1-68653697-3623822a02e171272e2ecfe4", + "id": "42" + } +] \ No newline at end of file diff --git a/examples/snippets/event-handler/appsync-graphql/templates/advancedNestedMappingsSchema.graphql b/examples/snippets/event-handler/appsync-graphql/templates/advancedNestedMappingsSchema.graphql new file mode 100644 index 0000000000..a72ae629b4 --- /dev/null +++ b/examples/snippets/event-handler/appsync-graphql/templates/advancedNestedMappingsSchema.graphql @@ -0,0 +1,21 @@ +schema { + query: Query +} + +type Query { + listLocations: [Location] +} + +type Location { + id: ID! + name: String! + description: String + address: String +} + +type Merchant { + id: String! + name: String! + description: String + locations: [Location] +} \ No newline at end of file diff --git a/examples/snippets/event-handler/appsync-graphql/templates/gettingStartedSam.yaml b/examples/snippets/event-handler/appsync-graphql/templates/gettingStartedSam.yaml new file mode 100644 index 0000000000..57e05a6c37 --- /dev/null +++ b/examples/snippets/event-handler/appsync-graphql/templates/gettingStartedSam.yaml @@ -0,0 +1,121 @@ +AWSTemplateFormatVersion: "2010-09-09" +Transform: AWS::Serverless-2016-10-31 +Description: Hello world Direct Lambda Resolver + +Globals: + Function: + Timeout: 5 + MemorySize: 256 + Runtime: nodejs22.x + Environment: + Variables: + # Powertools for AWS Lambda (TypeScript) env vars: https://docs.powertools.aws.dev/lambda/typescript/latest/environment-variables/ + POWERTOOLS_LOG_LEVEL: INFO + POWERTOOLS_LOGGER_SAMPLE_RATE: 0.1 + POWERTOOLS_LOGGER_LOG_EVENT: true + POWERTOOLS_SERVICE_NAME: example + +Resources: + TodosFunction: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + CodeUri: hello_world + + # IAM Permissions and Roles + + AppSyncServiceRole: + Type: "AWS::IAM::Role" + Properties: + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: "Allow" + Principal: + Service: + - "appsync.amazonaws.com" + Action: + - "sts:AssumeRole" + + InvokeLambdaResolverPolicy: + Type: "AWS::IAM::Policy" + Properties: + PolicyName: "DirectAppSyncLambda" + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: "Allow" + Action: "lambda:invokeFunction" + Resource: + - !GetAtt TodosFunction.Arn + Roles: + - !Ref AppSyncServiceRole + + # GraphQL API + + TodosApi: + Type: "AWS::AppSync::GraphQLApi" + Properties: + Name: TodosApi + AuthenticationType: "API_KEY" + XrayEnabled: true + + TodosApiKey: + Type: AWS::AppSync::ApiKey + Properties: + ApiId: !GetAtt TodosApi.ApiId + + TodosApiSchema: + Type: "AWS::AppSync::GraphQLSchema" + Properties: + ApiId: !GetAtt TodosApi.ApiId + DefinitionS3Location: ../src/getting_started_schema.graphql + Metadata: + cfn-lint: + config: + ignore_checks: + - W3002 # allow relative path in DefinitionS3Location + + # Lambda Direct Data Source and Resolver + + TodosFunctionDataSource: + Type: "AWS::AppSync::DataSource" + Properties: + ApiId: !GetAtt TodosApi.ApiId + Name: "HelloWorldLambdaDirectResolver" + Type: "AWS_LAMBDA" + ServiceRoleArn: !GetAtt AppSyncServiceRole.Arn + LambdaConfig: + LambdaFunctionArn: !GetAtt TodosFunction.Arn + + ListTodosResolver: + Type: "AWS::AppSync::Resolver" + Properties: + ApiId: !GetAtt TodosApi.ApiId + TypeName: "Query" + FieldName: "listTodos" + DataSourceName: !GetAtt TodosFunctionDataSource.Name + + GetTodoResolver: + Type: "AWS::AppSync::Resolver" + Properties: + ApiId: !GetAtt TodosApi.ApiId + TypeName: "Query" + FieldName: "getTodo" + DataSourceName: !GetAtt TodosFunctionDataSource.Name + + CreateTodoResolver: + Type: "AWS::AppSync::Resolver" + Properties: + ApiId: !GetAtt TodosApi.ApiId + TypeName: "Mutation" + FieldName: "createTodo" + DataSourceName: !GetAtt TodosFunctionDataSource.Name + +Outputs: + TodosFunction: + Description: "Hello World Lambda Function ARN" + Value: !GetAtt TodosFunction.Arn + + TodosApi: + Value: !GetAtt TodosApi.GraphQLUrl \ No newline at end of file diff --git a/examples/snippets/event-handler/appsync-graphql/templates/gettingStartedSchema.graphql b/examples/snippets/event-handler/appsync-graphql/templates/gettingStartedSchema.graphql new file mode 100644 index 0000000000..803b02969a --- /dev/null +++ b/examples/snippets/event-handler/appsync-graphql/templates/gettingStartedSchema.graphql @@ -0,0 +1,21 @@ +schema { + query: Query + mutation: Mutation +} + +type Query { + # these are fields you can attach resolvers to (type_name: Query, field_name: getTodo) + getTodo(id: ID!): Todo + listTodos: [Todo] +} + +type Mutation { + createTodo(title: String!): Todo +} + +type Todo { + id: ID! + userId: String + title: String + completed: Boolean +} \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index bd1f584dfb..8c06888903 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -46,7 +46,9 @@ nav: - features/logger.md - features/metrics.md - Event Handler: + - features/event-handler/index.md - features/event-handler/appsync-events.md + - features/event-handler/appsync-graphql.md - features/event-handler/bedrock-agents.md - features/parameters.md - features/idempotency.md @@ -175,6 +177,7 @@ plugins: - features/logger.md - features/metrics.md - features/event-handler/appsync-events.md + - features/event-handler/appsync-graphql.md - features/event-handler/bedrock-agents.md - features/parameters.md - features/idempotency.md diff --git a/packages/event-handler/README.md b/packages/event-handler/README.md index b7c4608025..27fa191778 100644 --- a/packages/event-handler/README.md +++ b/packages/event-handler/README.md @@ -6,7 +6,7 @@ You can use the library in both TypeScript and JavaScript code bases. ## Intro -Event handler for Amazon API Gateway REST and HTTP APIs, Application Loader Balancer (ALB), Lambda Function URLs, VPC Lattice, AWS AppSync Events APIs, and Amazon Bedrock Agent Functions. +Event handler for AWS AppSync GraphQL APIs, AWS AppSync Events APIs, and Amazon Bedrock Agent Functions. ## Usage @@ -102,6 +102,117 @@ export const handler = async (event, context) => app.resolve(event, context); ``` +## AppSync GraphQL + +The Event Handler for AWS AppSync GraphQL APIs allows you to easily handle GraphQL requests in your Lambda functions. It enables you to define resolvers for GraphQL types and fields, making it easier to handle GraphQL requests without the need for complex VTL or JavaScript templates. + +* Route events based on GraphQL type and field keys +* Automatically parse API arguments to function parameters +* Handle GraphQL responses and errors in the expected format + +### Handle query requests + +When registering a resolver for a Query type, you can use the `onQuery()` method. This method allows you to define a function that will be invoked when a GraphQL Query is made. + +```typescript +import { Logger } from '@aws-lambda-powertools/logger'; +import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; +import type { Context } from 'aws-lambda'; + +const logger = new Logger({ + serviceName: 'TodoManager', +}); +const app = new AppSyncGraphQLResolver({ logger }); + +app.onQuery<{ id: string }>('getTodo', async ({ id }) => { + logger.debug('Resolving todo', { id }); + // Simulate fetching a todo from a database or external service + return { + id, + title: 'Todo Title', + completed: false, + }; +}); + +export const handler = async (event: unknown, context: Context) => + app.resolve(event, context); +``` + +### Handle mutation requests + +Similarly, you can register a resolver for a Mutation type using the `onMutation()` method. This method allows you to define a function that will be invoked when a GraphQL Mutation is made. + +```typescript +import { Logger } from '@aws-lambda-powertools/logger'; +import { + AppSyncGraphQLResolver, + makeId, +} from '@aws-lambda-powertools/event-handler/appsync-graphql'; +import type { Context } from 'aws-lambda'; + +const logger = new Logger({ + serviceName: 'TodoManager', +}); +const app = new AppSyncGraphQLResolver({ logger }); + +app.onMutation<{ title: string }>('createTodo', async ({ title }) => { + logger.debug('Creating todo', { title }); + const todoId = makeId(); + // Simulate creating a todo in a database or external service + return { + id: todoId, + title, + completed: false, + }; +}); + +export const handler = async (event: unknown, context: Context) => + app.resolve(event, context); +``` + +### Generic resolver + +When you want to have more control over the type and field, you can use the `resolver()` method. This method allows you to register a function for a specific GraphQL type and field including custom types. + +```typescript +import { Logger } from '@aws-lambda-powertools/logger'; +import { AppSyncGraphQLResolver } from '@aws-lambda-powertools/event-handler/appsync-graphql'; +import type { Context } from 'aws-lambda'; + +const logger = new Logger({ + serviceName: 'TodoManager', +}); +const app = new AppSyncGraphQLResolver({ logger }); + +app.resolver( + async () => { + logger.debug('Resolving todos'); + // Simulate fetching a todo from a database or external service + return [ + { + id: 'todo-id', + title: 'Todo Title', + completed: false, + }, + { + id: 'todo-id-2', + title: 'Todo Title 2', + completed: true, + }, + ]; + }, + { + fieldName: 'listTodos', + typeName: 'Query', + } +); + +export const handler = async (event: unknown, context: Context) => + app.resolve(event, context); +``` + +See the [documentation](https://docs.powertools.aws.dev/lambda/typescript/latest/features/event-handler/appsync-events) for more details on how to use the AppSync event handler. + ## Bedrock Agent Functions Event Handler for Amazon Bedrock Agent Functions.