diff --git a/docs/features/parser.md b/docs/features/parser.md index b60feed4da..59f961258f 100644 --- a/docs/features/parser.md +++ b/docs/features/parser.md @@ -1,63 +1,57 @@ --- -title: Parser (Zod) +title: Parser (Standard Schema) descrition: Utility --- -This utility provides data validation and parsing using [Zod](https://zod.dev){target="_blank"}, a TypeScript-first schema declaration and validation library. +This utility provides data validation and parsing for [Standard Schema](https://github.com/standard-schema/standard-schema){target="_blank"}, together with a collection of built-in [Zod](https://zod.dev){target="_blank"} schemas and envelopes to parse and unwrap popular AWS event sources payloads. ## 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 +* Accept a [Standard Schema](https://github.com/standard-schema/standard-schema) and parse incoming payloads +* Built-in Zod schemas and envelopes to unwrap and validate popular AWS event sources payloads +* Extend and customize built-in Zod schemas to fit your needs +* Safe parsing option to avoid throwing errors and allow custom error handling +* Available as Middy.js middleware and TypeScript class method decorator ## Getting started ```bash -npm install @aws-lambda-powertools/parser zod@~3 +npm install @aws-lambda-powertools/parser zod ``` -!!! warning "Zod version" - The package is compatible with Zod v3 only.
- We're considering Zod v4 support and we'd love to hear your feedback. Please [leave a comment here](https://github.com/aws-powertools/powertools-lambda-typescript/issues/3951) to let us know your thoughts. - -## Define schema - -You can define your schema using Zod: - -```typescript title="schema.ts" ---8<-- "examples/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, Middy.js 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. +When using the decorator or middleware, you can specify a schema to parse the event, this can be a [built-in Zod schema](#built-in-schemas) or a custom schema you defined. Custom schemas can be defined using Zod or any other [Standard Schema compatible library](https://standardschema.dev/#what-schema-libraries-implement-the-spec){target="_blank"}. -=== "Middy middleware" +=== "Middy.js middleware with Zod schema" ```typescript hl_lines="22" --8<-- "examples/snippets/parser/middy.ts" ``` +=== "Middy.js middleware with Valibot schema" + ```typescript hl_lines="30" + --8<-- "examples/snippets/parser/middyValibot.ts" + ``` + === "Decorator" + !!! warning + The decorator and middleware will replace the event object with the parsed schema if successful. + Be cautious when using multiple decorators that expect an event to have a specific structure, the order of evaluation for decorators is from the inner to the outermost decorator. + ```typescript hl_lines="25" --8<-- "examples/snippets/parser/decorator.ts" ``` ## Built-in schemas -**Parser** comes with the following built-in schemas: +**Parser** comes with the following built-in Zod schemas: + +!!! note "Looking for other libraries?" + The built-in schemas are defined using Zod, if you would like us to support other libraries like [valibot](https://valibot.dev){target="_blank"} please [open an issue](https://github.com/aws-powertools/powertools-lambda-typescript/issues/new?template=feature_request.yml){target="_blank"} and we will consider it based on the community's feedback. | Model name | Description | | -------------------------------------------- | ------------------------------------------------------------------------------------- | @@ -198,7 +192,7 @@ This can become difficult quite quickly. Parser simplifies the development throu 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" +=== "Middy.js middleware" ```typescript hl_lines="23" --8<-- "examples/snippets/parser/envelopeMiddy.ts" ``` @@ -221,24 +215,27 @@ We have also complex envelopes that parse the payload from a string, decode base ### Built-in envelopes -Parser comes with the following built-in envelopes: +Parser comes with the following built-in Zod envelopes: + +!!! note "Looking for other libraries?" + The built-in schemas are defined using Zod, if you would like us to support other libraries like [valibot](https://valibot.dev){target="_blank"} please [open an issue](https://github.com/aws-powertools/powertools-lambda-typescript/issues/new?template=feature_request.yml){target="_blank"} and we will consider it based on the community's feedback. | 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. | +| **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. | +| **SnsEnvelope** | 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 @@ -248,7 +245,7 @@ The handler `event` object will be replaced with `ParsedResult 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" +=== "Middy.js middleware" ```typescript hl_lines="23 28 32-33" --8<-- "examples/snippets/parser/safeParseMiddy.ts" ``` @@ -320,10 +317,11 @@ Use `z.infer` to extract the type of the schema, so you can use types during dev ### 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. +The package `@types/aws-lambda` is a popular project that contains type definitions for many AWS service event invocations, support for these types is provided on a best effort basis. + +We recommend using the types provided by the Parser utility under `@aws-powertools/parser/types` when using the built-in schemas and envelopes, as they are inferred directly from the Zod schemas and are more accurate. -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). +If you encounter any type compatibility issues with `@types/aws-lambda`, please [submit an issue](https://github.com/aws-powertools/powertools-lambda-typescript/issues/new/choose). ## Testing your code diff --git a/examples/snippets/package.json b/examples/snippets/package.json index 468af38cd5..5d9814256b 100644 --- a/examples/snippets/package.json +++ b/examples/snippets/package.json @@ -43,7 +43,7 @@ "@valkey/valkey-glide": "^2.0.1", "aws-sdk": "^2.1692.0", "aws-sdk-client-mock": "^4.1.0", - "zod": "^3.25.76" + "zod": "^4.0.5" }, "dependencies": { "arktype": "^2.1.20", diff --git a/examples/snippets/parser/extendAlbSchema.ts b/examples/snippets/parser/extendAlbSchema.ts index 7de794973e..d38f802022 100644 --- a/examples/snippets/parser/extendAlbSchema.ts +++ b/examples/snippets/parser/extendAlbSchema.ts @@ -8,7 +8,7 @@ const customSchema = z.object({ }); const extendedSchema = AlbSchema.extend({ - body: JSONStringified(customSchema), + body: JSONStringified(customSchema), // (1)! }); type _ExtendedAlbEvent = z.infer; diff --git a/examples/snippets/parser/extendSqsSchema.ts b/examples/snippets/parser/extendSqsSchema.ts index f5e4ed43c0..a01f365785 100644 --- a/examples/snippets/parser/extendSqsSchema.ts +++ b/examples/snippets/parser/extendSqsSchema.ts @@ -13,7 +13,7 @@ const customSchema = z.object({ const extendedSchema = SqsSchema.extend({ Records: z.array( SqsRecordSchema.extend({ - body: JSONStringified(customSchema), // (1)! + body: JSONStringified(customSchema), }) ), }); diff --git a/examples/snippets/parser/middy.ts b/examples/snippets/parser/middy.ts index e8c19fb3cf..396341483d 100644 --- a/examples/snippets/parser/middy.ts +++ b/examples/snippets/parser/middy.ts @@ -11,7 +11,7 @@ const orderSchema = z.object({ items: z.array( z.object({ id: z.number().positive(), - quantity: z.number(), + quantity: z.number().positive(), description: z.string(), }) ), diff --git a/examples/snippets/parser/middyValibot.ts b/examples/snippets/parser/middyValibot.ts new file mode 100644 index 0000000000..5a37de5daa --- /dev/null +++ b/examples/snippets/parser/middyValibot.ts @@ -0,0 +1,36 @@ +import { Logger } from '@aws-lambda-powertools/logger'; +import { parser } from '@aws-lambda-powertools/parser/middleware'; +import middy from '@middy/core'; +import { + array, + number, + object, + optional, + pipe, + string, + toMinValue, +} from 'valibot'; + +const logger = new Logger(); + +const orderSchema = object({ + id: pipe(number(), toMinValue(0)), + description: string(), + items: array( + object({ + id: pipe(number(), toMinValue(0)), + quantity: pipe(number(), toMinValue(1)), + description: string(), + }) + ), + optionalField: optional(string()), +}); + +export const handler = middy() + .use(parser({ schema: orderSchema })) + .handler(async (event): Promise => { + for (const item of event.items) { + // item is parsed as OrderItem + logger.info('Processing item', { item }); + } + }); diff --git a/package-lock.json b/package-lock.json index c64cb98a3d..4cc7f5ff52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -107,7 +107,7 @@ "@valkey/valkey-glide": "^2.0.1", "aws-sdk": "^2.1692.0", "aws-sdk-client-mock": "^4.1.0", - "zod": "^3.25.76" + "zod": "^4.0.5" } }, "examples/snippets/node_modules/@valkey/valkey-glide": { @@ -14926,9 +14926,9 @@ "dev": true }, "node_modules/agent-base": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", - "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, "license": "MIT", "engines": { @@ -24287,9 +24287,16 @@ "license": "MIT" }, "node_modules/strnum": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", - "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", + "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" }, "node_modules/strong-log-transformer": { "version": "2.1.0", @@ -25721,10 +25728,10 @@ } }, "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "devOptional": true, + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.0.5.tgz", + "integrity": "sha512-/5UuuRPStvHXu7RS+gmvRf4NXrNxpSllGwDnCBcJZtQsKrviYXm54yDGV2KYNLT5kq0lHGcl7lqWJLgSaG+tgA==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" @@ -25812,12 +25819,12 @@ "devDependencies": { "avro-js": "^1.12.0", "protobufjs": "^7.5.3", - "zod": "^3.25.76" + "zod": "^4.0.5" }, "peerDependencies": { "arktype": ">=2.0.0", "valibot": ">=1.0.0", - "zod": ">=3.24.0" + "zod": "^3.25.0 || ^4.0.0" }, "peerDependenciesMeta": { "arktype": { @@ -25929,11 +25936,15 @@ "version": "2.23.0", "license": "MIT-0", "dependencies": { - "@aws-lambda-powertools/commons": "2.23.0" + "@aws-lambda-powertools/commons": "2.23.0", + "@standard-schema/spec": "^1.0.0" + }, + "devDependencies": { + "zod": "^4.0.5" }, "peerDependencies": { "@middy/core": "4.x || 5.x || 6.x", - "zod": ">=3.x" + "zod": "^3.25.0 || ^4.0.0" }, "peerDependenciesMeta": { "@middy/core": { diff --git a/packages/kafka/package.json b/packages/kafka/package.json index 3f3f3f56a1..6cac43dfa1 100644 --- a/packages/kafka/package.json +++ b/packages/kafka/package.json @@ -53,7 +53,7 @@ "peerDependencies": { "arktype": ">=2.0.0", "valibot": ">=1.0.0", - "zod": ">=3.24.0" + "zod": "^3.25.0 || ^4.0.0" }, "peerDependenciesMeta": { "zod": { @@ -117,6 +117,6 @@ "devDependencies": { "avro-js": "^1.12.0", "protobufjs": "^7.5.3", - "zod": "^3.25.76" + "zod": "^4.0.5" } } diff --git a/packages/parser/README.md b/packages/parser/README.md index 2b79ea02f4..091e48fabd 100644 --- a/packages/parser/README.md +++ b/packages/parser/README.md @@ -7,7 +7,7 @@ You can use the package in both TypeScript and JavaScript code bases. - [Intro](#intro) - [Key features](#key-features) - [Usage](#usage) - - [Middleware](#middleware) + - [Middy.js Middleware](#middyjs-middleware) - [Decorator](#decorator) - [Manual parsing](#manual-parsing) - [Safe parsing](#safe-parsing) @@ -23,96 +23,112 @@ You can use the package in both TypeScript and JavaScript code bases. ## Intro -The parser utility provides data validation and parsing using [Zod](https://zod.dev), a TypeScript-first schema declaration and validation library. +This utility provides data validation and parsing for [Standard Schema](https://github.com/standard-schema/standard-schema), together with a collection of built-in [Zod](https://zod.dev) schemas and envelopes to parse and unwrap popular AWS event source payloads. ## 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 +- Accept a [Standard Schema](https://github.com/standard-schema/standard-schema) and parse incoming payloads +- Built-in Zod schemas and envelopes to unwrap and validate popular AWS event sources payloads +- Extend and customize built-in Zod schemas to fit your needs +- Safe parsing option to avoid throwing errors and allow custom error handling +- Available as Middy.js middleware and TypeScript class method decorator ## Usage To get started, install the library by running: ```sh -npm install @aws-lambda-powertools/parser zod@~3 +npm install @aws-lambda-powertools/parser zod ``` -Then, define your schema using Zod: +You can parse inbound events using the `parser` decorator, Middy.js middleware, or [manually](#manual-parsing) using built-in envelopes and schemas. + +When using the decorator or middleware, you can specify a schema to parse the event: this can be a [built-in Zod schema](https://docs.powertools.aws.dev/lambda/typescript/latest/features/parser/#built-in-schemas) or a custom schema you defined. Custom schemas can be defined using Zod or any other [Standard Schema compatible library](https://standardschema.dev/#what-schema-libraries-implement-the-spec). + +### Middy.js Middleware + +Using Zod schemas: ```typescript +import { Logger } from '@aws-lambda-powertools/logger'; +import { parser } from '@aws-lambda-powertools/parser/middleware'; +import middy from '@middy/core'; import { z } from 'zod'; +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(), + quantity: z.number().positive(), description: z.string(), }) ), optionalField: z.string().optional(), }); -export { orderSchema }; +export const handler = middy() + .use(parser({ schema: orderSchema })) + .handler(async (event): Promise => { + for (const item of event.items) { + // item is parsed as OrderItem + logger.info('Processing item', { item }); + } + }); ``` -Next, you can parse incoming events using the `parser` decorator or Middy.js middleware: - -### Middleware +Using Valibot schemas: ```typescript -import type { Context } from 'aws-lambda'; +import { Logger } from '@aws-lambda-powertools/logger'; import { parser } from '@aws-lambda-powertools/parser/middleware'; -import { z } from 'zod'; import middy from '@middy/core'; -import { Logger } from '@aws-lambda-powertools/logger'; +import { + array, + number, + object, + optional, + pipe, + string, + toMinValue, +} from 'valibot'; 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(), +const orderSchema = object({ + id: pipe(number(), toMinValue(0)), + description: string(), + items: array( + object({ + id: pipe(number(), toMinValue(0)), + quantity: pipe(number(), toMinValue(1)), + description: string(), }) ), - optionalField: z.string().optional(), + optionalField: optional(string()), }); -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 }) -); +export const handler = middy() + .use(parser({ schema: orderSchema })) + .handler(async (event): Promise => { + for (const item of event.items) { + // item is parsed as OrderItem + logger.info('Processing item', { item }); + } + }); ``` ### Decorator ```typescript -import type { Context } from 'aws-lambda'; import type { LambdaInterface } from '@aws-lambda-powertools/commons/types'; +import { Logger } from '@aws-lambda-powertools/logger'; import { parser } from '@aws-lambda-powertools/parser'; +import type { Context } from 'aws-lambda'; import { z } from 'zod'; -import { Logger } from '@aws-lambda-powertools/logger'; const logger = new Logger(); @@ -147,7 +163,7 @@ export const handler = myFunction.handler.bind(myFunction); ### Manual parsing -If you don't want to add an additional dependency, or you prefer the manual approach, you can `parse` the event directly by calling the `parse` method on schemas and envelopes: +If you don't want to add an additional middleware dependency, or you prefer the manual approach, you can parse the event directly by calling the `parse` method on schemas and envelopes: ```typescript import type { Context } from 'aws-lambda'; diff --git a/packages/parser/package.json b/packages/parser/package.json index 6ae674fc65..88650bfa89 100644 --- a/packages/parser/package.json +++ b/packages/parser/package.json @@ -200,11 +200,12 @@ "nodejs" ], "dependencies": { - "@aws-lambda-powertools/commons": "2.23.0" + "@aws-lambda-powertools/commons": "2.23.0", + "@standard-schema/spec": "^1.0.0" }, "peerDependencies": { "@middy/core": "4.x || 5.x || 6.x", - "zod": ">=3.x" + "zod": "^3.25.0 || ^4.0.0" }, "peerDependenciesMeta": { "zod": { @@ -213,5 +214,8 @@ "@middy/core": { "optional": true } + }, + "devDependencies": { + "zod": "^4.0.5" } } diff --git a/packages/parser/src/envelopes/api-gateway.ts b/packages/parser/src/envelopes/api-gateway.ts index 0abaecc6d8..b464ed561d 100644 --- a/packages/parser/src/envelopes/api-gateway.ts +++ b/packages/parser/src/envelopes/api-gateway.ts @@ -1,4 +1,4 @@ -import type { ZodSchema, z } from 'zod'; +import type { ZodType } from 'zod'; import { ParseError } from '../errors.js'; import { APIGatewayProxyEventSchema } from '../schemas/api-gateway.js'; import type { ParsedResult } from '../types/parser.js'; @@ -13,7 +13,7 @@ export const ApiGatewayEnvelope = { * @hidden */ [envelopeDiscriminator]: 'object' as const, - parse(data: unknown, schema: T): z.infer { + parse(data: unknown, schema: ZodType): T { try { return APIGatewayProxyEventSchema.extend({ body: schema, @@ -25,10 +25,7 @@ export const ApiGatewayEnvelope = { } }, - safeParse( - data: unknown, - schema: T - ): ParsedResult> { + safeParse(data: unknown, schema: ZodType): ParsedResult { const result = APIGatewayProxyEventSchema.extend({ body: schema, }).safeParse(data); diff --git a/packages/parser/src/envelopes/api-gatewayv2.ts b/packages/parser/src/envelopes/api-gatewayv2.ts index 408bcde42c..828bfaab0c 100644 --- a/packages/parser/src/envelopes/api-gatewayv2.ts +++ b/packages/parser/src/envelopes/api-gatewayv2.ts @@ -1,4 +1,4 @@ -import type { ZodSchema, z } from 'zod'; +import type { ZodType } from 'zod'; import { ParseError } from '../errors.js'; import { APIGatewayProxyEventV2Schema } from '../schemas/api-gatewayv2.js'; import type { ParsedResult } from '../types/index.js'; @@ -13,7 +13,7 @@ export const ApiGatewayV2Envelope = { * @hidden */ [envelopeDiscriminator]: 'object' as const, - parse(data: unknown, schema: T): z.infer { + parse(data: unknown, schema: ZodType): T { try { return APIGatewayProxyEventV2Schema.extend({ body: schema, @@ -25,10 +25,7 @@ export const ApiGatewayV2Envelope = { } }, - safeParse( - data: unknown, - schema: T - ): ParsedResult> { + safeParse(data: unknown, schema: ZodType): ParsedResult { const result = APIGatewayProxyEventV2Schema.extend({ body: schema, }).safeParse(data); diff --git a/packages/parser/src/envelopes/cloudwatch.ts b/packages/parser/src/envelopes/cloudwatch.ts index 36a57ffaf3..55a6b81056 100644 --- a/packages/parser/src/envelopes/cloudwatch.ts +++ b/packages/parser/src/envelopes/cloudwatch.ts @@ -1,4 +1,4 @@ -import { ZodError, type ZodIssue, type ZodSchema, type z } from 'zod'; +import { ZodError, type ZodType, type z } from 'zod'; import { ParseError } from '../errors.js'; import { CloudWatchLogsSchema } from '../schemas/index.js'; import type { ParsedResult } from '../types/index.js'; @@ -13,7 +13,7 @@ export const CloudWatchEnvelope = { * @hidden */ [envelopeDiscriminator]: 'array' as const, - parse(data: unknown, schema: T): z.infer[] { + parse(data: unknown, schema: ZodType): T[] { const parsedEnvelope = CloudWatchLogsSchema.parse(data); return parsedEnvelope.awslogs.data.logEvents.map((record, index) => { @@ -42,19 +42,8 @@ export const CloudWatchEnvelope = { }); }, - safeParse( - data: unknown, - schema: T - ): ParsedResult[]> { - let parsedEnvelope: ParsedResult>; - try { - parsedEnvelope = CloudWatchLogsSchema.safeParse(data); - } catch (error) { - parsedEnvelope = { - success: false, - error: error as Error, - }; - } + safeParse(data: unknown, schema: ZodType): ParsedResult { + const parsedEnvelope = CloudWatchLogsSchema.safeParse(data); if (!parsedEnvelope.success) { return { @@ -70,8 +59,8 @@ export const CloudWatchEnvelope = { ( acc: { success: boolean; - messages: z.infer; - errors: { [key: number]: { issues: ZodIssue[] } }; + messages: T[]; + errors: { [key: number]: { issues: z.core.$ZodIssue[] } }; }, record: { message: string }, index: number @@ -114,7 +103,6 @@ export const CloudWatchEnvelope = { ? `Failed to parse CloudWatch Log messages at indexes ${Object.keys(result.errors).join(', ')}` : `Failed to parse CloudWatch Log message at index ${Object.keys(result.errors)[0]}`; const errorCause = new ZodError( - // @ts-expect-error - issues are assigned because success is false Object.values(result.errors).flatMap((error) => error.issues) ); diff --git a/packages/parser/src/envelopes/dynamodb.ts b/packages/parser/src/envelopes/dynamodb.ts index bbe0914303..bcf951d647 100644 --- a/packages/parser/src/envelopes/dynamodb.ts +++ b/packages/parser/src/envelopes/dynamodb.ts @@ -1,4 +1,4 @@ -import { ZodError, type ZodIssue, type ZodSchema, type z } from 'zod'; +import { ZodError, type ZodType, type z } from 'zod'; import { ParseError } from '../errors.js'; import { DynamoDBStreamSchema } from '../schemas/index.js'; import type { DynamoDBStreamEnvelopeResponse } from '../types/envelope.js'; @@ -17,10 +17,10 @@ export const DynamoDBStreamEnvelope = { * @hidden */ [envelopeDiscriminator]: 'array' as const, - parse( + parse( data: unknown, - schema: T - ): DynamoDBStreamEnvelopeResponse>[] { + schema: ZodType + ): DynamoDBStreamEnvelopeResponse[] { const parsedEnvelope = DynamoDBStreamSchema.parse(data); const processImage = ( @@ -57,10 +57,10 @@ export const DynamoDBStreamEnvelope = { })); }, - safeParse( + safeParse( data: unknown, - schema: T - ): ParsedResult>[]> { + schema: ZodType + ): ParsedResult[]> { const parsedEnvelope = DynamoDBStreamSchema.safeParse(data); if (!parsedEnvelope.success) { return { @@ -77,15 +77,15 @@ export const DynamoDBStreamEnvelope = { const result = parsedEnvelope.data.Records.reduce<{ success: boolean; - records: DynamoDBStreamEnvelopeResponse>[]; - errors: { index?: number; issues?: ZodIssue[] }; + records: DynamoDBStreamEnvelopeResponse[]; + errors: { [key: number]: { issues: z.core.$ZodIssue[] } }; }>( (acc, record, index) => { const newImage = processImage(record.dynamodb.NewImage); const oldImage = processImage(record.dynamodb.OldImage); if (newImage?.success === false || oldImage?.success === false) { - const issues: ZodIssue[] = []; + const issues: z.core.$ZodIssue[] = []; for (const key of ['NewImage', 'OldImage']) { const image = key === 'NewImage' ? newImage : oldImage; if (image?.success === false) { @@ -98,7 +98,6 @@ export const DynamoDBStreamEnvelope = { } } acc.success = false; - // @ts-expect-error - index is assigned acc.errors[index] = { issues }; return acc; } @@ -121,7 +120,6 @@ export const DynamoDBStreamEnvelope = { ? `Failed to parse records at indexes ${Object.keys(result.errors).join(', ')}` : `Failed to parse record at index ${Object.keys(result.errors)[0]}`; const errorCause = new ZodError( - // @ts-expect-error - issues are assigned because success is false Object.values(result.errors).flatMap((error) => error.issues) ); diff --git a/packages/parser/src/envelopes/eventbridge.ts b/packages/parser/src/envelopes/eventbridge.ts index adc47d75b3..601c04b6b2 100644 --- a/packages/parser/src/envelopes/eventbridge.ts +++ b/packages/parser/src/envelopes/eventbridge.ts @@ -1,4 +1,4 @@ -import type { ZodError, ZodSchema, z } from 'zod'; +import type { ZodType } from 'zod'; import { ParseError } from '../errors.js'; import { EventBridgeSchema } from '../schemas/index.js'; import type { ParsedResult } from '../types/index.js'; @@ -13,29 +13,22 @@ export const EventBridgeEnvelope = { * @hidden */ [envelopeDiscriminator]: 'object' as const, - parse(data: unknown, schema: T): z.infer { - const extendedSchema = EventBridgeSchema.extend({ - detail: schema, - }); + parse(data: unknown, schema: ZodType): T { try { - const parsed = extendedSchema.parse(data); - return parsed.detail; + return EventBridgeSchema.extend({ + detail: schema, + }).parse(data).detail; } catch (error) { throw new ParseError('Failed to parse EventBridge envelope', { - cause: error as ZodError, + cause: error, }); } }, - safeParse( - data: unknown, - schema: T - ): ParsedResult> { - const extendedSchema = EventBridgeSchema.extend({ + safeParse(data: unknown, schema: ZodType): ParsedResult { + const parsedResult = EventBridgeSchema.extend({ detail: schema, - }); - - const parsedResult = extendedSchema.safeParse(data); + }).safeParse(data); if (!parsedResult.success) { return { success: false, diff --git a/packages/parser/src/envelopes/kafka.ts b/packages/parser/src/envelopes/kafka.ts index 04fbb678d6..055d8091c1 100644 --- a/packages/parser/src/envelopes/kafka.ts +++ b/packages/parser/src/envelopes/kafka.ts @@ -1,4 +1,4 @@ -import { ZodError, type ZodIssue, type ZodSchema, z } from 'zod'; +import { ZodError, type ZodType, z } from 'zod'; import { ParseError } from '../errors.js'; import { KafkaMskEventSchema, @@ -43,7 +43,7 @@ export const KafkaEnvelope = { * @hidden */ [envelopeDiscriminator]: 'array' as const, - parse(data: unknown, schema: T): z.infer[] { + parse(data: unknown, schema: ZodType): z.infer>[] { const eventSource = extractEventSource(data); const parsedEnvelope = @@ -51,7 +51,7 @@ export const KafkaEnvelope = { ? KafkaMskEventSchema.parse(data) : KafkaSelfManagedEventSchema.parse(data); - const values: z.infer[] = []; + const values: z.infer>[] = []; for (const topicRecord of Object.values(parsedEnvelope.records)) { for (const record of topicRecord) { values.push(schema.parse(record.value)); @@ -61,10 +61,10 @@ export const KafkaEnvelope = { return values; }, - safeParse( + safeParse( data: unknown, - schema: T - ): ParsedResult[]> { + schema: ZodType + ): ParsedResult>[]> { // manually fetch event source to deside between Msk or SelfManaged const eventSource = (data as KafkaMskEvent).eventSource; @@ -83,8 +83,8 @@ export const KafkaEnvelope = { }; } - const values: z.infer[] = []; - const issues: ZodIssue[] = []; + const values: z.infer>[] = []; + const issues: z.core.$ZodIssue[] = []; for (const [topicKey, topicRecord] of Object.entries( parsedEnvelope.data.records )) { @@ -92,11 +92,12 @@ export const KafkaEnvelope = { const parsedRecord = schema.safeParse(record.value); if (!parsedRecord.success) { issues.push( - ...(parsedRecord.error as ZodError).issues.map((issue) => ({ + ...parsedRecord.error.issues.map((issue) => ({ ...issue, path: ['records', topicKey, ...issue.path], })) ); + continue; } values.push(parsedRecord.data); } diff --git a/packages/parser/src/envelopes/kinesis-firehose.ts b/packages/parser/src/envelopes/kinesis-firehose.ts index 859f5b15f3..0a7a905607 100644 --- a/packages/parser/src/envelopes/kinesis-firehose.ts +++ b/packages/parser/src/envelopes/kinesis-firehose.ts @@ -1,9 +1,6 @@ -import { ZodError, type ZodIssue, type ZodSchema, type z } from 'zod'; +import { ZodError, type ZodType, type z } from 'zod'; import { ParseError } from '../errors.js'; -import { - type KinesisFirehoseRecordSchema, - KinesisFirehoseSchema, -} from '../schemas/index.js'; +import { KinesisFirehoseSchema } from '../schemas/index.js'; import type { ParsedResult } from '../types/index.js'; import { envelopeDiscriminator } from './envelope.js'; @@ -25,7 +22,7 @@ export const KinesisFirehoseEnvelope = { * @hidden */ [envelopeDiscriminator]: 'array' as const, - parse(data: unknown, schema: T): z.infer[] { + parse(data: unknown, schema: ZodType): T[] { let parsedEnvelope: z.infer; try { parsedEnvelope = KinesisFirehoseSchema.parse(data); @@ -36,7 +33,7 @@ export const KinesisFirehoseEnvelope = { } return parsedEnvelope.records.map((record, recordIndex) => { - let parsedRecord: z.infer; + let parsedRecord: T; try { parsedRecord = schema.parse(record.data); } catch (error) { @@ -56,10 +53,7 @@ export const KinesisFirehoseEnvelope = { }); }, - safeParse( - data: unknown, - schema: T - ): ParsedResult[]> { + safeParse(data: unknown, schema: ZodType): ParsedResult { const parsedEnvelope = KinesisFirehoseSchema.safeParse(data); if (!parsedEnvelope.success) { return { @@ -73,9 +67,9 @@ export const KinesisFirehoseEnvelope = { const result = parsedEnvelope.data.records.reduce<{ success: boolean; - records: z.infer[]; + records: T[]; errors: { - [key: number | string]: { issues: ZodIssue[] }; + [key: number | string]: { issues: z.core.$ZodIssue[] }; }; }>( (acc, record, index) => { diff --git a/packages/parser/src/envelopes/kinesis.ts b/packages/parser/src/envelopes/kinesis.ts index 9259a56f0b..a7cbfc461b 100644 --- a/packages/parser/src/envelopes/kinesis.ts +++ b/packages/parser/src/envelopes/kinesis.ts @@ -1,9 +1,6 @@ -import { ZodError, type ZodIssue, type ZodSchema, type z } from 'zod'; +import { ZodError, type ZodType, type z } from 'zod'; import { ParseError } from '../errors.js'; -import { - type KinesisDataStreamRecord, - KinesisDataStreamSchema, -} from '../schemas/kinesis.js'; +import { KinesisDataStreamSchema } from '../schemas/kinesis.js'; import type { ParsedResult } from '../types/index.js'; import { envelopeDiscriminator } from './envelope.js'; @@ -23,7 +20,7 @@ export const KinesisEnvelope = { * @hidden */ [envelopeDiscriminator]: 'array' as const, - parse(data: unknown, schema: T): z.infer[] { + parse(data: unknown, schema: ZodType): T[] { let parsedEnvelope: z.infer; try { parsedEnvelope = KinesisDataStreamSchema.parse(data); @@ -34,7 +31,7 @@ export const KinesisEnvelope = { } return parsedEnvelope.Records.map((record, recordIndex) => { - let parsedRecord: z.infer; + let parsedRecord: T; try { parsedRecord = schema.parse(record.kinesis.data); } catch (error) { @@ -60,10 +57,7 @@ export const KinesisEnvelope = { }); }, - safeParse( - data: unknown, - schema: T - ): ParsedResult[]> { + safeParse(data: unknown, schema: ZodType): ParsedResult { const parsedEnvelope = KinesisDataStreamSchema.safeParse(data); if (!parsedEnvelope.success) { return { @@ -77,8 +71,8 @@ export const KinesisEnvelope = { const result = parsedEnvelope.data.Records.reduce<{ success: boolean; - records: z.infer[]; - errors: { index?: number; issues?: ZodIssue[] }; + records: T[]; + errors: { [key: number]: { issues: z.core.$ZodIssue[] } }; }>( (acc, record, index) => { const parsedRecord = schema.safeParse(record.kinesis.data); @@ -89,7 +83,6 @@ export const KinesisEnvelope = { path: ['Records', index, 'kinesis', 'data', ...issue.path], })); acc.success = false; - // @ts-expect-error - index is assigned acc.errors[index] = { issues }; return acc; } @@ -112,7 +105,6 @@ export const KinesisEnvelope = { success: false, error: new ParseError(errorMessage, { cause: new ZodError( - // @ts-expect-error - issues are assigned because success is false Object.values(result.errors).flatMap((error) => error.issues) ), }), diff --git a/packages/parser/src/envelopes/lambda.ts b/packages/parser/src/envelopes/lambda.ts index d68af29a88..96a1f13268 100644 --- a/packages/parser/src/envelopes/lambda.ts +++ b/packages/parser/src/envelopes/lambda.ts @@ -1,4 +1,4 @@ -import type { ZodSchema, z } from 'zod'; +import type { ZodType } from 'zod'; import { ParseError } from '../errors.js'; import { LambdaFunctionUrlSchema } from '../schemas/index.js'; import type { ParsedResult } from '../types/index.js'; @@ -13,22 +13,19 @@ export const LambdaFunctionUrlEnvelope = { * @hidden */ [envelopeDiscriminator]: 'object' as const, - parse(data: unknown, schema: T): z.infer { + parse(data: unknown, schema: ZodType): T { try { return LambdaFunctionUrlSchema.extend({ body: schema, }).parse(data).body; } catch (error) { throw new ParseError('Failed to parse Lambda function URL body', { - cause: error as Error, + cause: error, }); } }, - safeParse( - data: unknown, - schema: T - ): ParsedResult> { + safeParse(data: unknown, schema: ZodType): ParsedResult { const results = LambdaFunctionUrlSchema.extend({ body: schema, }).safeParse(data); diff --git a/packages/parser/src/envelopes/sns-sqs.ts b/packages/parser/src/envelopes/sns-sqs.ts index a75a709f31..1aa17288e0 100644 --- a/packages/parser/src/envelopes/sns-sqs.ts +++ b/packages/parser/src/envelopes/sns-sqs.ts @@ -1,11 +1,11 @@ -import { ZodError, type ZodIssue, type ZodSchema, type z } from 'zod'; +import { ZodError, type ZodType, type z } from 'zod'; import { ParseError } from '../errors.js'; import { SnsSqsNotificationSchema } from '../schemas/sns.js'; import { SqsSchema } from '../schemas/sqs.js'; import type { ParsedResult, SnsSqsNotification } from '../types/index.js'; import { envelopeDiscriminator } from './envelope.js'; -const createError = (index: number, issues: ZodIssue[]) => ({ +const createError = (index: number, issues: z.core.$ZodIssue[]) => ({ issues: issues.map((issue) => ({ ...issue, path: ['Records', index, 'body', ...issue.path], @@ -19,13 +19,13 @@ type ParseStepSuccess = { type ParseStepError = { success: false; - error: { issues: ZodIssue[] }; + error: { issues: z.core.$ZodIssue[] }; }; type ParseStepResult = ParseStepSuccess | ParseStepError; const parseStep = ( - parser: (data: unknown) => z.SafeParseReturnType, + parser: (data: unknown) => z.ZodSafeParseResult, data: unknown, index: number ): ParseStepResult => { @@ -56,7 +56,7 @@ export const SnsSqsEnvelope = { * @hidden */ [envelopeDiscriminator]: 'array' as const, - parse(data: unknown, schema: T): z.infer[] { + parse(data: unknown, schema: ZodType): T[] { let parsedEnvelope: z.infer; try { parsedEnvelope = SqsSchema.parse(data); @@ -84,7 +84,8 @@ export const SnsSqsEnvelope = { : [ { code: 'custom', - message: 'Invalid JSON', + input: record.body, + message: `Invalid JSON - ${(error as Error).message}`, path: ['Records', recordIndex, 'body'], }, ] @@ -95,10 +96,7 @@ export const SnsSqsEnvelope = { }); }, - safeParse( - data: unknown, - schema: T - ): ParsedResult[]> { + safeParse(data: unknown, schema: ZodType): ParsedResult { const parsedEnvelope = SqsSchema.safeParse(data); if (!parsedEnvelope.success) { return { @@ -113,7 +111,7 @@ export const SnsSqsEnvelope = { const parseRecord = ( record: { body: string }, index: number - ): ParseStepResult> => { + ): ParseStepResult => { try { const body = JSON.parse(record.body); const notification = parseStep( @@ -123,18 +121,19 @@ export const SnsSqsEnvelope = { ); if (!notification.success) return notification; - return parseStep>( + return parseStep( (data) => schema.safeParse(data), notification.data.Message, index ); - } catch { + } catch (error) { return { success: false, error: createError(index, [ { code: 'custom', - message: 'Invalid JSON', + message: `Invalid JSON - ${(error as Error).message}`, + input: record.body, path: [], }, ]), @@ -144,9 +143,9 @@ export const SnsSqsEnvelope = { const result = parsedEnvelope.data.Records.reduce<{ success: boolean; - records: z.infer[]; + records: T[]; errors: { - [key: number | string]: { issues: ZodIssue[] }; + [key: number | string]: { issues: z.core.$ZodIssue[] }; }; }>( (acc, record, index) => { diff --git a/packages/parser/src/envelopes/sns.ts b/packages/parser/src/envelopes/sns.ts index 3a7d40cb32..038b2b1f13 100644 --- a/packages/parser/src/envelopes/sns.ts +++ b/packages/parser/src/envelopes/sns.ts @@ -1,4 +1,4 @@ -import { ZodError, type ZodIssue, type ZodSchema, type z } from 'zod'; +import { ZodError, type ZodType, type z } from 'zod'; import { ParseError } from '../errors.js'; import { SnsSchema } from '../schemas/sns.js'; import type { ParsedResult } from '../types/index.js'; @@ -19,7 +19,7 @@ export const SnsEnvelope = { * @hidden */ [envelopeDiscriminator]: 'array' as const, - parse(data: unknown, schema: T): z.infer[] { + parse(data: unknown, schema: ZodType): T[] { const parsedEnvelope = SnsSchema.parse(data); return parsedEnvelope.Records.map((record, index) => { @@ -38,10 +38,7 @@ export const SnsEnvelope = { }); }, - safeParse( - data: unknown, - schema: T - ): ParsedResult[]> { + safeParse(data: unknown, schema: ZodType): ParsedResult { const parsedEnvelope = SnsSchema.safeParse(data); if (!parsedEnvelope.success) { @@ -56,8 +53,8 @@ export const SnsEnvelope = { const result = parsedEnvelope.data.Records.reduce<{ success: boolean; - messages: z.infer[]; - errors: { index?: number; issues?: ZodIssue[] }; + messages: T[]; + errors: { [key: number]: { issues: z.core.$ZodIssue[] } }; }>( (acc, message, index) => { const parsedMessage = schema.safeParse(message.Sns.Message); @@ -67,7 +64,6 @@ export const SnsEnvelope = { ...issue, path: ['Records', index, 'Sns', 'Message', ...issue.path], })); - // @ts-expect-error - index is assigned acc.errors[index] = { issues }; return acc; } @@ -87,7 +83,6 @@ export const SnsEnvelope = { ? `Failed to parse SNS messages at indexes ${Object.keys(result.errors).join(', ')}` : `Failed to parse SNS message at index ${Object.keys(result.errors)[0]}`; const errorCause = new ZodError( - // @ts-expect-error - issues are assigned because success is false Object.values(result.errors).flatMap((error) => error.issues) ); diff --git a/packages/parser/src/envelopes/sqs.ts b/packages/parser/src/envelopes/sqs.ts index 6d57606f7b..71472ffd4a 100644 --- a/packages/parser/src/envelopes/sqs.ts +++ b/packages/parser/src/envelopes/sqs.ts @@ -1,6 +1,6 @@ -import { ZodError, type ZodIssue, type ZodSchema, type z } from 'zod'; +import { ZodError, type ZodType, type z } from 'zod'; import { ParseError } from '../errors.js'; -import { type SqsRecordSchema, SqsSchema } from '../schemas/sqs.js'; +import { SqsSchema } from '../schemas/sqs.js'; import type { ParsedResult } from '../types/index.js'; import { envelopeDiscriminator } from './envelope.js'; @@ -29,7 +29,7 @@ const SqsEnvelope = { * @hidden */ [envelopeDiscriminator]: 'array' as const, - parse(data: unknown, schema: T): z.infer[] { + parse(data: unknown, schema: ZodType): T[] { let parsedEnvelope: z.infer; try { parsedEnvelope = SqsSchema.parse(data); @@ -40,7 +40,7 @@ const SqsEnvelope = { } return parsedEnvelope.Records.map((record, recordIndex) => { - let parsedRecord: z.infer; + let parsedRecord: T; try { parsedRecord = schema.parse(record.body); } catch (error) { @@ -60,10 +60,7 @@ const SqsEnvelope = { }); }, - safeParse( - data: unknown, - schema: T - ): ParsedResult[]> { + safeParse(data: unknown, schema: ZodType): ParsedResult { const parsedEnvelope = SqsSchema.safeParse(data); if (!parsedEnvelope.success) { return { @@ -77,8 +74,8 @@ const SqsEnvelope = { const result = parsedEnvelope.data.Records.reduce<{ success: boolean; - records: z.infer[]; - errors: { index?: number; issues?: ZodIssue[] }; + records: T[]; + errors: { index?: number; issues?: z.core.$ZodIssue[] }; }>( (acc, record, index) => { const parsedRecord = schema.safeParse(record.body); diff --git a/packages/parser/src/envelopes/vpc-lattice.ts b/packages/parser/src/envelopes/vpc-lattice.ts index 6c8190ae8c..7a686f64b0 100644 --- a/packages/parser/src/envelopes/vpc-lattice.ts +++ b/packages/parser/src/envelopes/vpc-lattice.ts @@ -1,4 +1,4 @@ -import type { ZodSchema, z } from 'zod'; +import type { ZodType, z } from 'zod'; import { ParseError } from '../errors.js'; import { VpcLatticeSchema } from '../schemas/index.js'; import type { ParsedResult } from '../types/index.js'; @@ -7,14 +7,13 @@ import { envelopeDiscriminator } from './envelope.js'; /** * Amazon VPC Lattice envelope to extract data within body key */ - export const VpcLatticeEnvelope = { /** * This is a discriminator to differentiate whether an envelope returns an array or an object * @hidden */ [envelopeDiscriminator]: 'object' as const, - parse(data: unknown, schema: T): z.infer { + parse(data: unknown, schema: ZodType): z.infer> { try { return VpcLatticeSchema.extend({ body: schema, @@ -26,10 +25,10 @@ export const VpcLatticeEnvelope = { } }, - safeParse( + safeParse( data: unknown, - schema: T - ): ParsedResult> { + schema: ZodType + ): ParsedResult>> { const result = VpcLatticeSchema.extend({ body: schema, }).safeParse(data); diff --git a/packages/parser/src/envelopes/vpc-latticev2.ts b/packages/parser/src/envelopes/vpc-latticev2.ts index 0ff99dffe6..7a7f245518 100644 --- a/packages/parser/src/envelopes/vpc-latticev2.ts +++ b/packages/parser/src/envelopes/vpc-latticev2.ts @@ -1,4 +1,4 @@ -import type { ZodSchema, z } from 'zod'; +import type { ZodType } from 'zod'; import { ParseError } from '../errors.js'; import { VpcLatticeV2Schema } from '../schemas/index.js'; import type { ParsedResult } from '../types/index.js'; @@ -13,7 +13,7 @@ export const VpcLatticeV2Envelope = { * @hidden */ [envelopeDiscriminator]: 'object' as const, - parse(data: unknown, schema: T): z.infer { + parse(data: unknown, schema: ZodType): T { try { return VpcLatticeV2Schema.extend({ body: schema, @@ -25,10 +25,7 @@ export const VpcLatticeV2Envelope = { } }, - safeParse( - data: unknown, - schema: T - ): ParsedResult> { + safeParse(data: unknown, schema: ZodType): ParsedResult { const result = VpcLatticeV2Schema.extend({ body: schema, }).safeParse(data); diff --git a/packages/parser/src/errors.ts b/packages/parser/src/errors.ts index d4aac4d7ad..089d414a93 100644 --- a/packages/parser/src/errors.ts +++ b/packages/parser/src/errors.ts @@ -3,23 +3,14 @@ * The cause of the error is included in the message, if possible. */ class ParseError extends Error { - public constructor(message: string, options?: { cause?: Error }) { - const errorMessage = options?.cause - ? `${message}. This error was caused by: ${options?.cause.message}.` - : message; + public constructor(message: string, options?: ErrorOptions) { + const errorMessage = + options?.cause && options.cause instanceof Error + ? `${message}. This error was caused by: ${options?.cause.message}.` + : message; super(errorMessage, options); this.name = 'ParseError'; } } -/** - * Custom error thrown when decompression fails. - */ -class DecompressError extends ParseError { - constructor(message: string, options?: { cause?: Error }) { - super(message, options); - this.name = 'DecompressError'; - } -} - -export { ParseError, DecompressError }; +export { ParseError }; diff --git a/packages/parser/src/helpers/dynamodb.ts b/packages/parser/src/helpers/dynamodb.ts index 56c9dc9144..ace793d3ca 100644 --- a/packages/parser/src/helpers/dynamodb.ts +++ b/packages/parser/src/helpers/dynamodb.ts @@ -1,6 +1,6 @@ import { unmarshallDynamoDB } from '@aws-lambda-powertools/commons/utils/unmarshallDynamoDB'; import type { AttributeValue } from '@aws-sdk/client-dynamodb'; -import { type ZodTypeAny, z } from 'zod'; +import { type ZodType, z } from 'zod'; /** * A helper function to unmarshall DynamoDB stream events and validate them against a schema. @@ -61,7 +61,7 @@ import { type ZodTypeAny, z } from 'zod'; * * @param schema - The schema to validate the JSON string against */ -const DynamoDBMarshalled = (schema: T) => +const DynamoDBMarshalled = (schema: ZodType) => z .union([ z.custom(), diff --git a/packages/parser/src/helpers/index.ts b/packages/parser/src/helpers/index.ts index 17cf945274..4098233df2 100644 --- a/packages/parser/src/helpers/index.ts +++ b/packages/parser/src/helpers/index.ts @@ -1,4 +1,4 @@ -import { type ZodTypeAny, z } from 'zod'; +import { type ZodType, z } from 'zod'; /** * A helper function to parse a JSON string and validate it against a schema. @@ -38,16 +38,17 @@ import { type ZodTypeAny, z } from 'zod'; * * @param schema - The schema to validate the JSON string against */ -const JSONStringified = (schema: T) => +const JSONStringified = (schema: T) => z .string() .transform((str, ctx) => { try { return JSON.parse(str); - } catch { + } catch (error) { ctx.addIssue({ code: 'custom', - message: 'Invalid JSON', + message: `Invalid JSON - ${(error as Error).message}`, + fatal: true, }); } }) diff --git a/packages/parser/src/middleware/index.ts b/packages/parser/src/middleware/index.ts index 3a31e0992f..c57cdf34cd 100644 --- a/packages/parser/src/middleware/index.ts +++ b/packages/parser/src/middleware/index.ts @@ -1,12 +1,12 @@ import type { MiddyLikeRequest } from '@aws-lambda-powertools/commons/types'; import type { MiddlewareObj } from '@middy/core'; -import type { ZodType } from 'zod'; +import type { StandardSchemaV1 } from '@standard-schema/spec'; import { parse } from '../parser.js'; import type { Envelope } from '../types/envelope.js'; import type { ParserOptions, ParserOutput } from '../types/parser.js'; /** - * A middiy middleware to parse your event. + * A Middy.js middleware to parse incoming events using a specified schema and optional envelope. * * @example * ```typescript @@ -22,19 +22,17 @@ import type { ParserOptions, ParserOutput } from '../types/parser.js'; * * type Order = z.infer; * - * export const handler = middy( - * async (event: Order, _context: unknown): Promise => { - * // event is validated as sqs message envelope - * // the body is unwrapped and parsed into object ready to use - * // you can now use event as Order in your code - * } - * ).use(parser({ schema: oderSchema, envelope: sqsEnvelope })); + * export const handler = middy() + * .use(parser({ schema: oderSchema, envelope: sqsEnvelope })) + * .handler(async (event) => { + * // ^ event is inferred as Order[] + * }) * ``` * - * @param options + * @param options - options for the parser */ const parser = < - TSchema extends ZodType, + TSchema extends StandardSchemaV1, TEnvelope extends Envelope, TSafeParse extends boolean = false, >( diff --git a/packages/parser/src/parser.ts b/packages/parser/src/parser.ts index dccb6fd8f9..9a7077f8c1 100644 --- a/packages/parser/src/parser.ts +++ b/packages/parser/src/parser.ts @@ -1,10 +1,15 @@ -import type { ZodSchema, z } from 'zod'; +import type { StandardSchemaV1 } from '@standard-schema/spec'; import { ParseError } from './errors.js'; -import type { Envelope } from './types/index.js'; -import type { ParseFunction } from './types/parser.js'; +import type { + ArrayEnvelope, + DynamoDBArrayEnvelope, + DynamoDBStreamEnvelopeResponse, + Envelope, +} from './types/index.js'; +import type { InferOutput, ParsedResult } from './types/parser.js'; /** - * Parse the data using the provided schema, envelope and safeParse flag + * Parse the data using the provided schema and optional envelope. * * @example * ```typescript @@ -20,55 +25,128 @@ import type { ParseFunction } from './types/parser.js'; * * const handler = async (event: SqsEvent, context: unknown): Promise => { * const parsedEvent = parse(event, SqsEnvelope, Order); - * - * const parsedSafe: ParsedResult = parse(event, SqsEnvelope, Order, true) * } - * @param data the data to parse - * @param envelope the envelope to use, can be undefined - * @param schema the schema to use - * @param safeParse whether to use safeParse or not, if true it will return a ParsedResult with the original event if the parsing fails + * ``` + * + * @param data - the data to parse + * @param envelope - optional envelope to use when parsing the data + * @param schema - the schema to use + * @param safeParse - whether to throw on error, if `true` it will return a `ParsedResult` with the original event if the parsing fails */ -const parse: ParseFunction = ( - data: z.infer, +function parse( + data: unknown, + envelope: undefined, + schema: T, + safeParse?: false +): InferOutput; + +// No envelope, with safeParse +function parse( + data: unknown, + envelope: undefined, + schema: T, + safeParse: true +): ParsedResult>; + +// No envelope, with boolean safeParse +function parse( + data: unknown, + envelope: undefined, + schema: T, + safeParse: boolean +): InferOutput | ParsedResult>; + +// With envelope, no safeParse +function parse( + data: unknown, + envelope: E, + schema: T, + safeParse?: false +): E extends DynamoDBArrayEnvelope + ? DynamoDBStreamEnvelopeResponse>[] + : E extends ArrayEnvelope + ? InferOutput[] + : InferOutput; + +// With envelope, with safeParse +function parse( + data: unknown, + envelope: E, + schema: T, + safeParse: true +): E extends DynamoDBArrayEnvelope + ? ParsedResult>[]> + : E extends ArrayEnvelope + ? ParsedResult[]> + : ParsedResult>; + +// No envelope, with boolean | undefined safeParse +function parse( + data: unknown, + envelope: undefined, + schema: T, + safeParse?: boolean +): InferOutput | ParsedResult>; + +// With envelope, with boolean | undefined safeParse +function parse( + data: unknown, + envelope: E, + schema: T, + safeParse?: boolean +): E extends DynamoDBArrayEnvelope + ? + | DynamoDBStreamEnvelopeResponse>[] + | ParsedResult>[]> + : E extends ArrayEnvelope + ? InferOutput[] | ParsedResult[]> + : InferOutput | ParsedResult>; + +// Implementation +function parse( + data: unknown, envelope: E | undefined, schema: T, safeParse?: boolean -) => { +): InferOutput | ParsedResult> { if (envelope && safeParse) { - return envelope.safeParse(data, schema); + // biome-ignore lint/suspicious/noExplicitAny: at least for now, we need to broaden the type because the envelope's parse and safeParse methods are not typed with StandardSchemaV1 but with ZodSchema + return envelope.safeParse(data, schema as any); } if (envelope) { - return envelope.parse(data, schema); + // biome-ignore lint/suspicious/noExplicitAny: at least for now, we need to broaden the type because the envelope's parse and safeParse methods are not typed with StandardSchemaV1 but with ZodSchema + return envelope.parse(data, schema as any); } - if (safeParse) { - return safeParseSchema(data, schema); - } - try { - return schema.parse(data); - } catch (error) { - throw new ParseError('Failed to parse schema', { cause: error as Error }); - } -}; -/** - * Parse the data safely using the provided schema. - * This function will not throw an error if the parsing fails, instead it will return a ParsedResultError with the original event. - * Otherwise, it will return ParsedResultSuccess with the parsed data. - * @param data the data to parse - * @param schema the zod schema to use - */ -const safeParseSchema = (data: z.infer, schema: T) => { - const result = schema.safeParse(data); + const result = schema['~standard'].validate(data); + /* v8 ignore start */ + if (result instanceof Promise) { + throw new ParseError('Schema parsing supports only synchronous validation'); + } + /* v8 ignore stop */ - return result.success - ? result - : { + if (result.issues) { + const error = new ParseError('Failed to parse schema', { + cause: result.issues, + }); + if (safeParse) { + return { success: false, - error: new ParseError('Failed to parse schema safely', { - cause: result.error, - }), + error, originalEvent: data, }; -}; + } + throw error; + } + + if (safeParse) { + return { + success: true, + data: result.value, + }; + } + + return result.value; +} export { parse }; diff --git a/packages/parser/src/parserDecorator.ts b/packages/parser/src/parserDecorator.ts index 0e9ee7ed87..bb9656ca2a 100644 --- a/packages/parser/src/parserDecorator.ts +++ b/packages/parser/src/parserDecorator.ts @@ -1,6 +1,6 @@ import type { HandlerMethodDecorator } from '@aws-lambda-powertools/commons/types'; +import type { StandardSchemaV1 } from '@standard-schema/spec'; import type { Context, Handler } from 'aws-lambda'; -import type { ZodSchema } from 'zod'; import { parse } from './parser.js'; import type { Envelope, ParserOptions } from './types/index.js'; import type { ParserOutput } from './types/parser.js'; @@ -69,7 +69,7 @@ import type { ParserOutput } from './types/parser.js'; * @param options Configure the parser with the `schema`, `envelope` and whether to `safeParse` or not */ export const parser = < - TSchema extends ZodSchema, + TSchema extends StandardSchemaV1, TEnvelope extends Envelope = undefined, TSafeParse extends boolean = false, >( diff --git a/packages/parser/src/schemas/alb.ts b/packages/parser/src/schemas/alb.ts index 732cabc31d..9b61825ba4 100644 --- a/packages/parser/src/schemas/alb.ts +++ b/packages/parser/src/schemas/alb.ts @@ -1,4 +1,5 @@ import { z } from 'zod'; +import type { ALBEvent } from '../types/schema.js'; /** * Zod schema for Application Load Balancer events. @@ -59,7 +60,7 @@ import { z } from 'zod'; * } * ``` * - * @see {@link types.ALBEvent | ALBEvent} + * @see {@link ALBEvent | `ALBEvent`} * @see {@link https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html} * @see {@link https://docs.aws.amazon.com/lambda/latest/dg/services-alb.html} */ diff --git a/packages/parser/src/schemas/api-gateway-websocket.ts b/packages/parser/src/schemas/api-gateway-websocket.ts index a70148c9ba..ca26281e79 100644 --- a/packages/parser/src/schemas/api-gateway-websocket.ts +++ b/packages/parser/src/schemas/api-gateway-websocket.ts @@ -1,4 +1,6 @@ import { z } from 'zod'; +import type { APIGatewayProxyWebsocketEvent } from '../types/schema.js'; +import { APIGatewayRecord, APIGatewayStringArray } from './apigw-proxy.js'; /** * A zod schema for API Gateway Proxy WebSocket events. @@ -59,19 +61,19 @@ import { z } from 'zod'; * } * ``` * + * @see {@link APIGatewayProxyWebsocketEvent | `APIGatewayProxyWebsocketEvent`} * @see {@link https://docs.aws.amazon.com/apigateway/latest/developerguide/websocket-api-develop-integrations.html} */ export const APIGatewayProxyWebsocketEventSchema = z.object({ type: z.string(), methodArn: z.string(), - headers: z.record(z.string()), - multiValueHeaders: z.record(z.array(z.string())), - queryStringParameters: z.record(z.string()).nullable().optional(), + headers: z.record(z.string(), z.string()).nullish(), + multiValueHeaders: z.record(z.string(), APIGatewayStringArray), + queryStringParameters: APIGatewayRecord.nullable(), multiValueQueryStringParameters: z - .record(z.array(z.string())) - .nullable() - .optional(), - stageVariables: z.record(z.string()).nullable().optional(), + .record(z.string(), APIGatewayStringArray) + .nullable(), + stageVariables: APIGatewayRecord.nullable().optional(), requestContext: z.object({ routeKey: z.string(), eventType: z.enum(['CONNECT', 'DISCONNECT', 'MESSAGE']), diff --git a/packages/parser/src/schemas/api-gateway.ts b/packages/parser/src/schemas/api-gateway.ts index 991cd88bcb..9baa7c322e 100644 --- a/packages/parser/src/schemas/api-gateway.ts +++ b/packages/parser/src/schemas/api-gateway.ts @@ -1,4 +1,5 @@ import { z } from 'zod'; +import type { APIGatewayProxyEvent } from '../types/schema.js'; import { APIGatewayCert, APIGatewayHttpMethod, @@ -29,9 +30,7 @@ const APIGatewayEventIdentity = z.object({ * * See aws-powertools/powertools-lambda-python#1562 for more information. */ - sourceIp: z - .union([z.string().ip(), z.literal('test-invoke-source-ip')]) - .optional(), + sourceIp: z.union([z.ipv4(), z.literal('test-invoke-source-ip')]).optional(), user: z.string().nullish(), userAgent: z.string().nullish(), userArn: z.string().nullish(), @@ -147,8 +146,7 @@ const APIGatewayEventRequestContextSchema = z * "apiId": "abcdef123" * } * ``` - * @see {@link types.APIGatewayProxyEvent | APIGatewayProxyEvent} - * + * @see {@link APIGatewayProxyEvent | `APIGatewayProxyEvent`} * @see {@link https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html} */ const APIGatewayProxyEventSchema = z.object({ @@ -156,9 +154,11 @@ const APIGatewayProxyEventSchema = z.object({ path: z.string(), httpMethod: APIGatewayHttpMethod, headers: APIGatewayRecord.nullish(), - multiValueHeaders: z.record(APIGatewayStringArray).nullish(), + multiValueHeaders: z.record(z.string(), APIGatewayStringArray).nullish(), queryStringParameters: APIGatewayRecord.nullable(), - multiValueQueryStringParameters: z.record(APIGatewayStringArray).nullable(), + multiValueQueryStringParameters: z + .record(z.string(), APIGatewayStringArray) + .nullable(), pathParameters: APIGatewayRecord.nullish(), stageVariables: APIGatewayRecord.nullish(), requestContext: APIGatewayEventRequestContextSchema, @@ -227,9 +227,9 @@ const APIGatewayRequestAuthorizerEventSchema = z.object({ path: z.string(), httpMethod: APIGatewayHttpMethod, headers: APIGatewayRecord, - multiValueHeaders: z.record(APIGatewayStringArray), + multiValueHeaders: z.record(z.string(), APIGatewayStringArray), queryStringParameters: APIGatewayRecord, - multiValueQueryStringParameters: z.record(APIGatewayStringArray), + multiValueQueryStringParameters: z.record(z.string(), APIGatewayStringArray), pathParameters: APIGatewayRecord, stageVariables: APIGatewayRecord, requestContext: APIGatewayEventRequestContextSchema, diff --git a/packages/parser/src/schemas/api-gatewayv2.ts b/packages/parser/src/schemas/api-gatewayv2.ts index 6bab874fb0..bff5fdfdd6 100644 --- a/packages/parser/src/schemas/api-gatewayv2.ts +++ b/packages/parser/src/schemas/api-gatewayv2.ts @@ -1,4 +1,8 @@ import { z } from 'zod'; +import { + APIGatewayProxyEventV2, + APIGatewayRequestAuthorizerEventV2, +} from '../types/schema.js'; import { APIGatewayCert, APIGatewayHttpMethod, @@ -106,7 +110,7 @@ const APIGatewayRequestContextV2Schema = z.object({ method: APIGatewayHttpMethod, path: z.string(), protocol: z.string(), - sourceIp: z.string().ip(), + sourceIp: z.ipv4(), userAgent: z.string(), }), requestId: z.string(), @@ -161,6 +165,7 @@ const APIGatewayRequestContextV2Schema = z.object({ * } * ``` * + * @see {@link APIGatewayProxyEventV2 | `APIGatewayProxyEventV2`} * @see {@link https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html} */ const APIGatewayProxyEventV2Schema = z.object({ @@ -224,6 +229,7 @@ const APIGatewayProxyEventV2Schema = z.object({ * } * ``` * + * @see {@link APIGatewayRequestAuthorizerEventV2 | `APIGatewayRequestAuthorizerEventV2`} * @see {@link https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-lambda-authorizer.html} */ const APIGatewayRequestAuthorizerEventV2Schema = z.object({ diff --git a/packages/parser/src/schemas/apigw-proxy.ts b/packages/parser/src/schemas/apigw-proxy.ts index ea35ee5191..35ce6b5110 100644 --- a/packages/parser/src/schemas/apigw-proxy.ts +++ b/packages/parser/src/schemas/apigw-proxy.ts @@ -17,7 +17,7 @@ const APIGatewayCert = z.object({ /** * A zod schema for an object with string keys and string values */ -const APIGatewayRecord = z.record(z.string()); +const APIGatewayRecord = z.record(z.string(), z.string()); /** * A zod schema for an array of strings diff --git a/packages/parser/src/schemas/appsync-events.ts b/packages/parser/src/schemas/appsync-events.ts index 7c90d68a99..5c7b39f809 100644 --- a/packages/parser/src/schemas/appsync-events.ts +++ b/packages/parser/src/schemas/appsync-events.ts @@ -1,4 +1,8 @@ import { z } from 'zod'; +import type { + AppSyncEventsPublishEvent, + AppSyncEventsSubscribeEvent, +} from '../types/schema.js'; import { AppSyncCognitoIdentity, AppSyncIamIdentity, @@ -105,6 +109,8 @@ const AppSyncEventsBaseSchema = z.object({ * ] * } * ``` + * + * @see {@link AppSyncEventsPublishEvent | `AppSyncEventsPublishEvent`} */ const AppSyncEventsPublishSchema = AppSyncEventsBaseSchema.extend({ info: AppSyncEventsInfoSchema.extend({ @@ -151,6 +157,8 @@ const AppSyncEventsPublishSchema = AppSyncEventsBaseSchema.extend({ * "events": null, * } * ``` + * + * @see {@link AppSyncEventsSubscribeEvent | `AppSyncEventsSubscribeEvent`} */ const AppSyncEventsSubscribeSchema = AppSyncEventsBaseSchema.extend({ info: AppSyncEventsInfoSchema.extend({ diff --git a/packages/parser/src/schemas/appsync-shared.ts b/packages/parser/src/schemas/appsync-shared.ts index c04d4916e9..7e7278bf49 100644 --- a/packages/parser/src/schemas/appsync-shared.ts +++ b/packages/parser/src/schemas/appsync-shared.ts @@ -16,7 +16,7 @@ const AppSyncCognitoIdentity = z.object({ issuer: z.string(), username: z.string(), claims: z.record(z.string(), z.unknown()), - sourceIp: z.array(z.string().ip()), + sourceIp: z.array(z.ipv4()), defaultAuthStrategy: z.string().nullable(), groups: z.array(z.string()).nullable(), }); diff --git a/packages/parser/src/schemas/appsync.ts b/packages/parser/src/schemas/appsync.ts index 610c507d75..ca2c554bec 100644 --- a/packages/parser/src/schemas/appsync.ts +++ b/packages/parser/src/schemas/appsync.ts @@ -1,4 +1,8 @@ import { z } from 'zod'; +import type { + AppSyncBatchResolverEvent, + AppSyncResolverEvent, +} from '../types/schema.js'; import { AppSyncCognitoIdentity, AppSyncIamIdentity, @@ -80,30 +84,30 @@ const AppSyncIdentity = z.union([ * } * ``` * + * @see {@link AppSyncResolverEvent | `AppSyncResolverEvent`} * @see {@link https://docs.aws.amazon.com/appsync/latest/devguide/resolver-context-reference-js.html} */ - const AppSyncResolverSchema = z.object({ - arguments: z.record(z.any()), + arguments: z.record(z.string(), z.any()), identity: z.optional(AppSyncIdentity), - source: z.record(z.any()).nullable(), + source: z.record(z.string(), z.any()).nullable(), request: z.object({ domainName: z.string().nullable(), - headers: z.record(z.string()), + headers: z.record(z.string(), z.string()), }), info: z.object({ selectionSetList: z.array(z.string()), selectionSetGraphQL: z.string(), parentTypeName: z.string(), fieldName: z.string(), - variables: z.record(z.any()), + variables: z.record(z.string(), z.any()), }), prev: z .object({ - result: z.record(z.any()), + result: z.record(z.string(), z.any()), }) .nullable(), - stash: z.record(z.any()), + stash: z.record(z.string(), z.any()), }); /** @@ -224,9 +228,9 @@ const AppSyncResolverSchema = z.object({ * }] * ``` * + * @see {@link AppSyncBatchResolverEvent | `AppSyncBatchResolverEvent`} * @see {@link https://docs.aws.amazon.com/appsync/latest/devguide/tutorial-lambda-resolvers.html#advanced-use-case-batching} */ - const AppSyncBatchResolverSchema = z.array(AppSyncResolverSchema); export { diff --git a/packages/parser/src/schemas/cloudformation-custom-resource.ts b/packages/parser/src/schemas/cloudformation-custom-resource.ts index cb18cad58c..ac88f3c03d 100644 --- a/packages/parser/src/schemas/cloudformation-custom-resource.ts +++ b/packages/parser/src/schemas/cloudformation-custom-resource.ts @@ -1,13 +1,18 @@ import { z } from 'zod'; +import type { + CloudFormationCustomResourceCreateEvent, + CloudFormationCustomResourceDeleteEvent, + CloudFormationCustomResourceUpdateEvent, +} from '../types/schema.js'; const CloudFormationCustomResourceBaseSchema = z.object({ ServiceToken: z.string(), - ResponseURL: z.string().url(), + ResponseURL: z.url(), StackId: z.string(), RequestId: z.string(), LogicalResourceId: z.string(), ResourceType: z.string(), - ResourceProperties: z.record(z.any()), + ResourceProperties: z.record(z.string(), z.any()), }); /** @@ -29,15 +34,13 @@ const CloudFormationCustomResourceBaseSchema = z.object({ * } * } * ``` - * @see {@link types.CloudFormationCustomResourceCreateEvent | CloudFormationCustomResourceCreateEvent} + * @see {@link CloudFormationCustomResourceCreateEvent | `CloudFormationCustomResourceCreateEvent`} * @see {@link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/crpg-ref-requesttypes-create.html} */ -const CloudFormationCustomResourceCreateSchema = - CloudFormationCustomResourceBaseSchema.merge( - z.object({ - RequestType: z.literal('Create'), - }) - ); +const CloudFormationCustomResourceCreateSchema = z.object({ + ...CloudFormationCustomResourceBaseSchema.shape, + RequestType: z.literal('Create'), +}); /** * Zod schema for CloudFormation Custom Resource event with RequestType = 'Delete' @@ -58,15 +61,13 @@ const CloudFormationCustomResourceCreateSchema = * } * } * ``` - * @see {@link types.CloudFormationCustomResourceDeleteEvent | CloudFormationCustomResourceDeleteEvent} + * @see {@link CloudFormationCustomResourceDeleteEvent | `CloudFormationCustomResourceDeleteEvent`} * @see {@link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/crpg-ref-requesttypes-delete.html} */ -const CloudFormationCustomResourceDeleteSchema = - CloudFormationCustomResourceBaseSchema.merge( - z.object({ - RequestType: z.literal('Delete'), - }) - ); +const CloudFormationCustomResourceDeleteSchema = z.object({ + ...CloudFormationCustomResourceBaseSchema.shape, + RequestType: z.literal('Delete'), +}); /** * Zod schema for CloudFormation Custom Resource event with RequestType = 'Update' @@ -91,16 +92,14 @@ const CloudFormationCustomResourceDeleteSchema = * } * } * ``` - * @see {@link types.CloudFormationCustomResourceUpdateEvent | CloudFormationCustomResourceUpdateEvent} + * @see {@link CloudFormationCustomResourceUpdateEvent | `CloudFormationCustomResourceUpdateEvent`} * @see {@link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/crpg-ref-requesttypes-update.html} */ -const CloudFormationCustomResourceUpdateSchema = - CloudFormationCustomResourceBaseSchema.merge( - z.object({ - RequestType: z.literal('Update'), - OldResourceProperties: z.record(z.any()), - }) - ); +const CloudFormationCustomResourceUpdateSchema = z.object({ + ...CloudFormationCustomResourceBaseSchema.shape, + RequestType: z.literal('Update'), + OldResourceProperties: z.record(z.string(), z.any()), +}); export { CloudFormationCustomResourceCreateSchema, diff --git a/packages/parser/src/schemas/cloudwatch.ts b/packages/parser/src/schemas/cloudwatch.ts index 0ab849f448..b599160bf5 100644 --- a/packages/parser/src/schemas/cloudwatch.ts +++ b/packages/parser/src/schemas/cloudwatch.ts @@ -1,6 +1,6 @@ import { gunzipSync } from 'node:zlib'; import { z } from 'zod'; -import { DecompressError } from '../errors.js'; +import type { CloudWatchLogsEvent } from '../types/schema.js'; const CloudWatchLogEventSchema = z.object({ id: z.string(), @@ -14,23 +14,9 @@ const CloudWatchLogsDecodeSchema = z.object({ logGroup: z.string(), logStream: z.string(), subscriptionFilters: z.array(z.string()), - logEvents: z.array(CloudWatchLogEventSchema).min(1), + logEvents: z.array(CloudWatchLogEventSchema).nonempty(), }); -const decompressRecordToJSON = ( - data: string -): z.infer => { - try { - const uncompressed = gunzipSync(Buffer.from(data, 'base64')).toString( - 'utf8' - ); - - return CloudWatchLogsDecodeSchema.parse(JSON.parse(uncompressed)); - } catch { - throw new DecompressError('Failed to decompress CloudWatch log data'); - } -}; - /** * Zod schema for CloudWatch Logs. * @@ -74,18 +60,33 @@ const decompressRecordToJSON = ( * } * ``` * - * @see {@link types.CloudWatchLogsEvent | CloudWatchLogsEvent} + * @see {@link CloudWatchLogsEvent | `CloudWatchLogsEvent`} * @see {@link https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/SubscriptionFilters.html#LambdaFunctionExample} */ const CloudWatchLogsSchema = z.object({ awslogs: z.object({ - data: z.string().transform((data) => decompressRecordToJSON(data)), + data: z.base64().transform((data, ctx) => { + try { + const uncompressed = gunzipSync(Buffer.from(data, 'base64')).toString( + 'utf8' + ); + + return CloudWatchLogsDecodeSchema.parse(JSON.parse(uncompressed)); + } catch { + ctx.addIssue({ + code: 'custom', + message: 'Failed to decompress CloudWatch log data', + fatal: true, + }); + + return z.NEVER; + } + }), }), }); export { CloudWatchLogsSchema, CloudWatchLogsDecodeSchema, - decompressRecordToJSON, CloudWatchLogEventSchema, }; diff --git a/packages/parser/src/schemas/cognito.ts b/packages/parser/src/schemas/cognito.ts index d38b582722..a19ab0db52 100644 --- a/packages/parser/src/schemas/cognito.ts +++ b/packages/parser/src/schemas/cognito.ts @@ -568,8 +568,8 @@ const CreateAuthChallengeTriggerSchema = CognitoTriggerBaseSchema.extend({ userNotFound: z.boolean().optional(), }), response: z.object({ - publicChallengeParameters: z.record(z.string()).nullish(), - privateChallengeParameters: z.record(z.string()).nullish(), + publicChallengeParameters: z.record(z.string(), z.string()).nullish(), + privateChallengeParameters: z.record(z.string(), z.string()).nullish(), challengeMetadata: z.string().nullish(), }), }); diff --git a/packages/parser/src/schemas/dynamodb.ts b/packages/parser/src/schemas/dynamodb.ts index f61ec57734..445bb749a5 100644 --- a/packages/parser/src/schemas/dynamodb.ts +++ b/packages/parser/src/schemas/dynamodb.ts @@ -2,6 +2,7 @@ import { unmarshallDynamoDB } from '@aws-lambda-powertools/commons/utils/unmarsh import { z } from 'zod'; import type { KinesisEnvelope } from '../envelopes/kinesis.js'; import type { DynamoDBMarshalled } from '../helpers/dynamodb.js'; +import type { DynamoDBStreamEvent } from '../types/schema.js'; const DynamoDBStreamChangeRecordBase = z.object({ ApproximateCreationDateTime: z.number().optional(), @@ -251,15 +252,15 @@ const DynamoDBStreamToKinesisRecord = DynamoDBStreamRecord.extend({ * } * ``` * - * @see {@link types.DynamoDBStreamEvent | DynamoDBStreamEvent} + * @see {@link DynamoDBStreamEvent | DynamoDBStreamEvent} * @see {@link https://docs.aws.amazon.com/lambda/latest/dg/with-ddb.html} */ const DynamoDBStreamSchema = z.object({ - Records: z.array(DynamoDBStreamRecord).min(1), + Records: z.array(DynamoDBStreamRecord).nonempty(), window: z .object({ - start: z.string().datetime(), - end: z.string().datetime(), + start: z.iso.datetime(), + end: z.iso.datetime(), }) .optional(), state: z.record(z.string(), z.string()).optional(), diff --git a/packages/parser/src/schemas/eventbridge.ts b/packages/parser/src/schemas/eventbridge.ts index 82b3a48674..fae595dda9 100644 --- a/packages/parser/src/schemas/eventbridge.ts +++ b/packages/parser/src/schemas/eventbridge.ts @@ -1,4 +1,5 @@ import { z } from 'zod'; +import type { EventBridgeEvent } from '../types/schema.js'; /** * Zod schema for EventBridge event @@ -24,7 +25,7 @@ import { z } from 'zod'; * } * ``` * - * @see {@link types.EventBridgeEvent | EventBridgeEvent} + * @see {@link EventBridgeEvent | `EventBridgeEvent`} * @see {@link https://docs.aws.amazon.com/eventbridge/latest/userguide/eventbridge-and-events.html} */ const EventBridgeSchema = z.object({ @@ -32,7 +33,7 @@ const EventBridgeSchema = z.object({ id: z.string(), source: z.string(), account: z.string(), - time: z.string().datetime(), + time: z.iso.datetime(), region: z.string(), resources: z.array(z.string()), 'detail-type': z.string(), diff --git a/packages/parser/src/schemas/kafka.ts b/packages/parser/src/schemas/kafka.ts index eb23e78e3d..3747755a2d 100644 --- a/packages/parser/src/schemas/kafka.ts +++ b/packages/parser/src/schemas/kafka.ts @@ -1,4 +1,5 @@ import { z } from 'zod'; +import type { KafkaMskEvent, KafkaSelfManagedEvent } from '../types/schema.js'; /** * Zod schema for a Kafka record from an Kafka event. @@ -33,7 +34,7 @@ const KafkaBaseEventSchema = z.object({ .string() .transform((bootstrapServers) => bootstrapServers.split(',')) .nullish(), - records: z.record(z.string(), z.array(KafkaRecordSchema).min(1)), + records: z.record(z.string(), z.array(KafkaRecordSchema).nonempty()), }); /** Zod schema for Kafka event from Self Managed Kafka @@ -76,7 +77,7 @@ const KafkaBaseEventSchema = z.object({ * } * ``` * - * @see {@link types.KafkaSelfManagedEvent | KafkaSelfManagedEvent} + * @see {@link KafkaSelfManagedEvent | `KafkaSelfManagedEvent`} * @see {@link https://docs.aws.amazon.com/lambda/latest/dg/with-kafka.html} */ const KafkaSelfManagedEventSchema = KafkaBaseEventSchema.extend({ @@ -125,7 +126,7 @@ const KafkaSelfManagedEventSchema = KafkaBaseEventSchema.extend({ * } * ``` * - * @see {@link types.KafkaMskEvent | KafkaMskEvent} + * @see {@link KafkaMskEvent | `KafkaMskEvent`} * @see {@link https://docs.aws.amazon.com/lambda/latest/dg/with-msk.html} */ const KafkaMskEventSchema = KafkaBaseEventSchema.extend({ diff --git a/packages/parser/src/schemas/kinesis-firehose.ts b/packages/parser/src/schemas/kinesis-firehose.ts index dddb628e01..2b63ef2e01 100644 --- a/packages/parser/src/schemas/kinesis-firehose.ts +++ b/packages/parser/src/schemas/kinesis-firehose.ts @@ -1,4 +1,8 @@ import { z } from 'zod'; +import type { + KinesisFireHoseEvent, + KinesisFireHoseSqsEvent, +} from '../types/schema.js'; import { SqsRecordSchema } from './sqs.js'; const KinesisRecordMetadata = z.object({ @@ -27,7 +31,7 @@ const KinesisFireHoseBaseSchema = z.object({ */ const KinesisFirehoseRecordSchema = KinesisFireHoseRecordBase.extend({ data: z - .string() + .base64() .transform((data) => Buffer.from(data, 'base64').toString('utf8')), }); @@ -42,7 +46,7 @@ const KinesisFirehoseSqsRecordSchema = KinesisFireHoseRecordBase.extend({ ); } catch { ctx.addIssue({ - code: z.ZodIssueCode.custom, + code: 'custom', message: 'Failed to parse SQS record', fatal: true, }); @@ -91,11 +95,11 @@ const KinesisFirehoseSqsRecordSchema = KinesisFireHoseRecordBase.extend({ * } * ``` * - * @see {@link types.KinesisFireHoseEvent | KinesisFireHoseEvent} + * @see {@link KinesisFireHoseEvent | `KinesisFireHoseEvent`} * @see {@link https://docs.aws.amazon.com/lambda/latest/dg/services-kinesisfirehose.html} */ const KinesisFirehoseSchema = KinesisFireHoseBaseSchema.extend({ - records: z.array(KinesisFirehoseRecordSchema).min(1), + records: z.array(KinesisFirehoseRecordSchema).nonempty(), }); /** @@ -117,10 +121,10 @@ const KinesisFirehoseSchema = KinesisFireHoseBaseSchema.extend({ * } * ``` * - * @see {@link types.KinesisFireHoseSqsEvent | KinesisFireHoseSqsEvent} + * @see {@link KinesisFireHoseSqsEvent | `KinesisFireHoseSqsEvent`} */ const KinesisFirehoseSqsSchema = KinesisFireHoseBaseSchema.extend({ - records: z.array(KinesisFirehoseSqsRecordSchema).min(1), + records: z.array(KinesisFirehoseSqsRecordSchema).nonempty(), }); export { diff --git a/packages/parser/src/schemas/kinesis.ts b/packages/parser/src/schemas/kinesis.ts index e0ea622359..882812e3ff 100644 --- a/packages/parser/src/schemas/kinesis.ts +++ b/packages/parser/src/schemas/kinesis.ts @@ -1,6 +1,7 @@ import { gunzipSync } from 'node:zlib'; import { fromBase64 } from '@aws-lambda-powertools/commons/utils/base64'; import { z } from 'zod'; +import type { KinesisDataStreamEvent } from '../types/schema.js'; import { DynamoDBStreamToKinesisRecord } from './dynamodb.js'; const decoder = new TextDecoder(); @@ -95,7 +96,7 @@ const KinesisDynamoDBStreamSchema = z.object({ * "isWindowTerminatedEarly": false * } *``` - * @see {@link types.KinesisDataStreamEvent | KinesisDataStreamEvent} + * @see {@link KinesisDataStreamEvent | `KinesisDataStreamEvent`} * @see {@link https://docs.aws.amazon.com/lambda/latest/dg/services-kinesis-windows.html#streams-tumbling-processing} * */ @@ -103,8 +104,8 @@ const KinesisDataStreamSchema = z.object({ Records: z.array(KinesisDataStreamRecord).min(1), window: z .object({ - start: z.string().datetime(), - end: z.string().datetime(), + start: z.iso.datetime(), + end: z.iso.datetime(), }) .optional(), state: z.record(z.string(), z.unknown()).optional(), diff --git a/packages/parser/src/schemas/lambda.ts b/packages/parser/src/schemas/lambda.ts index e5fe679329..471e6a09e4 100644 --- a/packages/parser/src/schemas/lambda.ts +++ b/packages/parser/src/schemas/lambda.ts @@ -1,3 +1,4 @@ +import type { LambdaFunctionUrlEvent } from '../types/schema.js'; import { APIGatewayProxyEventV2Schema } from './api-gatewayv2.js'; /** @@ -56,7 +57,7 @@ import { APIGatewayProxyEventV2Schema } from './api-gatewayv2.js'; * } * ``` * - * @see {@link types.LambdaFunctionUrlEvent | LambdaFunctionUrlEvent} + * @see {@link LambdaFunctionUrlEvent | `LambdaFunctionUrlEvent`} * @see {@link https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html#urls-payloads} */ const LambdaFunctionUrlSchema = APIGatewayProxyEventV2Schema.extend({}); diff --git a/packages/parser/src/schemas/s3.ts b/packages/parser/src/schemas/s3.ts index 650712a78a..66fd81ee7a 100644 --- a/packages/parser/src/schemas/s3.ts +++ b/packages/parser/src/schemas/s3.ts @@ -1,5 +1,11 @@ import { z } from 'zod'; import { JSONStringified } from '../helpers/index.js'; +import type { + S3Event, + S3EventNotificationEventBridge, + S3ObjectLambdaEvent, + S3SqsEventNotification, +} from '../types/schema.js'; import { EventBridgeSchema } from './eventbridge.js'; import { SqsRecordSchema } from './sqs.js'; @@ -8,7 +14,7 @@ const S3Identity = z.object({ }); const S3RequestParameters = z.object({ - sourceIPAddress: z.union([z.string().ip(), z.literal('s3.amazonaws.com')]), + sourceIPAddress: z.union([z.ipv4(), z.literal('s3.amazonaws.com')]), }); const S3ResponseElements = z.object({ @@ -45,7 +51,7 @@ const S3RecordSchema = z.object({ eventVersion: z.string(), eventSource: z.literal('aws:s3'), awsRegion: z.string(), - eventTime: z.string().datetime(), + eventTime: z.iso.datetime(), eventName: z.string(), userIdentity: S3Identity, requestParameters: S3RequestParameters, @@ -68,7 +74,7 @@ const S3EventNotificationEventBridgeDetailSchema = z.object({ }), 'request-id': z.string(), requester: z.string(), - 'source-ip-address': z.string().ip().optional(), + 'source-ip-address': z.ipv4().optional(), reason: z.string().optional(), 'deletion-type': z.string().optional(), 'restore-expiry-time': z.string().optional(), @@ -112,7 +118,7 @@ const S3EventNotificationEventBridgeDetailSchema = z.object({ * } * ``` * - * @see {@link types.S3EventNotificationEventBridge | S3EventNotificationEventBridge } + * @see {@link S3EventNotificationEventBridge | `S3EventNotificationEventBridge` } * @see {@link https://docs.aws.amazon.com/AmazonS3/latest/userguide/ev-events.html#ev-events-list} */ const S3EventNotificationEventBridgeSchema = EventBridgeSchema.extend({ @@ -163,7 +169,7 @@ const S3EventNotificationEventBridgeSchema = EventBridgeSchema.extend({ * ] * } * ``` - * @see {@link types.S3Event | S3Event } + * @see {@link S3Event | `S3Event` } * @see {@link https://docs.aws.amazon.com/AmazonS3/latest/userguide/notification-content-structure.html} */ const S3Schema = z.object({ @@ -205,14 +211,14 @@ const S3SqsEventNotificationRecordSchema = SqsRecordSchema.extend({ * } * ``` * - * @see {@link types.S3SqsEventNotification | S3SqsEventNotification } + * @see {@link S3SqsEventNotification | `S3SqsEventNotification` } */ const S3SqsEventNotificationSchema = z.object({ - Records: z.array(S3SqsEventNotificationRecordSchema).min(1), + Records: z.array(S3SqsEventNotificationRecordSchema).nonempty(), }); const S3ObjectContext = z.object({ - inputS3Url: z.string().url(), + inputS3Url: z.string(), outputRoute: z.string(), outputToken: z.string(), }); @@ -291,8 +297,8 @@ const S3ObjectUserIdentity = z.object({ * } * ``` * + * @see {@link S3ObjectLambdaEvent | `S3ObjectLambdaEvent` } * @see {@link https://docs.aws.amazon.com/AmazonS3/latest/userguide/olap-event-context.html} - * @see {@link types.S3ObjectLambdaEvent | S3ObjectLambdaEvent } */ const S3ObjectLambdaEventSchema = z.object({ xAmzRequestId: z.string(), diff --git a/packages/parser/src/schemas/ses.ts b/packages/parser/src/schemas/ses.ts index 8121d70c70..09dc915492 100644 --- a/packages/parser/src/schemas/ses.ts +++ b/packages/parser/src/schemas/ses.ts @@ -1,11 +1,12 @@ import { z } from 'zod'; +import type { SesEvent } from '../types/schema.js'; const SesReceiptVerdict = z.object({ status: z.enum(['PASS', 'FAIL', 'GRAY', 'PROCESSING_FAILED']), }); const SesReceipt = z.object({ - timestamp: z.string().datetime(), + timestamp: z.iso.datetime(), processingTimeMillis: z.number().int().positive(), recipients: z.array(z.string()), spamVerdict: SesReceiptVerdict, @@ -22,7 +23,7 @@ const SesReceipt = z.object({ }); const SesMail = z.object({ - timestamp: z.string().datetime(), + timestamp: z.iso.datetime(), source: z.string(), messageId: z.string(), destination: z.array(z.string()), @@ -169,11 +170,11 @@ const SesRecordSchema = z.object({ * } * ``` * - * @see {@link types.SesEvent | SesEvent} + * @see {@link SesEvent | SesEvent} * @see {@link https://docs.aws.amazon.com/ses/latest/dg/receiving-email-notifications-examples.html} */ const SesSchema = z.object({ - Records: z.array(SesRecordSchema).min(1), + Records: z.array(SesRecordSchema).nonempty(), }); export { SesSchema, SesRecordSchema }; diff --git a/packages/parser/src/schemas/sns.ts b/packages/parser/src/schemas/sns.ts index a76711e014..b62cb950a1 100644 --- a/packages/parser/src/schemas/sns.ts +++ b/packages/parser/src/schemas/sns.ts @@ -1,4 +1,5 @@ import { z } from 'zod'; +import type { SnsEvent, SnsSqsNotification } from '../types/schema.js'; const SnsMsgAttribute = z.object({ Type: z.string(), @@ -11,17 +12,17 @@ const SnsMsgAttribute = z.object({ const SnsNotificationSchema = z.object({ Subject: z.string().nullish(), TopicArn: z.string(), - UnsubscribeUrl: z.string().url(), - UnsubscribeURL: z.string().url().optional(), - SigningCertUrl: z.string().url().optional(), - SigningCertURL: z.string().url().optional(), + UnsubscribeUrl: z.url(), + UnsubscribeURL: z.url().optional(), + SigningCertUrl: z.url().optional(), + SigningCertURL: z.url().optional(), Type: z.literal('Notification'), MessageAttributes: z.record(z.string(), SnsMsgAttribute).optional(), Message: z.string(), MessageId: z.string(), Signature: z.string().optional(), SignatureVersion: z.string().optional(), - Timestamp: z.string().datetime(), + Timestamp: z.iso.datetime(), }); /** @@ -51,11 +52,11 @@ const SnsNotificationSchema = z.object({ * } * ``` * - * @see {@link types.SnsSqsNotification | SnsSqsNotification} + * @see {@link SnsSqsNotification | `SnsSqsNotification`} */ const SnsSqsNotificationSchema = SnsNotificationSchema.extend({ UnsubscribeURL: z.string().optional(), - SigningCertURL: z.string().url().optional(), + SigningCertURL: z.url().optional(), }).omit({ UnsubscribeUrl: true, SigningCertUrl: true, @@ -109,11 +110,11 @@ const SnsRecordSchema = z.object({ * } * ``` * - * @see {@link types.SnsEvent | SnsEvent} + * @see {@link SnsEvent | `SnsEvent`} * @see {@link https://docs.aws.amazon.com/lambda/latest/dg/with-sns.html#sns-sample-event} */ const SnsSchema = z.object({ - Records: z.array(SnsRecordSchema).min(1), + Records: z.array(SnsRecordSchema).nonempty(), }); export { diff --git a/packages/parser/src/schemas/sqs.ts b/packages/parser/src/schemas/sqs.ts index 83fde66e6c..0947fea96a 100644 --- a/packages/parser/src/schemas/sqs.ts +++ b/packages/parser/src/schemas/sqs.ts @@ -1,4 +1,5 @@ import { z } from 'zod'; +import type { SqsEvent } from '../types/schema.js'; const SqsMsgAttributeDataTypeSchema = z.union([ z.literal('String'), @@ -96,11 +97,11 @@ const SqsRecordSchema = z.object({ * } * ``` * - * @see {@link types.SqsEvent | SqsEvent} + * @see {@link SqsEvent | `SqsEvent`} * @see {@link https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html#example-standard-queue-message-event} */ const SqsSchema = z.object({ - Records: z.array(SqsRecordSchema).min(1), + Records: z.array(SqsRecordSchema).nonempty(), }); export { diff --git a/packages/parser/src/schemas/transfer-family.ts b/packages/parser/src/schemas/transfer-family.ts index 145a45d43a..feb272f01a 100644 --- a/packages/parser/src/schemas/transfer-family.ts +++ b/packages/parser/src/schemas/transfer-family.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; +import type { TransferFamilyEvent } from '../types/schema.js'; /** - * * Zod schema for AWS Transfer Family events. * * @example @@ -15,15 +15,15 @@ import { z } from 'zod'; * } * ``` * - * TransferFamilySchema validates events coming from AWS Transfer Family. - * + * @see {@link TransferFamilyEvent | `TransferFamilyEvent`} + * @see {@link https://docs.aws.amazon.com/transfer/latest/userguide/custom-lambda-idp.html} */ const TransferFamilySchema = z.object({ username: z.string(), password: z.string(), protocol: z.string(), serverId: z.string(), - sourceIp: z.string().ip(), + sourceIp: z.ipv4(), }); export { TransferFamilySchema }; diff --git a/packages/parser/src/schemas/vpc-lattice.ts b/packages/parser/src/schemas/vpc-lattice.ts index 569caf2c93..c9bf434ad1 100644 --- a/packages/parser/src/schemas/vpc-lattice.ts +++ b/packages/parser/src/schemas/vpc-lattice.ts @@ -1,4 +1,5 @@ import { z } from 'zod'; +import type { VpcLatticeEvent } from '../types/schema.js'; /** * Zod schema for VPC Lattice event @@ -22,7 +23,7 @@ import { z } from 'zod'; *} * ``` * - * @see {@link types.VpcLatticeEvent | VpcLatticeEvent} + * @see {@link VpcLatticeEvent | `VpcLatticeEvent`} * @see {@link https://docs.aws.amazon.com/vpc-lattice/latest/ug/lambda-functions.html#receive-event-from-service} */ const VpcLatticeSchema = z.object({ diff --git a/packages/parser/src/schemas/vpc-latticev2.ts b/packages/parser/src/schemas/vpc-latticev2.ts index ef80f0a8e3..19c4d80c40 100644 --- a/packages/parser/src/schemas/vpc-latticev2.ts +++ b/packages/parser/src/schemas/vpc-latticev2.ts @@ -1,4 +1,5 @@ import { z } from 'zod'; +import type { VpcLatticeEventV2 } from '../types/schema.js'; const VpcLatticeV2RequestContextIdentity = z.object({ sourceVpcArn: z.string().optional(), @@ -58,7 +59,7 @@ const VpcLatticeV2RequestContext = z.object({ * } * } * ``` - * @see {@link types.VpcLatticeEventV2 | VpcLatticeEventV2} + * @see {@link VpcLatticeEventV2 | `VpcLatticeEventV2`} * @see {@link https://docs.aws.amazon.com/vpc-lattice/latest/ug/lambda-functions.html#receive-event-from-service} */ const VpcLatticeV2Schema = z.object({ diff --git a/packages/parser/src/types/envelope.ts b/packages/parser/src/types/envelope.ts index 4659ccad76..6d5669fb5e 100644 --- a/packages/parser/src/types/envelope.ts +++ b/packages/parser/src/types/envelope.ts @@ -1,34 +1,45 @@ -import type { ZodSchema, z } from 'zod'; +import type { ZodType } from 'zod'; import type { envelopeDiscriminator } from '../envelopes/envelope.js'; import type { ParsedResult } from './parser.js'; -type DynamoDBStreamEnvelopeResponse = { - NewImage?: z.infer; - OldImage?: z.infer; +type DynamoDBStreamEnvelopeResponse = { + NewImage?: T; + OldImage?: T; }; interface ArrayEnvelope { [envelopeDiscriminator]: 'array'; - parse(data: unknown, schema: T): z.infer[]; - safeParse( + parse(data: unknown, schema: ZodType): T[]; + safeParse(data: unknown, schema: ZodType): ParsedResult; +} + +interface DynamoDBArrayEnvelope { + [envelopeDiscriminator]: 'array'; + parse( data: unknown, - schema: T - ): ParsedResult[]>; + schema: ZodType + ): DynamoDBStreamEnvelopeResponse[]; + safeParse( + data: unknown, + schema: ZodType + ): ParsedResult[]>; } interface ObjectEnvelope { [envelopeDiscriminator]: 'object'; - parse(data: unknown, schema: T): z.infer; - safeParse( - data: unknown, - schema: T - ): ParsedResult>; + parse(data: unknown, schema: ZodType): T; + safeParse(data: unknown, schema: ZodType): ParsedResult; } -type Envelope = ArrayEnvelope | ObjectEnvelope | undefined; +type Envelope = + | ArrayEnvelope + | DynamoDBArrayEnvelope + | ObjectEnvelope + | undefined; export type { ArrayEnvelope, + DynamoDBArrayEnvelope, DynamoDBStreamEnvelopeResponse, Envelope, ObjectEnvelope, diff --git a/packages/parser/src/types/index.ts b/packages/parser/src/types/index.ts index 213e89691e..d325dadc81 100644 --- a/packages/parser/src/types/index.ts +++ b/packages/parser/src/types/index.ts @@ -5,16 +5,22 @@ export type { ParseFunction, ParserOptions, } from '../types/parser.js'; -export type { Envelope } from './envelope.js'; +export type { + ArrayEnvelope, + DynamoDBArrayEnvelope, + DynamoDBStreamEnvelopeResponse, + Envelope, + ObjectEnvelope, +} from './envelope.js'; export type { ALBEvent, APIGatewayEventRequestContext, APIGatewayProxyEvent, APIGatewayProxyEventV2, + APIGatewayProxyWebsocketEvent, APIGatewayRequestAuthorizerEvent, - APIGatewayRequestAuthorizerV2, - APIGatewayRequestContextV2, + APIGatewayRequestAuthorizerEventV2, APIGatewayTokenAuthorizerEvent, AppSyncBatchResolverEvent, AppSyncEventsPublishEvent, diff --git a/packages/parser/src/types/parser.ts b/packages/parser/src/types/parser.ts index 91b423e8cc..56f4f1d109 100644 --- a/packages/parser/src/types/parser.ts +++ b/packages/parser/src/types/parser.ts @@ -1,11 +1,16 @@ -import type { ZodError, ZodSchema, z } from 'zod'; -import type { ArrayEnvelope, Envelope } from './envelope.js'; +import type { StandardSchemaV1 } from '@standard-schema/spec'; +import type { + ArrayEnvelope, + DynamoDBArrayEnvelope, + DynamoDBStreamEnvelopeResponse, + Envelope, +} from './envelope.js'; /** * Options for the parser used in middy middleware and decorator */ type ParserOptions< - TSchema extends ZodSchema, + TSchema extends StandardSchemaV1, TEnvelope extends Envelope, TSafeParse extends boolean, > = { @@ -27,7 +32,7 @@ type ParsedResultSuccess = { */ type ParsedResultError = { success: false; - error: ZodError | Error; + error: Error; originalEvent?: Input; }; @@ -42,28 +47,35 @@ type ParsedResult = * The inferred result of the schema, can be either an array or a single object depending on the envelope */ type ZodInferredResult< - TSchema extends ZodSchema, + TSchema extends StandardSchemaV1, TEnvelope extends Envelope, > = undefined extends TEnvelope - ? z.infer - : TEnvelope extends ArrayEnvelope - ? z.infer[] - : z.infer; + ? InferOutput + : TEnvelope extends DynamoDBArrayEnvelope + ? DynamoDBStreamEnvelopeResponse>[] + : TEnvelope extends ArrayEnvelope + ? InferOutput[] + : InferOutput; type ZodInferredSafeParseResult< - TSchema extends ZodSchema, + TSchema extends StandardSchemaV1, TEnvelope extends Envelope, > = undefined extends TEnvelope - ? ParsedResult, z.infer> - : TEnvelope extends ArrayEnvelope - ? ParsedResult[]> - : ParsedResult>; + ? ParsedResult, InferOutput> + : TEnvelope extends DynamoDBArrayEnvelope + ? ParsedResult< + unknown, + DynamoDBStreamEnvelopeResponse>[] + > + : TEnvelope extends ArrayEnvelope + ? ParsedResult[]> + : ParsedResult>; /** * The output of the parser function, can be either schema inferred type or a ParsedResult */ type ParserOutput< - TSchema extends ZodSchema, + TSchema extends StandardSchemaV1, TEnvelope extends Envelope, TSafeParse = false, > = TSafeParse extends true @@ -75,55 +87,54 @@ type ParserOutput< * we use function overloads to provide the correct return type based on the provided envelope **/ type ParseFunction = { - // No envelope cases - ( - data: z.infer, + // No envelope, no safeParse + ( + data: unknown, envelope: undefined, schema: T, safeParse?: false - ): z.infer; + ): InferOutput; - ( - data: z.infer, + // No envelope, with safeParse + ( + data: unknown, envelope: undefined, schema: T, safeParse: true - ): ParsedResult>; + ): ParsedResult>; - // Generic envelope case - ( + // With envelope, no safeParse + ( data: unknown, envelope: E, schema: T, safeParse?: false - ): E extends ArrayEnvelope ? z.infer[] : z.infer; + ): E extends DynamoDBArrayEnvelope + ? DynamoDBStreamEnvelopeResponse>[] + : E extends ArrayEnvelope + ? InferOutput[] + : InferOutput; - ( + // With envelope, with safeParse + ( data: unknown, envelope: E, schema: T, safeParse: true - ): E extends ArrayEnvelope - ? ParsedResult[]> - : ParsedResult>; - - // Generic envelope case with safeParse - ( - data: unknown, - envelope: E, - schema: T, - safeParse?: S - ): S extends true - ? E extends ArrayEnvelope - ? ParsedResult[]> - : ParsedResult> + ): E extends DynamoDBArrayEnvelope + ? ParsedResult>[]> : E extends ArrayEnvelope - ? z.infer[] - : z.infer; + ? ParsedResult[]> + : ParsedResult>; }; +type InferOutput = NonNullable< + Schema['~standard']['types'] +>['output']; + export type { ParseFunction, + InferOutput, ParsedResult, ParsedResultError, ParsedResultSuccess, diff --git a/packages/parser/src/types/schema.ts b/packages/parser/src/types/schema.ts index eec4e44885..f40ee9a269 100644 --- a/packages/parser/src/types/schema.ts +++ b/packages/parser/src/types/schema.ts @@ -6,8 +6,7 @@ import type { APIGatewayProxyEventV2Schema, APIGatewayProxyWebsocketEventSchema, APIGatewayRequestAuthorizerEventSchema, - APIGatewayRequestAuthorizerV2Schema, - APIGatewayRequestContextV2Schema, + APIGatewayRequestAuthorizerEventV2Schema, APIGatewayTokenAuthorizerEventSchema, AppSyncBatchResolverSchema, AppSyncEventsPublishSchema, @@ -72,12 +71,8 @@ type APIGatewayProxyWebsocketEvent = z.infer< typeof APIGatewayProxyWebsocketEventSchema >; -type APIGatewayRequestAuthorizerV2 = z.infer< - typeof APIGatewayRequestAuthorizerV2Schema ->; - -type APIGatewayRequestContextV2 = z.infer< - typeof APIGatewayRequestContextV2Schema +type APIGatewayRequestAuthorizerEventV2 = z.infer< + typeof APIGatewayRequestAuthorizerEventV2Schema >; type AppSyncResolverEvent = z.infer; @@ -175,8 +170,7 @@ export type { APIGatewayProxyEventV2, APIGatewayProxyWebsocketEvent, APIGatewayRequestAuthorizerEvent, - APIGatewayRequestAuthorizerV2, - APIGatewayRequestContextV2, + APIGatewayRequestAuthorizerEventV2, APIGatewayTokenAuthorizerEvent, AppSyncBatchResolverEvent, AppSyncResolverEvent, diff --git a/packages/parser/tests/types/envelopes.test.ts b/packages/parser/tests/types/envelopes.test-d.ts similarity index 78% rename from packages/parser/tests/types/envelopes.test.ts rename to packages/parser/tests/types/envelopes.test-d.ts index 729b78b404..00870efe23 100644 --- a/packages/parser/tests/types/envelopes.test.ts +++ b/packages/parser/tests/types/envelopes.test-d.ts @@ -1,9 +1,9 @@ -import { describe, expect, expectTypeOf, it } from 'vitest'; +import { describe, expectTypeOf, it } from 'vitest'; import { z } from 'zod'; import { ApiGatewayEnvelope } from '../../src/envelopes/api-gateway.js'; import { ApiGatewayV2Envelope } from '../../src/envelopes/api-gatewayv2.js'; import { CloudWatchEnvelope } from '../../src/envelopes/cloudwatch.js'; -import { DynamoDBStreamEnvelope } from '../../src/envelopes/dynamodb.js'; +import type { DynamoDBStreamEnvelope } from '../../src/envelopes/dynamodb.js'; import { EventBridgeEnvelope } from '../../src/envelopes/eventbridge.js'; import { KafkaEnvelope } from '../../src/envelopes/kafka.js'; import { KinesisEnvelope } from '../../src/envelopes/kinesis.js'; @@ -14,6 +14,7 @@ import { SnsSqsEnvelope } from '../../src/envelopes/sns-sqs.js'; import { SqsEnvelope } from '../../src/envelopes/sqs.js'; import { VpcLatticeEnvelope } from '../../src/envelopes/vpc-lattice.js'; import { VpcLatticeV2Envelope } from '../../src/envelopes/vpc-latticev2.js'; +import type { DynamoDBStreamEnvelopeResponse } from '../../src/types/envelope.js'; import type { ParserOutput } from '../../src/types/parser.js'; describe('Types ', () => { @@ -37,14 +38,11 @@ describe('Types ', () => { const result = { name: 'John', age: 30 } satisfies Result; // Assess - expect(Array.isArray(result)).toBe(false); - expect(result).toEqual({ name: 'John', age: 30 }); expectTypeOf(result).toEqualTypeOf>(); }); it.each([ { envelope: CloudWatchEnvelope, name: 'CloudWatch' }, - { envelope: DynamoDBStreamEnvelope, name: 'DynamoDBStream' }, { envelope: KafkaEnvelope, name: 'Kafka' }, { envelope: KinesisFirehoseEnvelope, name: 'KinesisFirehose' }, { envelope: KinesisEnvelope, name: 'Kinesis' }, @@ -59,8 +57,29 @@ describe('Types ', () => { const result = [{ name: 'John', age: 30 }] satisfies Result; // Assess - expect(Array.isArray(result)).toBe(true); - expect(result).toEqual([{ name: 'John', age: 30 }]); expectTypeOf(result).toEqualTypeOf[]>(); }); + + it('infers DynamoDB stream envelope response type', () => { + // Prepare + type Result = ParserOutput< + typeof userSchema, + typeof DynamoDBStreamEnvelope + >; + + // Act + const result: Result = [ + { + NewImage: { name: 'John', age: 30 }, + }, + { + OldImage: { name: 'Jane', age: 25 }, + }, + ]; + + // Assess + expectTypeOf(result).toEqualTypeOf< + DynamoDBStreamEnvelopeResponse>[] + >(); + }); }); diff --git a/packages/parser/tests/types/parser.test-d.ts b/packages/parser/tests/types/parser.test-d.ts new file mode 100644 index 0000000000..6ac8f86cf5 --- /dev/null +++ b/packages/parser/tests/types/parser.test-d.ts @@ -0,0 +1,80 @@ +import middy from '@middy/core'; +import { describe, expectTypeOf, it } from 'vitest'; +import { z } from 'zod'; +import { EventBridgeEnvelope } from '../../src/envelopes/eventbridge.js'; +import { SqsEnvelope } from '../../src/envelopes/sqs.js'; +import { JSONStringified } from '../../src/helpers/index.js'; +import { parser } from '../../src/middleware/index.js'; +import { parse } from '../../src/parser.js'; + +describe('Parser types', () => { + const userSchema = z.object({ + name: z.string(), + age: z.number(), + }); + type User = z.infer; + + it('infers return type for schema and safeParse', () => { + // Act + const result = parse({}, undefined, userSchema, true); + + // Assess + if (result.success) { + expectTypeOf(result.data).toEqualTypeOf(); + } else { + expectTypeOf(result.error).toEqualTypeOf(); + expectTypeOf(result.originalEvent).toEqualTypeOf(); + } + }); + + it('infers return type for schema', () => { + // Act + const result = parse({}, undefined, userSchema); + + // Assess + expectTypeOf(result).toEqualTypeOf(); + }); + + it('infers return type for schema and object envelope', () => { + // Act + const result = parse({}, EventBridgeEnvelope, userSchema); + + // Assess + expectTypeOf(result).toEqualTypeOf(); + }); + + it('infers return type for schema and array envelope', () => { + // Act + const result = parse({}, SqsEnvelope, JSONStringified(userSchema)); + + // Assess + expectTypeOf(result).toEqualTypeOf(); + }); + + it('infers return type for schema, object envelope and safeParse $case', () => { + // Act + const result = parse({}, EventBridgeEnvelope, userSchema, true); + + // Assess + expectTypeOf(result.success).toEqualTypeOf(); + if (result.success) { + expectTypeOf(result.data).toEqualTypeOf(); + } else { + expectTypeOf(result.error).toEqualTypeOf(); + expectTypeOf(result.originalEvent).toEqualTypeOf(); + } + }); + + it('infers the return type when using parse middleware', () => { + middy() + .use( + parser({ + schema: JSONStringified(userSchema), + envelope: SqsEnvelope, + }) + ) + .handler(async (event) => { + expectTypeOf(event).toEqualTypeOf(); + }); + }); +}); diff --git a/packages/parser/tests/types/parser.test.ts b/packages/parser/tests/types/parser.test.ts deleted file mode 100644 index fb49248bcc..0000000000 --- a/packages/parser/tests/types/parser.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { describe } from 'node:test'; -import { expect, expectTypeOf, it } from 'vitest'; -import { z } from 'zod'; -import { EventBridgeEnvelope } from '../../src/envelopes/eventbridge.js'; -import { SqsEnvelope } from '../../src/envelopes/sqs.js'; -import { parse } from '../../src/parser.js'; -import type { EventBridgeEvent, SqsEvent } from '../../src/types/schema.js'; -import { getTestEvent } from '../unit/helpers/utils.js'; - -describe('Parser types', () => { - const userSchema = z.object({ - name: z.string(), - age: z.number(), - }); - type User = z.infer; - const input = { name: 'John', age: 30 }; - - const eventBridgeBaseEvent = getTestEvent({ - eventsPath: 'eventbridge', - filename: 'base', - }); - - const sqsBaseEvent = getTestEvent({ - eventsPath: 'sqs', - filename: 'base', - }); - it('infers return type for schema and safeParse', () => { - // Act - const result = parse(input, undefined, userSchema, true); - - // Assert - if (result.success) { - expectTypeOf(result.data).toEqualTypeOf(); - } else { - expectTypeOf(result.originalEvent).toEqualTypeOf(); - } - }); - - it('infers return type for schema', () => { - // Act - const result = parse(input, undefined, userSchema); - - // Assert - expectTypeOf(result).toEqualTypeOf(); - }); - - it('infers return type for schema and envelope', () => { - // Prepare - const event = structuredClone(eventBridgeBaseEvent); - event.detail = input; - - // Act - const result = parse(event, EventBridgeEnvelope, userSchema); - - // Assert - expectTypeOf(result).toEqualTypeOf(); - }); - - it('infert return type for schema, object envelope and safeParse', () => { - // Prepare - const event = structuredClone(eventBridgeBaseEvent); - event.detail = input; - - // Act - const result = parse(event, EventBridgeEnvelope, userSchema, true); - - // Assert - if (result.success) { - expectTypeOf(result.data).toEqualTypeOf(); - expect(result.data).toEqual(input); - } else { - throw new Error('Parsing failed'); - } - }); - - it('infers return type for schema, array envelope and safeParse', () => { - // Prepare - const event = structuredClone(sqsBaseEvent); - event.Records[0].body = JSON.stringify(input); - - // Act - const result = parse(input, SqsEnvelope, userSchema, true); - - // Assert - if (result.success) { - expectTypeOf(result.data).toEqualTypeOf(); - } - }); -}); diff --git a/packages/parser/tests/unit/envelopes/api-gateway.test.ts b/packages/parser/tests/unit/envelopes/api-gateway.test.ts index 83c6b3b455..8360bbdb5c 100644 --- a/packages/parser/tests/unit/envelopes/api-gateway.test.ts +++ b/packages/parser/tests/unit/envelopes/api-gateway.test.ts @@ -1,7 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { ZodError, z } from 'zod'; +import { z } from 'zod'; import { ApiGatewayEnvelope } from '../../../src/envelopes/index.js'; -import { ParseError } from '../../../src/errors.js'; import { JSONStringified } from '../../../src/helpers/index.js'; import type { APIGatewayProxyEvent } from '../../../src/types/schema.js'; import { getTestEvent, omit } from '../helpers/utils.js'; @@ -31,9 +30,8 @@ describe('Envelope: API Gateway REST', () => { { code: 'invalid_type', expected: 'object', - received: 'null', path: ['body'], - message: 'Expected object, received null', + message: 'Invalid input: expected object, received null', }, ], }), @@ -108,25 +106,27 @@ describe('Envelope: API Gateway REST', () => { const result = ApiGatewayEnvelope.safeParse(event, schema); // Assess - expect(result).be.deep.equal({ + expect(result).toEqual({ success: false, - error: new ParseError('Failed to parse API Gateway body', { - cause: new ZodError([ - { - code: 'invalid_type', - expected: 'string', - received: 'undefined', - path: ['path'], - message: 'Required', - }, - { - code: 'invalid_type', - expected: 'object', - received: 'null', - path: ['body'], - message: 'Expected object, received null', - }, - ]), + error: expect.objectContaining({ + name: 'ParseError', + message: expect.stringContaining('Failed to parse API Gateway body'), + cause: expect.objectContaining({ + issues: [ + { + code: 'invalid_type', + expected: 'string', + path: ['path'], + message: 'Invalid input: expected string, received undefined', + }, + { + code: 'invalid_type', + expected: 'object', + path: ['body'], + message: 'Invalid input: expected object, received null', + }, + ], + }), }), originalEvent: event, }); diff --git a/packages/parser/tests/unit/envelopes/api-gatewayv2.test.ts b/packages/parser/tests/unit/envelopes/api-gatewayv2.test.ts index 4d03b6f2f1..2e3b4ec832 100644 --- a/packages/parser/tests/unit/envelopes/api-gatewayv2.test.ts +++ b/packages/parser/tests/unit/envelopes/api-gatewayv2.test.ts @@ -1,7 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { ZodError, z } from 'zod'; +import { z } from 'zod'; import { ApiGatewayV2Envelope } from '../../../src/envelopes/api-gatewayv2.js'; -import { ParseError } from '../../../src/errors.js'; import { JSONStringified } from '../../../src/helpers/index.js'; import type { APIGatewayProxyEventV2 } from '../../../src/types/schema.js'; import { getTestEvent, omit } from '../helpers/utils.js'; @@ -33,9 +32,8 @@ describe('Envelope: API Gateway HTTP', () => { { code: 'invalid_type', expected: 'object', - received: 'undefined', path: ['body'], - message: 'Required', + message: 'Invalid input: expected object, received undefined', }, ], }), @@ -109,25 +107,29 @@ describe('Envelope: API Gateway HTTP', () => { const result = ApiGatewayV2Envelope.safeParse(event, schema); // Assess - expect(result).be.deep.equal({ + expect(result).toEqual({ success: false, - error: new ParseError('Failed to parse API Gateway HTTP body', { - cause: new ZodError([ - { - code: 'invalid_type', - expected: 'string', - received: 'undefined', - path: ['rawPath'], - message: 'Required', - }, - { - code: 'invalid_type', - expected: 'object', - received: 'undefined', - path: ['body'], - message: 'Required', - }, - ]), + error: expect.objectContaining({ + name: 'ParseError', + message: expect.stringContaining( + 'Failed to parse API Gateway HTTP body' + ), + cause: expect.objectContaining({ + issues: [ + { + expected: 'string', + code: 'invalid_type', + path: ['rawPath'], + message: 'Invalid input: expected string, received undefined', + }, + { + expected: 'object', + code: 'invalid_type', + path: ['body'], + message: 'Invalid input: expected object, received undefined', + }, + ], + }), }), originalEvent: event, }); diff --git a/packages/parser/tests/unit/envelopes/cloudwatch.test.ts b/packages/parser/tests/unit/envelopes/cloudwatch.test.ts index 139666b57d..87e06836f5 100644 --- a/packages/parser/tests/unit/envelopes/cloudwatch.test.ts +++ b/packages/parser/tests/unit/envelopes/cloudwatch.test.ts @@ -1,10 +1,8 @@ import { gunzipSync, gzipSync } from 'node:zlib'; import { describe, expect, it } from 'vitest'; -import { ZodError, z } from 'zod'; -import { ParseError } from '../../../src'; +import { z } from 'zod'; import { CloudWatchEnvelope } from '../../../src/envelopes/cloudwatch.js'; -import { DecompressError } from '../../../src/errors.js'; -import { JSONStringified } from '../../../src/helpers/index'; +import { JSONStringified } from '../../../src/helpers/index.js'; import { getTestEvent } from '../helpers/utils.js'; const decompressRecordToJSON = ( @@ -92,9 +90,8 @@ describe('Envelope: CloudWatch', () => { { code: 'invalid_type', expected: 'object', - received: 'string', path: ['awslogs', 'data', 'logEvents', 0, 'message'], - message: 'Expected object, received string', + message: 'Invalid input: expected object, received string', }, ], }), @@ -159,12 +156,29 @@ describe('Envelope: CloudWatch', () => { const result = CloudWatchEnvelope.safeParse(event, z.object({})); // Assess - expect(result).toStrictEqual({ + expect(result).toEqual({ success: false, - error: new ParseError('Failed to parse CloudWatch Log envelope', { - cause: new DecompressError( - 'Failed to decompress CloudWatch log data' + error: expect.objectContaining({ + name: 'ParseError', + message: expect.stringContaining( + 'Failed to parse CloudWatch Log envelope' ), + cause: expect.objectContaining({ + issues: [ + { + code: 'invalid_format', + format: 'base64', + path: ['awslogs', 'data'], + message: 'Invalid base64-encoded string', + }, + { + code: 'custom', + message: 'Failed to decompress CloudWatch log data', + fatal: true, + path: ['awslogs', 'data'], + }, + ], + }), }), originalEvent: event, }); @@ -203,20 +217,24 @@ describe('Envelope: CloudWatch', () => { ); // Assess - expect(result).be.deep.equal({ + expect(result).toEqual({ success: false, - error: new ParseError( - 'Failed to parse CloudWatch Log message at index 0', - { - cause: new ZodError([ + error: expect.objectContaining({ + name: 'ParseError', + message: expect.stringContaining( + 'Failed to parse CloudWatch Log message at index 0' + ), + cause: expect.objectContaining({ + issues: [ { code: 'custom', - message: 'Invalid JSON', + fatal: true, + message: expect.stringMatching(/^Invalid JSON - /), path: ['awslogs', 'data', 'logEvents', 0, 'message'], }, - ]), - } - ), + ], + }), + }), originalEvent: event, }); }); @@ -234,36 +252,36 @@ describe('Envelope: CloudWatch', () => { ); // Assess - expect(result).be.deep.equal({ + expect(result).toEqual({ success: false, - error: new ParseError( - 'Failed to parse CloudWatch Log messages at indexes 0, 1, 2', - { - cause: new ZodError([ + error: expect.objectContaining({ + name: 'ParseError', + message: expect.stringContaining( + 'Failed to parse CloudWatch Log messages at indexes 0, 1, 2' + ), + cause: expect.objectContaining({ + issues: [ { code: 'invalid_type', expected: 'object', - received: 'string', path: ['awslogs', 'data', 'logEvents', 0, 'message'], - message: 'Expected object, received string', + message: 'Invalid input: expected object, received string', }, { code: 'invalid_type', expected: 'object', - received: 'string', path: ['awslogs', 'data', 'logEvents', 1, 'message'], - message: 'Expected object, received string', + message: 'Invalid input: expected object, received string', }, { code: 'invalid_type', expected: 'object', - received: 'string', path: ['awslogs', 'data', 'logEvents', 2, 'message'], - message: 'Expected object, received string', + message: 'Invalid input: expected object, received string', }, - ]), - } - ), + ], + }), + }), originalEvent: event, }); }); diff --git a/packages/parser/tests/unit/envelopes/dynamodb.test.ts b/packages/parser/tests/unit/envelopes/dynamodb.test.ts index 3eb745bc8b..dc5b920904 100644 --- a/packages/parser/tests/unit/envelopes/dynamodb.test.ts +++ b/packages/parser/tests/unit/envelopes/dynamodb.test.ts @@ -1,8 +1,7 @@ import { marshall } from '@aws-sdk/util-dynamodb'; import { describe, expect, it } from 'vitest'; -import { ZodError, z } from 'zod'; +import { z } from 'zod'; import { DynamoDBStreamEnvelope } from '../../../src/envelopes/dynamodb.js'; -import { ParseError } from '../../../src/errors.js'; import type { DynamoDBStreamEvent } from '../../../src/types/schema.js'; import { getTestEvent } from '../helpers/utils.js'; @@ -43,7 +42,7 @@ describe('Envelope: DynamoDB Stream', () => { { code: 'unrecognized_keys', keys: ['Id'], - message: "Unrecognized key(s) in object: 'Id'", + message: 'Unrecognized key: "Id"', path: ['Records', 0, 'dynamodb', 'NewImage'], }, ], @@ -121,18 +120,23 @@ describe('Envelope: DynamoDB Stream', () => { const result = DynamoDBStreamEnvelope.safeParse(event, schema); // Assess - expect(result).be.deep.equal({ + expect(result).toEqual({ success: false, - error: new ParseError('Failed to parse DynamoDB Stream envelope', { - cause: new ZodError([ - { - code: 'invalid_type', - expected: 'object', - received: 'undefined', - path: ['Records', 0, 'dynamodb'], - message: 'Required', - }, - ]), + error: expect.objectContaining({ + name: 'ParseError', + message: expect.stringContaining( + 'Failed to parse DynamoDB Stream envelope' + ), + cause: expect.objectContaining({ + issues: [ + { + code: 'invalid_type', + expected: 'object', + path: ['Records', 0, 'dynamodb'], + message: 'Invalid input: expected object, received undefined', + }, + ], + }), }), originalEvent: event, }); @@ -150,18 +154,21 @@ describe('Envelope: DynamoDB Stream', () => { const result = DynamoDBStreamEnvelope.safeParse(event, schema); // Assess - expect(result).be.deep.equal({ + expect(result).toEqual({ success: false, - error: new ParseError('Failed to parse record at index 1', { - cause: new ZodError([ - { - code: 'invalid_type', - expected: 'string', - received: 'number', - path: ['Records', 1, 'dynamodb', 'NewImage', 'Message'], - message: 'Expected string, received number', - }, - ]), + error: expect.objectContaining({ + name: 'ParseError', + message: expect.stringContaining('Failed to parse record at index 1'), + cause: expect.objectContaining({ + issues: [ + { + code: 'invalid_type', + expected: 'string', + path: ['Records', 1, 'dynamodb', 'NewImage', 'Message'], + message: 'Invalid input: expected string, received number', + }, + ], + }), }), originalEvent: event, }); @@ -180,25 +187,29 @@ describe('Envelope: DynamoDB Stream', () => { const result = DynamoDBStreamEnvelope.safeParse(event, schema); // Assess - expect(result).be.deep.equal({ + expect(result).toEqual({ success: false, - error: new ParseError('Failed to parse records at indexes 0, 1', { - cause: new ZodError([ - { - code: 'invalid_type', - expected: 'string', - received: 'undefined', - path: ['Records', 0, 'dynamodb', 'NewImage', 'Message'], - message: 'Required', - }, - { - code: 'invalid_type', - expected: 'string', - received: 'number', - path: ['Records', 1, 'dynamodb', 'NewImage', 'Message'], - message: 'Expected string, received number', - }, - ]), + error: expect.objectContaining({ + name: 'ParseError', + message: expect.stringContaining( + 'Failed to parse records at indexes 0, 1' + ), + cause: expect.objectContaining({ + issues: [ + { + code: 'invalid_type', + expected: 'string', + path: ['Records', 0, 'dynamodb', 'NewImage', 'Message'], + message: 'Invalid input: expected string, received undefined', + }, + { + code: 'invalid_type', + expected: 'string', + path: ['Records', 1, 'dynamodb', 'NewImage', 'Message'], + message: 'Invalid input: expected string, received number', + }, + ], + }), }), originalEvent: event, }); diff --git a/packages/parser/tests/unit/envelopes/event-bridge.test.ts b/packages/parser/tests/unit/envelopes/event-bridge.test.ts index 25137e39ad..47278afbdd 100644 --- a/packages/parser/tests/unit/envelopes/event-bridge.test.ts +++ b/packages/parser/tests/unit/envelopes/event-bridge.test.ts @@ -1,7 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { ZodError, z } from 'zod'; +import { z } from 'zod'; import { EventBridgeEnvelope } from '../../../src/envelopes/eventbridge.js'; -import { ParseError } from '../../../src/errors.js'; import type { EventBridgeEvent } from '../../../src/types/schema.js'; import { getTestEvent, omit } from '../helpers/utils.js'; @@ -39,9 +38,8 @@ describe('Envelope: EventBridgeEnvelope', () => { { code: 'invalid_type', expected: 'string', - received: 'undefined', path: ['detail', 'owner'], - message: 'Required', + message: 'Invalid input: expected string, received undefined', }, ], }), @@ -90,18 +88,23 @@ describe('Envelope: EventBridgeEnvelope', () => { const result = EventBridgeEnvelope.safeParse(event, schema); // Assess - expect(result).be.deep.equal({ + expect(result).toEqual({ success: false, - error: new ParseError('Failed to parse EventBridge envelope', { - cause: new ZodError([ - { - code: 'invalid_type', - expected: 'object', - received: 'undefined', - path: ['detail'], - message: 'Required', - }, - ]), + error: expect.objectContaining({ + name: 'ParseError', + message: expect.stringContaining( + 'Failed to parse EventBridge envelope' + ), + cause: expect.objectContaining({ + issues: [ + { + code: 'invalid_type', + expected: 'object', + path: ['detail'], + message: 'Invalid input: expected object, received undefined', + }, + ], + }), }), originalEvent: event, }); diff --git a/packages/parser/tests/unit/envelopes/kafka.test.ts b/packages/parser/tests/unit/envelopes/kafka.test.ts index 284bd89ea9..9961c8491a 100644 --- a/packages/parser/tests/unit/envelopes/kafka.test.ts +++ b/packages/parser/tests/unit/envelopes/kafka.test.ts @@ -1,7 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { ZodError, z } from 'zod'; +import { z } from 'zod'; import { KafkaEnvelope } from '../../../src/envelopes/kafka.js'; -import { ParseError } from '../../../src/errors.js'; import { JSONStringified } from '../../../src/helpers/index.js'; import { getTestEvent } from '../helpers/utils.js'; @@ -84,20 +83,23 @@ describe('Envelope: Kafka', () => { const result = KafkaEnvelope.safeParse(event, z.string()); // Assess - expect(result).be.deep.equal({ + expect(result).toEqual({ success: false, - error: new ParseError('Failed to parse Kafka envelope', { - cause: new ZodError([ - { - code: 'too_small', - minimum: 1, - type: 'array', - inclusive: true, - exact: false, - message: 'Array must contain at least 1 element(s)', - path: ['records', 'mytopic-0'], - }, - ]), + error: expect.objectContaining({ + name: 'ParseError', + message: expect.stringContaining('Failed to parse Kafka envelope'), + cause: expect.objectContaining({ + issues: [ + { + origin: 'array', + code: 'too_small', + minimum: 1, + inclusive: true, + message: 'Too small: expected array to have >=1 items', + path: ['records', 'mytopic-0'], + }, + ], + }), }), originalEvent: event, }); @@ -111,18 +113,11 @@ describe('Envelope: Kafka', () => { const result = KafkaEnvelope.safeParse(event, z.number()); // Assess - expect(result).be.deep.equal({ + expect(result).toEqual({ success: false, - error: new ParseError('Failed to parse Kafka envelope', { - cause: new ZodError([ - { - code: 'invalid_type', - expected: 'number', - received: 'string', - path: ['records', 'mytopic-0'], - message: 'Expected number, received string', - }, - ]), + error: expect.objectContaining({ + name: 'ParseError', + message: expect.stringContaining('Failed to parse Kafka envelope'), }), originalEvent: event, }); diff --git a/packages/parser/tests/unit/envelopes/kinesis-firehose.test.ts b/packages/parser/tests/unit/envelopes/kinesis-firehose.test.ts index 77e788d87c..b040673524 100644 --- a/packages/parser/tests/unit/envelopes/kinesis-firehose.test.ts +++ b/packages/parser/tests/unit/envelopes/kinesis-firehose.test.ts @@ -1,7 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { ZodError, z } from 'zod'; +import { z } from 'zod'; import { KinesisFirehoseEnvelope } from '../../../src/envelopes/kinesis-firehose.js'; -import { ParseError } from '../../../src/errors.js'; import { JSONStringified } from '../../../src/helpers/index.js'; import type { KinesisFireHoseEvent, @@ -38,9 +37,8 @@ describe('Envelope: Kinesis Firehose', () => { { code: 'invalid_type', expected: 'number', - received: 'string', path: ['records', 0, 'data'], - message: 'Expected number, received string', + message: 'Invalid input: expected number, received string', }, ], }), @@ -82,18 +80,24 @@ describe('Envelope: Kinesis Firehose', () => { // Act & Assess expect(() => KinesisFirehoseEnvelope.parse(event, z.string())).toThrow( - new ParseError('Failed to parse Kinesis Firehose envelope', { - cause: new ZodError([ - { - code: 'too_small', - minimum: 1, - type: 'array', - inclusive: true, - exact: false, - message: 'Array must contain at least 1 element(s)', - path: ['records'], - }, - ]), + expect.objectContaining({ + name: 'ParseError', + + message: expect.stringContaining( + 'Failed to parse Kinesis Firehose envelope' + ), + cause: expect.objectContaining({ + issues: [ + { + origin: 'array', + code: 'too_small', + minimum: 1, + inclusive: true, + path: ['records'], + message: 'Too small: expected array to have >=1 items', + }, + ], + }), }) ); }); @@ -136,18 +140,23 @@ describe('Envelope: Kinesis Firehose', () => { const result = KinesisFirehoseEnvelope.safeParse(event, z.string()); // Assess - expect(result).be.deep.equal({ + expect(result).toEqual({ success: false, - error: new ParseError('Failed to parse Kinesis Firehose envelope', { - cause: new ZodError([ - { - code: 'invalid_type', - expected: 'string', - received: 'undefined', - path: ['invocationId'], - message: 'Required', - }, - ]), + error: expect.objectContaining({ + name: 'ParseError', + message: expect.stringContaining( + 'Failed to parse Kinesis Firehose envelope' + ), + cause: expect.objectContaining({ + issues: [ + { + expected: 'string', + code: 'invalid_type', + path: ['invocationId'], + message: 'Invalid input: expected string, received undefined', + }, + ], + }), }), originalEvent: event, }); @@ -169,22 +178,24 @@ describe('Envelope: Kinesis Firehose', () => { ); // Assess - expect(result).be.deep.equal({ + expect(result).toEqual({ success: false, - error: new ParseError( - 'Failed to parse Kinesis Firehose record at index 1', - { - cause: new ZodError([ + error: expect.objectContaining({ + name: 'ParseError', + message: expect.stringContaining( + 'Failed to parse Kinesis Firehose record at index 1' + ), + cause: expect.objectContaining({ + issues: [ { code: 'invalid_type', expected: 'string', - received: 'undefined', path: ['records', 1, 'data', 'foo'], - message: 'Required', + message: 'Invalid input: expected string, received undefined', }, - ]), - } - ), + ], + }), + }), originalEvent: event, }); }); @@ -202,29 +213,30 @@ describe('Envelope: Kinesis Firehose', () => { ); // Assess - expect(result).be.deep.equal({ + expect(result).toEqual({ success: false, - error: new ParseError( - 'Failed to parse Kinesis Firehose records at indexes 0, 1', - { - cause: new ZodError([ + error: expect.objectContaining({ + name: 'ParseError', + message: expect.stringContaining( + 'Failed to parse Kinesis Firehose records at indexes 0, 1' + ), + cause: expect.objectContaining({ + issues: [ { code: 'invalid_type', expected: 'object', - received: 'string', path: ['records', 0, 'data'], - message: 'Expected object, received string', + message: 'Invalid input: expected object, received string', }, { code: 'invalid_type', expected: 'object', - received: 'string', path: ['records', 1, 'data'], - message: 'Expected object, received string', + message: 'Invalid input: expected object, received string', }, - ]), - } - ), + ], + }), + }), originalEvent: event, }); }); diff --git a/packages/parser/tests/unit/envelopes/kinesis.test.ts b/packages/parser/tests/unit/envelopes/kinesis.test.ts index 57ae688888..517ec91a98 100644 --- a/packages/parser/tests/unit/envelopes/kinesis.test.ts +++ b/packages/parser/tests/unit/envelopes/kinesis.test.ts @@ -1,7 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { ZodError, z } from 'zod'; +import { z } from 'zod'; import { KinesisEnvelope } from '../../../src/envelopes/kinesis.js'; -import { ParseError } from '../../../src/errors.js'; import type { KinesisDataStreamEvent } from '../../../src/types/schema.js'; import { getTestEvent } from '../helpers/utils.js'; @@ -30,9 +29,8 @@ describe('Envelope: Kinesis', () => { { code: 'invalid_type', expected: 'number', - received: 'string', path: ['Records', 0, 'kinesis', 'data'], - message: 'Expected number, received string', + message: 'Invalid input: expected number, received string', }, ], }), @@ -62,18 +60,23 @@ describe('Envelope: Kinesis', () => { // Act & Assess expect(() => KinesisEnvelope.parse(event, z.string())).toThrow( - new ParseError('Failed to parse Kinesis Data Stream envelope', { - cause: new ZodError([ - { - code: 'too_small', - minimum: 1, - type: 'array', - inclusive: true, - exact: false, - message: 'Array must contain at least 1 element(s)', - path: ['Records'], - }, - ]), + expect.objectContaining({ + name: 'ParseError', + message: expect.stringContaining( + 'Failed to parse Kinesis Data Stream envelope' + ), + cause: expect.objectContaining({ + issues: [ + { + origin: 'array', + code: 'too_small', + minimum: 1, + inclusive: true, + path: ['Records'], + message: 'Too small: expected array to have >=1 items', + }, + ], + }), }) ); }); @@ -104,20 +107,25 @@ describe('Envelope: Kinesis', () => { const result = KinesisEnvelope.safeParse(event, z.string()); // Assess - expect(result).be.deep.equal({ + expect(result).toEqual({ success: false, - error: new ParseError('Failed to parse Kinesis Data Stream envelope', { - cause: new ZodError([ - { - code: 'too_small', - minimum: 1, - type: 'array', - inclusive: true, - exact: false, - message: 'Array must contain at least 1 element(s)', - path: ['Records'], - }, - ]), + error: expect.objectContaining({ + name: 'ParseError', + message: expect.stringContaining( + 'Failed to parse Kinesis Data Stream envelope' + ), + cause: expect.objectContaining({ + issues: [ + { + origin: 'array', + code: 'too_small', + minimum: 1, + inclusive: true, + path: ['Records'], + message: 'Too small: expected array to have >=1 items', + }, + ], + }), }), originalEvent: event, }); @@ -137,22 +145,24 @@ describe('Envelope: Kinesis', () => { ); // Assess - expect(result).be.deep.equal({ + expect(result).toEqual({ success: false, - error: new ParseError( - 'Failed to parse Kinesis Data Stream record at index 1', - { - cause: new ZodError([ + error: expect.objectContaining({ + name: 'ParseError', + message: expect.stringContaining( + 'Failed to parse Kinesis Data Stream record at index 1' + ), + cause: expect.objectContaining({ + issues: [ { code: 'invalid_type', expected: 'object', - received: 'string', path: ['Records', 1, 'kinesis', 'data'], - message: 'Expected object, received string', + message: 'Invalid input: expected object, received string', }, - ]), - } - ), + ], + }), + }), originalEvent: event, }); }); @@ -170,29 +180,30 @@ describe('Envelope: Kinesis', () => { ); // Assess - expect(result).be.deep.equal({ + expect(result).toEqual({ success: false, - error: new ParseError( - 'Failed to parse Kinesis Data Stream records at indexes 0, 1', - { - cause: new ZodError([ + error: expect.objectContaining({ + name: 'ParseError', + message: expect.stringContaining( + 'Failed to parse Kinesis Data Stream records at indexes 0, 1' + ), + cause: expect.objectContaining({ + issues: [ { code: 'invalid_type', expected: 'object', - received: 'string', path: ['Records', 0, 'kinesis', 'data'], - message: 'Expected object, received string', + message: 'Invalid input: expected object, received string', }, { code: 'invalid_type', expected: 'object', - received: 'string', path: ['Records', 1, 'kinesis', 'data'], - message: 'Expected object, received string', + message: 'Invalid input: expected object, received string', }, - ]), - } - ), + ], + }), + }), originalEvent: event, }); }); diff --git a/packages/parser/tests/unit/envelopes/lambda.test.ts b/packages/parser/tests/unit/envelopes/lambda.test.ts index ff5f954a74..1e20fe3c8b 100644 --- a/packages/parser/tests/unit/envelopes/lambda.test.ts +++ b/packages/parser/tests/unit/envelopes/lambda.test.ts @@ -1,9 +1,8 @@ import { describe, expect, it } from 'vitest'; -import { ZodError, z } from 'zod'; -import { ParseError } from '../../../src'; +import { z } from 'zod'; import { LambdaFunctionUrlEnvelope } from '../../../src/envelopes/lambda.js'; -import { JSONStringified } from '../../../src/helpers'; -import type { LambdaFunctionUrlEvent } from '../../../src/types'; +import { JSONStringified } from '../../../src/helpers/index.js'; +import type { LambdaFunctionUrlEvent } from '../../../src/types/schema.js'; import { getTestEvent, omit } from '../helpers/utils.js'; describe('Envelope: Lambda function URL', () => { @@ -26,6 +25,7 @@ describe('Envelope: Lambda function URL', () => { // Act & Assess expect(() => LambdaFunctionUrlEnvelope.parse(event, schema)).toThrow( expect.objectContaining({ + name: 'ParseError', message: expect.stringContaining( 'Failed to parse Lambda function URL body' ), @@ -34,9 +34,8 @@ describe('Envelope: Lambda function URL', () => { { code: 'invalid_type', expected: 'object', - received: 'null', path: ['body'], - message: 'Expected object, received null', + message: 'Invalid input: expected object, received null', }, ], }), @@ -85,6 +84,7 @@ describe('Envelope: Lambda function URL', () => { expect(result).toEqual('aGVsbG8gd29ybGQ='); }); }); + describe('Method: safeParse', () => { it('parses Lambda function URL event', () => { // Prepare @@ -112,25 +112,29 @@ describe('Envelope: Lambda function URL', () => { const result = LambdaFunctionUrlEnvelope.safeParse(event, schema); // Assess - expect(result).be.deep.equal({ + expect(result).toEqual({ success: false, - error: new ParseError('Failed to parse Lambda function URL body', { - cause: new ZodError([ - { - code: 'invalid_type', - expected: 'string', - received: 'undefined', - path: ['rawPath'], - message: 'Required', - }, - { - code: 'invalid_type', - expected: 'object', - received: 'null', - path: ['body'], - message: 'Expected object, received null', - }, - ]), + error: expect.objectContaining({ + name: 'ParseError', + message: expect.stringContaining( + 'Failed to parse Lambda function URL body' + ), + cause: expect.objectContaining({ + issues: [ + { + code: 'invalid_type', + expected: 'string', + path: ['rawPath'], + message: 'Invalid input: expected string, received undefined', + }, + { + code: 'invalid_type', + expected: 'object', + path: ['body'], + message: 'Invalid input: expected object, received null', + }, + ], + }), }), originalEvent: event, }); diff --git a/packages/parser/tests/unit/envelopes/sns-sqs.test.ts b/packages/parser/tests/unit/envelopes/sns-sqs.test.ts index 8d8c0d7e07..9630379fd4 100644 --- a/packages/parser/tests/unit/envelopes/sns-sqs.test.ts +++ b/packages/parser/tests/unit/envelopes/sns-sqs.test.ts @@ -1,7 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { ZodError, z } from 'zod'; +import { z } from 'zod'; import { SnsSqsEnvelope } from '../../../src/envelopes/sns-sqs.js'; -import { ParseError } from '../../../src/errors.js'; import { JSONStringified } from '../../../src/helpers/index.js'; import type { SqsEvent } from '../../../src/types/schema.js'; import { getTestEvent } from '../helpers/utils.js'; @@ -28,15 +27,16 @@ describe('Envelope: SnsSqsEnvelope', () => { message: expect.stringContaining( 'Failed to parse SQS Record at index 0' ), - cause: new ZodError([ - { - code: 'invalid_type', - expected: 'number', - received: 'string', - path: ['Records', 0, 'body'], - message: 'Expected number, received string', - }, - ]), + cause: expect.objectContaining({ + issues: [ + { + code: 'invalid_type', + expected: 'number', + path: ['Records', 0, 'body'], + message: 'Invalid input: expected number, received string', + }, + ], + }), }) ); }); @@ -60,18 +60,21 @@ describe('Envelope: SnsSqsEnvelope', () => { // Act & Assess expect(() => SnsSqsEnvelope.parse(event, schema)).toThrow( - new ParseError('Failed to parse SQS Envelope', { - cause: new ZodError([ - { - code: 'too_small', - minimum: 1, - type: 'array', - inclusive: true, - exact: false, - message: 'Array must contain at least 1 element(s)', - path: ['Records'], - }, - ]), + expect.objectContaining({ + name: 'ParseError', + message: expect.stringContaining('Failed to parse SQS Envelope'), + cause: expect.objectContaining({ + issues: [ + { + origin: 'array', + code: 'too_small', + minimum: 1, + inclusive: true, + path: ['Records'], + message: 'Too small: expected array to have >=1 items', + }, + ], + }), }) ); }); @@ -83,14 +86,21 @@ describe('Envelope: SnsSqsEnvelope', () => { // Act & Assess expect(() => SnsSqsEnvelope.parse(event, schema)).toThrow( - new ParseError('Failed to parse SQS Record at index 0', { - cause: new ZodError([ - { - code: 'custom', - message: 'Invalid JSON', - path: ['Records', 0, 'body'], - }, - ]), + expect.objectContaining({ + name: 'ParseError', + message: expect.stringContaining( + 'Failed to parse SQS Record at index 0' + ), + cause: expect.objectContaining({ + issues: [ + { + code: 'custom', + input: 'invalid', + message: expect.stringMatching(/^Invalid JSON - /), + path: ['Records', 0, 'body'], + }, + ], + }), }) ); }); @@ -102,44 +112,45 @@ describe('Envelope: SnsSqsEnvelope', () => { // Act & Assess expect(() => SnsSqsEnvelope.parse(event, schema)).toThrow( - new ParseError('Failed to parse SQS Record at index 0', { - cause: new ZodError([ - { - code: 'invalid_type', - expected: 'string', - received: 'undefined', - path: ['Records', 0, 'body', 'TopicArn'], - message: 'Required', - }, - { - code: 'invalid_literal', - expected: 'Notification', - path: ['Records', 0, 'body', 'Type'], - message: 'Invalid literal value, expected "Notification"', - received: undefined, - }, - { - code: 'invalid_type', - expected: 'string', - received: 'undefined', - path: ['Records', 0, 'body', 'Message'], - message: 'Required', - }, - { - code: 'invalid_type', - expected: 'string', - received: 'undefined', - path: ['Records', 0, 'body', 'MessageId'], - message: 'Required', - }, - { - code: 'invalid_type', - expected: 'string', - received: 'undefined', - path: ['Records', 0, 'body', 'Timestamp'], - message: 'Required', - }, - ]), + expect.objectContaining({ + name: 'ParseError', + message: expect.stringContaining( + 'Failed to parse SQS Record at index 0' + ), + cause: expect.objectContaining({ + issues: [ + { + code: 'invalid_type', + expected: 'string', + path: ['Records', 0, 'body', 'TopicArn'], + message: 'Invalid input: expected string, received undefined', + }, + { + code: 'invalid_value', + values: ['Notification'], + path: ['Records', 0, 'body', 'Type'], + message: 'Invalid input: expected "Notification"', + }, + { + code: 'invalid_type', + expected: 'string', + path: ['Records', 0, 'body', 'Message'], + message: 'Invalid input: expected string, received undefined', + }, + { + code: 'invalid_type', + expected: 'string', + path: ['Records', 0, 'body', 'MessageId'], + message: 'Invalid input: expected string, received undefined', + }, + { + code: 'invalid_type', + expected: 'string', + path: ['Records', 0, 'body', 'Timestamp'], + message: 'Invalid input: expected string, received undefined', + }, + ], + }), }) ); }); @@ -170,20 +181,23 @@ describe('Envelope: SnsSqsEnvelope', () => { const result = SnsSqsEnvelope.safeParse(event, schema); // Assess - expect(result).be.deep.equal({ + expect(result).toEqual({ success: false, - error: new ParseError('Failed to parse SQS envelope', { - cause: new ZodError([ - { - code: 'too_small', - minimum: 1, - type: 'array', - inclusive: true, - exact: false, - message: 'Array must contain at least 1 element(s)', - path: ['Records'], - }, - ]), + error: expect.objectContaining({ + name: 'ParseError', + message: expect.stringContaining('Failed to parse SQS envelope'), + cause: expect.objectContaining({ + issues: [ + { + origin: 'array', + code: 'too_small', + minimum: 1, + inclusive: true, + path: ['Records'], + message: 'Too small: expected array to have >=1 items', + }, + ], + }), }), originalEvent: event, }); @@ -198,16 +212,23 @@ describe('Envelope: SnsSqsEnvelope', () => { const result = SnsSqsEnvelope.safeParse(event, schema); // Assess - expect(result).be.deep.equal({ + expect(result).toEqual({ success: false, - error: new ParseError('Failed to parse SQS Record at index 0', { - cause: new ZodError([ - { - code: 'custom', - message: 'Invalid JSON', - path: ['Records', 0, 'body'], - }, - ]), + error: expect.objectContaining({ + name: 'ParseError', + message: expect.stringContaining( + 'Failed to parse SQS Record at index 0' + ), + cause: expect.objectContaining({ + issues: [ + { + code: 'custom', + input: 'invalid', + message: expect.stringMatching(/^Invalid JSON - /), + path: ['Records', 0, 'body'], + }, + ], + }), }), originalEvent: event, }); @@ -222,46 +243,48 @@ describe('Envelope: SnsSqsEnvelope', () => { const result = SnsSqsEnvelope.safeParse(event, schema); // Assess - expect(result).be.deep.equal({ + expect(result).toEqual({ success: false, - error: new ParseError('Failed to parse SQS Record at index 0', { - cause: new ZodError([ - { - code: 'invalid_type', - expected: 'string', - received: 'undefined', - path: ['Records', 0, 'body', 'TopicArn'], - message: 'Required', - }, - { - code: 'invalid_literal', - expected: 'Notification', - path: ['Records', 0, 'body', 'Type'], - message: 'Invalid literal value, expected "Notification"', - received: undefined, - }, - { - code: 'invalid_type', - expected: 'string', - received: 'undefined', - path: ['Records', 0, 'body', 'Message'], - message: 'Required', - }, - { - code: 'invalid_type', - expected: 'string', - received: 'undefined', - path: ['Records', 0, 'body', 'MessageId'], - message: 'Required', - }, - { - code: 'invalid_type', - expected: 'string', - received: 'undefined', - path: ['Records', 0, 'body', 'Timestamp'], - message: 'Required', - }, - ]), + + error: expect.objectContaining({ + name: 'ParseError', + message: expect.stringContaining( + 'Failed to parse SQS Record at index 0' + ), + cause: expect.objectContaining({ + issues: [ + { + code: 'invalid_type', + expected: 'string', + path: ['Records', 0, 'body', 'TopicArn'], + message: 'Invalid input: expected string, received undefined', + }, + { + code: 'invalid_value', + values: ['Notification'], + path: ['Records', 0, 'body', 'Type'], + message: 'Invalid input: expected "Notification"', + }, + { + code: 'invalid_type', + expected: 'string', + path: ['Records', 0, 'body', 'Message'], + message: 'Invalid input: expected string, received undefined', + }, + { + code: 'invalid_type', + expected: 'string', + path: ['Records', 0, 'body', 'MessageId'], + message: 'Invalid input: expected string, received undefined', + }, + { + code: 'invalid_type', + expected: 'string', + path: ['Records', 0, 'body', 'Timestamp'], + message: 'Invalid input: expected string, received undefined', + }, + ], + }), }), originalEvent: event, }); @@ -282,16 +305,23 @@ describe('Envelope: SnsSqsEnvelope', () => { const result = SnsSqsEnvelope.safeParse(event, JSONStringified(schema)); // Assess - expect(result).be.deep.equal({ + expect(result).toEqual({ success: false, - error: new ParseError('Failed to parse SQS Record at index 1', { - cause: new ZodError([ - { - code: 'custom', - message: 'Invalid JSON', - path: ['Records', 1, 'body'], - }, - ]), + error: expect.objectContaining({ + name: 'ParseError', + message: expect.stringContaining( + 'Failed to parse SQS Record at index 1' + ), + cause: expect.objectContaining({ + issues: [ + { + code: 'custom', + message: expect.stringMatching(/^Invalid JSON - /), + fatal: true, + path: ['Records', 1, 'body'], + }, + ], + }), }), originalEvent: event, }); @@ -323,25 +353,29 @@ describe('Envelope: SnsSqsEnvelope', () => { const result = SnsSqsEnvelope.safeParse(event, z.number()); // Assess - expect(result).be.deep.equal({ + expect(result).toEqual({ success: false, - error: new ParseError('Failed to parse SQS Records at indexes 0, 1', { - cause: new ZodError([ - { - code: 'invalid_type', - expected: 'number', - received: 'string', - path: ['Records', 0, 'body'], - message: 'Expected number, received string', - }, - { - code: 'invalid_type', - expected: 'number', - received: 'string', - path: ['Records', 1, 'body'], - message: 'Expected number, received string', - }, - ]), + error: expect.objectContaining({ + name: 'ParseError', + message: expect.stringContaining( + 'Failed to parse SQS Records at indexes 0, 1' + ), + cause: expect.objectContaining({ + issues: [ + { + code: 'invalid_type', + expected: 'number', + path: ['Records', 0, 'body'], + message: 'Invalid input: expected number, received string', + }, + { + code: 'invalid_type', + expected: 'number', + path: ['Records', 1, 'body'], + message: 'Invalid input: expected number, received string', + }, + ], + }), }), originalEvent: event, }); diff --git a/packages/parser/tests/unit/envelopes/sns.test.ts b/packages/parser/tests/unit/envelopes/sns.test.ts index 7e4eee6552..75effbb290 100644 --- a/packages/parser/tests/unit/envelopes/sns.test.ts +++ b/packages/parser/tests/unit/envelopes/sns.test.ts @@ -1,7 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { ZodError, z } from 'zod'; +import { z } from 'zod'; import { SnsEnvelope } from '../../../src/envelopes/sns.js'; -import { ParseError } from '../../../src/errors.js'; import { JSONStringified } from '../../../src/helpers/index.js'; import type { SnsEvent } from '../../../src/types/schema.js'; import { getTestEvent } from '../helpers/utils.js'; @@ -29,6 +28,7 @@ describe('Envelope: SnsEnvelope', () => { ) ).toThrow( expect.objectContaining({ + name: 'ParseError', message: expect.stringContaining( 'Failed to parse SNS record at index 0' ), @@ -37,9 +37,8 @@ describe('Envelope: SnsEnvelope', () => { { code: 'invalid_type', expected: 'object', - received: 'string', path: ['Records', 0, 'Sns', 'Message'], - message: 'Expected object, received string', + message: 'Invalid input: expected object, received string', }, ], }), @@ -84,18 +83,21 @@ describe('Envelope: SnsEnvelope', () => { const result = SnsEnvelope.safeParse(event, z.string()); // Assess - expect(result).be.deep.equal({ + expect(result).toEqual({ success: false, - error: new ParseError('Failed to parse SNS envelope', { - cause: new ZodError([ - { - code: 'invalid_type', - expected: 'object', - received: 'undefined', - path: ['Records', 0, 'Sns'], - message: 'Required', - }, - ]), + error: expect.objectContaining({ + name: 'ParseError', + message: expect.stringContaining('Failed to parse SNS envelope'), + cause: expect.objectContaining({ + issues: [ + { + code: 'invalid_type', + expected: 'object', + path: ['Records', 0, 'Sns'], + message: 'Invalid input: expected object, received undefined', + }, + ], + }), }), originalEvent: event, }); @@ -119,18 +121,23 @@ describe('Envelope: SnsEnvelope', () => { ); // Assess - expect(result).be.deep.equal({ + expect(result).toEqual({ success: false, - error: new ParseError('Failed to parse SNS message at index 1', { - cause: new ZodError([ - { - code: 'invalid_type', - expected: 'string', - received: 'number', - path: ['Records', 1, 'Sns', 'Message', 'foo'], - message: 'Expected string, received number', - }, - ]), + error: expect.objectContaining({ + name: 'ParseError', + message: expect.stringContaining( + 'Failed to parse SNS message at index 1' + ), + cause: expect.objectContaining({ + issues: [ + { + code: 'invalid_type', + expected: 'string', + path: ['Records', 1, 'Sns', 'Message', 'foo'], + message: 'Invalid input: expected string, received number', + }, + ], + }), }), originalEvent: event, }); @@ -152,21 +159,29 @@ describe('Envelope: SnsEnvelope', () => { ); // Assess - expect(result).be.deep.equal({ + expect(result).toEqual({ success: false, - error: new ParseError('Failed to parse SNS messages at indexes 0, 1', { - cause: new ZodError([ - { - code: 'custom', - message: 'Invalid JSON', - path: ['Records', 0, 'Sns', 'Message'], - }, - { - code: 'custom', - message: 'Invalid JSON', - path: ['Records', 1, 'Sns', 'Message'], - }, - ]), + error: expect.objectContaining({ + name: 'ParseError', + message: expect.stringContaining( + 'Failed to parse SNS messages at indexes 0, 1' + ), + cause: expect.objectContaining({ + issues: [ + { + code: 'custom', + message: expect.stringMatching(/^Invalid JSON - /), + fatal: true, + path: ['Records', 0, 'Sns', 'Message'], + }, + { + code: 'custom', + message: expect.stringMatching(/^Invalid JSON - /), + fatal: true, + path: ['Records', 1, 'Sns', 'Message'], + }, + ], + }), }), originalEvent: event, }); diff --git a/packages/parser/tests/unit/envelopes/sqs.test.ts b/packages/parser/tests/unit/envelopes/sqs.test.ts index 99fd3cb35f..6083a2cbe9 100644 --- a/packages/parser/tests/unit/envelopes/sqs.test.ts +++ b/packages/parser/tests/unit/envelopes/sqs.test.ts @@ -1,7 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { ZodError, z } from 'zod'; +import { z } from 'zod'; import { SqsEnvelope } from '../../../src/envelopes/sqs.js'; -import { ParseError } from '../../../src/errors.js'; import { JSONStringified } from '../../../src/helpers/index.js'; import type { SqsEvent } from '../../../src/types/schema.js'; import { getTestEvent } from '../helpers/utils.js'; @@ -25,16 +24,20 @@ describe('Envelope: SqsEnvelope', () => { // Act & Assess expect(() => SqsEnvelope.parse(event, JSONStringified(schema))).toThrow( expect.objectContaining({ + name: 'ParseError', message: expect.stringContaining( 'Failed to parse SQS Record at index 0' ), - cause: new ZodError([ - { - code: 'custom', - message: 'Invalid JSON', - path: ['Records', 0, 'body'], - }, - ]), + cause: expect.objectContaining({ + issues: [ + { + code: 'custom', + message: expect.stringMatching(/^Invalid JSON - /), + fatal: true, + path: ['Records', 0, 'body'], + }, + ], + }), }) ); }); @@ -78,20 +81,23 @@ describe('Envelope: SqsEnvelope', () => { const result = SqsEnvelope.safeParse(event, z.string()); // Assess - expect(result).be.deep.equal({ + expect(result).toEqual({ success: false, - error: new ParseError('Failed to parse SQS envelope', { - cause: new ZodError([ - { - code: 'too_small', - minimum: 1, - type: 'array', - inclusive: true, - exact: false, - message: 'Array must contain at least 1 element(s)', - path: ['Records'], - }, - ]), + error: expect.objectContaining({ + name: 'ParseError', + message: expect.stringContaining('Failed to parse SQS envelope'), + cause: expect.objectContaining({ + issues: [ + { + origin: 'array', + code: 'too_small', + minimum: 1, + inclusive: true, + path: ['Records'], + message: 'Too small: expected array to have >=1 items', + }, + ], + }), }), originalEvent: event, }); @@ -105,16 +111,22 @@ describe('Envelope: SqsEnvelope', () => { const result = SqsEnvelope.safeParse(event, JSONStringified(schema)); // Assess - expect(result).be.deep.equal({ + expect(result).toEqual({ success: false, - error: new ParseError('Failed to parse SQS Record at index 0', { - cause: new ZodError([ - { - code: 'custom', - message: 'Invalid JSON', - path: ['Records', 0, 'body'], - }, - ]), + error: expect.objectContaining({ + message: expect.stringContaining( + 'Failed to parse SQS Record at index 0' + ), + cause: expect.objectContaining({ + issues: [ + { + code: 'custom', + message: expect.stringMatching(/^Invalid JSON - /), + fatal: true, + path: ['Records', 0, 'body'], + }, + ], + }), }), originalEvent: event, }); @@ -128,25 +140,29 @@ describe('Envelope: SqsEnvelope', () => { const result = SqsEnvelope.safeParse(event, z.number()); // Assess - expect(result).be.deep.equal({ + expect(result).toEqual({ success: false, - error: new ParseError('Failed to parse SQS Records at indexes 0, 1', { - cause: new ZodError([ - { - code: 'invalid_type', - expected: 'number', - received: 'string', - path: ['Records', 0, 'body'], - message: 'Expected number, received string', - }, - { - code: 'invalid_type', - expected: 'number', - received: 'string', - path: ['Records', 1, 'body'], - message: 'Expected number, received string', - }, - ]), + error: expect.objectContaining({ + name: 'ParseError', + message: expect.stringContaining( + 'Failed to parse SQS Records at indexes 0, 1' + ), + cause: expect.objectContaining({ + issues: [ + { + code: 'invalid_type', + expected: 'number', + message: 'Invalid input: expected number, received string', + path: ['Records', 0, 'body'], + }, + { + code: 'invalid_type', + expected: 'number', + message: 'Invalid input: expected number, received string', + path: ['Records', 1, 'body'], + }, + ], + }), }), originalEvent: event, }); diff --git a/packages/parser/tests/unit/envelopes/vpc-lattice.test.ts b/packages/parser/tests/unit/envelopes/vpc-lattice.test.ts index 1980db49e9..2702c7c130 100644 --- a/packages/parser/tests/unit/envelopes/vpc-lattice.test.ts +++ b/packages/parser/tests/unit/envelopes/vpc-lattice.test.ts @@ -1,7 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { ZodError, z } from 'zod'; +import { z } from 'zod'; import { VpcLatticeEnvelope } from '../../../src/envelopes/vpc-lattice.js'; -import { ParseError } from '../../../src/errors.js'; import { JSONStringified } from '../../../src/helpers/index.js'; import type { VpcLatticeEvent } from '../../../src/types/index.js'; import { getTestEvent, omit } from '../helpers/utils.js'; @@ -25,15 +24,15 @@ describe('Envelope: VPC Lattice', () => { // Act & Assess expect(() => VpcLatticeEnvelope.parse(event, schema)).toThrow( expect.objectContaining({ + name: 'ParseError', message: expect.stringContaining('Failed to parse VPC Lattice body'), cause: expect.objectContaining({ issues: [ { code: 'invalid_type', expected: 'object', - received: 'string', path: ['body'], - message: 'Expected object, received string', + message: 'Invalid input: expected object, received string', }, ], }), @@ -104,18 +103,21 @@ describe('Envelope: VPC Lattice', () => { const result = VpcLatticeEnvelope.safeParse(event, z.string()); // Assess - expect(result).be.deep.equal({ + expect(result).toEqual({ success: false, - error: new ParseError('Failed to parse VPC Lattice body', { - cause: new ZodError([ - { - code: 'invalid_type', - expected: 'boolean', - received: 'undefined', - path: ['is_base64_encoded'], - message: 'Required', - }, - ]), + error: expect.objectContaining({ + name: 'ParseError', + message: expect.stringContaining('Failed to parse VPC Lattice body'), + cause: expect.objectContaining({ + issues: [ + { + code: 'invalid_type', + expected: 'boolean', + path: ['is_base64_encoded'], + message: 'Invalid input: expected boolean, received undefined', + }, + ], + }), }), originalEvent: event, }); diff --git a/packages/parser/tests/unit/envelopes/vpc-latticev2.test.ts b/packages/parser/tests/unit/envelopes/vpc-latticev2.test.ts index 49f5ec81bf..08021ad288 100644 --- a/packages/parser/tests/unit/envelopes/vpc-latticev2.test.ts +++ b/packages/parser/tests/unit/envelopes/vpc-latticev2.test.ts @@ -1,7 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { ZodError, z } from 'zod'; +import { z } from 'zod'; import { VpcLatticeV2Envelope } from '../../../src/envelopes/vpc-latticev2.js'; -import { ParseError } from '../../../src/errors.js'; import { JSONStringified } from '../../../src/helpers/index.js'; import type { VpcLatticeEventV2 } from '../../../src/types/index.js'; import { getTestEvent, omit } from '../helpers/utils.js'; @@ -25,6 +24,7 @@ describe('Envelope: VPC Lattice v2', () => { // Act & Assess expect(() => VpcLatticeV2Envelope.parse(event, schema)).toThrow( expect.objectContaining({ + name: 'ParseError', message: expect.stringContaining( 'Failed to parse VPC Lattice v2 body' ), @@ -33,9 +33,8 @@ describe('Envelope: VPC Lattice v2', () => { { code: 'invalid_type', expected: 'object', - received: 'string', path: ['body'], - message: 'Expected object, received string', + message: 'Invalid input: expected object, received string', }, ], }), @@ -106,18 +105,23 @@ describe('Envelope: VPC Lattice v2', () => { const result = VpcLatticeV2Envelope.safeParse(event, z.string()); // Assess - expect(result).be.deep.equal({ + expect(result).toEqual({ success: false, - error: new ParseError('Failed to parse VPC Lattice v2 body', { - cause: new ZodError([ - { - code: 'invalid_type', - expected: 'string', - received: 'undefined', - path: ['path'], - message: 'Required', - }, - ]), + error: expect.objectContaining({ + name: 'ParseError', + message: expect.stringContaining( + 'Failed to parse VPC Lattice v2 body' + ), + cause: expect.objectContaining({ + issues: [ + { + code: 'invalid_type', + expected: 'string', + message: 'Invalid input: expected string, received undefined', + path: ['path'], + }, + ], + }), }), originalEvent: event, }); diff --git a/packages/parser/tests/unit/parser.decorator.test.ts b/packages/parser/tests/unit/parser.decorator.test.ts index 48a647736e..7955bca551 100644 --- a/packages/parser/tests/unit/parser.decorator.test.ts +++ b/packages/parser/tests/unit/parser.decorator.test.ts @@ -1,7 +1,7 @@ import type { LambdaInterface } from '@aws-lambda-powertools/commons/types'; import type { Context } from 'aws-lambda'; import { describe, expect, it } from 'vitest'; -import { type ZodSchema, z } from 'zod'; +import { z } from 'zod'; import { EventBridgeEnvelope } from '../../src/envelopes/eventbridge.js'; import { ParseError } from '../../src/errors.js'; import { parser } from '../../src/index.js'; @@ -64,9 +64,7 @@ describe('Decorator: parser', () => { return event; } - private async anotherMethod( - event: z.infer - ): Promise> { + private async anotherMethod(event: unknown): Promise { return event; } } diff --git a/packages/parser/tests/unit/schema/transfer-family.test.ts b/packages/parser/tests/unit/schema/transfer-family.test.ts index f6041433cc..0d1b977c42 100644 --- a/packages/parser/tests/unit/schema/transfer-family.test.ts +++ b/packages/parser/tests/unit/schema/transfer-family.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest'; -import { TransferFamilySchema } from '../../../src/schemas/transfer-family'; +import { TransferFamilySchema } from '../../../src/schemas/transfer-family.js'; import type { TransferFamilyEvent } from '../../../src/types/schema.js'; -import { getTestEvent } from '../helpers/utils'; +import { getTestEvent } from '../helpers/utils.js'; describe('Schema: TransferFamily', () => { const baseEvent = getTestEvent({ diff --git a/packages/parser/vitest.config.ts b/packages/parser/vitest.config.ts index 9f1196ef1f..c8737a7bbc 100644 --- a/packages/parser/vitest.config.ts +++ b/packages/parser/vitest.config.ts @@ -4,5 +4,9 @@ export default defineProject({ test: { environment: 'node', setupFiles: ['../testing/src/setupEnv.ts'], + typecheck: { + tsconfig: './tests/tsconfig.json', + include: ['./tests/types/**/*.ts'], + }, }, });