Skip to content

feat(parser): allow parser set event type of handler with middy #2786

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/parser/src/envelopes/cloudwatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export class CloudWatchEnvelope extends Envelope {
public static parse<T extends ZodSchema>(
data: unknown,
schema: T
): z.infer<T> {
): z.infer<T>[] {
const parsedEnvelope = CloudWatchLogsSchema.parse(data);

return parsedEnvelope.awslogs.data.logEvents.map((record) => {
Expand Down
6 changes: 1 addition & 5 deletions packages/parser/src/envelopes/dynamodb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,7 @@ import { DynamoDBStreamSchema } from '../schemas/index.js';
import type { ParsedResult, ParsedResultError } from '../types/index.js';
import { Envelope } from './envelope.js';
import { ParseError } from '../errors.js';

type DynamoDBStreamEnvelopeResponse<T extends ZodSchema> = {
NewImage: z.infer<T>;
OldImage: z.infer<T>;
};
import type { DynamoDBStreamEnvelopeResponse } from '../types/envelope.js';

/**
* DynamoDB Stream Envelope to extract data within NewImage/OldImage
Expand Down
2 changes: 1 addition & 1 deletion packages/parser/src/envelopes/kafka.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export class KafkaEnvelope extends Envelope {
public static parse<T extends ZodSchema>(
data: unknown,
schema: T
): z.infer<T> {
): z.infer<T>[] {
// manually fetch event source to deside between Msk or SelfManaged
const eventSource = (data as KafkaMskEvent)['eventSource'];

Expand Down
2 changes: 1 addition & 1 deletion packages/parser/src/envelopes/kinesis-firehose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export class KinesisFirehoseEnvelope extends Envelope {
public static parse<T extends ZodSchema>(
data: unknown,
schema: T
): z.infer<T> {
): z.infer<T>[] {
const parsedEnvelope = KinesisFirehoseSchema.parse(data);

return parsedEnvelope.records.map((record) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/parser/src/envelopes/kinesis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export class KinesisEnvelope extends Envelope {
public static parse<T extends ZodSchema>(
data: unknown,
schema: T
): z.infer<T> {
): z.infer<T>[] {
const parsedEnvelope = KinesisDataStreamSchema.parse(data);

return parsedEnvelope.Records.map((record) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/parser/src/envelopes/sns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export class SnsEnvelope extends Envelope {
public static parse<T extends ZodSchema>(
data: unknown,
schema: T
): z.infer<T> {
): z.infer<T>[] {
const parsedEnvelope = SnsSchema.parse(data);

return parsedEnvelope.Records.map((record) => {
Expand Down
4 changes: 2 additions & 2 deletions packages/parser/src/envelopes/sqs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ import { ParseError } from '../errors.js';
*
* The record's body parameter is a string, though it can also be a JSON encoded string.
* Regardless of its type it'll be parsed into a BaseModel object.
*
*w
* Note: Records will be parsed the same way so if model is str,
* all items in the list will be parsed as str and npt as JSON (and vice versa)
*/
export class SqsEnvelope extends Envelope {
public static parse<T extends ZodSchema>(
data: unknown,
schema: T
): z.infer<T> {
): z.infer<T>[] {
const parsedEnvelope = SqsSchema.parse(data);

return parsedEnvelope.Records.map((record) => {
Expand Down
15 changes: 10 additions & 5 deletions packages/parser/src/middleware/parser.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { type MiddyLikeRequest } from '@aws-lambda-powertools/commons/types';
import { type MiddlewareObj } from '@middy/core';
import { type ZodSchema } from 'zod';
import { type ParserOptions } from '../types/parser.js';
import { ZodType } from 'zod';
import type { ParserOptions, ParseOutput } from '../types/parser.js';
import { parse } from '../parser.js';
import type { Envelope } from '../types/envelope.js';

/**
* A middiy middleware to parse your event.
Expand Down Expand Up @@ -32,9 +33,13 @@ import { parse } from '../parser.js';
*
* @param options
*/
const parser = <S extends ZodSchema>(
options: ParserOptions<S>
): MiddlewareObj => {
const parser = <
TSchema extends ZodType,
TEnvelope extends Envelope = undefined,
TSafeParse extends boolean = false,
>(
options: ParserOptions<TSchema, TEnvelope, TSafeParse>
): MiddlewareObj<ParseOutput<TSchema, TEnvelope, TSafeParse>> => {
const before = (request: MiddyLikeRequest): void => {
const { schema, envelope, safeParse } = options;

Expand Down
20 changes: 11 additions & 9 deletions packages/parser/src/parserDecorator.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { HandlerMethodDecorator } from '@aws-lambda-powertools/commons/types';
import type { Context, Handler } from 'aws-lambda';
import { ZodSchema, z } from 'zod';
import { type ZodSchema } from 'zod';
import { parse } from './parser.js';
import type { ParserOptions, ParsedResult } from './types/index.js';
import type { ParserOptions, Envelope } from './types/index.js';
import type { ParseOutput } from './types/parser.js';

/**
* A decorator to parse your event.
Expand Down Expand Up @@ -67,8 +68,12 @@ import type { ParserOptions, ParsedResult } from './types/index.js';
*
* @param options Configure the parser with the `schema`, `envelope` and whether to `safeParse` or not
*/
export const parser = <S extends ZodSchema>(
options: ParserOptions<S>
export const parser = <
TSchema extends ZodSchema,
TEnvelope extends Envelope = undefined,
TSafeParse extends boolean = false,
>(
options: ParserOptions<TSchema, TEnvelope, TSafeParse>
): HandlerMethodDecorator => {
return (_target, _propertyKey, descriptor) => {
const original = descriptor.value!;
Expand All @@ -77,14 +82,11 @@ export const parser = <S extends ZodSchema>(

descriptor.value = async function (
this: Handler,
event: unknown,
event: ParseOutput<TSchema, TEnvelope, TSafeParse>,
context: Context,
callback
) {
const parsedEvent: ParsedResult<
typeof event,
z.infer<typeof schema>
> = parse(event, envelope, schema, safeParse);
const parsedEvent = parse(event, envelope, schema, safeParse);

return original.call(this, parsedEvent, context, callback);
};
Expand Down
29 changes: 27 additions & 2 deletions packages/parser/src/types/envelope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,14 @@ import type {
VpcLatticeEnvelope,
VpcLatticeV2Envelope,
} from '../envelopes/index.js';
import { z, type ZodSchema } from 'zod';

export type Envelope =
type DynamoDBStreamEnvelopeResponse<Schema extends ZodSchema> = {
NewImage: z.infer<Schema>;
OldImage: z.infer<Schema>;
};

type Envelope =
| typeof ApiGatewayEnvelope
| typeof ApiGatewayV2Envelope
| typeof CloudWatchEnvelope
Expand All @@ -29,4 +35,23 @@ export type Envelope =
| typeof SnsSqsEnvelope
| typeof SqsEnvelope
| typeof VpcLatticeEnvelope
| typeof VpcLatticeV2Envelope;
| typeof VpcLatticeV2Envelope
| undefined;

/**
* Envelopes that return an array, needed to narrow down the return type of the parser
*/
type EnvelopeArrayReturnType =
| typeof CloudWatchEnvelope
| typeof DynamoDBStreamEnvelope
| typeof KafkaEnvelope
| typeof KinesisEnvelope
| typeof KinesisFirehoseEnvelope
| typeof SnsEnvelope
| typeof SqsEnvelope;

export type {
Envelope,
DynamoDBStreamEnvelopeResponse,
EnvelopeArrayReturnType,
};
53 changes: 47 additions & 6 deletions packages/parser/src/types/parser.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import type { ZodSchema, ZodError } from 'zod';
import type { Envelope } from './envelope.js';
import { type ZodSchema, type ZodError, z } from 'zod';
import type { Envelope, EnvelopeArrayReturnType } from './envelope.js';

/**
* Options for the parser used in middy middleware and decorator
*/
type ParserOptions<S extends ZodSchema> = {
schema: S;
envelope?: Envelope;
safeParse?: boolean;
type ParserOptions<
TSchema extends ZodSchema,
TEnvelope extends Envelope,
TSafeParse extends boolean,
> = {
schema: TSchema;
envelope?: TEnvelope;
safeParse?: TSafeParse;
};

/**
Expand All @@ -34,9 +38,46 @@ type ParsedResult<Input = unknown, Output = unknown> =
| ParsedResultSuccess<Output>
| ParsedResultError<Input>;

/**
* The inferred result of the schema, can be either an array or a single object depending on the envelope
*/
type ZodInferredResult<
TSchema extends ZodSchema,
TEnvelope extends Envelope,
> = undefined extends TEnvelope
? z.infer<TSchema>
: TEnvelope extends EnvelopeArrayReturnType
? z.infer<TSchema>[]
: z.infer<TSchema>;

type ZodInferredSafeParseResult<
TSchema extends ZodSchema,
TEnvelope extends Envelope,
> = undefined extends TEnvelope
? ParsedResult<unknown, z.infer<TSchema>>
: TEnvelope extends EnvelopeArrayReturnType
? ParsedResult<unknown, z.infer<TSchema>>
: ParsedResult<unknown, z.infer<TSchema>[]>;

/**
* The output of the parser function, can be either schema inferred type or a ParsedResult
*/
type ParseOutput<
TSchema extends ZodSchema,
TEnvelope extends Envelope,
TSafeParse = false,
> = undefined extends TSafeParse
? ZodInferredResult<TSchema, TEnvelope>
: TSafeParse extends true
? ZodInferredSafeParseResult<TSchema, TEnvelope>
: TSafeParse extends false
? ZodInferredResult<TSchema, TEnvelope>
: never;

export type {
ParserOptions,
ParsedResult,
ParsedResultError,
ParsedResultSuccess,
ParseOutput,
};
31 changes: 11 additions & 20 deletions packages/parser/tests/unit/parser.decorator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe('Parser Decorator', () => {
public async handler(
event: TestEvent,
_context: Context
): Promise<unknown> {
): Promise<TestEvent> {
return event;
}

Expand Down Expand Up @@ -60,7 +60,7 @@ describe('Parser Decorator', () => {
safeParse: true,
})
public async handlerWithSchemaAndSafeParse(
event: ParsedResult<TestEvent, TestEvent>,
event: ParsedResult<unknown, TestEvent>,
_context: Context
): Promise<ParsedResult> {
return event;
Expand Down Expand Up @@ -99,9 +99,7 @@ describe('Parser Decorator', () => {
testEvent.detail = customPayload;

const resp = await lambda.handlerWithSchemaAndEnvelope(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
testEvent,
testEvent as unknown as TestEvent,
{} as Context
);

Expand Down Expand Up @@ -130,9 +128,7 @@ describe('Parser Decorator', () => {
testEvent.detail = customPayload;

const resp = await lambda.handlerWithParserCallsAnotherMethod(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
testEvent,
testEvent as unknown as TestEvent,
{} as Context
);

Expand All @@ -143,9 +139,7 @@ describe('Parser Decorator', () => {
const testEvent = generateMock(TestSchema);

const resp = await lambda.handlerWithSchemaAndSafeParse(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
testEvent,
testEvent as unknown as ParsedResult<unknown, TestEvent>,
{} as Context
);

Expand All @@ -157,9 +151,10 @@ describe('Parser Decorator', () => {

it('should parse event with schema and safeParse and return error', async () => {
expect(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
await lambda.handlerWithSchemaAndSafeParse({ foo: 'bar' }, {} as Context)
await lambda.handlerWithSchemaAndSafeParse(
{ foo: 'bar' } as unknown as ParsedResult<unknown, TestEvent>,
{} as Context
)
).toEqual({
error: expect.any(ParseError),
success: false,
Expand All @@ -173,9 +168,7 @@ describe('Parser Decorator', () => {
event.detail = testEvent;

const resp = await lambda.harndlerWithEnvelopeAndSafeParse(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
event,
event as unknown as ParsedResult<TestEvent, TestEvent>,
{} as Context
);

Expand All @@ -188,9 +181,7 @@ describe('Parser Decorator', () => {
it('should parse event with envelope and safeParse and return error', async () => {
expect(
await lambda.harndlerWithEnvelopeAndSafeParse(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
{ foo: 'bar' },
{ foo: 'bar' } as unknown as ParsedResult<TestEvent, TestEvent>,
{} as Context
)
).toEqual({
Expand Down
Loading