diff --git a/packages/parser/src/envelopes/eventbridge.ts b/packages/parser/src/envelopes/event-bridge.ts similarity index 100% rename from packages/parser/src/envelopes/eventbridge.ts rename to packages/parser/src/envelopes/event-bridge.ts diff --git a/packages/parser/src/middleware/parser.ts b/packages/parser/src/middleware/parser.ts new file mode 100644 index 0000000000..2a79ad0a3f --- /dev/null +++ b/packages/parser/src/middleware/parser.ts @@ -0,0 +1,56 @@ +import { type MiddyLikeRequest } from '@aws-lambda-powertools/commons/types'; +import { type MiddlewareObj } from '@middy/core'; +import { type ZodSchema } from 'zod'; +import { type Envelope } from '../types/envelope.js'; + +interface ParserOptions { + schema: S; + envelope?: Envelope; +} + +/** + * A middiy middleware to parse your event. + * + * @exmaple + * ```typescript + * import { parser } from '@aws-lambda-powertools/parser/middleware'; + * import middy from '@middy/core'; + * import { sqsEnvelope } from '@aws-lambda-powertools/parser/envelopes/sqs;' + * + * const oderSchema = z.object({ + * id: z.number(), + * description: z.string(), + * quantity: z.number(), + * }); + * + * 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 })); + * ``` + * + * @param options + */ +const parser = ( + options: ParserOptions +): MiddlewareObj => { + const before = (request: MiddyLikeRequest): void => { + const { schema, envelope } = options; + if (envelope) { + request.event = envelope(request.event, schema); + } else { + request.event = schema.parse(request.event); + } + }; + + return { + before, + }; +}; + +export { parser }; diff --git a/packages/parser/src/types/envelope.ts b/packages/parser/src/types/envelope.ts new file mode 100644 index 0000000000..e54958dca2 --- /dev/null +++ b/packages/parser/src/types/envelope.ts @@ -0,0 +1,29 @@ +import { type apiGatewayEnvelope } from '../envelopes/apigw.js'; +import { type apiGatewayV2Envelope } from '../envelopes/apigwv2.js'; +import { type cloudWatchEnvelope } from '../envelopes/cloudwatch.js'; +import { type dynamoDDStreamEnvelope } from '../envelopes/dynamodb.js'; +import { type kafkaEnvelope } from '../envelopes/kafka.js'; +import { type kinesisEnvelope } from '../envelopes/kinesis.js'; +import { type kinesisFirehoseEnvelope } from '../envelopes/kinesis-firehose.js'; +import { type lambdaFunctionUrlEnvelope } from '../envelopes/lambda.js'; +import { type snsEnvelope, type snsSqsEnvelope } from '../envelopes/sns.js'; +import { type sqsEnvelope } from '../envelopes/sqs.js'; +import { type vpcLatticeEnvelope } from '../envelopes/vpc-lattice.js'; +import { type vpcLatticeV2Envelope } from '../envelopes/vpc-latticev2.js'; +import { type eventBridgeEnvelope } from '../envelopes/event-bridge.js'; + +export type Envelope = + | typeof apiGatewayEnvelope + | typeof apiGatewayV2Envelope + | typeof cloudWatchEnvelope + | typeof dynamoDDStreamEnvelope + | typeof eventBridgeEnvelope + | typeof kafkaEnvelope + | typeof kinesisEnvelope + | typeof kinesisFirehoseEnvelope + | typeof lambdaFunctionUrlEnvelope + | typeof snsEnvelope + | typeof snsSqsEnvelope + | typeof sqsEnvelope + | typeof vpcLatticeEnvelope + | typeof vpcLatticeV2Envelope; diff --git a/packages/parser/tests/unit/envelopes/eventbridge.test.ts b/packages/parser/tests/unit/envelopes/eventbridge.test.ts index ee8c62a95c..8212e77d9f 100644 --- a/packages/parser/tests/unit/envelopes/eventbridge.test.ts +++ b/packages/parser/tests/unit/envelopes/eventbridge.test.ts @@ -7,7 +7,7 @@ import { TestEvents, TestSchema } from '../schema/utils.js'; import { generateMock } from '@anatine/zod-mock'; import { EventBridgeEvent } from 'aws-lambda'; -import { eventBridgeEnvelope } from '../../../src/envelopes/eventbridge.js'; +import { eventBridgeEnvelope } from '../../../src/envelopes/event-bridge.js'; describe('EventBridgeEnvelope ', () => { it('should parse eventbridge event', () => { diff --git a/packages/parser/tests/unit/parser.test.ts b/packages/parser/tests/unit/parser.test.ts new file mode 100644 index 0000000000..979892a21c --- /dev/null +++ b/packages/parser/tests/unit/parser.test.ts @@ -0,0 +1,123 @@ +/** + * Test middleware parser + * + * @group unit/parser + */ + +import middy from '@middy/core'; +import { Context } from 'aws-lambda'; +import { parser } from '../../src/middleware/parser.js'; +import { generateMock } from '@anatine/zod-mock'; +import { SqsSchema } from '../../src/schemas/sqs.js'; +import { z, type ZodSchema } from 'zod'; +import { sqsEnvelope } from '../../src/envelopes/sqs'; +import { TestSchema } from './schema/utils'; + +describe('Middleware: parser', () => { + type schema = z.infer; + const handler = async ( + event: unknown, + _context: Context + ): Promise => { + return event; + }; + + describe(' when envelope is provided ', () => { + const middyfiedHandler = middy(handler).use( + parser({ schema: TestSchema, envelope: sqsEnvelope }) + ); + + it('should parse request body with schema and envelope', async () => { + const bodyMock = generateMock(TestSchema); + parser({ schema: TestSchema, envelope: sqsEnvelope }); + + const event = generateMock(SqsSchema, { + stringMap: { + body: () => JSON.stringify(bodyMock), + }, + }); + + const result = (await middyfiedHandler(event, {} as Context)) as schema[]; + result.forEach((item) => { + expect(item).toEqual(bodyMock); + }); + }); + + it('should throw when envelope does not match', async () => { + await expect(async () => { + await middyfiedHandler({ name: 'John', age: 18 }, {} as Context); + }).rejects.toThrowError(); + }); + + it('should throw when schema does not match', async () => { + const event = generateMock(SqsSchema, { + stringMap: { + body: () => '42', + }, + }); + + await expect(middyfiedHandler(event, {} as Context)).rejects.toThrow(); + }); + it('should throw when provided schema is invalid', async () => { + const middyfiedHandler = middy(handler).use( + parser({ schema: {} as ZodSchema, envelope: sqsEnvelope }) + ); + + await expect(middyfiedHandler(42, {} as Context)).rejects.toThrowError(); + }); + it('should throw when envelope is correct but schema is invalid', async () => { + const event = generateMock(SqsSchema, { + stringMap: { + body: () => JSON.stringify({ name: 'John', foo: 'bar' }), + }, + }); + + const middyfiedHandler = middy(handler).use( + parser({ schema: {} as ZodSchema, envelope: sqsEnvelope }) + ); + + await expect( + middyfiedHandler(event, {} as Context) + ).rejects.toThrowError(); + }); + }); + + describe(' when envelope is not provided', () => { + it('should parse the event with built-in schema', async () => { + const event = generateMock(SqsSchema); + + const middyfiedHandler = middy(handler).use( + parser({ schema: SqsSchema }) + ); + + expect(await middyfiedHandler(event, {} as Context)).toEqual(event); + }); + + it('should parse custom event', async () => { + const event = { name: 'John', age: 18 }; + const middyfiedHandler = middy(handler).use( + parser({ schema: TestSchema }) + ); + + expect(await middyfiedHandler(event, {} as Context)).toEqual(event); + }); + + it('should throw when the schema does not match', async () => { + const middyfiedHandler = middy(handler).use( + parser({ schema: TestSchema }) + ); + + await expect(middyfiedHandler(42, {} as Context)).rejects.toThrow(); + }); + + it('should throw when provided schema is invalid', async () => { + const middyfiedHandler = middy(handler).use( + parser({ schema: {} as ZodSchema }) + ); + + await expect( + middyfiedHandler({ foo: 'bar' }, {} as Context) + ).rejects.toThrowError(); + }); + }); +});