diff --git a/docs/index.md b/docs/index.md index 47ea41e2b6..ed1c1e514b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -70,7 +70,7 @@ You can include Powertools for AWS Lambda (TypeScript) Lambda Layer using [AWS L | `il-central-1` | [arn:aws:lambda:il-central-1:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:3](#){: .copyMe}:clipboard: | ??? note "Click to expand and copy code snippets for popular frameworks" - + === "SAM" ```yaml hl_lines="5" @@ -252,7 +252,7 @@ You can include Powertools for AWS Lambda (TypeScript) Lambda Layer using [AWS L !!! info "Using Powertools for AWS Lambda (TypeScript) via Lambda Layer? Simply add the Powertools for AWS Lambda (TypeScript) utilities you are using as a development dependency" ??? question "Want to inspect the contents of the Layer?" - Change {region} to your AWS region, e.g. `eu-west-1` + Change {region} to your AWS region, e.g. `eu-west-1` ```bash title="AWS CLI" aws lambda get-layer-version-by-arn --arn arn:aws:lambda:{aws::region}:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:3 --region {region} @@ -262,7 +262,7 @@ You can include Powertools for AWS Lambda (TypeScript) Lambda Layer using [AWS L ## Instrumentation -You can instrument your code with Powertools for AWS Lambda (TypeScript) in three different ways: +You can instrument your code with Powertools for AWS Lambda (TypeScript) in three different ways: * **Middy** middleware. It is the best choice if your existing code base relies on the [Middy 4.x](https://middy.js.org/docs/) middleware engine. Powertools for AWS Lambda (TypeScript) offers compatible Middy middleware to make this integration seamless. * **Method decorator**. Use [TypeScript method decorators](https://www.typescriptlang.org/docs/handbook/decorators.html#method-decorators) if you prefer writing your business logic using [TypeScript Classes](https://www.typescriptlang.org/docs/handbook/classes.html). If you aren’t using Classes, this requires the most significant refactoring. @@ -289,11 +289,12 @@ Core utilities such as Tracing, Logging, and Metrics will be available across al | [Parameters](./utilities/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](./utilities/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](./utilities/batch.md) | Utility to handle partial failures when processing batches from Amazon SQS, Amazon Kinesis Data Streams, and Amazon DynamoDB Streams. | +| [Parser](./utilities/parser.md) | Utility to parse and validate AWS Lambda event payloads using Zod, a TypeScript-first schema declaration and validation library. | ## Environment variables ???+ info - Explicit parameters take precedence over environment variables + Explicit parameters take precedence over environment variables | Environment variable | Description | Utility | Default | | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | --------------------------------------- | ------------------- | @@ -351,4 +352,4 @@ These are our core principles to guide our decision making. * **We strive for backwards compatibility**. New features and changes should keep backwards compatibility. If a breaking change cannot be avoided, the deprecation and migration process should be clearly defined. * **We work backwards from the community**. We aim to strike a balance of what would work best for 80% of customers. Emerging practices are considered and discussed via Requests for Comment (RFCs) * **Progressive**. Utilities are designed to be incrementally adoptable for customers at any stage of their Serverless journey. They follow language idioms and their community’s common practices. - + \ No newline at end of file diff --git a/docs/snippets/package.json b/docs/snippets/package.json index 7dbf011cea..16b9b8aada 100644 --- a/docs/snippets/package.json +++ b/docs/snippets/package.json @@ -2,6 +2,7 @@ "name": "docs", "version": "2.0.3", "description": "A collection code snippets for the Powertools for AWS Lambda (TypeScript) docs", + "type": "module", "author": { "name": "Amazon Web Services", "url": "https://aws.amazon.com" @@ -34,10 +35,12 @@ "@aws-sdk/client-secrets-manager": "^3.543.0", "@aws-sdk/client-ssm": "^3.540.0", "@aws-sdk/util-dynamodb": "^3.540.0", + "@middy/core": "^4.7.0", "aws-sdk": "^2.1589.0", "aws-sdk-client-mock": "^4.0.0", "aws-sdk-client-mock-jest": "^4.0.0", "axios": "^1.6.8", - "hashi-vault-js": "^0.4.14" + "hashi-vault-js": "^0.4.14", + "zod": "^3.22.4" } } diff --git a/docs/snippets/parser/decorator.ts b/docs/snippets/parser/decorator.ts new file mode 100644 index 0000000000..c1a91ad096 --- /dev/null +++ b/docs/snippets/parser/decorator.ts @@ -0,0 +1,35 @@ +import type { Context } from 'aws-lambda'; +import type { LambdaInterface } from '@aws-lambda-powertools/commons/types'; +import { parser } from '@aws-lambda-powertools/parser'; +import { z } from 'zod'; +import { Logger } from '@aws-lambda-powertools/logger'; + +const logger = new Logger(); + +const orderSchema = z.object({ + id: z.number().positive(), + description: z.string(), + items: z.array( + z.object({ + id: z.number().positive(), + quantity: z.number(), + description: z.string(), + }) + ), + optionalField: z.string().optional(), +}); + +type Order = z.infer; + +class Lambda implements LambdaInterface { + @parser({ schema: orderSchema }) + public async handler(event: Order, _context: Context): Promise { + // event is now typed as Order + for (const item of event.items) { + logger.info('Processing item', { item }); + } + } +} + +const myFunction = new Lambda(); +export const handler = myFunction.handler.bind(myFunction); diff --git a/docs/snippets/parser/envelopeDecorator.ts b/docs/snippets/parser/envelopeDecorator.ts new file mode 100644 index 0000000000..e54e0f7387 --- /dev/null +++ b/docs/snippets/parser/envelopeDecorator.ts @@ -0,0 +1,36 @@ +import type { Context } from 'aws-lambda'; +import type { LambdaInterface } from '@aws-lambda-powertools/commons/types'; +import { parser } from '@aws-lambda-powertools/parser'; +import { z } from 'zod'; +import { EventBridgeEnvelope } from '@aws-lambda-powertools/parser/envelopes'; +import { Logger } from '@aws-lambda-powertools/logger'; + +const logger = new Logger(); + +const orderSchema = z.object({ + id: z.number().positive(), + description: z.string(), + items: z.array( + z.object({ + id: z.number().positive(), + quantity: z.number(), + description: z.string(), + }) + ), + optionalField: z.string().optional(), +}); + +type Order = z.infer; + +class Lambda implements LambdaInterface { + @parser({ schema: orderSchema, envelope: EventBridgeEnvelope }) // (1)! + public async handler(event: Order, _context: Context): Promise { + // event is now typed as Order + for (const item of event.items) { + logger.info('Processing item', item); // (2)! + } + } +} + +const myFunction = new Lambda(); +export const handler = myFunction.handler.bind(myFunction); diff --git a/docs/snippets/parser/envelopeMiddy.ts b/docs/snippets/parser/envelopeMiddy.ts new file mode 100644 index 0000000000..94f8f012af --- /dev/null +++ b/docs/snippets/parser/envelopeMiddy.ts @@ -0,0 +1,37 @@ +import type { Context } from 'aws-lambda'; +import { parser } from '@aws-lambda-powertools/parser/middleware'; +import { z } from 'zod'; +import middy from '@middy/core'; +import { EventBridgeEnvelope } from '@aws-lambda-powertools/parser/envelopes'; +import { Logger } from '@aws-lambda-powertools/logger'; + +const logger = new Logger(); + +const orderSchema = z.object({ + id: z.number().positive(), + description: z.string(), + items: z.array( + z.object({ + id: z.number().positive(), + quantity: z.number(), + description: z.string(), + }) + ), + optionalField: z.string().optional(), +}); + +type Order = z.infer; + +const lambdaHandler = async ( + event: Order, + _context: Context +): Promise => { + for (const item of event.items) { + // item is parsed as OrderItem + logger.info('Processing item', { item }); + } +}; + +export const handler = middy(lambdaHandler).use( + parser({ schema: orderSchema, envelope: EventBridgeEnvelope }) +); diff --git a/docs/snippets/parser/examplePayload.json b/docs/snippets/parser/examplePayload.json new file mode 100644 index 0000000000..225be75a69 --- /dev/null +++ b/docs/snippets/parser/examplePayload.json @@ -0,0 +1,21 @@ +{ + "version": "0", + "id": "6a7e8feb-b491-4cf7-a9f1-bf3703467718", + "detail-type": "OrderPurchased", + "source": "OrderService", + "account": "111122223333", + "time": "2020-10-22T18:43:48Z", + "region": "us-west-1", + "resources": ["some_additional"], + "detail": { + "id": 10876546789, + "description": "My order", + "items": [ + { + "id": 1015938732, + "quantity": 1, + "description": "item xpto" + } + ] + } +} diff --git a/docs/snippets/parser/extend.ts b/docs/snippets/parser/extend.ts new file mode 100644 index 0000000000..d14845c10f --- /dev/null +++ b/docs/snippets/parser/extend.ts @@ -0,0 +1,40 @@ +import type { Context } from 'aws-lambda'; +import type { LambdaInterface } from '@aws-lambda-powertools/commons/types'; +import { parser } from '@aws-lambda-powertools/parser'; +import { z } from 'zod'; +import { EventBridgeSchema } from '@aws-lambda-powertools/parser/schemas'; +import { Logger } from '@aws-lambda-powertools/logger'; + +const logger = new Logger(); + +const orderSchema = z.object({ + id: z.number().positive(), + description: z.string(), + items: z.array( + z.object({ + id: z.number().positive(), + quantity: z.number(), + description: z.string(), + }) + ), + optionalField: z.string().optional(), +}); + +const orderEventSchema = EventBridgeSchema.extend({ + detail: orderSchema, // (1)! +}); + +type OrderEvent = z.infer; + +class Lambda implements LambdaInterface { + @parser({ schema: orderEventSchema }) // (2)! + public async handler(event: OrderEvent, _context: Context): Promise { + for (const item of event.detail.items) { + // process OrderItem + logger.info('Processing item', { item }); // (3)! + } + } +} + +const myFunction = new Lambda(); +export const handler = myFunction.handler.bind(myFunction); diff --git a/docs/snippets/parser/manual.ts b/docs/snippets/parser/manual.ts new file mode 100644 index 0000000000..2be8204cad --- /dev/null +++ b/docs/snippets/parser/manual.ts @@ -0,0 +1,33 @@ +import type { Context } from 'aws-lambda'; +import { z } from 'zod'; +import { EventBridgeEnvelope } from '@aws-lambda-powertools/parser/envelopes'; +import { EventBridgeSchema } from '@aws-lambda-powertools/parser/schemas'; +import type { EventBridgeEvent } from '@aws-lambda-powertools/parser/types'; +import { Logger } from '@aws-lambda-powertools/logger'; + +const logger = new Logger(); + +const orderSchema = z.object({ + id: z.number().positive(), + description: z.string(), + items: z.array( + z.object({ + id: z.number().positive(), + quantity: z.number(), + description: z.string(), + }) + ), + optionalField: z.string().optional(), +}); +type Order = z.infer; + +export const handler = async ( + event: EventBridgeEvent, + _context: Context +): Promise => { + const parsedEvent = EventBridgeSchema.parse(event); // (1)! + logger.info('Parsed event', parsedEvent); + + const orders: Order = EventBridgeEnvelope.parse(event, orderSchema); // (2)! + logger.info('Parsed orders', orders); +}; diff --git a/docs/snippets/parser/manualSafeParse.ts b/docs/snippets/parser/manualSafeParse.ts new file mode 100644 index 0000000000..17106c1bda --- /dev/null +++ b/docs/snippets/parser/manualSafeParse.ts @@ -0,0 +1,35 @@ +import type { Context } from 'aws-lambda'; +import { z } from 'zod'; +import { EventBridgeEnvelope } from '@aws-lambda-powertools/parser/envelopes'; +import { EventBridgeSchema } from '@aws-lambda-powertools/parser/schemas'; +import type { EventBridgeEvent } from '@aws-lambda-powertools/parser/types'; +import { Logger } from '@aws-lambda-powertools/logger'; + +const logger = new Logger(); + +const orderSchema = z.object({ + id: z.number().positive(), + description: z.string(), + items: z.array( + z.object({ + id: z.number().positive(), + quantity: z.number(), + description: z.string(), + }) + ), + optionalField: z.string().optional(), +}); + +export const handler = async ( + event: EventBridgeEvent, + _context: Context +): Promise => { + const parsedEvent = EventBridgeSchema.safeParse(event); // (1)! + parsedEvent.success + ? logger.info('Event parsed successfully', parsedEvent.data) + : logger.error('Event parsing failed', parsedEvent.error); + const parsedEvenlope = EventBridgeEnvelope.safeParse(event, orderSchema); // (2)! + parsedEvenlope.success + ? logger.info('Event envelope parsed successfully', parsedEvenlope.data) + : logger.error('Event envelope parsing failed', parsedEvenlope.error); +}; diff --git a/docs/snippets/parser/middy.ts b/docs/snippets/parser/middy.ts new file mode 100644 index 0000000000..ff3bf3a879 --- /dev/null +++ b/docs/snippets/parser/middy.ts @@ -0,0 +1,36 @@ +import type { Context } from 'aws-lambda'; +import { parser } from '@aws-lambda-powertools/parser/middleware'; +import { z } from 'zod'; +import middy from '@middy/core'; +import { Logger } from '@aws-lambda-powertools/logger'; + +const logger = new Logger(); + +const orderSchema = z.object({ + id: z.number().positive(), + description: z.string(), + items: z.array( + z.object({ + id: z.number().positive(), + quantity: z.number(), + description: z.string(), + }) + ), + optionalField: z.string().optional(), +}); + +type Order = z.infer; + +const lambdaHandler = async ( + event: Order, + _context: Context +): Promise => { + for (const item of event.items) { + // item is parsed as OrderItem + logger.info('Processing item', { item }); + } +}; + +export const handler = middy(lambdaHandler).use( + parser({ schema: orderSchema }) +); diff --git a/docs/snippets/parser/refine.ts b/docs/snippets/parser/refine.ts new file mode 100644 index 0000000000..b3e58fad08 --- /dev/null +++ b/docs/snippets/parser/refine.ts @@ -0,0 +1,21 @@ +import { z } from 'zod'; + +const orderItemSchema = z.object({ + id: z.number().positive(), + quantity: z.number(), + description: z.string(), +}); + +export const orderSchema = z + .object({ + id: z.number().positive(), + description: z.string(), + items: z.array(orderItemSchema).refine((items) => items.length > 0, { + message: 'Order must have at least one item', // (1)! + }), + optionalField: z.string().optional(), + }) + .refine((order) => order.id > 100 && order.items.length > 100, { + message: + 'All orders with more than 100 items must have an id greater than 100', // (2)! + }); diff --git a/docs/snippets/parser/safeParseDecorator.ts b/docs/snippets/parser/safeParseDecorator.ts new file mode 100644 index 0000000000..aaf4d19ca9 --- /dev/null +++ b/docs/snippets/parser/safeParseDecorator.ts @@ -0,0 +1,47 @@ +import type { Context } from 'aws-lambda'; +import type { LambdaInterface } from '@aws-lambda-powertools/commons/types'; +import { parser } from '@aws-lambda-powertools/parser'; +import { z } from 'zod'; +import type { + ParsedResult, + EventBridgeEvent, +} from '@aws-lambda-powertools/parser/types'; +import { Logger } from '@aws-lambda-powertools/logger'; + +const logger = new Logger(); + +const orderSchema = z.object({ + id: z.number().positive(), + description: z.string(), + items: z.array( + z.object({ + id: z.number().positive(), + quantity: z.number(), + description: z.string(), + }) + ), + optionalField: z.string().optional(), +}); + +type Order = z.infer; + +class Lambda implements LambdaInterface { + @parser({ schema: orderSchema, safeParse: true }) // (1)! + public async handler( + event: ParsedResult, + _context: Context + ): Promise { + if (event.success) { + // (2)! + for (const item of event.data.items) { + logger.info('Processing item', { item }); // (3)! + } + } else { + logger.error('Failed to parse event', event.error); // (4)! + logger.error('Original event is: ', event.originalEvent); // (5)! + } + } +} + +const myFunction = new Lambda(); +export const handler = myFunction.handler.bind(myFunction); diff --git a/docs/snippets/parser/safeParseMiddy.ts b/docs/snippets/parser/safeParseMiddy.ts new file mode 100644 index 0000000000..547eae73b7 --- /dev/null +++ b/docs/snippets/parser/safeParseMiddy.ts @@ -0,0 +1,45 @@ +import type { Context } from 'aws-lambda'; +import { parser } from '@aws-lambda-powertools/parser/middleware'; +import { z } from 'zod'; +import middy from '@middy/core'; +import type { + ParsedResult, + EventBridgeEvent, +} from '@aws-lambda-powertools/parser/types'; +import { Logger } from '@aws-lambda-powertools/logger'; + +const logger = new Logger(); + +const orderSchema = z.object({ + id: z.number().positive(), + description: z.string(), + items: z.array( + z.object({ + id: z.number().positive(), + quantity: z.number(), + description: z.string(), + }) + ), + optionalField: z.string().optional(), +}); + +type Order = z.infer; + +const lambdaHandler = async ( + event: ParsedResult, + _context: Context +): Promise => { + if (event.success) { + // (2)! + for (const item of event.data.items) { + logger.info('Processing item', { item }); // (3)! + } + } else { + logger.error('Error parsing event', { event: event.error }); // (4)! + logger.error('Original event', { event: event.originalEvent }); // (5)! + } +}; + +export const handler = middy(lambdaHandler).use( + parser({ schema: orderSchema, safeParse: true }) // (1)! +); diff --git a/docs/snippets/parser/schema.ts b/docs/snippets/parser/schema.ts new file mode 100644 index 0000000000..f14db32fff --- /dev/null +++ b/docs/snippets/parser/schema.ts @@ -0,0 +1,16 @@ +import { z } from 'zod'; + +const orderSchema = z.object({ + id: z.number().positive(), + description: z.string(), + items: z.array( + z.object({ + id: z.number().positive(), + quantity: z.number(), + description: z.string(), + }) + ), + optionalField: z.string().optional(), +}); + +export { orderSchema }; diff --git a/docs/snippets/parser/types.ts b/docs/snippets/parser/types.ts new file mode 100644 index 0000000000..eba3862835 --- /dev/null +++ b/docs/snippets/parser/types.ts @@ -0,0 +1,36 @@ +import type { Context } from 'aws-lambda'; +import { parser } from '@aws-lambda-powertools/parser/middleware'; +import { z } from 'zod'; +import middy from '@middy/core'; +import { Logger } from '@aws-lambda-powertools/logger'; + +const logger = new Logger(); + +const orderSchema = z.object({ + id: z.number().positive(), + description: z.string(), + items: z.array( + z.object({ + id: z.number().positive(), + quantity: z.number(), + description: z.string(), + }) + ), + optionalField: z.string().optional(), +}); + +type Order = z.infer; // (1)! + +const lambdaHandler = async ( + event: Order, // (2)! + _context: Context +): Promise => { + for (const item of event.items) { + // item is parsed as OrderItem + logger.info('Processing item', { item }); // (3)! + } +}; + +export const handler = middy(lambdaHandler).use( + parser({ schema: orderSchema }) +); diff --git a/docs/snippets/tsconfig.json b/docs/snippets/tsconfig.json index d6aec30ce7..7c921276c8 100644 --- a/docs/snippets/tsconfig.json +++ b/docs/snippets/tsconfig.json @@ -4,6 +4,8 @@ "rootDir": "./", "baseUrl": ".", "noEmit": true, + "noUnusedLocals": false, + "allowUnusedLabels": true, "paths": { "@aws-lambda-powertools/parameters/ssm": [ "../../packages/parameters/lib/ssm" @@ -31,7 +33,9 @@ "@aws-lambda-powertools/jmespath": ["../../packages/jmespath/lib"], "@aws-lambda-powertools/jmespath/envelopes": [ "../../packages/jmespath/lib/envelopes" - ] + ], + "@aws-lambda-powertools/parser": ["../../packages/parser/lib"], + "@aws-lambda-powertools/logger": ["../../packages/logger/lib"] } } } diff --git a/docs/utilities/parser.md b/docs/utilities/parser.md new file mode 100644 index 0000000000..7a31119c29 --- /dev/null +++ b/docs/utilities/parser.md @@ -0,0 +1,258 @@ +--- +title: Parser (Zod) +descrition: Utility +status: new +--- + + +???+ warning + **This utility is currently released as beta developer preview** and is intended strictly for feedback and testing purposes **and not for production workloads**. The version and all future versions tagged with the `-beta` suffix should be treated as not stable. Up until before the [General Availability release](https://github.com/aws-powertools/powertools-lambda-typescript/milestone/16) we might introduce significant breaking changes and improvements in response to customers feedback. + +This utility provides data validation and parsing using [Zod](https://zod.dev){target="_blank"}. +Zod is a TypeScript-first schema declaration and validation library. + +## Key features + +* Define data schema as Zod schema, then parse, validate and extract only what you want +* Built-in envelopes to unwrap and validate popular AWS event sources payloads +* Extend and customize envelopes to fit your needs +* Safe parsing option to avoid throwing errors and custom error handling +* Available for Middy.js middleware and TypeScript method decorators + +## Getting started + +### Install + +```bash +npm install @aws-lambda-powertools/parser zod@~3 +``` + +This utility supports Zod v3.x and above. + +## Define schema + +You can define your schema using Zod: + +```typescript title="schema.ts" +--8<-- "docs/snippets/parser/schema.ts" +``` + +This is a schema for `Order` object using Zod. +You can create complex schemas by using nested objects, arrays, unions, and other types, see [Zod documentation](https://zod.dev) for more details. + +## Parse events + +You can parse inbound events using `parser` decorator or middy middleware, or [manually](#manual-parsing) using built-in envelopes and schemas. +Both are also able to parse either an object or JSON string as an input. + +???+ warning + The decorator and middleware will replace the event object with the parsed schema if successful. + Be cautious when using multiple decorators that expect event to have a specific structure, the order of evaluation for decorators is from bottom to top. + +=== "Middy middleware" + ```typescript hl_lines="34" + --8<-- "docs/snippets/parser/middy.ts" + ``` + +=== "Decorator" + ```typescript hl_lines="25" + --8<-- "docs/snippets/parser/decorator.ts" + ``` + +## Built-in schemas + + +Parser comes with the following built-in schemas: + +| Model name | Description | +|-----------------------------------------------| ------------------------------------------------------------------------------------- | +| **AlbSchema** | Lambda Event Source payload for Amazon Application Load Balancer | +| **APIGatewayProxyEventSchema** | Lambda Event Source payload for Amazon API Gateway | +| **APIGatewayProxyEventV2Schema** | Lambda Event Source payload for Amazon API Gateway v2 payload | +| **CloudFormationCustomResourceCreateSchema** | Lambda Event Source payload for AWS CloudFormation `CREATE` operation | +| **CloudFormationCustomResourceUpdateSchema** | Lambda Event Source payload for AWS CloudFormation `UPDATE` operation | +| **CloudFormationCustomResourceDeleteSchema** | Lambda Event Source payload for AWS CloudFormation `DELETE` operation | +| **CloudwatchLogsSchema** | Lambda Event Source payload for Amazon CloudWatch Logs | +| **DynamoDBStreamSchema** | Lambda Event Source payload for Amazon DynamoDB Streams | +| **EventBridgeSchema** | Lambda Event Source payload for Amazon EventBridge | +| **KafkaMskEventSchema** | Lambda Event Source payload for AWS MSK payload | +| **KafkaSelfManagedEventSchema** | Lambda Event Source payload for self managed Kafka payload | +| **KinesisDataStreamSchema** | Lambda Event Source payload for Amazon Kinesis Data Streams | +| **KinesisFirehoseSchema** | Lambda Event Source payload for Amazon Kinesis Firehose | +| **KinesisFirehoseSqsSchema** | Lambda Event Source payload for SQS messages wrapped in Kinesis Firehose records | +| **LambdaFunctionUrlSchema** | Lambda Event Source payload for Lambda Function URL payload | +| **S3EventNotificationEventBridgeSchema** | Lambda Event Source payload for Amazon S3 Event Notification to EventBridge. | +| **S3Schema** | Lambda Event Source payload for Amazon S3 | +| **S3ObjectLambdaEvent** | Lambda Event Source payload for Amazon S3 Object Lambda | +| **S3SqsEventNotificationSchema** | Lambda Event Source payload for S3 event notifications wrapped in SQS event (S3->SQS) | +| **SesSchema** | Lambda Event Source payload for Amazon Simple Email Service | +| **SnsSchema** | Lambda Event Source payload for Amazon Simple Notification Service | +| **SqsSchema** | Lambda Event Source payload for Amazon SQS | +| **VpcLatticeSchema** | Lambda Event Source payload for Amazon VPC Lattice | +| **VpcLatticeV2Schema** | Lambda Event Source payload for Amazon VPC Lattice v2 payload | + +### Extend built-in schemas + +You can extend every built-in schema to include your own schema, and yet have all other known fields parsed along the way. + +=== "handler.ts" + ```typescript hl_lines="23-25 30 34" + --8<-- "docs/snippets/parser/extend.ts" + ``` + + 1. Extend built-in `EventBridgeSchema` with your own detail schema + 2. Pass the extended schema to `parser` decorator or middy middleware + 3. `event` is validated including your custom schema and now available in your handler + + +=== "Example payload" + + ```json + --8<-- "docs/snippets/parser/examplePayload.json" + ``` + +## Envelopes + +When trying to parse your payload you might encounter the following situations: + +* Your actual payload is wrapped around a known structure, for example Lambda Event Sources like EventBridge +* You're only interested in a portion of the payload, for example parsing the detail of custom events in EventBridge, or body of SQS records +* You can either solve these situations by creating a schema of these known structures, parsing them, then extracting and parsing a key where your payload is. + +This can become difficult quite quickly. Parser simplifies the development through a feature named Envelope. +Envelopes can be used via envelope parameter available in middy and decorator. +Here's an example of parsing a custom schema in an event coming from EventBridge, where all you want is what's inside the detail key. + +=== "Middy middleware" + ```typescript hl_lines="5 36" + --8<-- "docs/snippets/parser/envelopeMiddy.ts" + ``` + +=== "Decorator" + ```typescript hl_lines="5 26 30" + --8<-- "docs/snippets/parser/envelopeDecorator.ts" + ``` + + 1. Pass `eventBridgeEnvelope` to `parser` decorator + 2. `event` is parsed and replaced as `Order` object + + + +The envelopes are functions that take an event and the schema to parse, and return the result of the inner schema. +Depending on the envelope it can be something simple like extracting a key. +We have also complex envelopes that parse the payload from a string, decode base64, uncompress gzip, etc. + +!!! tip "Envelopes vs schema extension" + Use envelopes if you want to extract only the inner part of an event payload and don't use the information from the Lambda event. + Otherwise, extend built-in schema to parse the whole payload and use the metadata from the Lambda event. + +### Built-in envelopes + +Parser comes with the following built-in envelopes: + +| Envelope name | Behaviour | +| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **apiGatewayEnvelope** | 1. Parses data using `APIGatewayProxyEventSchema`.
2. Parses `body` key using your schema and returns it. | +| **apiGatewayV2Envelope** | 1. Parses data using `APIGatewayProxyEventV2Schema`.
2. Parses `body` key using your schema and returns it. | +| **cloudWatchEnvelope** | 1. Parses data using `CloudwatchLogsSchema` which will base64 decode and decompress it.
2. Parses records in `message` key using your schema and return them in a list. | +| **dynamoDBStreamEnvelope** | 1. Parses data using `DynamoDBStreamSchema`.
2. Parses records in `NewImage` and `OldImage` keys using your schema.
3. Returns a list with a dictionary containing `NewImage` and `OldImage` keys | +| **eventBridgeEnvelope** | 1. Parses data using `EventBridgeSchema`.
2. Parses `detail` key using your schema and returns it. | +| **kafkaEnvelope** | 1. Parses data using `KafkaRecordSchema`.
2. Parses `value` key using your schema and returns it. | +| **kinesisEnvelope** | 1. Parses data using `KinesisDataStreamSchema` which will base64 decode it.
2. Parses records in `Records` key using your schema and returns them in a list. | +| **kinesisFirehoseEnvelope** | 1. Parses data using `KinesisFirehoseSchema` which will base64 decode it.
2. Parses records in `Records` key using your schema and returns them in a list. | +| **lambdaFunctionUrlEnvelope** | 1. Parses data using `LambdaFunctionUrlSchema`.
2. Parses `body` key using your schema and returns it. | +| **snsEnvelope** | 1. Parses data using `SnsSchema`.
2. Parses records in `body` key using your schema and return them in a list. | +| **snsSqsEnvelope** | 1. Parses data using `SqsSchema`.
2. Parses SNS records in `body` key using `SnsNotificationSchema`.
3. Parses data in `Message` key using your schema and return them in a list. | +| **sqsEnvelope** | 1. Parses data using `SqsSchema`.
2. Parses records in `body` key using your schema and return them in a list. | +| **vpcLatticeEnvelope** | 1. Parses data using `VpcLatticeSchema`.
2. Parses `value` key using your schema and returns it. | +| **vpcLatticeV2Envelope** | 1. Parses data using `VpcLatticeSchema`.
2. Parses `value` key using your schema and returns it. | + + +## Safe parsing + +If you want to parse the event without throwing an error, use the `safeParse` option. +The handler `event` object will be replaced with `ParsedResult`, for example `ParsedResult`, where `SqsEvent` is the original event and `Order` is the parsed schema. + +The `ParsedResult` object will have `success`, `data`, or `error` and `originalEvent` fields, depending on the outcome. +If the parsing is successful, the `data` field will contain the parsed event, otherwise you can access the `error` field and the `originalEvent` to handle the error and recover the original event. + +=== "Middy middleware" + ```typescript hl_lines="32 35 38 39 44" + --8<-- "docs/snippets/parser/safeParseMiddy.ts" + ``` + + 1. Use `safeParse` option to parse the event without throwing an error + 2. Check if the result is successful or not and handle the error accordingly + 3. Use `data` to access the parsed event + 4. Use `error` to handle the error message + 5. Use `originalEvent` to get the original event and recover + +=== "Decorator" + ```typescript hl_lines="29 35 37 40 41" + --8<-- "docs/snippets/parser/safeParseDecorator.ts" + ``` + + 1. Use `safeParse` option to parse the event without throwing an error + 2. Check if the result is successful or not and handle the error accordingly + 3. Use `data` to access the parsed event + 4. Use `error` to handle the error message + 5. Use `originalEvent` to get the original event and recover + + +## Manual parsing + +You can use built-in envelopes and schemas to parse the incoming events manually, without using middy or decorator. + + +=== "Manual parse" + ```typescript hl_lines="28 31" + --8<-- "docs/snippets/parser/manual.ts" + ``` + + 1. Use `EventBridgeSchema` to parse the event, the `details` fields will be parsed as a generic record. + 2. Use `eventBridgeEnvelope` with a combination of `orderSchema` to get `Order` object from the `details` field. + +=== "Manual safeParse" + ```typescript hl_lines="27 31" + --8<-- "docs/snippets/parser/manualSafeParse.ts" + ``` + + 1. Use `safeParse` option to parse the event without throwing an error + 2. `safeParse` is also available for envelopes + +## Custom validation + +Because Parser uses Zod, you can use all the features of Zod to validate your data. +For example, you can use `refine` to validate a field or a combination of fields: + +=== "Custom validation" + ```typescript hl_lines="13 18" + --8<-- "docs/snippets/parser/refine.ts" + ``` + + 1. validate a single field + 2. validate an object with multiple fields + +Zod provides a lot of other features and customization, see [Zod documentation](https://zod.dev) for more details. + + +## Types + +### Schema and Type inference +Use `z.infer` to extract the type of the schema, so you can use types during development and avoid type errors. + +=== "Types" + ```typescript hl_lines="22 25 30" + --8<-- "docs/snippets/parser/types.ts" + ``` + + 1. Use `z.infer` to extract the type of the schema, also works for nested schemas + 2. `event` is of type `Order` + 3. infer types from deeply nested schemas + +### Compatibility with @types/aws-lambda + +The package `@types/aws-lambda` is a popular project that contains type definitions for many AWS service event invocations. +Powertools parser utility also bring AWS Lambda event types based on the built-in schema definitions. + +We recommend to use the types provided by the parser utility. If you encounter any issues or have any feedback, please [submit an issue](https://github.com/aws-powertools/powertools-lambda-typescript/issues/new/choose). diff --git a/mkdocs.yml b/mkdocs.yml index 107ca2b45f..31e1be3d24 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -20,6 +20,7 @@ nav: - utilities/idempotency.md - utilities/batch.md - utilities/jmespath.md + - utilities/parser.md - Processes: - Roadmap: roadmap.md - Versioning policy: versioning.md @@ -119,3 +120,5 @@ extra: - icon: fontawesome/brands/discord link: https://discord.gg/B8zZKbbyET name: Join our Discord Server! + status: + new: New Utility diff --git a/package-lock.json b/package-lock.json index 5bd18ee54e..f726c5f6ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -62,11 +62,13 @@ "@aws-sdk/client-secrets-manager": "^3.543.0", "@aws-sdk/client-ssm": "^3.540.0", "@aws-sdk/util-dynamodb": "^3.540.0", + "@middy/core": "^4.7.0", "aws-sdk": "^2.1589.0", "aws-sdk-client-mock": "^4.0.0", "aws-sdk-client-mock-jest": "^4.0.0", "axios": "^1.6.8", - "hashi-vault-js": "^0.4.14" + "hashi-vault-js": "^0.4.14", + "zod": "^3.22.4" } }, "examples/app": { @@ -17370,7 +17372,6 @@ "version": "3.22.4", "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -17523,6 +17524,7 @@ } }, "packages/parser": { + "name": "@aws-lambda-powertools/parser", "version": "0.0.0", "license": "MIT-0", "devDependencies": { diff --git a/packages/parser/src/schemas/cloudformation-custom-resource.ts b/packages/parser/src/schemas/cloudformation-custom-resource.ts index 13f9e9760d..84369dbdae 100644 --- a/packages/parser/src/schemas/cloudformation-custom-resource.ts +++ b/packages/parser/src/schemas/cloudformation-custom-resource.ts @@ -36,5 +36,4 @@ export { CloudFormationCustomResourceCreateSchema, CloudFormationCustomResourceDeleteSchema, CloudFormationCustomResourceUpdateSchema, - CloudFormationCustomResourceBaseSchema, };