From ec94942fc7a00a4ee1a0c2585398d4407b8fed44 Mon Sep 17 00:00:00 2001 From: Alexander Schueren Date: Fri, 7 Feb 2025 10:26:27 +0100 Subject: [PATCH 1/4] fix(parser): update ParsedResult type for correct output inference --- packages/parser/src/types/parser.ts | 4 +-- packages/parser/tests/types/envelopes.test.ts | 33 +++++++++++++++---- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/packages/parser/src/types/parser.ts b/packages/parser/src/types/parser.ts index 16178fa958..2e240bdadd 100644 --- a/packages/parser/src/types/parser.ts +++ b/packages/parser/src/types/parser.ts @@ -34,7 +34,7 @@ type ParsedResultError = { /** * The result of parsing an event using the safeParse, can either be a success or an error */ -type ParsedResult = +type ParsedResult = | ParsedResultSuccess | ParsedResultError; @@ -54,7 +54,7 @@ type ZodInferredSafeParseResult< TSchema extends ZodSchema, TEnvelope extends Envelope, > = undefined extends TEnvelope - ? ParsedResult> + ? ParsedResult, z.infer> : TEnvelope extends ArrayEnvelope ? ParsedResult[]> : ParsedResult>; diff --git a/packages/parser/tests/types/envelopes.test.ts b/packages/parser/tests/types/envelopes.test.ts index 72cfbca86b..b654d8ad79 100644 --- a/packages/parser/tests/types/envelopes.test.ts +++ b/packages/parser/tests/types/envelopes.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'vitest'; +import { describe, expect, expectTypeOf, it } from 'vitest'; import { z } from 'zod'; import { ApiGatewayEnvelope, @@ -16,7 +16,8 @@ import { VpcLatticeEnvelope, VpcLatticeV2Envelope, } from '../../src/envelopes/index.js'; -import type { ParserOutput } from '../../src/types/parser.js'; +import { parse } from '../../src/parser.js'; +import type { ParsedResult, ParserOutput } from '../../src/types/parser.js'; describe('Types ', () => { const userSchema = z.object({ @@ -69,10 +70,28 @@ describe('Types ', () => { expect(Array.isArray(result)).toBe(true); expect(result).toEqual([{ name: 'John', age: 30 }]); - // Type assertion to ensure it's specifically User[] - type AssertIsUserArray = T extends z.infer[] - ? true - : false; - type Test = AssertIsUserArray; + expectTypeOf(result).toEqualTypeOf[]>(); + }); + + it('infers types of schema and safeParse result', () => { + // Prepare + const schema = z.object({ + name: z.string(), + age: z.number(), + }); + + const input = { name: 'John', age: 30 }; + type User = z.infer; + type Result = ParsedResult; + + // Act + const result = parse(input, undefined, schema, true) as ParsedResult; + + // Assert + if (result.success) { + expectTypeOf(result.data).toEqualTypeOf(); + } else { + expectTypeOf(result.originalEvent).toEqualTypeOf(); + } }); }); From 871dc80e9b54906c31071ac4ee2a0d02b5ab76e8 Mon Sep 17 00:00:00 2001 From: Alexander Schueren Date: Fri, 7 Feb 2025 11:25:53 +0100 Subject: [PATCH 2/4] feat(parser): enhance type inference with new ParseFunction --- packages/parser/src/parser.ts | 12 ++- packages/parser/src/types/parser.ts | 56 +++++++++++- packages/parser/tests/types/envelopes.test.ts | 25 +----- packages/parser/tests/types/parser.test.ts | 88 +++++++++++++++++++ 4 files changed, 148 insertions(+), 33 deletions(-) create mode 100644 packages/parser/tests/types/parser.test.ts diff --git a/packages/parser/src/parser.ts b/packages/parser/src/parser.ts index 43c8ad09c8..dccb6fd8f9 100644 --- a/packages/parser/src/parser.ts +++ b/packages/parser/src/parser.ts @@ -1,6 +1,7 @@ import type { ZodSchema, z } from 'zod'; import { ParseError } from './errors.js'; -import type { Envelope, ParsedResult } from './types/index.js'; +import type { Envelope } from './types/index.js'; +import type { ParseFunction } from './types/parser.js'; /** * Parse the data using the provided schema, envelope and safeParse flag @@ -27,12 +28,12 @@ import type { Envelope, ParsedResult } from './types/index.js'; * @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 */ -const parse = ( +const parse: ParseFunction = ( data: z.infer, envelope: E | undefined, schema: T, safeParse?: boolean -): ParsedResult | z.infer => { +) => { if (envelope && safeParse) { return envelope.safeParse(data, schema); } @@ -56,10 +57,7 @@ const parse = ( * @param data the data to parse * @param schema the zod schema to use */ -const safeParseSchema = ( - data: z.infer, - schema: T -): ParsedResult => { +const safeParseSchema = (data: z.infer, schema: T) => { const result = schema.safeParse(data); return result.success diff --git a/packages/parser/src/types/parser.ts b/packages/parser/src/types/parser.ts index 2e240bdadd..612816c6f0 100644 --- a/packages/parser/src/types/parser.ts +++ b/packages/parser/src/types/parser.ts @@ -1,5 +1,5 @@ import type { ZodError, ZodSchema, z } from 'zod'; -import type { ArrayEnvelope, Envelope } from './envelope.js'; +import type { ArrayEnvelope, Envelope, ObjectEnvelope } from './envelope.js'; /** * Options for the parser used in middy middleware and decorator @@ -70,10 +70,62 @@ type ParserOutput< ? ZodInferredSafeParseResult : ZodInferredResult; +/** + * The parser function that can parse the data using the provided schema and envelope + * we use function overloads to provide the correct return type based on the provided envelope + **/ +type ParseFunction = { + // No envelope cases + ( + data: z.infer, + envelope: undefined, + schema: T, + safeParse?: false + ): z.infer; + + ( + data: z.infer, + envelope: undefined, + schema: T, + safeParse: true + ): ParsedResult>; + + // Object envelope cases + ( + data: unknown, + envelope: ObjectEnvelope, + schema: T, + safeParse?: false + ): z.infer; + + ( + data: unknown, + envelope: ObjectEnvelope, + schema: T, + safeParse: true + ): ParsedResult>; + + // Array envelope cases + ( + data: unknown, + envelope: ArrayEnvelope, + schema: T, + safeParse?: false + ): z.infer[]; + + ( + data: unknown, + envelope: ArrayEnvelope, + schema: T, + safeParse: true + ): ParsedResult[]>; +}; + export type { - ParserOptions, + ParseFunction, ParsedResult, ParsedResultError, ParsedResultSuccess, + ParserOptions, ParserOutput, }; diff --git a/packages/parser/tests/types/envelopes.test.ts b/packages/parser/tests/types/envelopes.test.ts index b654d8ad79..f211feb5f5 100644 --- a/packages/parser/tests/types/envelopes.test.ts +++ b/packages/parser/tests/types/envelopes.test.ts @@ -16,8 +16,7 @@ import { VpcLatticeEnvelope, VpcLatticeV2Envelope, } from '../../src/envelopes/index.js'; -import { parse } from '../../src/parser.js'; -import type { ParsedResult, ParserOutput } from '../../src/types/parser.js'; +import type { ParserOutput } from '../../src/types/parser.js'; describe('Types ', () => { const userSchema = z.object({ @@ -72,26 +71,4 @@ describe('Types ', () => { expectTypeOf(result).toEqualTypeOf[]>(); }); - - it('infers types of schema and safeParse result', () => { - // Prepare - const schema = z.object({ - name: z.string(), - age: z.number(), - }); - - const input = { name: 'John', age: 30 }; - type User = z.infer; - type Result = ParsedResult; - - // Act - const result = parse(input, undefined, schema, true) as ParsedResult; - - // Assert - if (result.success) { - expectTypeOf(result.data).toEqualTypeOf(); - } else { - expectTypeOf(result.originalEvent).toEqualTypeOf(); - } - }); }); diff --git a/packages/parser/tests/types/parser.test.ts b/packages/parser/tests/types/parser.test.ts new file mode 100644 index 0000000000..292acfb36c --- /dev/null +++ b/packages/parser/tests/types/parser.test.ts @@ -0,0 +1,88 @@ +import { describe } from 'node:test'; +import { expect, expectTypeOf, it } from 'vitest'; +import { z } from 'zod'; +import { EventBridgeEnvelope, SqsEnvelope } from '../../src/envelopes/index.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(); + } + }); +}); From 1005fdf54928c18ad4efaac92a1eccc109eb615b Mon Sep 17 00:00:00 2001 From: Alexander Schueren Date: Fri, 7 Feb 2025 11:30:18 +0100 Subject: [PATCH 3/4] add parsefunction to exports --- packages/parser/src/types/index.ts | 31 +++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/packages/parser/src/types/index.ts b/packages/parser/src/types/index.ts index 1bc68a549a..e4f331e88b 100644 --- a/packages/parser/src/types/index.ts +++ b/packages/parser/src/types/index.ts @@ -1,42 +1,43 @@ export type { - ParserOptions, ParsedResult, - ParsedResultSuccess, ParsedResultError, + ParsedResultSuccess, + ParseFunction, + ParserOptions, } from '../types/parser.js'; export type { Envelope } from './envelope.js'; export type { ALBEvent, - APIGatewayProxyEvent, ALBMultiValueHeadersEvent, + APIGatewayProxyEvent, APIGatewayProxyEventV2, - APIGatewayRequestContextV2, APIGatewayRequestAuthorizerV2, - AppSyncResolverEvent, + APIGatewayRequestContextV2, AppSyncBatchResolverEvent, - S3Event, - S3EventNotificationEventBridge, - S3SqsEventNotification, - SnsEvent, - SqsEvent, - DynamoDBStreamEvent, - DynamoDBStreamToKinesisRecordEvent, - CloudWatchLogsEvent, + AppSyncResolverEvent, CloudFormationCustomResourceCreateEvent, CloudFormationCustomResourceDeleteEvent, CloudFormationCustomResourceUpdateEvent, + CloudWatchLogsEvent, + DynamoDBStreamEvent, + DynamoDBStreamToKinesisRecordEvent, EventBridgeEvent, - KafkaSelfManagedEvent, KafkaMskEvent, + KafkaSelfManagedEvent, KinesisDataStreamEvent, KinesisDynamoDBStreamEvent, KinesisFireHoseEvent, KinesisFireHoseSqsEvent, LambdaFunctionUrlEvent, + S3Event, + S3EventNotificationEventBridge, + S3ObjectLambdaEvent, + S3SqsEventNotification, SesEvent, + SnsEvent, SnsSqsNotification, - S3ObjectLambdaEvent, + SqsEvent, VpcLatticeEvent, VpcLatticeEventV2, } from './schema.js'; From c20386a7bc0903a9dfb2129dac9522bd7172fc4c Mon Sep 17 00:00:00 2001 From: Alexander Schueren Date: Fri, 7 Feb 2025 13:20:31 +0100 Subject: [PATCH 4/4] chore(parser): merged envelope subtypes for return type, to fix build for middleware. --- packages/parser/src/types/parser.ts | 41 +++++++++++++++-------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/packages/parser/src/types/parser.ts b/packages/parser/src/types/parser.ts index 612816c6f0..91b423e8cc 100644 --- a/packages/parser/src/types/parser.ts +++ b/packages/parser/src/types/parser.ts @@ -1,5 +1,5 @@ import type { ZodError, ZodSchema, z } from 'zod'; -import type { ArrayEnvelope, Envelope, ObjectEnvelope } from './envelope.js'; +import type { ArrayEnvelope, Envelope } from './envelope.js'; /** * Options for the parser used in middy middleware and decorator @@ -90,35 +90,36 @@ type ParseFunction = { safeParse: true ): ParsedResult>; - // Object envelope cases - ( + // Generic envelope case + ( data: unknown, - envelope: ObjectEnvelope, + envelope: E, schema: T, safeParse?: false - ): z.infer; + ): E extends ArrayEnvelope ? z.infer[] : z.infer; - ( + ( data: unknown, - envelope: ObjectEnvelope, + envelope: E, schema: T, safeParse: true - ): ParsedResult>; - - // Array envelope cases - ( - data: unknown, - envelope: ArrayEnvelope, - schema: T, - safeParse?: false - ): z.infer[]; + ): E extends ArrayEnvelope + ? ParsedResult[]> + : ParsedResult>; - ( + // Generic envelope case with safeParse + ( data: unknown, - envelope: ArrayEnvelope, + envelope: E, schema: T, - safeParse: true - ): ParsedResult[]>; + safeParse?: S + ): S extends true + ? E extends ArrayEnvelope + ? ParsedResult[]> + : ParsedResult> + : E extends ArrayEnvelope + ? z.infer[] + : z.infer; }; export type {