From a365f8ad7c61b65ad2ffd12d27b2cbb23d9386bf Mon Sep 17 00:00:00 2001 From: Alexander Schueren Date: Tue, 19 Dec 2023 16:05:12 +0100 Subject: [PATCH 1/3] add middy middleware --- .../{eventbridge.ts => event-bridge.ts} | 0 packages/parser/src/middleware/parser.ts | 56 ++++++++ packages/parser/src/types/envelope.ts | 30 +++++ .../tests/unit/envelopes/eventbridge.test.ts | 2 +- packages/parser/tests/unit/parser.test.ts | 127 ++++++++++++++++++ 5 files changed, 214 insertions(+), 1 deletion(-) rename packages/parser/src/envelopes/{eventbridge.ts => event-bridge.ts} (100%) create mode 100644 packages/parser/src/middleware/parser.ts create mode 100644 packages/parser/src/types/envelope.ts create mode 100644 packages/parser/tests/unit/parser.test.ts 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..20f93c673a --- /dev/null +++ b/packages/parser/src/middleware/parser.ts @@ -0,0 +1,56 @@ +import { MiddyLikeRequest } from '@aws-lambda-powertools/commons/types'; +import { MiddlewareObj } from '@middy/core'; +import { ZodSchema } from 'zod'; +import { Envelope } from '../types/envelope.js'; + +interface ParserOptions { + schema: S; + envelope?: Envelope; +} + +/** + * A middiy middleware to parse your event. + * + * @exmaple + * ```typescirpt + * 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..3bbc227f59 --- /dev/null +++ b/packages/parser/src/types/envelope.ts @@ -0,0 +1,30 @@ +import { apiGatewayEnvelope } from '../envelopes/apigw.js'; +import { apiGatewayV2Envelope } from '../envelopes/apigwv2.js'; +import { cloudWatchEnvelope } from '../envelopes/cloudwatch.js'; +import { dynamoDDStreamEnvelope } from '../envelopes/dynamodb.js'; +import { kafkaEnvelope } from '../envelopes/kafka.js'; +import { kinesisEnvelope } from '../envelopes/kinesis.js'; +import { kinesisFirehoseEnvelope } from '../envelopes/kinesis-firehose.js'; +import { lambdaFunctionUrlEnvelope } from '../envelopes/lambda.js'; +import { snsEnvelope } from '../envelopes/sns.js'; +import { snsSqsEnvelope } from '../envelopes/sns.js'; +import { sqsEnvelope } from '../envelopes/sqs.js'; +import { vpcLatticeEnvelope } from '../envelopes/vpc-lattice.js'; +import { vpcLatticeV2Envelope } from '../envelopes/vpc-latticev2.js'; +import { 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..19bbfdb52d --- /dev/null +++ b/packages/parser/tests/unit/parser.test.ts @@ -0,0 +1,127 @@ +/** + * Test middelware 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, ZodSchema } from 'zod'; +import { sqsEnvelope } from '../../src/envelopes/sqs'; +import { TestSchema } from './schema/utils'; + +describe('Middleware: parser', () => { + const schema = z.object({ + name: z.string(), + age: z.number().min(18).max(99), + }); + type schema = z.infer; + const handler = async ( + event: schema | 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: schema, 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(); + }); + }); +}); From 2260f5b95e3f59f051c119bf4a1a1221873882aa Mon Sep 17 00:00:00 2001 From: Alexander Schueren Date: Wed, 20 Dec 2023 07:10:07 +0100 Subject: [PATCH 2/3] add type to imports --- packages/parser/src/middleware/parser.ts | 10 ++++----- packages/parser/src/types/envelope.ts | 28 ++++++++++++------------ 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/parser/src/middleware/parser.ts b/packages/parser/src/middleware/parser.ts index 20f93c673a..2a79ad0a3f 100644 --- a/packages/parser/src/middleware/parser.ts +++ b/packages/parser/src/middleware/parser.ts @@ -1,7 +1,7 @@ -import { MiddyLikeRequest } from '@aws-lambda-powertools/commons/types'; -import { MiddlewareObj } from '@middy/core'; -import { ZodSchema } from 'zod'; -import { Envelope } from '../types/envelope.js'; +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; @@ -12,7 +12,7 @@ interface ParserOptions { * A middiy middleware to parse your event. * * @exmaple - * ```typescirpt + * ```typescript * import { parser } from '@aws-lambda-powertools/parser/middleware'; * import middy from '@middy/core'; * import { sqsEnvelope } from '@aws-lambda-powertools/parser/envelopes/sqs;' diff --git a/packages/parser/src/types/envelope.ts b/packages/parser/src/types/envelope.ts index 3bbc227f59..81d9edfcfd 100644 --- a/packages/parser/src/types/envelope.ts +++ b/packages/parser/src/types/envelope.ts @@ -1,17 +1,17 @@ -import { apiGatewayEnvelope } from '../envelopes/apigw.js'; -import { apiGatewayV2Envelope } from '../envelopes/apigwv2.js'; -import { cloudWatchEnvelope } from '../envelopes/cloudwatch.js'; -import { dynamoDDStreamEnvelope } from '../envelopes/dynamodb.js'; -import { kafkaEnvelope } from '../envelopes/kafka.js'; -import { kinesisEnvelope } from '../envelopes/kinesis.js'; -import { kinesisFirehoseEnvelope } from '../envelopes/kinesis-firehose.js'; -import { lambdaFunctionUrlEnvelope } from '../envelopes/lambda.js'; -import { snsEnvelope } from '../envelopes/sns.js'; -import { snsSqsEnvelope } from '../envelopes/sns.js'; -import { sqsEnvelope } from '../envelopes/sqs.js'; -import { vpcLatticeEnvelope } from '../envelopes/vpc-lattice.js'; -import { vpcLatticeV2Envelope } from '../envelopes/vpc-latticev2.js'; -import { eventBridgeEnvelope } from '../envelopes/event-bridge.js'; +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 } from '../envelopes/sns.js'; +import { 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 From 5aa809f357fd9a8011b014a6ed96d5ad6e4d4a41 Mon Sep 17 00:00:00 2001 From: Alexander Schueren Date: Wed, 20 Dec 2023 08:23:30 +0100 Subject: [PATCH 3/3] remove schema type, stick with unkown --- packages/parser/src/types/envelope.ts | 3 +-- packages/parser/tests/unit/parser.test.ts | 16 ++++++---------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/packages/parser/src/types/envelope.ts b/packages/parser/src/types/envelope.ts index 81d9edfcfd..e54958dca2 100644 --- a/packages/parser/src/types/envelope.ts +++ b/packages/parser/src/types/envelope.ts @@ -6,8 +6,7 @@ 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 } from '../envelopes/sns.js'; -import { type snsSqsEnvelope } from '../envelopes/sns.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'; diff --git a/packages/parser/tests/unit/parser.test.ts b/packages/parser/tests/unit/parser.test.ts index 19bbfdb52d..979892a21c 100644 --- a/packages/parser/tests/unit/parser.test.ts +++ b/packages/parser/tests/unit/parser.test.ts @@ -1,5 +1,5 @@ /** - * Test middelware parser + * Test middleware parser * * @group unit/parser */ @@ -9,20 +9,16 @@ 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, ZodSchema } from 'zod'; +import { z, type ZodSchema } from 'zod'; import { sqsEnvelope } from '../../src/envelopes/sqs'; import { TestSchema } from './schema/utils'; describe('Middleware: parser', () => { - const schema = z.object({ - name: z.string(), - age: z.number().min(18).max(99), - }); - type schema = z.infer; + type schema = z.infer; const handler = async ( - event: schema | unknown, + event: unknown, _context: Context - ): Promise => { + ): Promise => { return event; }; @@ -33,7 +29,7 @@ describe('Middleware: parser', () => { it('should parse request body with schema and envelope', async () => { const bodyMock = generateMock(TestSchema); - parser({ schema: schema, envelope: sqsEnvelope }); + parser({ schema: TestSchema, envelope: sqsEnvelope }); const event = generateMock(SqsSchema, { stringMap: {