From a3c7cb137a2239253fdfc5edc8915161dd2fc74a Mon Sep 17 00:00:00 2001 From: Alexander Schueren Date: Wed, 22 Nov 2023 14:43:55 +0100 Subject: [PATCH 01/19] first envelope --- package-lock.json | 66 ++++++++++++++++++- packages/parser/package.json | 7 +- packages/parser/src/envelopes/sqs.ts | 16 +++++ .../parser/tests/unit/envelopes/sqs.test.ts | 29 ++++++++ 4 files changed, 115 insertions(+), 3 deletions(-) create mode 100644 packages/parser/src/envelopes/sqs.ts create mode 100644 packages/parser/tests/unit/envelopes/sqs.test.ts diff --git a/package-lock.json b/package-lock.json index 2b28ed8c51..07b8a5b164 100644 --- a/package-lock.json +++ b/package-lock.json @@ -196,6 +196,19 @@ "node": ">=6.0.0" } }, + "node_modules/@anatine/zod-mock": { + "version": "3.13.3", + "resolved": "https://registry.npmjs.org/@anatine/zod-mock/-/zod-mock-3.13.3.tgz", + "integrity": "sha512-AN+0YEFE7s6BpuALQHhEoVmJmD+0gPnf4Fehc6oE5NHbM3X2ZD5fW5M6vvot29NWUB6nxvj0gu+BPQ9cVnxALw==", + "dev": true, + "dependencies": { + "randexp": "^0.5.3" + }, + "peerDependencies": { + "@faker-js/faker": "^7.0.0 || ^8.0.0", + "zod": "^3.21.4" + } + }, "node_modules/@aws-cdk/asset-awscli-v1": { "version": "2.2.200", "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.200.tgz", @@ -2291,6 +2304,22 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@faker-js/faker": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.3.1.tgz", + "integrity": "sha512-FdgpFxY6V6rLZE9mmIBb9hM0xpfvQOSNOLnzolzKwsE1DH+gC7lEKV1p1IbR0lAYyvYd5a4u3qWJzowUkw1bIw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0", + "npm": ">=6.14.13" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.11", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", @@ -7597,6 +7626,15 @@ "node": ">=12" } }, + "node_modules/drange": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/drange/-/drange-1.1.1.tgz", + "integrity": "sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -14053,6 +14091,19 @@ "node": ">=8" } }, + "node_modules/randexp": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.5.3.tgz", + "integrity": "sha512-U+5l2KrcMNOUPYvazA3h5ekF80FHTUG+87SEAmHZmolh1M+i/WyTCxVzmi+tidIa1tM4BSe8g2Y/D3loWDjj+w==", + "dev": true, + "dependencies": { + "drange": "^1.0.2", + "ret": "^0.2.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -14545,6 +14596,15 @@ "node": ">=8" } }, + "node_modules/ret": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz", + "integrity": "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -16763,8 +16823,12 @@ "name": "@aws-lambda-powertools/parser", "version": "0.0.0", "license": "MIT-0", + "devDependencies": { + "@anatine/zod-mock": "^3.13.3", + "@faker-js/faker": "^8.3.1" + }, "peerDependencies": { - "zod": "^3.22.4" + "zod": ">=3.x" } }, "packages/testing": { diff --git a/packages/parser/package.json b/packages/parser/package.json index 6dd191ca17..fb4b54fe51 100644 --- a/packages/parser/package.json +++ b/packages/parser/package.json @@ -60,8 +60,11 @@ "serverless", "nodejs" ], - "peerDependencies": { "zod": ">=3.x" + }, + "devDependencies": { + "@anatine/zod-mock": "^3.13.3", + "@faker-js/faker": "^8.3.1" } -} \ No newline at end of file +} diff --git a/packages/parser/src/envelopes/sqs.ts b/packages/parser/src/envelopes/sqs.ts new file mode 100644 index 0000000000..9429270bfd --- /dev/null +++ b/packages/parser/src/envelopes/sqs.ts @@ -0,0 +1,16 @@ +import { ZodSchema } from 'zod'; +import { SqsSchema } from '../schemas/sqs.js'; + +class SqsEnvelope { + public parse(data: unknown, schema: T): unknown[] { + const parsedEnvelope = SqsSchema.parse(data); + + return parsedEnvelope.Records.map((record) => { + const body = JSON.parse(record.body); + + return schema.parse(body); + }); + } +} + +export { SqsEnvelope }; diff --git a/packages/parser/tests/unit/envelopes/sqs.test.ts b/packages/parser/tests/unit/envelopes/sqs.test.ts new file mode 100644 index 0000000000..6432e3313f --- /dev/null +++ b/packages/parser/tests/unit/envelopes/sqs.test.ts @@ -0,0 +1,29 @@ +/** + * Test built in schema + * + * @group unit/parser/envelopes/ + */ + +import { z } from 'zod'; +import { generateMock } from '@anatine/zod-mock'; +import { SqsRecordSchema } from '../../../src/schemas/sqs.js'; +import { SqsEnvelope } from '../../../src/envelopes/sqs.js'; + +describe('SQS', () => { + it('should parse custom schema in envelope', () => { + const schema = z.object({ + name: z.string(), + age: z.number().min(18).max(99), + }); + + const testCustomSchemaObject = generateMock(schema); + const mock = generateMock(SqsRecordSchema, { + stringMap: { + body: () => JSON.stringify(testCustomSchemaObject), + }, + }); + + const resp = new SqsEnvelope().parse({ Records: [mock] }, schema); + expect(resp).toEqual([testCustomSchemaObject]); + }); +}); From a9e3e040cd1f0e9cd5a516093b62b5fe5e085c29 Mon Sep 17 00:00:00 2001 From: Alexander Schueren Date: Fri, 24 Nov 2023 09:35:19 +0100 Subject: [PATCH 02/19] add abstract class --- packages/parser/src/envelopes/Envelope.ts | 20 ++++++++ packages/parser/src/envelopes/SqsEnvelope.ts | 20 ++++++++ packages/parser/src/envelopes/sqs.ts | 16 ------ .../tests/unit/envelopes/SqsEnvelope.test.ts | 49 +++++++++++++++++++ .../parser/tests/unit/envelopes/sqs.test.ts | 29 ----------- 5 files changed, 89 insertions(+), 45 deletions(-) create mode 100644 packages/parser/src/envelopes/Envelope.ts create mode 100644 packages/parser/src/envelopes/SqsEnvelope.ts delete mode 100644 packages/parser/src/envelopes/sqs.ts create mode 100644 packages/parser/tests/unit/envelopes/SqsEnvelope.test.ts delete mode 100644 packages/parser/tests/unit/envelopes/sqs.test.ts diff --git a/packages/parser/src/envelopes/Envelope.ts b/packages/parser/src/envelopes/Envelope.ts new file mode 100644 index 0000000000..4343b9bf7a --- /dev/null +++ b/packages/parser/src/envelopes/Envelope.ts @@ -0,0 +1,20 @@ +import { z, ZodSchema } from 'zod'; + +export abstract class Envelope { + protected static _parse( + data: unknown, + schema: T + ): z.infer { + if (!schema) throw new Error('Schema is required'); + if (!data) return data; + + return schema.parse(data); + } + + protected static parse( + _data: unknown, + _schema: T + ): z.infer { + throw new Error('Not implemented'); + } +} diff --git a/packages/parser/src/envelopes/SqsEnvelope.ts b/packages/parser/src/envelopes/SqsEnvelope.ts new file mode 100644 index 0000000000..eae9eef736 --- /dev/null +++ b/packages/parser/src/envelopes/SqsEnvelope.ts @@ -0,0 +1,20 @@ +import { z, ZodSchema } from 'zod'; +import { SqsSchema } from '../schemas/sqs.js'; +import { Envelope } from './Envelope.js'; + +class SqsEnvelope extends Envelope { + public static parse( + data: unknown, + schema: T + ): z.infer[] { + const parsedEnvelope = SqsSchema.parse(data); + + return parsedEnvelope.Records.map((record) => { + const body = JSON.parse(record.body); + + return this._parse(body, schema); + }); + } +} + +export { SqsEnvelope }; diff --git a/packages/parser/src/envelopes/sqs.ts b/packages/parser/src/envelopes/sqs.ts deleted file mode 100644 index 9429270bfd..0000000000 --- a/packages/parser/src/envelopes/sqs.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { ZodSchema } from 'zod'; -import { SqsSchema } from '../schemas/sqs.js'; - -class SqsEnvelope { - public parse(data: unknown, schema: T): unknown[] { - const parsedEnvelope = SqsSchema.parse(data); - - return parsedEnvelope.Records.map((record) => { - const body = JSON.parse(record.body); - - return schema.parse(body); - }); - } -} - -export { SqsEnvelope }; diff --git a/packages/parser/tests/unit/envelopes/SqsEnvelope.test.ts b/packages/parser/tests/unit/envelopes/SqsEnvelope.test.ts new file mode 100644 index 0000000000..82e4ce9550 --- /dev/null +++ b/packages/parser/tests/unit/envelopes/SqsEnvelope.test.ts @@ -0,0 +1,49 @@ +/** + * Test built in schema + * + * @group unit/parser/envelopes/ + */ + +import { z } from 'zod'; +import { generateMock } from '@anatine/zod-mock'; +import { SqsRecordSchema } from '../../../src/schemas/sqs.js'; +import { SqsEnvelope } from '../../../src/envelopes/SqsEnvelope.js'; + +describe('SqsEnvelope', () => { + const schema = z.object({ + name: z.string(), + age: z.number().min(18).max(99), + }); + + it('should parse custom schema in envelope', () => { + const testCustomSchemaObject = generateMock(schema); + const mock = generateMock(SqsRecordSchema, { + stringMap: { + body: () => JSON.stringify(testCustomSchemaObject), + }, + }); + + const resp = SqsEnvelope.parse({ Records: [mock] }, schema); + expect(resp).toEqual([testCustomSchemaObject]); + }); + + it('should throw error if invalid schema', () => { + expect(() => { + SqsEnvelope.parse({ Records: [{ foo: 'bar' }] }, schema); + }).toThrow(); + + expect(() => { + SqsEnvelope.parse( + { + Records: [ + { + name: 'foo', + age: 17, + }, + ], + }, + schema + ); + }); + }); +}); diff --git a/packages/parser/tests/unit/envelopes/sqs.test.ts b/packages/parser/tests/unit/envelopes/sqs.test.ts deleted file mode 100644 index 6432e3313f..0000000000 --- a/packages/parser/tests/unit/envelopes/sqs.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Test built in schema - * - * @group unit/parser/envelopes/ - */ - -import { z } from 'zod'; -import { generateMock } from '@anatine/zod-mock'; -import { SqsRecordSchema } from '../../../src/schemas/sqs.js'; -import { SqsEnvelope } from '../../../src/envelopes/sqs.js'; - -describe('SQS', () => { - it('should parse custom schema in envelope', () => { - const schema = z.object({ - name: z.string(), - age: z.number().min(18).max(99), - }); - - const testCustomSchemaObject = generateMock(schema); - const mock = generateMock(SqsRecordSchema, { - stringMap: { - body: () => JSON.stringify(testCustomSchemaObject), - }, - }); - - const resp = new SqsEnvelope().parse({ Records: [mock] }, schema); - expect(resp).toEqual([testCustomSchemaObject]); - }); -}); From f8a346e71d13a94b4a9257c62c356210ac960cab Mon Sep 17 00:00:00 2001 From: Alexander Schueren Date: Fri, 24 Nov 2023 15:19:51 +0100 Subject: [PATCH 03/19] add tests --- packages/parser/src/envelopes/Envelope.ts | 27 ++++++---- packages/parser/src/envelopes/SqsEnvelope.ts | 24 ++++++--- packages/parser/src/middleware/parser.ts | 32 +++++++++++ packages/parser/tests/unit/parser.test.ts | 57 ++++++++++++++++++++ 4 files changed, 123 insertions(+), 17 deletions(-) create mode 100644 packages/parser/src/middleware/parser.ts create mode 100644 packages/parser/tests/unit/parser.test.ts diff --git a/packages/parser/src/envelopes/Envelope.ts b/packages/parser/src/envelopes/Envelope.ts index 4343b9bf7a..fa12059aa4 100644 --- a/packages/parser/src/envelopes/Envelope.ts +++ b/packages/parser/src/envelopes/Envelope.ts @@ -1,20 +1,25 @@ import { z, ZodSchema } from 'zod'; export abstract class Envelope { - protected static _parse( + protected constructor() {} + + public abstract parse( + data: unknown, + _schema: T + ): z.infer; + + protected _parse( data: unknown, schema: T - ): z.infer { - if (!schema) throw new Error('Schema is required'); - if (!data) return data; + ): z.infer[] { + if (typeof data !== 'object') { + throw new Error('Data must be an object'); + } - return schema.parse(data); - } + if (!schema) { + throw new Error('Schema must be provided'); + } - protected static parse( - _data: unknown, - _schema: T - ): z.infer { - throw new Error('Not implemented'); + return schema.parse(data); } } diff --git a/packages/parser/src/envelopes/SqsEnvelope.ts b/packages/parser/src/envelopes/SqsEnvelope.ts index eae9eef736..bc20f024d9 100644 --- a/packages/parser/src/envelopes/SqsEnvelope.ts +++ b/packages/parser/src/envelopes/SqsEnvelope.ts @@ -3,18 +3,30 @@ import { SqsSchema } from '../schemas/sqs.js'; import { Envelope } from './Envelope.js'; class SqsEnvelope extends Envelope { - public static parse( - data: unknown, - schema: T - ): z.infer[] { + public constructor() { + super(); + } + + public parse(data: unknown, schema: T): z.infer[] { + if (typeof data !== 'object') { + throw new Error('Data must be an object'); + } + + if (!schema) { + throw new Error('Schema must be provided'); + } const parsedEnvelope = SqsSchema.parse(data); return parsedEnvelope.Records.map((record) => { const body = JSON.parse(record.body); - return this._parse(body, schema); + return schema.parse(body); }); } } -export { SqsEnvelope }; +class Envelopes { + public static readonly SQS_ENVELOPE = new SqsEnvelope(); +} + +export { SqsEnvelope, Envelopes }; diff --git a/packages/parser/src/middleware/parser.ts b/packages/parser/src/middleware/parser.ts new file mode 100644 index 0000000000..0df4401aea --- /dev/null +++ b/packages/parser/src/middleware/parser.ts @@ -0,0 +1,32 @@ +import { MiddyLikeRequest } from '@aws-lambda-powertools/commons/types'; +import { MiddlewareObj } from '@middy/core'; +import { ZodSchema } from 'zod'; +import { Envelope } from '../envelopes/Envelope.js'; + +interface ParserOptions { + schema: ZodSchema; + envelope?: Envelope; +} + +const parser = (options: ParserOptions): MiddlewareObj => { + const before = (request: MiddyLikeRequest): void => { + const { schema, envelope } = options; + if (envelope) { + request.event = envelope.parse(request.event, schema); + } else { + request.event = schema.parse(request.event); + } + }; + + const after = (_request: MiddyLikeRequest): void => {}; + + const onError = (_request: MiddyLikeRequest): void => {}; + + return { + before, + after, + onError, + }; +}; + +export { parser }; diff --git a/packages/parser/tests/unit/parser.test.ts b/packages/parser/tests/unit/parser.test.ts new file mode 100644 index 0000000000..65d238dd8c --- /dev/null +++ b/packages/parser/tests/unit/parser.test.ts @@ -0,0 +1,57 @@ +/** + * 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 { Envelopes } from '../../src/envelopes/SqsEnvelope.js'; +import { z } from 'zod'; + +describe('Middleware: parser', () => { + const schema = z.object({ + name: z.string(), + age: z.number().min(18).max(99), + }); + const handler = async ( + event: unknown, + _context: Context + ): Promise | unknown> => { + return event; + }; + + it('should parse the event with built-in schema', async () => { + const event = generateMock(SqsSchema); + + const handler = middy(async (event: unknown, _context: Context) => { + return event; + }).use(parser({ schema: SqsSchema })); + const result = await handler(event, {} as Context); + expect(result).toEqual(event); + }); + + it('should parse request body with schema and envelope', async () => { + const bodyMock = generateMock(schema); + + const event = generateMock(SqsSchema, { + stringMap: { + body: () => JSON.stringify(bodyMock), + }, + }); + + const middyfiedHandler = middy(handler).use( + parser({ schema: schema, envelope: Envelopes.SQS_ENVELOPE }) + ); + + const result = (await middyfiedHandler(event, {} as Context)) as z.infer< + typeof schema + >[]; + result.forEach((item) => { + expect(item).toEqual(bodyMock); + }); + }); +}); From c760e426a6b1dc10e18ac037835c23ac8a31421d Mon Sep 17 00:00:00 2001 From: Alexander Schueren Date: Sat, 25 Nov 2023 11:16:31 +0100 Subject: [PATCH 04/19] add more tests --- packages/parser/src/envelopes/SqsEnvelope.ts | 9 +- packages/parser/tests/unit/parser.test.ts | 96 +++++++++++++++----- 2 files changed, 73 insertions(+), 32 deletions(-) diff --git a/packages/parser/src/envelopes/SqsEnvelope.ts b/packages/parser/src/envelopes/SqsEnvelope.ts index bc20f024d9..2972d9708a 100644 --- a/packages/parser/src/envelopes/SqsEnvelope.ts +++ b/packages/parser/src/envelopes/SqsEnvelope.ts @@ -8,19 +8,12 @@ class SqsEnvelope extends Envelope { } public parse(data: unknown, schema: T): z.infer[] { - if (typeof data !== 'object') { - throw new Error('Data must be an object'); - } - - if (!schema) { - throw new Error('Schema must be provided'); - } const parsedEnvelope = SqsSchema.parse(data); return parsedEnvelope.Records.map((record) => { const body = JSON.parse(record.body); - return schema.parse(body); + return this._parse(body, schema); }); } } diff --git a/packages/parser/tests/unit/parser.test.ts b/packages/parser/tests/unit/parser.test.ts index 65d238dd8c..c864712a29 100644 --- a/packages/parser/tests/unit/parser.test.ts +++ b/packages/parser/tests/unit/parser.test.ts @@ -10,48 +10,96 @@ import { parser } from '../../src/middleware/parser.js'; import { generateMock } from '@anatine/zod-mock'; import { SqsSchema } from '../../src/schemas/sqs.js'; import { Envelopes } from '../../src/envelopes/SqsEnvelope.js'; -import { z } from 'zod'; +import { z, ZodSchema } from 'zod'; 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: unknown, + event: schema | unknown, _context: Context - ): Promise | unknown> => { + ): Promise => { return event; }; - it('should parse the event with built-in schema', async () => { - const event = generateMock(SqsSchema); + describe(' when envelope is provided', () => { + const middyfiedHandler = middy(handler).use( + parser({ schema: schema, envelope: Envelopes.SQS_ENVELOPE }) + ); + + it('should parse request body with schema and envelope', async () => { + const bodyMock = generateMock(schema); + + 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 the 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: Envelopes.SQS_ENVELOPE }) + ); - const handler = middy(async (event: unknown, _context: Context) => { - return event; - }).use(parser({ schema: SqsSchema })); - const result = await handler(event, {} as Context); - expect(result).toEqual(event); + await expect(middyfiedHandler(42, {} as Context)).rejects.toThrowError(); + }); }); - it('should parse request body with schema and envelope', async () => { - const bodyMock = generateMock(schema); + 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 }) + ); - const event = generateMock(SqsSchema, { - stringMap: { - body: () => JSON.stringify(bodyMock), - }, + expect(await middyfiedHandler(event, {} as Context)).toEqual(event); }); - const middyfiedHandler = middy(handler).use( - parser({ schema: schema, envelope: Envelopes.SQS_ENVELOPE }) - ); + it('should parse custom event', async () => { + const event = { name: 'John', age: 18 }; + const middyfiedHandler = middy(handler).use(parser({ schema })); + + 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 })); + + 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 }) + ); - const result = (await middyfiedHandler(event, {} as Context)) as z.infer< - typeof schema - >[]; - result.forEach((item) => { - expect(item).toEqual(bodyMock); + await expect( + middyfiedHandler({ foo: 'bar' }, {} as Context) + ).rejects.toThrowError(); }); }); }); From 4f821254111786c1e5e8d81626aafce09050a96e Mon Sep 17 00:00:00 2001 From: Alexander Schueren Date: Sat, 25 Nov 2023 11:35:05 +0100 Subject: [PATCH 05/19] fix tests --- packages/parser/src/envelopes/Envelope.ts | 6 +----- .../tests/unit/envelopes/SqsEnvelope.test.ts | 12 ++++++----- packages/parser/tests/unit/parser.test.ts | 20 +++++++++++++++++-- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/packages/parser/src/envelopes/Envelope.ts b/packages/parser/src/envelopes/Envelope.ts index fa12059aa4..415a60f739 100644 --- a/packages/parser/src/envelopes/Envelope.ts +++ b/packages/parser/src/envelopes/Envelope.ts @@ -5,7 +5,7 @@ export abstract class Envelope { public abstract parse( data: unknown, - _schema: T + _schema: z.ZodSchema ): z.infer; protected _parse( @@ -16,10 +16,6 @@ export abstract class Envelope { throw new Error('Data must be an object'); } - if (!schema) { - throw new Error('Schema must be provided'); - } - return schema.parse(data); } } diff --git a/packages/parser/tests/unit/envelopes/SqsEnvelope.test.ts b/packages/parser/tests/unit/envelopes/SqsEnvelope.test.ts index 82e4ce9550..4dcd07291c 100644 --- a/packages/parser/tests/unit/envelopes/SqsEnvelope.test.ts +++ b/packages/parser/tests/unit/envelopes/SqsEnvelope.test.ts @@ -7,14 +7,16 @@ import { z } from 'zod'; import { generateMock } from '@anatine/zod-mock'; import { SqsRecordSchema } from '../../../src/schemas/sqs.js'; -import { SqsEnvelope } from '../../../src/envelopes/SqsEnvelope.js'; +import { Envelopes } from '../../../src/envelopes/SqsEnvelope.js'; -describe('SqsEnvelope', () => { +describe('SqsEnvelope ', () => { const schema = z.object({ name: z.string(), age: z.number().min(18).max(99), }); + const envelope = Envelopes.SQS_ENVELOPE; + it('should parse custom schema in envelope', () => { const testCustomSchemaObject = generateMock(schema); const mock = generateMock(SqsRecordSchema, { @@ -23,17 +25,17 @@ describe('SqsEnvelope', () => { }, }); - const resp = SqsEnvelope.parse({ Records: [mock] }, schema); + const resp = envelope.parse({ Records: [mock] }, schema); expect(resp).toEqual([testCustomSchemaObject]); }); it('should throw error if invalid schema', () => { expect(() => { - SqsEnvelope.parse({ Records: [{ foo: 'bar' }] }, schema); + envelope.parse({ Records: [{ foo: 'bar' }] }, schema); }).toThrow(); expect(() => { - SqsEnvelope.parse( + envelope.parse( { Records: [ { diff --git a/packages/parser/tests/unit/parser.test.ts b/packages/parser/tests/unit/parser.test.ts index c864712a29..16097eb3e8 100644 --- a/packages/parser/tests/unit/parser.test.ts +++ b/packages/parser/tests/unit/parser.test.ts @@ -25,7 +25,7 @@ describe('Middleware: parser', () => { return event; }; - describe(' when envelope is provided', () => { + describe(' when envelope is provided ', () => { const middyfiedHandler = middy(handler).use( parser({ schema: schema, envelope: Envelopes.SQS_ENVELOPE }) ); @@ -50,7 +50,8 @@ describe('Middleware: parser', () => { await middyfiedHandler({ name: 'John', age: 18 }, {} as Context); }).rejects.toThrowError(); }); - it('should throw when the schema does not match', async () => { + + it('should throw when schema does not match', async () => { const event = generateMock(SqsSchema, { stringMap: { body: () => '42', @@ -66,6 +67,21 @@ describe('Middleware: parser', () => { 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: Envelopes.SQS_ENVELOPE }) + ); + + await expect( + middyfiedHandler(event, {} as Context) + ).rejects.toThrowError(); + }); }); describe(' when envelope is not provided', () => { From 20ba3a1a79ab793bc3108dd93e2c7c7069f0325a Mon Sep 17 00:00:00 2001 From: Alexander Schueren Date: Sat, 25 Nov 2023 23:02:36 +0100 Subject: [PATCH 06/19] add envelopes --- packages/parser/src/envelopes/Envelope.ts | 3 ++ packages/parser/src/envelopes/Envelopes.ts | 32 +++++++++++ packages/parser/src/envelopes/apigw.ts | 18 +++++++ packages/parser/src/envelopes/apigwv2.ts | 18 +++++++ packages/parser/src/envelopes/cloudwatch.ts | 26 +++++++++ packages/parser/src/envelopes/dynamodb.ts | 34 ++++++++++++ .../src/envelopes/eventBridgeEnvelope.ts | 18 +++++++ packages/parser/src/envelopes/kafka.ts | 40 ++++++++++++++ .../parser/src/envelopes/kinesis-firehose.ts | 29 ++++++++++ packages/parser/src/envelopes/kinesis.ts | 27 ++++++++++ packages/parser/src/envelopes/lambda.ts | 18 +++++++ packages/parser/src/envelopes/sns.ts | 54 +++++++++++++++++++ .../src/envelopes/{SqsEnvelope.ts => sqs.ts} | 17 +++--- packages/parser/src/envelopes/vpc-lattice.ts | 18 +++++++ .../parser/src/envelopes/vpc-latticev2.ts | 18 +++++++ packages/parser/src/types/index.ts | 0 packages/parser/src/types/schema.ts | 0 .../{SqsEnvelope.test.ts => sqs.test..ts} | 2 +- 18 files changed, 364 insertions(+), 8 deletions(-) create mode 100644 packages/parser/src/envelopes/Envelopes.ts create mode 100644 packages/parser/src/envelopes/apigw.ts create mode 100644 packages/parser/src/envelopes/apigwv2.ts create mode 100644 packages/parser/src/envelopes/cloudwatch.ts create mode 100644 packages/parser/src/envelopes/dynamodb.ts create mode 100644 packages/parser/src/envelopes/eventBridgeEnvelope.ts create mode 100644 packages/parser/src/envelopes/kafka.ts create mode 100644 packages/parser/src/envelopes/kinesis-firehose.ts create mode 100644 packages/parser/src/envelopes/kinesis.ts create mode 100644 packages/parser/src/envelopes/lambda.ts create mode 100644 packages/parser/src/envelopes/sns.ts rename packages/parser/src/envelopes/{SqsEnvelope.ts => sqs.ts} (52%) create mode 100644 packages/parser/src/envelopes/vpc-lattice.ts create mode 100644 packages/parser/src/envelopes/vpc-latticev2.ts create mode 100644 packages/parser/src/types/index.ts create mode 100644 packages/parser/src/types/schema.ts rename packages/parser/tests/unit/envelopes/{SqsEnvelope.test.ts => sqs.test..ts} (94%) diff --git a/packages/parser/src/envelopes/Envelope.ts b/packages/parser/src/envelopes/Envelope.ts index 415a60f739..c13022bdcd 100644 --- a/packages/parser/src/envelopes/Envelope.ts +++ b/packages/parser/src/envelopes/Envelope.ts @@ -1,5 +1,8 @@ import { z, ZodSchema } from 'zod'; +/** + * Abstract class for envelopes. + */ export abstract class Envelope { protected constructor() {} diff --git a/packages/parser/src/envelopes/Envelopes.ts b/packages/parser/src/envelopes/Envelopes.ts new file mode 100644 index 0000000000..92ea6cbeb9 --- /dev/null +++ b/packages/parser/src/envelopes/Envelopes.ts @@ -0,0 +1,32 @@ +import { ApiGatewayEnvelope } from './apigw.js'; +import { ApiGatwayV2Envelope } from './apigwv2.js'; +import { CloudWatchEnvelope } from './cloudwatch.js'; +import { KafkaEnvelope } from './kafka.js'; +import { SqsEnvelope } from './sqs.js'; +import { EventBridgeEnvelope } from './eventBridgeEnvelope.js'; +import { KinesisFirehoseEnvelope } from './kinesis-firehose.js'; +import { LambdaFunctionUrlEnvelope } from './lambda.js'; +import { SnsEnvelope, SnsSqsEnvelope } from './sns.js'; +import { VpcLatticeEnvelope } from './vpc-lattice.js'; +import { VpcLatticeV2Envelope } from './vpc-latticev2.js'; + +/** + * A collection of envelopes to create new envelopes. + */ +export class Envelopes { + public static readonly API_GW_ENVELOPE = new ApiGatewayEnvelope(); + public static readonly API_GW_V2_ENVELOPE = new ApiGatwayV2Envelope(); + public static readonly CLOUDWATCH_ENVELOPE = new CloudWatchEnvelope(); + public static readonly EVENT_BRIDGE_ENVELOPE = new EventBridgeEnvelope(); + public static readonly KAFKA_ENVELOPE = new KafkaEnvelope(); + public static readonly KINESIS_ENVELOPE = new KafkaEnvelope(); + public static readonly KINESIS_FIREHOSE_ENVELOPE = + new KinesisFirehoseEnvelope(); + public static readonly LAMBDA_FUCTION_URL_ENVELOPE = + new LambdaFunctionUrlEnvelope(); + public static readonly SNS_ENVELOPE = new SnsEnvelope(); + public static readonly SNS_SQS_ENVELOPE = new SnsSqsEnvelope(); + public static readonly SQS_ENVELOPE = new SqsEnvelope(); + public static readonly VPC_LATTICE_ENVELOPE = new VpcLatticeEnvelope(); + public static readonly VPC_LATTICE_V2_ENVELOPE = new VpcLatticeV2Envelope(); +} diff --git a/packages/parser/src/envelopes/apigw.ts b/packages/parser/src/envelopes/apigw.ts new file mode 100644 index 0000000000..c3d3fcc14c --- /dev/null +++ b/packages/parser/src/envelopes/apigw.ts @@ -0,0 +1,18 @@ +import { Envelope } from './Envelope.js'; +import { z, ZodSchema } from 'zod'; +import { APIGatewayProxyEventSchema } from '../schemas/apigw.js'; + +/** + * API Gateway envelope to extract data within body key" + */ +export class ApiGatewayEnvelope extends Envelope { + public constructor() { + super(); + } + + public parse(data: unknown, schema: T): z.infer { + const parsedEnvelope = APIGatewayProxyEventSchema.parse(data); + + return this._parse(parsedEnvelope.body, schema); + } +} diff --git a/packages/parser/src/envelopes/apigwv2.ts b/packages/parser/src/envelopes/apigwv2.ts new file mode 100644 index 0000000000..2b730693ad --- /dev/null +++ b/packages/parser/src/envelopes/apigwv2.ts @@ -0,0 +1,18 @@ +import { Envelope } from './Envelope.js'; +import { z, ZodSchema } from 'zod'; +import { APIGatewayProxyEventV2Schema } from '../schemas/apigwv2.js'; + +/** + * API Gateway V2 envelope to extract data within body key + */ +export class ApiGatwayV2Envelope extends Envelope { + public constructor() { + super(); + } + + public parse(data: unknown, schema: T): z.infer { + const parsedEnvelope = APIGatewayProxyEventV2Schema.parse(data); + + return this._parse(parsedEnvelope.body, schema); + } +} diff --git a/packages/parser/src/envelopes/cloudwatch.ts b/packages/parser/src/envelopes/cloudwatch.ts new file mode 100644 index 0000000000..0a79942816 --- /dev/null +++ b/packages/parser/src/envelopes/cloudwatch.ts @@ -0,0 +1,26 @@ +import { Envelope } from './Envelope.js'; +import { z, ZodSchema } from 'zod'; +import { CloudWatchLogsSchema } from '../schemas/cloudwatch.js'; + +/** + * CloudWatch Envelope to extract a List of log records. + * + * The record's body parameter is a string (after being base64 decoded and gzipped), + * though it can also be a JSON encoded string. + * Regardless of its type it'll be parsed into a BaseModel object. + * + * Note: The record will be parsed the same way so if model is str + */ +export class CloudWatchEnvelope extends Envelope { + public constructor() { + super(); + } + + public parse(data: unknown, schema: T): z.infer[] { + const parsedEnvelope = CloudWatchLogsSchema.parse(data); + + return parsedEnvelope.awslogs.data.logEvents.map((record) => { + return this._parse(record, schema); + }); + } +} diff --git a/packages/parser/src/envelopes/dynamodb.ts b/packages/parser/src/envelopes/dynamodb.ts new file mode 100644 index 0000000000..e1dd514a52 --- /dev/null +++ b/packages/parser/src/envelopes/dynamodb.ts @@ -0,0 +1,34 @@ +import { Envelope } from './Envelope.js'; +import { z, ZodSchema } from 'zod'; +import { DynamoDBStreamSchema } from '../schemas/dynamodb.js'; + +type DynamoDBStreamEnvelopeResponse = { + NewImage: z.infer; + OldImage: z.infer; +}; + +/** + * DynamoDB Stream Envelope to extract data within NewImage/OldImage + * + * Note: Values are the parsed models. Images' values can also be None, and + * length of the list is the record's amount in the original event. + */ +export class DynamoDBStreamEnvelope extends Envelope { + public constructor() { + super(); + } + + public parse( + data: unknown, + schema: T + ): DynamoDBStreamEnvelopeResponse[] { + const parsedEnvelope = DynamoDBStreamSchema.parse(data); + + return parsedEnvelope.Records.map((record) => { + return { + NewImage: this._parse(record.dynamodb.NewImage, schema), + OldImage: this._parse(record.dynamodb.OldImage, schema), + }; + }); + } +} diff --git a/packages/parser/src/envelopes/eventBridgeEnvelope.ts b/packages/parser/src/envelopes/eventBridgeEnvelope.ts new file mode 100644 index 0000000000..786f3d31e8 --- /dev/null +++ b/packages/parser/src/envelopes/eventBridgeEnvelope.ts @@ -0,0 +1,18 @@ +import { Envelope } from './Envelope.js'; +import { z, ZodSchema } from 'zod'; +import { EventBridgeSchema } from '../schemas/eventbridge.js'; + +/** + * Envelope for EventBridge schema that extracts and parses data from the `detail` key. + */ +export class EventBridgeEnvelope extends Envelope { + public constructor() { + super(); + } + + public parse(data: unknown, schema: T): z.infer { + const parsedEnvelope = EventBridgeSchema.parse(data); + + return this._parse(parsedEnvelope.detail, schema); + } +} diff --git a/packages/parser/src/envelopes/kafka.ts b/packages/parser/src/envelopes/kafka.ts new file mode 100644 index 0000000000..cb25266f64 --- /dev/null +++ b/packages/parser/src/envelopes/kafka.ts @@ -0,0 +1,40 @@ +import { z, ZodSchema } from 'zod'; +import { Envelope } from './Envelope.js'; +import { + KafkaMskEventSchema, + KafkaSelfManagedEventSchema, +} from '../schemas/kafka.js'; +import { type KafkaRecord } from '../types/schema.js'; + +/** + * Kafka event envelope to extract data within body key + * 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. + * + * Note: Records will be parsed the same way so if model is str, + * all items in the list will be parsed as str and not as JSON (and vice versa) + */ +export class KafkaEnvelope extends Envelope { + public constructor() { + super(); + } + + public parse(data: unknown, schema: T): z.infer { + // manually fetch event source to deside between Msk or SelfManaged + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const eventSource = data['eventSource']; + + const parsedEnvelope: + | z.infer + | z.infer = + eventSource === 'aws:kafka' + ? KafkaMskEventSchema.parse(data) + : KafkaSelfManagedEventSchema.parse(data); + + return parsedEnvelope.records.data.map((record: KafkaRecord) => { + return this._parse(record.value, schema); + }); + } +} diff --git a/packages/parser/src/envelopes/kinesis-firehose.ts b/packages/parser/src/envelopes/kinesis-firehose.ts new file mode 100644 index 0000000000..9632351d0b --- /dev/null +++ b/packages/parser/src/envelopes/kinesis-firehose.ts @@ -0,0 +1,29 @@ +import { Envelope } from './Envelope.js'; +import { z, ZodSchema } from 'zod'; +import { KinesisFirehoseSchema } from '../schemas/kinesis-firehose.js'; + +/** + * Kinesis Firehose Envelope to extract array of Records + * + * The record's data parameter is a base64 encoded string which is parsed into a bytes array, + * though it can also be a JSON encoded string. + * Regardless of its type it'll be parsed into a BaseModel object. + * + * Note: Records will be parsed the same way so if model is str, + * all items in the list will be parsed as str and not as JSON (and vice versa) + * + * https://docs.aws.amazon.com/lambda/latest/dg/services-kinesisfirehose.html + */ +export class KinesisFirehoseEnvelope extends Envelope { + public constructor() { + super(); + } + + public parse(data: unknown, schema: T): z.infer { + const parsedEnvelope = KinesisFirehoseSchema.parse(data); + + return parsedEnvelope.records.map((record) => { + return this._parse(record.data, schema); + }); + } +} diff --git a/packages/parser/src/envelopes/kinesis.ts b/packages/parser/src/envelopes/kinesis.ts new file mode 100644 index 0000000000..775adb6a1c --- /dev/null +++ b/packages/parser/src/envelopes/kinesis.ts @@ -0,0 +1,27 @@ +import { Envelope } from './Envelope.js'; +import { z, ZodSchema } from 'zod'; +import { KinesisDataStreamSchema } from '../schemas/kinesis.js'; + +/** + * Kinesis Data Stream Envelope to extract array of Records + * + * The record's data parameter is a base64 encoded string which is parsed into a bytes array, + * though it can also be a JSON encoded string. + * Regardless of its type it'll be parsed into a BaseModel object. + * + * Note: Records will be parsed the same way so if model is str, + * all items in the list will be parsed as str and not as JSON (and vice versa) + */ +export class KinesisEnvelope extends Envelope { + public constructor() { + super(); + } + + public parse(data: unknown, schema: T): z.infer { + const parsedEnvelope = KinesisDataStreamSchema.parse(data); + + return parsedEnvelope.Records.map((record) => { + return this._parse(record.kinesis.data, schema); + }); + } +} diff --git a/packages/parser/src/envelopes/lambda.ts b/packages/parser/src/envelopes/lambda.ts new file mode 100644 index 0000000000..61f4a9f78c --- /dev/null +++ b/packages/parser/src/envelopes/lambda.ts @@ -0,0 +1,18 @@ +import { Envelope } from './Envelope.js'; +import { z, ZodSchema } from 'zod'; +import { LambdaFunctionUrlSchema } from '../schemas/lambda.js'; + +/** + * Lambda function URL envelope to extract data within body key + */ +export class LambdaFunctionUrlEnvelope extends Envelope { + public constructor() { + super(); + } + + public parse(data: unknown, schema: T): z.infer { + const parsedEnvelope = LambdaFunctionUrlSchema.parse(data); + + return this.parse(parsedEnvelope.body, schema); + } +} diff --git a/packages/parser/src/envelopes/sns.ts b/packages/parser/src/envelopes/sns.ts new file mode 100644 index 0000000000..76c713bb00 --- /dev/null +++ b/packages/parser/src/envelopes/sns.ts @@ -0,0 +1,54 @@ +import { z, ZodSchema } from 'zod'; +import { Envelope } from './Envelope.js'; +import { SnsNotificationSchema, SnsSchema } from '../schemas/sns.js'; +import { SqsSchema } from '../schemas/sqs.js'; + +/** + * SNS Envelope to extract array of Records + * + * 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. + * + * 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 SnsEnvelope extends Envelope { + public constructor() { + super(); + } + + public parse(data: unknown, schema: T): z.infer { + const parsedEnvelope = SnsSchema.parse(data); + + return parsedEnvelope.Records.map((record) => { + return this._parse(record.Sns.Message, schema); + }); + } +} + +/** + * SNS plus SQS Envelope to extract array of Records + * + * Published messages from SNS to SQS has a slightly different payload. + * Since SNS payload is marshalled into `Record` key in SQS, we have to: + * + * 1. Parse SQS schema with incoming data + * 2. Unmarshall SNS payload and parse against SNS Notification schema not SNS/SNS Record + * 3. Finally, parse provided model against payload extracted + * + */ +export class SnsSqsEnvelope extends Envelope { + public constructor() { + super(); + } + + public parse(data: unknown, schema: T): z.infer { + const parsedEnvelope = SqsSchema.parse(data); + + return parsedEnvelope.Records.map((record) => { + const snsNotification = SnsNotificationSchema.parse(record.body); + + return this._parse(snsNotification, schema); + }); + } +} diff --git a/packages/parser/src/envelopes/SqsEnvelope.ts b/packages/parser/src/envelopes/sqs.ts similarity index 52% rename from packages/parser/src/envelopes/SqsEnvelope.ts rename to packages/parser/src/envelopes/sqs.ts index 2972d9708a..c65a7ea069 100644 --- a/packages/parser/src/envelopes/SqsEnvelope.ts +++ b/packages/parser/src/envelopes/sqs.ts @@ -2,7 +2,16 @@ import { z, ZodSchema } from 'zod'; import { SqsSchema } from '../schemas/sqs.js'; import { Envelope } from './Envelope.js'; -class SqsEnvelope extends Envelope { +/** + * SQS Envelope to extract array of Records + * + * 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. + * + * 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 constructor() { super(); } @@ -17,9 +26,3 @@ class SqsEnvelope extends Envelope { }); } } - -class Envelopes { - public static readonly SQS_ENVELOPE = new SqsEnvelope(); -} - -export { SqsEnvelope, Envelopes }; diff --git a/packages/parser/src/envelopes/vpc-lattice.ts b/packages/parser/src/envelopes/vpc-lattice.ts new file mode 100644 index 0000000000..e8a8164f37 --- /dev/null +++ b/packages/parser/src/envelopes/vpc-lattice.ts @@ -0,0 +1,18 @@ +import { Envelope } from './Envelope.js'; +import { z, ZodSchema } from 'zod'; +import { VpcLatticeSchema } from '../schemas/vpc-lattice.js'; + +/** + * Amazon VPC Lattice envelope to extract data within body key + */ +export class VpcLatticeEnvelope extends Envelope { + public constructor() { + super(); + } + + public parse(data: unknown, schema: T): z.infer { + const parsedEnvelope = VpcLatticeSchema.parse(data); + + return this._parse(parsedEnvelope.body, schema); + } +} diff --git a/packages/parser/src/envelopes/vpc-latticev2.ts b/packages/parser/src/envelopes/vpc-latticev2.ts new file mode 100644 index 0000000000..0e42762f6c --- /dev/null +++ b/packages/parser/src/envelopes/vpc-latticev2.ts @@ -0,0 +1,18 @@ +import { Envelope } from './Envelope.js'; +import { z, ZodSchema } from 'zod'; +import { VpcLatticeV2Schema } from '../schemas/vpc-latticev2.js'; + +/** + * Amazon VPC Lattice envelope to extract data within body key + */ +export class VpcLatticeV2Envelope extends Envelope { + public constructor() { + super(); + } + + public parse(data: unknown, schema: T): z.infer { + const parsedEnvelope = VpcLatticeV2Schema.parse(data); + + return this._parse(parsedEnvelope.body, schema); + } +} diff --git a/packages/parser/src/types/index.ts b/packages/parser/src/types/index.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/parser/src/types/schema.ts b/packages/parser/src/types/schema.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/parser/tests/unit/envelopes/SqsEnvelope.test.ts b/packages/parser/tests/unit/envelopes/sqs.test..ts similarity index 94% rename from packages/parser/tests/unit/envelopes/SqsEnvelope.test.ts rename to packages/parser/tests/unit/envelopes/sqs.test..ts index 4dcd07291c..4270c24855 100644 --- a/packages/parser/tests/unit/envelopes/SqsEnvelope.test.ts +++ b/packages/parser/tests/unit/envelopes/sqs.test..ts @@ -7,7 +7,7 @@ import { z } from 'zod'; import { generateMock } from '@anatine/zod-mock'; import { SqsRecordSchema } from '../../../src/schemas/sqs.js'; -import { Envelopes } from '../../../src/envelopes/SqsEnvelope.js'; +import { Envelopes } from '../../../src/envelopes/Envelope.js'; describe('SqsEnvelope ', () => { const schema = z.object({ From 1f20c01007dc467ba206809878ff4506c0577d47 Mon Sep 17 00:00:00 2001 From: Alexander Schueren Date: Sat, 25 Nov 2023 23:04:31 +0100 Subject: [PATCH 07/19] add middy parser --- packages/parser/src/middleware/parser.ts | 28 ++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/packages/parser/src/middleware/parser.ts b/packages/parser/src/middleware/parser.ts index 0df4401aea..6c5c4f5bd9 100644 --- a/packages/parser/src/middleware/parser.ts +++ b/packages/parser/src/middleware/parser.ts @@ -8,6 +8,34 @@ interface ParserOptions { 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 { SQS_ENVELOPE } from '@aws-lambda-powertools/parser/envelopes;' + * + * const oderSchema = z.object({ + * id: z.number(), + * description: z.string(), + * quantity: z.number() + * } + * + * type Order = z.infer; + * + * export class 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: SQS_ENVELOPE })); + * ``` + * + * @param options + */ const parser = (options: ParserOptions): MiddlewareObj => { const before = (request: MiddyLikeRequest): void => { const { schema, envelope } = options; From f08f064683abf2795874ed18df312f9fcaa02f01 Mon Sep 17 00:00:00 2001 From: Alexander Schueren Date: Sun, 26 Nov 2023 09:50:27 -0800 Subject: [PATCH 08/19] minor schema changes --- packages/parser/src/schemas/kafka.ts | 2 +- packages/parser/src/schemas/kinesis.ts | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/parser/src/schemas/kafka.ts b/packages/parser/src/schemas/kafka.ts index 880fb404d8..08bd5d03ab 100644 --- a/packages/parser/src/schemas/kafka.ts +++ b/packages/parser/src/schemas/kafka.ts @@ -41,4 +41,4 @@ const KafkaMskEventSchema = KafkaBaseEventSchema.extend({ eventSourceArn: z.string(), }); -export { KafkaSelfManagedEventSchema, KafkaMskEventSchema }; +export { KafkaSelfManagedEventSchema, KafkaMskEventSchema, KafkaRecordSchema }; diff --git a/packages/parser/src/schemas/kinesis.ts b/packages/parser/src/schemas/kinesis.ts index d598715420..4756a91f76 100644 --- a/packages/parser/src/schemas/kinesis.ts +++ b/packages/parser/src/schemas/kinesis.ts @@ -22,4 +22,8 @@ const KinesisDataStreamSchema = z.object({ Records: z.array(KinesisDataStreamRecord), }); -export { KinesisDataStreamSchema }; +export { + KinesisDataStreamSchema, + KinesisDataStreamRecord, + KinesisDataStreamRecordPayload, +}; From 461ad30fbc80547757d3578fc93840a6e1846a88 Mon Sep 17 00:00:00 2001 From: Alexander Schueren Date: Fri, 8 Dec 2023 13:24:23 +0100 Subject: [PATCH 09/19] add more envelopes and tests, refactored utils to autocomplete event files --- packages/parser/src/envelopes/Envelope.ts | 13 +- packages/parser/src/envelopes/Envelopes.ts | 6 +- packages/parser/src/envelopes/apigw.ts | 7 +- packages/parser/src/envelopes/apigwv2.ts | 3 + packages/parser/src/envelopes/cloudwatch.ts | 2 +- packages/parser/src/envelopes/kafka.ts | 6 +- packages/parser/src/envelopes/lambda.ts | 6 +- packages/parser/src/envelopes/sns.ts | 8 +- packages/parser/src/envelopes/sqs.ts | 4 +- packages/parser/src/middleware/parser.ts | 16 +-- packages/parser/src/schemas/cloudwatch.ts | 8 +- packages/parser/src/schemas/kinesis.ts | 20 ++- packages/parser/src/schemas/sns.ts | 20 ++- packages/parser/src/types/schema.ts | 17 +++ .../tests/unit/envelopes/apigwt.test.ts | 29 +++++ .../tests/unit/envelopes/apigwv2.test.ts | 32 +++++ .../tests/unit/envelopes/cloudwatch.test.ts | 65 ++++++++++ .../tests/unit/envelopes/dynamodb.test.ts | 44 +++++++ .../tests/unit/envelopes/eventbridge.test.ts | 53 ++++++++ .../parser/tests/unit/envelopes/kafka.test.ts | 41 +++++++ .../unit/envelopes/kinesis-firehose.test.ts | 58 +++++++++ .../tests/unit/envelopes/kinesis.test.ts | 27 ++++ .../tests/unit/envelopes/lambda.test.ts | 32 +++++ .../parser/tests/unit/envelopes/sns.test.ts | 55 +++++++++ .../parser/tests/unit/envelopes/sqs.test..ts | 51 -------- .../parser/tests/unit/envelopes/sqs.test.ts | 47 +++++++ .../tests/unit/envelopes/vpc-lattice.test.ts | 39 ++++++ .../unit/envelopes/vpc-latticev2.test.ts | 39 ++++++ packages/parser/tests/unit/parser.test.ts | 4 +- packages/parser/tests/unit/schema/alb.test.ts | 13 +- .../parser/tests/unit/schema/apigw.test.ts | 44 ++++--- .../parser/tests/unit/schema/apigwv2.test.ts | 40 +++--- .../cloudformation-custom-resource.test.ts | 20 +-- .../tests/unit/schema/cloudwatch.test.ts | 4 +- .../parser/tests/unit/schema/dynamodb.test.ts | 4 +- .../tests/unit/schema/eventbridge.test.ts | 5 +- .../parser/tests/unit/schema/kafka.test.ts | 22 ++-- .../parser/tests/unit/schema/kinesis.test.ts | 52 +++----- .../parser/tests/unit/schema/lambda.test.ts | 7 +- packages/parser/tests/unit/schema/s3.test.ts | 38 +++--- packages/parser/tests/unit/schema/ses.test.ts | 4 +- packages/parser/tests/unit/schema/sns.test.ts | 4 +- packages/parser/tests/unit/schema/sqs.test.ts | 4 +- packages/parser/tests/unit/schema/utils.ts | 115 +++++++++++++++++- .../tests/unit/schema/vpc-lattice.test.ts | 9 +- .../tests/unit/schema/vpc-latticev2.test.ts | 9 +- 46 files changed, 900 insertions(+), 246 deletions(-) create mode 100644 packages/parser/tests/unit/envelopes/apigwt.test.ts create mode 100644 packages/parser/tests/unit/envelopes/apigwv2.test.ts create mode 100644 packages/parser/tests/unit/envelopes/cloudwatch.test.ts create mode 100644 packages/parser/tests/unit/envelopes/dynamodb.test.ts create mode 100644 packages/parser/tests/unit/envelopes/eventbridge.test.ts create mode 100644 packages/parser/tests/unit/envelopes/kafka.test.ts create mode 100644 packages/parser/tests/unit/envelopes/kinesis-firehose.test.ts create mode 100644 packages/parser/tests/unit/envelopes/kinesis.test.ts create mode 100644 packages/parser/tests/unit/envelopes/lambda.test.ts create mode 100644 packages/parser/tests/unit/envelopes/sns.test.ts delete mode 100644 packages/parser/tests/unit/envelopes/sqs.test..ts create mode 100644 packages/parser/tests/unit/envelopes/sqs.test.ts create mode 100644 packages/parser/tests/unit/envelopes/vpc-lattice.test.ts create mode 100644 packages/parser/tests/unit/envelopes/vpc-latticev2.test.ts diff --git a/packages/parser/src/envelopes/Envelope.ts b/packages/parser/src/envelopes/Envelope.ts index c13022bdcd..1b2f01f848 100644 --- a/packages/parser/src/envelopes/Envelope.ts +++ b/packages/parser/src/envelopes/Envelope.ts @@ -15,10 +15,13 @@ export abstract class Envelope { data: unknown, schema: T ): z.infer[] { - if (typeof data !== 'object') { - throw new Error('Data must be an object'); - } - - return schema.parse(data); + if (typeof data === 'string') { + return schema.parse(JSON.parse(data)); + } else if (typeof data === 'object') { + return schema.parse(data); + } else + throw new Error( + `Invalid data type for envelope. Expected string or object, got ${typeof data}` + ); } } diff --git a/packages/parser/src/envelopes/Envelopes.ts b/packages/parser/src/envelopes/Envelopes.ts index 92ea6cbeb9..2f0e1d7d65 100644 --- a/packages/parser/src/envelopes/Envelopes.ts +++ b/packages/parser/src/envelopes/Envelopes.ts @@ -9,6 +9,8 @@ import { LambdaFunctionUrlEnvelope } from './lambda.js'; import { SnsEnvelope, SnsSqsEnvelope } from './sns.js'; import { VpcLatticeEnvelope } from './vpc-lattice.js'; import { VpcLatticeV2Envelope } from './vpc-latticev2.js'; +import { DynamoDBStreamEnvelope } from './dynamodb.js'; +import { KinesisEnvelope } from './kinesis.js'; /** * A collection of envelopes to create new envelopes. @@ -17,9 +19,11 @@ export class Envelopes { public static readonly API_GW_ENVELOPE = new ApiGatewayEnvelope(); public static readonly API_GW_V2_ENVELOPE = new ApiGatwayV2Envelope(); public static readonly CLOUDWATCH_ENVELOPE = new CloudWatchEnvelope(); + public static readonly DYNAMO_DB_STREAM_ENVELOPE = + new DynamoDBStreamEnvelope(); public static readonly EVENT_BRIDGE_ENVELOPE = new EventBridgeEnvelope(); public static readonly KAFKA_ENVELOPE = new KafkaEnvelope(); - public static readonly KINESIS_ENVELOPE = new KafkaEnvelope(); + public static readonly KINESIS_ENVELOPE = new KinesisEnvelope(); public static readonly KINESIS_FIREHOSE_ENVELOPE = new KinesisFirehoseEnvelope(); public static readonly LAMBDA_FUCTION_URL_ENVELOPE = diff --git a/packages/parser/src/envelopes/apigw.ts b/packages/parser/src/envelopes/apigw.ts index c3d3fcc14c..0f0f6116fe 100644 --- a/packages/parser/src/envelopes/apigw.ts +++ b/packages/parser/src/envelopes/apigw.ts @@ -1,6 +1,7 @@ import { Envelope } from './Envelope.js'; import { z, ZodSchema } from 'zod'; import { APIGatewayProxyEventSchema } from '../schemas/apigw.js'; +import { type ApiGatewayProxyEvent } from '../types/schema.js'; /** * API Gateway envelope to extract data within body key" @@ -11,7 +12,11 @@ export class ApiGatewayEnvelope extends Envelope { } public parse(data: unknown, schema: T): z.infer { - const parsedEnvelope = APIGatewayProxyEventSchema.parse(data); + const parsedEnvelope: ApiGatewayProxyEvent = + APIGatewayProxyEventSchema.parse(data); + if (parsedEnvelope.body === undefined) { + throw new Error('Body field of API Gateway event is undefined'); + } return this._parse(parsedEnvelope.body, schema); } diff --git a/packages/parser/src/envelopes/apigwv2.ts b/packages/parser/src/envelopes/apigwv2.ts index 2b730693ad..dd1b4a5ec6 100644 --- a/packages/parser/src/envelopes/apigwv2.ts +++ b/packages/parser/src/envelopes/apigwv2.ts @@ -12,6 +12,9 @@ export class ApiGatwayV2Envelope extends Envelope { public parse(data: unknown, schema: T): z.infer { const parsedEnvelope = APIGatewayProxyEventV2Schema.parse(data); + if (parsedEnvelope.body === undefined) { + throw new Error('Body field of API Gateway V2 event is undefined'); + } return this._parse(parsedEnvelope.body, schema); } diff --git a/packages/parser/src/envelopes/cloudwatch.ts b/packages/parser/src/envelopes/cloudwatch.ts index 0a79942816..5f1c55c463 100644 --- a/packages/parser/src/envelopes/cloudwatch.ts +++ b/packages/parser/src/envelopes/cloudwatch.ts @@ -20,7 +20,7 @@ export class CloudWatchEnvelope extends Envelope { const parsedEnvelope = CloudWatchLogsSchema.parse(data); return parsedEnvelope.awslogs.data.logEvents.map((record) => { - return this._parse(record, schema); + return this._parse(record.message, schema); }); } } diff --git a/packages/parser/src/envelopes/kafka.ts b/packages/parser/src/envelopes/kafka.ts index cb25266f64..84d573555e 100644 --- a/packages/parser/src/envelopes/kafka.ts +++ b/packages/parser/src/envelopes/kafka.ts @@ -33,8 +33,10 @@ export class KafkaEnvelope extends Envelope { ? KafkaMskEventSchema.parse(data) : KafkaSelfManagedEventSchema.parse(data); - return parsedEnvelope.records.data.map((record: KafkaRecord) => { - return this._parse(record.value, schema); + return Object.values(parsedEnvelope.records).map((topicRecord) => { + return topicRecord.map((record: KafkaRecord) => { + return this._parse(record.value, schema); + }); }); } } diff --git a/packages/parser/src/envelopes/lambda.ts b/packages/parser/src/envelopes/lambda.ts index 61f4a9f78c..2608da381e 100644 --- a/packages/parser/src/envelopes/lambda.ts +++ b/packages/parser/src/envelopes/lambda.ts @@ -13,6 +13,10 @@ export class LambdaFunctionUrlEnvelope extends Envelope { public parse(data: unknown, schema: T): z.infer { const parsedEnvelope = LambdaFunctionUrlSchema.parse(data); - return this.parse(parsedEnvelope.body, schema); + if (parsedEnvelope.body === undefined) { + throw new Error('Body field of Lambda function URL event is undefined'); + } + + return this._parse(parsedEnvelope.body, schema); } } diff --git a/packages/parser/src/envelopes/sns.ts b/packages/parser/src/envelopes/sns.ts index 76c713bb00..0ff391b2cb 100644 --- a/packages/parser/src/envelopes/sns.ts +++ b/packages/parser/src/envelopes/sns.ts @@ -1,6 +1,6 @@ import { z, ZodSchema } from 'zod'; import { Envelope } from './Envelope.js'; -import { SnsNotificationSchema, SnsSchema } from '../schemas/sns.js'; +import { SnsSchema, SnsSqsNotificationSchema } from '../schemas/sns.js'; import { SqsSchema } from '../schemas/sqs.js'; /** @@ -46,9 +46,11 @@ export class SnsSqsEnvelope extends Envelope { const parsedEnvelope = SqsSchema.parse(data); return parsedEnvelope.Records.map((record) => { - const snsNotification = SnsNotificationSchema.parse(record.body); + const snsNotification = SnsSqsNotificationSchema.parse( + JSON.parse(record.body) + ); - return this._parse(snsNotification, schema); + return this._parse(snsNotification.Message, schema); }); } } diff --git a/packages/parser/src/envelopes/sqs.ts b/packages/parser/src/envelopes/sqs.ts index c65a7ea069..98e0abaf06 100644 --- a/packages/parser/src/envelopes/sqs.ts +++ b/packages/parser/src/envelopes/sqs.ts @@ -20,9 +20,7 @@ export class SqsEnvelope extends Envelope { const parsedEnvelope = SqsSchema.parse(data); return parsedEnvelope.Records.map((record) => { - const body = JSON.parse(record.body); - - return this._parse(body, schema); + return this._parse(record.body, schema); }); } } diff --git a/packages/parser/src/middleware/parser.ts b/packages/parser/src/middleware/parser.ts index 6c5c4f5bd9..ee4336122b 100644 --- a/packages/parser/src/middleware/parser.ts +++ b/packages/parser/src/middleware/parser.ts @@ -3,9 +3,9 @@ import { MiddlewareObj } from '@middy/core'; import { ZodSchema } from 'zod'; import { Envelope } from '../envelopes/Envelope.js'; -interface ParserOptions { - schema: ZodSchema; - envelope?: Envelope; +interface ParserOptions { + schema: S; + envelope?: E; } /** @@ -36,7 +36,9 @@ interface ParserOptions { * * @param options */ -const parser = (options: ParserOptions): MiddlewareObj => { +const parser = ( + options: ParserOptions +): MiddlewareObj => { const before = (request: MiddyLikeRequest): void => { const { schema, envelope } = options; if (envelope) { @@ -46,14 +48,8 @@ const parser = (options: ParserOptions): MiddlewareObj => { } }; - const after = (_request: MiddyLikeRequest): void => {}; - - const onError = (_request: MiddyLikeRequest): void => {}; - return { before, - after, - onError, }; }; diff --git a/packages/parser/src/schemas/cloudwatch.ts b/packages/parser/src/schemas/cloudwatch.ts index 8c9e71f9a0..2694507b04 100644 --- a/packages/parser/src/schemas/cloudwatch.ts +++ b/packages/parser/src/schemas/cloudwatch.ts @@ -30,15 +30,9 @@ const CloudWatchLogsSchema = z.object({ }), }); -const extractCloudWatchLogFromEvent = ( - data: string -): z.infer => { - return decompressRecordToJSON(data); -}; - export { CloudWatchLogsSchema, CloudWatchLogsDecodeSchema, decompressRecordToJSON, - extractCloudWatchLogFromEvent, + CloudWatchLogEventSchema, }; diff --git a/packages/parser/src/schemas/kinesis.ts b/packages/parser/src/schemas/kinesis.ts index 4756a91f76..fbd734297e 100644 --- a/packages/parser/src/schemas/kinesis.ts +++ b/packages/parser/src/schemas/kinesis.ts @@ -1,13 +1,31 @@ import { z } from 'zod'; +import { gunzipSync } from 'node:zlib'; const KinesisDataStreamRecordPayload = z.object({ kinesisSchemaVersion: z.string(), partitionKey: z.string(), sequenceNumber: z.string(), approximateArrivalTimestamp: z.number(), - data: z.string(), + data: z.string().transform((data) => { + const decompresed = decompress(data); + const decoded = Buffer.from(data, 'base64').toString('utf-8'); + try { + // If data was not compressed, try to parse it as JSON otherwise it must be string + return decompresed === data ? JSON.parse(decoded) : decompresed; + } catch (e) { + return decoded; + } + }), }); +const decompress = (data: string): string => { + try { + return JSON.parse(gunzipSync(Buffer.from(data, 'base64')).toString('utf8')); + } catch (e) { + return data; + } +}; + const KinesisDataStreamRecord = z.object({ eventSource: z.literal('aws:kinesis'), eventVersion: z.string(), diff --git a/packages/parser/src/schemas/sns.ts b/packages/parser/src/schemas/sns.ts index f8d8d8bbc4..862306a66b 100644 --- a/packages/parser/src/schemas/sns.ts +++ b/packages/parser/src/schemas/sns.ts @@ -9,16 +9,26 @@ const SnsNotificationSchema = z.object({ Subject: z.string().optional(), TopicArn: z.string(), UnsubscribeUrl: z.string().url(), + UnsubscribeURL: z.string().url().optional(), + SigningCertUrl: z.string().url().optional(), + SigningCertURL: z.string().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(), - SigningCertUrl: z.string().url().optional(), Timestamp: z.string().datetime(), }); +const SnsSqsNotificationSchema = SnsNotificationSchema.extend({ + UnsubscribeURL: z.string().optional(), + SigningCertURL: z.string().url().optional(), +}).omit({ + UnsubscribeUrl: true, + SigningCertUrl: true, +}); + const SnsRecordSchema = z.object({ EventSource: z.literal('aws:sns'), EventVersion: z.string(), @@ -30,4 +40,10 @@ const SnsSchema = z.object({ Records: z.array(SnsRecordSchema), }); -export { SnsSchema, SnsRecordSchema, SnsNotificationSchema, SnsMsgAttribute }; +export { + SnsSchema, + SnsRecordSchema, + SnsNotificationSchema, + SnsMsgAttribute, + SnsSqsNotificationSchema, +}; diff --git a/packages/parser/src/types/schema.ts b/packages/parser/src/types/schema.ts index e69de29bb2..cc5869a1e2 100644 --- a/packages/parser/src/types/schema.ts +++ b/packages/parser/src/types/schema.ts @@ -0,0 +1,17 @@ +import { KafkaRecordSchema } from '../schemas/kafka.js'; +import { z } from 'zod'; +import { + KinesisDataStreamRecord, + KinesisDataStreamRecordPayload, +} from '../schemas/kinesis.js'; +import { APIGatewayProxyEventSchema } from '../schemas/apigw.js'; + +export type KafkaRecord = z.infer; + +export type KinesisDataStreamRecord = z.infer; + +export type KinesisDataStreamRecordPayload = z.infer< + typeof KinesisDataStreamRecordPayload +>; + +export type ApiGatewayProxyEvent = z.infer; diff --git a/packages/parser/tests/unit/envelopes/apigwt.test.ts b/packages/parser/tests/unit/envelopes/apigwt.test.ts new file mode 100644 index 0000000000..7fd07b399a --- /dev/null +++ b/packages/parser/tests/unit/envelopes/apigwt.test.ts @@ -0,0 +1,29 @@ +/** + * Test built in schema envelopes for api gateway + * + * @group unit/parser/envelopes + */ + +import { generateMock } from '@anatine/zod-mock'; +import { Envelopes } from '../../../src/envelopes/Envelopes.js'; +import { TestEvents, TestSchema } from '../schema/utils.js'; +import { ApiGatewayProxyEvent } from '../../../src/types/schema.js'; + +describe('ApigwEnvelope ', () => { + const envelope = Envelopes.API_GW_ENVELOPE; + it('should parse custom schema in envelope', () => { + const testCustomSchemaObject = generateMock(TestSchema); + const testEvent = TestEvents.apiGatewayProxyEvent as ApiGatewayProxyEvent; + + testEvent.body = JSON.stringify(testCustomSchemaObject); + const resp = envelope.parse(testEvent, TestSchema); + expect(resp).toEqual(testCustomSchemaObject); + }); + + it('should throw no body provided', () => { + const testEvent = TestEvents.apiGatewayProxyEvent as ApiGatewayProxyEvent; + testEvent.body = undefined; + + expect(() => envelope.parse(testEvent, TestSchema)).toThrow(); + }); +}); diff --git a/packages/parser/tests/unit/envelopes/apigwv2.test.ts b/packages/parser/tests/unit/envelopes/apigwv2.test.ts new file mode 100644 index 0000000000..ba12a566d4 --- /dev/null +++ b/packages/parser/tests/unit/envelopes/apigwv2.test.ts @@ -0,0 +1,32 @@ +/** + * Test built in schema envelopes for api gateway v2 + * + * @group unit/parser/envelopes + */ + +import { Envelopes } from '../../../src/envelopes/Envelopes.js'; +import { TestEvents, TestSchema } from '../schema/utils.js'; +import { generateMock } from '@anatine/zod-mock'; +import { APIGatewayProxyEventV2 } from 'aws-lambda'; + +describe('ApiGwV2Envelope ', () => { + const envelope = Envelopes.API_GW_V2_ENVELOPE; + + it('should parse custom schema in envelope', () => { + const testEvent = + TestEvents.apiGatewayProxyV2Event as APIGatewayProxyEventV2; + const data = generateMock(TestSchema); + + testEvent.body = JSON.stringify(data); + + expect(envelope.parse(testEvent, TestSchema)).toEqual(data); + }); + + it('should throw when no body provided', () => { + const testEvent = + TestEvents.apiGatewayProxyV2Event as APIGatewayProxyEventV2; + testEvent.body = undefined; + + expect(() => envelope.parse(testEvent, TestSchema)).toThrow(); + }); +}); diff --git a/packages/parser/tests/unit/envelopes/cloudwatch.test.ts b/packages/parser/tests/unit/envelopes/cloudwatch.test.ts new file mode 100644 index 0000000000..b7ee7c747a --- /dev/null +++ b/packages/parser/tests/unit/envelopes/cloudwatch.test.ts @@ -0,0 +1,65 @@ +/** + * Test built in schema envelopes for CloudWatch + * + * @group unit/parser/envelopes + */ + +import { Envelopes } from '../../../src/envelopes/Envelopes.js'; +import { generateMock } from '@anatine/zod-mock'; +import { gzipSync } from 'node:zlib'; +import { + CloudWatchLogEventSchema, + CloudWatchLogsDecodeSchema, +} from '../../../src/schemas/cloudwatch.js'; +import { TestSchema } from '../schema/utils.js'; + +describe('CloudWatch', () => { + it('should parse custom schema in envelope', () => { + const testEvent = { + awslogs: { + data: '', + }, + }; + const envelope = Envelopes.CLOUDWATCH_ENVELOPE; + + const data = generateMock(TestSchema); + const eventMock = generateMock(CloudWatchLogEventSchema, { + stringMap: { + message: () => JSON.stringify(data), + }, + }); + + const logMock = generateMock(CloudWatchLogsDecodeSchema); + logMock.logEvents = [eventMock]; + + testEvent.awslogs.data = gzipSync( + Buffer.from(JSON.stringify(logMock), 'utf8') + ).toString('base64'); + + expect(envelope.parse(testEvent, TestSchema)).toEqual([data]); + }); + + it('should throw when schema does not match', () => { + const testEvent = { + awslogs: { + data: '', + }, + }; + const envelope = Envelopes.CLOUDWATCH_ENVELOPE; + + const eventMock = generateMock(CloudWatchLogEventSchema, { + stringMap: { + message: () => JSON.stringify({ foo: 'bar' }), + }, + }); + + const logMock = generateMock(CloudWatchLogsDecodeSchema); + logMock.logEvents = [eventMock]; + + testEvent.awslogs.data = gzipSync( + Buffer.from(JSON.stringify(logMock), 'utf8') + ).toString('base64'); + + expect(() => envelope.parse(testEvent, TestSchema)).toThrow(); + }); +}); diff --git a/packages/parser/tests/unit/envelopes/dynamodb.test.ts b/packages/parser/tests/unit/envelopes/dynamodb.test.ts new file mode 100644 index 0000000000..65b45d81cb --- /dev/null +++ b/packages/parser/tests/unit/envelopes/dynamodb.test.ts @@ -0,0 +1,44 @@ +/** + * Test built in schema envelopes for api gateway v2 + * + * @group unit/parser/envelopes + */ + +import { generateMock } from '@anatine/zod-mock'; +import { TestEvents } from '../schema/utils.js'; +import { DynamoDBStreamEvent } from 'aws-lambda'; +import { z } from 'zod'; +import { Envelopes } from '../../../src/envelopes/Envelopes.js'; + +describe('DynamoDB', () => { + const schema = z.object({ + Message: z.record(z.literal('S'), z.string()), + Id: z.record(z.literal('N'), z.number().min(0).max(100)), + }); + + const envelope = Envelopes.DYNAMO_DB_STREAM_ENVELOPE; + it('should parse dynamodb envelope', () => { + const mockOldImage = generateMock(schema); + const mockNewImage = generateMock(schema); + const dynamodbEvent = TestEvents.dynamoStreamEvent as DynamoDBStreamEvent; + + (dynamodbEvent.Records[0].dynamodb!.NewImage as typeof mockNewImage) = + mockNewImage; + (dynamodbEvent.Records[1].dynamodb!.NewImage as typeof mockNewImage) = + mockNewImage; + (dynamodbEvent.Records[0].dynamodb!.OldImage as typeof mockOldImage) = + mockOldImage; + (dynamodbEvent.Records[1].dynamodb!.OldImage as typeof mockOldImage) = + mockOldImage; + + const parsed = envelope.parse(dynamodbEvent, schema); + expect(parsed[0]).toEqual({ + OldImage: mockOldImage, + NewImage: mockNewImage, + }); + expect(parsed[1]).toEqual({ + OldImage: mockOldImage, + NewImage: mockNewImage, + }); + }); +}); diff --git a/packages/parser/tests/unit/envelopes/eventbridge.test.ts b/packages/parser/tests/unit/envelopes/eventbridge.test.ts new file mode 100644 index 0000000000..d0f6164d76 --- /dev/null +++ b/packages/parser/tests/unit/envelopes/eventbridge.test.ts @@ -0,0 +1,53 @@ +/** + * Test built in schema envelopes for event bridge + * + * @group unit/parser/envelopes + */ + +import { TestEvents, TestSchema } from '../schema/utils.js'; +import { Envelopes } from '../../../src/envelopes/Envelopes.js'; +import { generateMock } from '@anatine/zod-mock'; +import { EventBridgeEvent } from 'aws-lambda'; + +describe('EventBridgeEnvelope ', () => { + const envelope = Envelopes.EVENT_BRIDGE_ENVELOPE; + + it('should parse eventbridge event', () => { + const eventBridgeEvent = TestEvents.eventBridgeEvent as EventBridgeEvent< + string, + object + >; + + const data = generateMock(TestSchema); + + eventBridgeEvent.detail = data; + + expect(envelope.parse(eventBridgeEvent, TestSchema)).toEqual(data); + }); + + it('should throw error if detail type does not match schema', () => { + const eventBridgeEvent = TestEvents.eventBridgeEvent as EventBridgeEvent< + string, + object + >; + + const envelope = Envelopes.EVENT_BRIDGE_ENVELOPE; + + eventBridgeEvent.detail = { + foo: 'bar', + }; + + expect(() => envelope.parse(eventBridgeEvent, TestSchema)).toThrowError(); + }); + + it('should throw when invalid data type provided', () => { + const testEvent = TestEvents.eventBridgeEvent as EventBridgeEvent< + string, + object + >; + + testEvent.detail = 1 as unknown as object; + + expect(() => envelope.parse(testEvent, TestSchema)).toThrow(); + }); +}); diff --git a/packages/parser/tests/unit/envelopes/kafka.test.ts b/packages/parser/tests/unit/envelopes/kafka.test.ts new file mode 100644 index 0000000000..fff8635d7e --- /dev/null +++ b/packages/parser/tests/unit/envelopes/kafka.test.ts @@ -0,0 +1,41 @@ +/** + * Test built in schema envelopes for api gateway v2 + * + * @group unit/parser/envelopes + */ + +import { generateMock } from '@anatine/zod-mock'; +import { TestEvents, TestSchema } from '../schema/utils.js'; +import { MSKEvent, SelfManagedKafkaEvent } from 'aws-lambda'; +import { Envelopes } from '../../../src/envelopes/Envelopes.js'; + +describe('Kafka', () => { + const envelope = Envelopes.KAFKA_ENVELOPE; + + it('should parse MSK kafka envelope', () => { + const mock = generateMock(TestSchema); + + const kafkaEvent = TestEvents.kafkaEventMsk as MSKEvent; + kafkaEvent.records['mytopic-0'][0].value = Buffer.from( + JSON.stringify(mock) + ).toString('base64'); + + const result = envelope.parse(kafkaEvent, TestSchema); + + expect(result).toEqual([[mock]]); + }); + + it('should parse Self Managed kafka envelope', () => { + const mock = generateMock(TestSchema); + + const kafkaEvent = + TestEvents.kafkaEventSelfManaged as SelfManagedKafkaEvent; + kafkaEvent.records['mytopic-0'][0].value = Buffer.from( + JSON.stringify(mock) + ).toString('base64'); + + const result = envelope.parse(kafkaEvent, TestSchema); + + expect(result).toEqual([[mock]]); + }); +}); diff --git a/packages/parser/tests/unit/envelopes/kinesis-firehose.test.ts b/packages/parser/tests/unit/envelopes/kinesis-firehose.test.ts new file mode 100644 index 0000000000..b329291b9d --- /dev/null +++ b/packages/parser/tests/unit/envelopes/kinesis-firehose.test.ts @@ -0,0 +1,58 @@ +/** + * Test built in schema envelopes for Kinesis Firehose + * + * @group unit/parser/envelopes + */ + +import { Envelopes } from '../../../src/envelopes/Envelopes.js'; +import { TestEvents, TestSchema } from '../schema/utils.js'; +import { generateMock } from '@anatine/zod-mock'; +import { KinesisFirehoseSchema } from '../../../src/schemas/kinesis-firehose.js'; +import { z } from 'zod'; + +describe('Kinesis Firehose Envelope', () => { + it('should parse records for PutEvent', () => { + const mock = generateMock(TestSchema); + const testEvent = TestEvents.kinesisFirehosePutEvent as z.infer< + typeof KinesisFirehoseSchema + >; + + testEvent.records.map((record) => { + record.data = Buffer.from(JSON.stringify(mock)).toString('base64'); + }); + const envelope = Envelopes.KINESIS_FIREHOSE_ENVELOPE; + + const resp = envelope.parse(testEvent, TestSchema); + expect(resp).toEqual([mock, mock]); + }); + + it('should parse a single record for SQS event', () => { + const mock = generateMock(TestSchema); + const testEvent = TestEvents.kinesisFirehoseSQSEvent as z.infer< + typeof KinesisFirehoseSchema + >; + + testEvent.records.map((record) => { + record.data = Buffer.from(JSON.stringify(mock)).toString('base64'); + }); + const envelope = Envelopes.KINESIS_FIREHOSE_ENVELOPE; + + const resp = envelope.parse(testEvent, TestSchema); + expect(resp).toEqual([mock]); + }); + + it('should parse records for kinesis event', () => { + const mock = generateMock(TestSchema); + const testEvent = TestEvents.kinesisFirehoseKinesisEvent as z.infer< + typeof KinesisFirehoseSchema + >; + + testEvent.records.map((record) => { + record.data = Buffer.from(JSON.stringify(mock)).toString('base64'); + }); + const envelope = Envelopes.KINESIS_FIREHOSE_ENVELOPE; + + const resp = envelope.parse(testEvent, TestSchema); + expect(resp).toEqual([mock, mock]); + }); +}); diff --git a/packages/parser/tests/unit/envelopes/kinesis.test.ts b/packages/parser/tests/unit/envelopes/kinesis.test.ts new file mode 100644 index 0000000000..297efc7636 --- /dev/null +++ b/packages/parser/tests/unit/envelopes/kinesis.test.ts @@ -0,0 +1,27 @@ +/** + * Test built in schema envelopes for Kinesis + * + * @group unit/parser/envelopes + */ + +import { generateMock } from '@anatine/zod-mock'; +import { KinesisStreamEvent } from 'aws-lambda'; +import { Envelopes } from '../../../src/envelopes/Envelopes.js'; +import { TestEvents, TestSchema } from '../schema/utils.js'; + +describe('Kinesis', () => { + const envelope = Envelopes.KINESIS_ENVELOPE; + it('should parse Kinesis Stream event', () => { + const mock = generateMock(TestSchema); + const testEvent = TestEvents.kinesisStreamEvent as KinesisStreamEvent; + + testEvent.Records.map((record) => { + record.kinesis.data = Buffer.from(JSON.stringify(mock)).toString( + 'base64' + ); + }); + + const resp = envelope.parse(testEvent, TestSchema); + expect(resp).toEqual([mock, mock]); + }); +}); diff --git a/packages/parser/tests/unit/envelopes/lambda.test.ts b/packages/parser/tests/unit/envelopes/lambda.test.ts new file mode 100644 index 0000000000..59a9442460 --- /dev/null +++ b/packages/parser/tests/unit/envelopes/lambda.test.ts @@ -0,0 +1,32 @@ +/** + * Test built in schema envelopes for Lambda Functions URL + * + * @group unit/parser/envelopes + */ + +import { Envelopes } from '../../../src/envelopes/Envelopes.js'; +import { TestEvents, TestSchema } from '../schema/utils.js'; +import { generateMock } from '@anatine/zod-mock'; +import { APIGatewayProxyEventV2 } from 'aws-lambda'; + +describe('Lambda Functions Url ', () => { + const envelope = Envelopes.LAMBDA_FUCTION_URL_ENVELOPE; + + it('should parse custom schema in envelope', () => { + const testEvent = + TestEvents.lambdaFunctionUrlEvent as APIGatewayProxyEventV2; + const data = generateMock(TestSchema); + + testEvent.body = JSON.stringify(data); + + expect(envelope.parse(testEvent, TestSchema)).toEqual(data); + }); + + it('should throw when no body provided', () => { + const testEvent = + TestEvents.apiGatewayProxyV2Event as APIGatewayProxyEventV2; + testEvent.body = undefined; + + expect(() => envelope.parse(testEvent, TestSchema)).toThrow(); + }); +}); diff --git a/packages/parser/tests/unit/envelopes/sns.test.ts b/packages/parser/tests/unit/envelopes/sns.test.ts new file mode 100644 index 0000000000..9d9bc511b8 --- /dev/null +++ b/packages/parser/tests/unit/envelopes/sns.test.ts @@ -0,0 +1,55 @@ +/** + * Test built in schema envelopes for SNS + * + * @group unit/parser/envelopes + */ + +import { z } from 'zod'; +import { generateMock } from '@anatine/zod-mock'; +import { SNSEvent, SQSEvent } from 'aws-lambda'; +import { Envelopes } from '../../../src/envelopes/Envelopes.js'; +import { TestEvents, TestSchema } from '../schema/utils.js'; + +describe('SNS Envelope', () => { + it('should parse custom schema in envelope', () => { + const testEvent = TestEvents.snsEvent as SNSEvent; + const envelope = Envelopes.SNS_ENVELOPE; + + const testRecords = [] as z.infer[]; + + testEvent.Records.map((record) => { + const value = generateMock(TestSchema); + testRecords.push(value); + record.Sns.Message = JSON.stringify(value); + }); + + expect(envelope.parse(testEvent, TestSchema)).toEqual(testRecords); + }); + + it('should throw if message does not macht schema', () => { + const testEvent = TestEvents.snsEvent as SNSEvent; + const envelope = Envelopes.SNS_ENVELOPE; + + testEvent.Records.map((record) => { + record.Sns.Message = JSON.stringify({ + foo: 'bar', + }); + }); + + expect(() => envelope.parse(testEvent, TestSchema)).toThrowError(); + }); + + it('should parse sqs inside sns envelope', () => { + const snsSqsTestEvent = TestEvents.snsSqsEvent as SQSEvent; + + const data = generateMock(TestSchema); + const snsEvent = JSON.parse(snsSqsTestEvent.Records[0].body); + snsEvent.Message = JSON.stringify(data); + + snsSqsTestEvent.Records[0].body = JSON.stringify(snsEvent); + + expect( + Envelopes.SNS_SQS_ENVELOPE.parse(snsSqsTestEvent, TestSchema) + ).toEqual([data]); + }); +}); diff --git a/packages/parser/tests/unit/envelopes/sqs.test..ts b/packages/parser/tests/unit/envelopes/sqs.test..ts deleted file mode 100644 index 4270c24855..0000000000 --- a/packages/parser/tests/unit/envelopes/sqs.test..ts +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Test built in schema - * - * @group unit/parser/envelopes/ - */ - -import { z } from 'zod'; -import { generateMock } from '@anatine/zod-mock'; -import { SqsRecordSchema } from '../../../src/schemas/sqs.js'; -import { Envelopes } from '../../../src/envelopes/Envelope.js'; - -describe('SqsEnvelope ', () => { - const schema = z.object({ - name: z.string(), - age: z.number().min(18).max(99), - }); - - const envelope = Envelopes.SQS_ENVELOPE; - - it('should parse custom schema in envelope', () => { - const testCustomSchemaObject = generateMock(schema); - const mock = generateMock(SqsRecordSchema, { - stringMap: { - body: () => JSON.stringify(testCustomSchemaObject), - }, - }); - - const resp = envelope.parse({ Records: [mock] }, schema); - expect(resp).toEqual([testCustomSchemaObject]); - }); - - it('should throw error if invalid schema', () => { - expect(() => { - envelope.parse({ Records: [{ foo: 'bar' }] }, schema); - }).toThrow(); - - expect(() => { - envelope.parse( - { - Records: [ - { - name: 'foo', - age: 17, - }, - ], - }, - schema - ); - }); - }); -}); diff --git a/packages/parser/tests/unit/envelopes/sqs.test.ts b/packages/parser/tests/unit/envelopes/sqs.test.ts new file mode 100644 index 0000000000..5cbeb86c74 --- /dev/null +++ b/packages/parser/tests/unit/envelopes/sqs.test.ts @@ -0,0 +1,47 @@ +/** + * Test built in schema envelopes for sqs + * + * @group unit/parser/envelopes + */ + +import { generateMock } from '@anatine/zod-mock'; +import { Envelopes } from '../../../src/envelopes/Envelopes.js'; +import { TestEvents, TestSchema } from '../schema/utils.js'; +import { SQSEvent } from 'aws-lambda'; + +describe('SqsEnvelope ', () => { + const envelope = Envelopes.SQS_ENVELOPE; + + it('should parse custom schema in envelope', () => { + const mock = generateMock(TestSchema); + + const sqsEvent = TestEvents.sqsEvent as SQSEvent; + sqsEvent.Records[0].body = JSON.stringify(mock); + sqsEvent.Records[1].body = JSON.stringify(mock); + + const resp = envelope.parse(sqsEvent, TestSchema); + expect(resp).toEqual([mock, mock]); + }); + + it('should throw error if invalid keys for a schema', () => { + expect(() => { + envelope.parse({ Records: [{ foo: 'bar' }] }, TestSchema); + }).toThrow(); + }); + + it('should throw error if invalid values for a schema', () => { + expect(() => { + envelope.parse( + { + Records: [ + { + name: 'foo', + age: 17, + }, + ], + }, + TestSchema + ); + }).toThrow(); + }); +}); diff --git a/packages/parser/tests/unit/envelopes/vpc-lattice.test.ts b/packages/parser/tests/unit/envelopes/vpc-lattice.test.ts new file mode 100644 index 0000000000..e754ba9b1a --- /dev/null +++ b/packages/parser/tests/unit/envelopes/vpc-lattice.test.ts @@ -0,0 +1,39 @@ +/** + * Test built in schema envelopes for VPC Lattice + * + * @group unit/parser/envelopes + */ + +import { Envelopes } from '../../../src/envelopes/Envelopes.js'; +import { generateMock } from '@anatine/zod-mock'; +import { TestEvents, TestSchema } from '../schema/utils.js'; +import { VpcLatticeSchema } from '../../../src/schemas/vpc-lattice.js'; +import { z } from 'zod'; + +describe('VPC Lattice envelope', () => { + const evnelope = Envelopes.VPC_LATTICE_ENVELOPE; + it('should parse VPC Lattice event', () => { + const mock = generateMock(TestSchema); + const testEvent = TestEvents.vpcLatticeEvent as z.infer< + typeof VpcLatticeSchema + >; + + testEvent.body = JSON.stringify(mock); + + const resp = evnelope.parse(testEvent, TestSchema); + + expect(resp).toEqual(mock); + }); + + it('should parse VPC Lattice event with trailing slash', () => { + const mock = generateMock(TestSchema); + const testEvent = TestEvents.vpcLatticeEventPathTrailingSlash as z.infer< + typeof VpcLatticeSchema + >; + + testEvent.body = JSON.stringify(mock); + + const resp = evnelope.parse(testEvent, TestSchema); + expect(resp).toEqual(mock); + }); +}); diff --git a/packages/parser/tests/unit/envelopes/vpc-latticev2.test.ts b/packages/parser/tests/unit/envelopes/vpc-latticev2.test.ts new file mode 100644 index 0000000000..878d15b04c --- /dev/null +++ b/packages/parser/tests/unit/envelopes/vpc-latticev2.test.ts @@ -0,0 +1,39 @@ +/** + * Test built in schema envelopes for VPC Lattice V2 + * + * @group unit/parser/envelopes + */ + +import { generateMock } from '@anatine/zod-mock'; +import { VpcLatticeSchema } from '../../../src/schemas/vpc-lattice.js'; +import { z } from 'zod'; +import { Envelopes } from '../../../src/envelopes/Envelopes.js'; +import { TestEvents, TestSchema } from '../schema/utils.js'; + +describe('VPC Lattice envelope', () => { + const evnelope = Envelopes.VPC_LATTICE_V2_ENVELOPE; + it('should parse VPC Lattice event', () => { + const mock = generateMock(TestSchema); + const testEvent = TestEvents.vpcLatticeV2Event as z.infer< + typeof VpcLatticeSchema + >; + + testEvent.body = JSON.stringify(mock); + + const resp = evnelope.parse(testEvent, TestSchema); + + expect(resp).toEqual(mock); + }); + + it('should parse VPC Lattice event with trailing slash', () => { + const mock = generateMock(TestSchema); + const testEvent = TestEvents.vpcLatticeEventV2PathTrailingSlash as z.infer< + typeof VpcLatticeSchema + >; + + testEvent.body = JSON.stringify(mock); + + const resp = evnelope.parse(testEvent, TestSchema); + expect(resp).toEqual(mock); + }); +}); diff --git a/packages/parser/tests/unit/parser.test.ts b/packages/parser/tests/unit/parser.test.ts index 16097eb3e8..dd0bde0aad 100644 --- a/packages/parser/tests/unit/parser.test.ts +++ b/packages/parser/tests/unit/parser.test.ts @@ -9,8 +9,8 @@ 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 { Envelopes } from '../../src/envelopes/SqsEnvelope.js'; import { z, ZodSchema } from 'zod'; +import { Envelopes } from '../../src/envelopes/Envelopes.js'; describe('Middleware: parser', () => { const schema = z.object({ @@ -27,6 +27,8 @@ describe('Middleware: parser', () => { describe(' when envelope is provided ', () => { const middyfiedHandler = middy(handler).use( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore parser({ schema: schema, envelope: Envelopes.SQS_ENVELOPE }) ); diff --git a/packages/parser/tests/unit/schema/alb.test.ts b/packages/parser/tests/unit/schema/alb.test.ts index 5e9144582e..071f4598ce 100644 --- a/packages/parser/tests/unit/schema/alb.test.ts +++ b/packages/parser/tests/unit/schema/alb.test.ts @@ -7,25 +7,22 @@ import { AlbSchema, AlbMultiValueHeadersSchema, } from '../../../src/schemas/alb.js'; -import { loadExampleEvent } from './utils.js'; +import { TestEvents } from './utils.js'; describe('ALB ', () => { it('should parse alb event', () => { - const albEvent = loadExampleEvent('albEvent.json'); + const albEvent = TestEvents.albEvent; expect(AlbSchema.parse(albEvent)).toEqual(albEvent); }); it('should parse alb event path trailing slash', () => { - const albEventPathTrailingSlash = loadExampleEvent( - 'albEventPathTrailingSlash.json' - ); + const albEventPathTrailingSlash = TestEvents.albEventPathTrailingSlash; expect(AlbSchema.parse(albEventPathTrailingSlash)).toEqual( albEventPathTrailingSlash ); }); it('should parse alb event with multi value headers event', () => { - const albMultiValueHeadersEvent = loadExampleEvent( - 'albMultiValueHeadersEvent.json' - ); + const albMultiValueHeadersEvent = TestEvents.albMultiValueHeadersEvent; + expect(AlbMultiValueHeadersSchema.parse(albMultiValueHeadersEvent)).toEqual( albMultiValueHeadersEvent ); diff --git a/packages/parser/tests/unit/schema/apigw.test.ts b/packages/parser/tests/unit/schema/apigw.test.ts index 9aa23c6694..472aa89d39 100644 --- a/packages/parser/tests/unit/schema/apigw.test.ts +++ b/packages/parser/tests/unit/schema/apigw.test.ts @@ -4,68 +4,66 @@ * @group unit/parser/schema/ */ -import { loadExampleEvent } from './utils.js'; import { APIGatewayProxyEventSchema } from '../../../src/schemas/apigw.js'; +import { TestEvents } from './utils.js'; describe('APIGateway ', () => { it('should parse api gateway event', () => { - const apiGatewayProxyEvent = loadExampleEvent('apiGatewayProxyEvent.json'); + const apiGatewayProxyEvent = TestEvents.apiGatewayProxyEvent; + expect(APIGatewayProxyEventSchema.parse(apiGatewayProxyEvent)).toEqual( apiGatewayProxyEvent ); }); it('should parse api gateway authorizer request event', () => { - const apiGatewayAuthorizerRequestEvent = loadExampleEvent( - 'apiGatewayAuthorizerRequestEvent.json' - ); + const apiGatewayAuthorizerRequestEvent = + TestEvents.apiGatewayAuthorizerRequestEvent; + expect( APIGatewayProxyEventSchema.parse(apiGatewayAuthorizerRequestEvent) ).toEqual(apiGatewayAuthorizerRequestEvent); }); it('should parse schema middleware invalid event', () => { - const apiGatewaySchemaMiddlewareInvalidEvent = loadExampleEvent( - 'apiGatewaySchemaMiddlewareInvalidEvent.json' - ); + const apiGatewaySchemaMiddlewareInvalidEvent = + TestEvents.apiGatewaySchemaMiddlewareInvalidEvent; + expect( APIGatewayProxyEventSchema.parse(apiGatewaySchemaMiddlewareInvalidEvent) ).toEqual(apiGatewaySchemaMiddlewareInvalidEvent); }); it('should parse schema middleware valid event', () => { - const apiGatewaySchemaMiddlewareValidEvent = loadExampleEvent( - 'apiGatewaySchemaMiddlewareValidEvent.json' - ); + const apiGatewaySchemaMiddlewareValidEvent = + TestEvents.apiGatewaySchemaMiddlewareValidEvent; + expect( APIGatewayProxyEventSchema.parse(apiGatewaySchemaMiddlewareValidEvent) ).toEqual(apiGatewaySchemaMiddlewareValidEvent); }); it('should parse proxy event with no version auth', () => { - const apiGatewayProxyEvent_noVersionAuth = loadExampleEvent( - 'apiGatewayProxyEvent_noVersionAuth.json' - ); + const apiGatewayProxyEvent_noVersionAuth = + TestEvents.apiGatewayProxyEvent_noVersionAuth; + expect( APIGatewayProxyEventSchema.parse(apiGatewayProxyEvent_noVersionAuth) ).toEqual(apiGatewayProxyEvent_noVersionAuth); }); it('should parse proxy event with another path', () => { - const apiGatewayProxyEventAnotherPath = loadExampleEvent( - 'apiGatewayProxyEventAnotherPath.json' - ); + const apiGatewayProxyEventAnotherPath = + TestEvents.apiGatewayProxyEventAnotherPath; + expect( APIGatewayProxyEventSchema.parse(apiGatewayProxyEventAnotherPath) ).toEqual(apiGatewayProxyEventAnotherPath); }); it('should parse proxy event with path trailing slash', () => { - const apiGatewayProxyEventPathTrailingSlash = loadExampleEvent( - 'apiGatewayProxyEventPathTrailingSlash.json' - ); + const apiGatewayProxyEventPathTrailingSlash = + TestEvents.apiGatewayProxyEventPathTrailingSlash; expect( APIGatewayProxyEventSchema.parse(apiGatewayProxyEventPathTrailingSlash) ).toEqual(apiGatewayProxyEventPathTrailingSlash); }); it('should parse other proxy event', () => { - const apiGatewayProxyOtherEvent = loadExampleEvent( - 'apiGatewayProxyOtherEvent.json' - ); + const apiGatewayProxyOtherEvent = TestEvents.apiGatewayProxyOtherEvent; expect(APIGatewayProxyEventSchema.parse(apiGatewayProxyOtherEvent)).toEqual( apiGatewayProxyOtherEvent ); diff --git a/packages/parser/tests/unit/schema/apigwv2.test.ts b/packages/parser/tests/unit/schema/apigwv2.test.ts index 85ac2da1a2..59faa6ca70 100644 --- a/packages/parser/tests/unit/schema/apigwv2.test.ts +++ b/packages/parser/tests/unit/schema/apigwv2.test.ts @@ -4,30 +4,27 @@ * @group unit/parser/schema/ */ -import { loadExampleEvent } from './utils.js'; import { APIGatewayProxyEventV2Schema } from '../../../src/schemas/apigwv2.js'; +import { TestEvents } from './utils.js'; describe('API GW v2 ', () => { it('should parse api gateway v2 event', () => { - const apiGatewayProxyV2Event = loadExampleEvent( - 'apiGatewayProxyV2Event.json' - ); + const apiGatewayProxyV2Event = TestEvents.apiGatewayProxyV2Event; + expect(APIGatewayProxyEventV2Schema.parse(apiGatewayProxyV2Event)).toEqual( apiGatewayProxyV2Event ); }); it('should parse api gateway v2 event with GET method', () => { - const apiGatewayProxyV2Event_GET = loadExampleEvent( - 'apiGatewayProxyV2Event_GET.json' - ); + const apiGatewayProxyV2Event_GET = TestEvents.apiGatewayProxyV2Event_GET; expect( APIGatewayProxyEventV2Schema.parse(apiGatewayProxyV2Event_GET) ).toEqual(apiGatewayProxyV2Event_GET); }); it('should parse api gateway v2 event with path trailing slash', () => { - const apiGatewayProxyV2EventPathTrailingSlash = loadExampleEvent( - 'apiGatewayProxyV2EventPathTrailingSlash.json' - ); + const apiGatewayProxyV2EventPathTrailingSlash = + TestEvents.apiGatewayProxyV2EventPathTrailingSlash; + expect( APIGatewayProxyEventV2Schema.parse( apiGatewayProxyV2EventPathTrailingSlash @@ -35,33 +32,32 @@ describe('API GW v2 ', () => { ).toEqual(apiGatewayProxyV2EventPathTrailingSlash); }); it('should parse api gateway v2 event with iam', () => { - const apiGatewayProxyV2IamEvent = loadExampleEvent( - 'apiGatewayProxyV2IamEvent.json' - ); + const apiGatewayProxyV2IamEvent = TestEvents.apiGatewayProxyV2IamEvent; + expect( APIGatewayProxyEventV2Schema.parse(apiGatewayProxyV2IamEvent) ).toEqual(apiGatewayProxyV2IamEvent); }); it('should parse api gateway v2 event with lambda authorizer', () => { - const apiGatewayProxyV2LambdaAuthorizerEvent = loadExampleEvent( - 'apiGatewayProxyV2LambdaAuthorizerEvent.json' - ); + const apiGatewayProxyV2LambdaAuthorizerEvent = + TestEvents.apiGatewayProxyV2LambdaAuthorizerEvent; + expect( APIGatewayProxyEventV2Schema.parse(apiGatewayProxyV2LambdaAuthorizerEvent) ).toEqual(apiGatewayProxyV2LambdaAuthorizerEvent); }); it('should parse api gateway v2 event with other get event', () => { - const apiGatewayProxyV2OtherGetEvent = loadExampleEvent( - 'apiGatewayProxyV2OtherGetEvent.json' - ); + const apiGatewayProxyV2OtherGetEvent = + TestEvents.apiGatewayProxyV2OtherGetEvent; + expect( APIGatewayProxyEventV2Schema.parse(apiGatewayProxyV2OtherGetEvent) ).toEqual(apiGatewayProxyV2OtherGetEvent); }); it('should parse api gateway v2 event with schema middleware', () => { - const apiGatewayProxyV2SchemaMiddlewareValidEvent = loadExampleEvent( - 'apiGatewayProxyV2SchemaMiddlewareValidEvent.json' - ); + const apiGatewayProxyV2SchemaMiddlewareValidEvent = + TestEvents.apiGatewayProxyV2SchemaMiddlewareValidEvent; + expect( APIGatewayProxyEventV2Schema.parse( apiGatewayProxyV2SchemaMiddlewareValidEvent diff --git a/packages/parser/tests/unit/schema/cloudformation-custom-resource.test.ts b/packages/parser/tests/unit/schema/cloudformation-custom-resource.test.ts index 66ec61680d..d7ac35ac29 100644 --- a/packages/parser/tests/unit/schema/cloudformation-custom-resource.test.ts +++ b/packages/parser/tests/unit/schema/cloudformation-custom-resource.test.ts @@ -4,18 +4,18 @@ * @group unit/parser/schema/ */ -import { loadExampleEvent } from './utils.js'; import { CloudFormationCustomResourceCreateSchema, CloudFormationCustomResourceUpdateSchema, CloudFormationCustomResourceDeleteSchema, } from '../../../src/schemas/cloudformation-custom-resource.js'; +import { TestEvents } from './utils.js'; describe('CloudFormationCustomResource ', () => { it('should parse create event', () => { - const cloudFormationCustomResourceCreateEvent = loadExampleEvent( - 'cloudFormationCustomResourceCreateEvent.json' - ); + const cloudFormationCustomResourceCreateEvent = + TestEvents.cloudFormationCustomResourceCreateEvent; + expect( CloudFormationCustomResourceCreateSchema.parse( cloudFormationCustomResourceCreateEvent @@ -23,9 +23,9 @@ describe('CloudFormationCustomResource ', () => { ).toEqual(cloudFormationCustomResourceCreateEvent); }); it('should parse update event', () => { - const cloudFormationCustomResourceUpdateEvent = loadExampleEvent( - 'cloudFormationCustomResourceUpdateEvent.json' - ); + const cloudFormationCustomResourceUpdateEvent = + TestEvents.cloudFormationCustomResourceUpdateEvent; + expect( CloudFormationCustomResourceUpdateSchema.parse( cloudFormationCustomResourceUpdateEvent @@ -33,9 +33,9 @@ describe('CloudFormationCustomResource ', () => { ).toEqual(cloudFormationCustomResourceUpdateEvent); }); it('should parse delete event', () => { - const cloudFormationCustomResourceDeleteEvent = loadExampleEvent( - 'cloudFormationCustomResourceDeleteEvent.json' - ); + const cloudFormationCustomResourceDeleteEvent = + TestEvents.cloudFormationCustomResourceDeleteEvent; + expect( CloudFormationCustomResourceDeleteSchema.parse( cloudFormationCustomResourceDeleteEvent diff --git a/packages/parser/tests/unit/schema/cloudwatch.test.ts b/packages/parser/tests/unit/schema/cloudwatch.test.ts index a978030de2..c12e0d608c 100644 --- a/packages/parser/tests/unit/schema/cloudwatch.test.ts +++ b/packages/parser/tests/unit/schema/cloudwatch.test.ts @@ -4,12 +4,12 @@ * @group unit/parser/schema/ */ -import { loadExampleEvent } from './utils.js'; import { CloudWatchLogsSchema } from '../../../src/schemas/cloudwatch.js'; +import { TestEvents } from './utils.js'; describe('CloudWatchLogs ', () => { it('should parse cloudwatch logs event', () => { - const cloudWatchLogEvent = loadExampleEvent('cloudWatchLogEvent.json'); + const cloudWatchLogEvent = TestEvents.cloudWatchLogEvent; const parsed = CloudWatchLogsSchema.parse(cloudWatchLogEvent); expect(parsed.awslogs.data).toBeDefined(); expect(parsed.awslogs.data?.logEvents[0]).toEqual({ diff --git a/packages/parser/tests/unit/schema/dynamodb.test.ts b/packages/parser/tests/unit/schema/dynamodb.test.ts index b152c07b72..821d484f40 100644 --- a/packages/parser/tests/unit/schema/dynamodb.test.ts +++ b/packages/parser/tests/unit/schema/dynamodb.test.ts @@ -5,10 +5,10 @@ */ import { DynamoDBStreamSchema } from '../../../src/schemas/dynamodb.js'; -import { loadExampleEvent } from './utils.js'; +import { TestEvents } from './utils.js'; describe('DynamoDB ', () => { - const dynamoStreamEvent = loadExampleEvent('dynamoStreamEvent.json'); + const dynamoStreamEvent = TestEvents.dynamoStreamEvent; it('should parse a stream of records', () => { expect(DynamoDBStreamSchema.parse(dynamoStreamEvent)).toEqual( dynamoStreamEvent diff --git a/packages/parser/tests/unit/schema/eventbridge.test.ts b/packages/parser/tests/unit/schema/eventbridge.test.ts index e92bd2248f..b7ed50d37c 100644 --- a/packages/parser/tests/unit/schema/eventbridge.test.ts +++ b/packages/parser/tests/unit/schema/eventbridge.test.ts @@ -4,12 +4,13 @@ * @group unit/parser/schema/ */ -import { loadExampleEvent } from './utils.js'; import { EventBridgeSchema } from '../../../src/schemas/eventbridge.js'; +import { TestEvents } from './utils.js'; describe('EventBridge ', () => { it('should parse eventbridge event', () => { - const eventBridgeEvent = loadExampleEvent('eventBridgeEvent.json'); + const eventBridgeEvent = TestEvents.eventBridgeEvent; + expect(EventBridgeSchema.parse(eventBridgeEvent)).toEqual(eventBridgeEvent); }); }); diff --git a/packages/parser/tests/unit/schema/kafka.test.ts b/packages/parser/tests/unit/schema/kafka.test.ts index 3b2bc50b83..1296130dd8 100644 --- a/packages/parser/tests/unit/schema/kafka.test.ts +++ b/packages/parser/tests/unit/schema/kafka.test.ts @@ -4,11 +4,11 @@ * @group unit/parser/schema/ */ -import { loadExampleEvent } from './utils.js'; import { KafkaMskEventSchema, KafkaSelfManagedEventSchema, } from '../../../src/schemas/kafka.js'; +import { TestEvents } from './utils.js'; describe('Kafka ', () => { const expectedTestEvent = { @@ -26,15 +26,15 @@ describe('Kafka ', () => { ], }; it('should parse kafka MSK event', () => { - const kafkaEventMsk = loadExampleEvent('kafkaEventMsk.json'); + const kafkaEventMsk = TestEvents.kafkaEventMsk; + expect( KafkaMskEventSchema.parse(kafkaEventMsk).records['mytopic-0'][0] ).toEqual(expectedTestEvent); }); it('should parse kafka self managed event', () => { - const kafkaEventSelfManaged = loadExampleEvent( - 'kafkaEventSelfManaged.json' - ); + const kafkaEventSelfManaged = TestEvents.kafkaEventSelfManaged; + expect( KafkaSelfManagedEventSchema.parse(kafkaEventSelfManaged).records[ 'mytopic-0' @@ -42,9 +42,8 @@ describe('Kafka ', () => { ).toEqual(expectedTestEvent); }); it('should transform bootstrapServers to array', () => { - const kafkaEventSelfManaged = loadExampleEvent( - 'kafkaEventSelfManaged.json' - ); + const kafkaEventSelfManaged = TestEvents.kafkaEventSelfManaged; + expect( KafkaSelfManagedEventSchema.parse(kafkaEventSelfManaged).bootstrapServers ).toEqual([ @@ -53,11 +52,12 @@ describe('Kafka ', () => { ]); }); it('should return undefined if bootstrapServers is not present', () => { - const kafkaEventSelfManaged = loadExampleEvent( - 'kafkaEventSelfManaged.json' - ) as { bootstrapServers: string }; + const kafkaEventSelfManaged = TestEvents.kafkaEventSelfManaged as { + bootstrapServers: string; + }; kafkaEventSelfManaged.bootstrapServers = ''; const parsed = KafkaSelfManagedEventSchema.parse(kafkaEventSelfManaged); + expect(parsed.bootstrapServers).toBeUndefined(); }); }); diff --git a/packages/parser/tests/unit/schema/kinesis.test.ts b/packages/parser/tests/unit/schema/kinesis.test.ts index 99fc5eebc4..8a9aabadec 100644 --- a/packages/parser/tests/unit/schema/kinesis.test.ts +++ b/packages/parser/tests/unit/schema/kinesis.test.ts @@ -4,75 +4,57 @@ * @group unit/parser/schema/ */ -import { loadExampleEvent } from './utils.js'; import { KinesisDataStreamSchema } from '../../../src/schemas/kinesis.js'; import { KinesisFirehoseSchema, KinesisFirehoseSqsSchema, } from '../../../src/schemas/kinesis-firehose.js'; -import { extractCloudWatchLogFromEvent } from '../../../src/schemas/cloudwatch.js'; +import { TestEvents } from './utils.js'; describe('Kinesis ', () => { it('should parse kinesis event', () => { - const kinesisStreamEvent = loadExampleEvent('kinesisStreamEvent.json'); + const kinesisStreamEvent = TestEvents.kinesisStreamEvent; const parsed = KinesisDataStreamSchema.parse(kinesisStreamEvent); - const decodedData = Buffer.from( - parsed.Records[0].kinesis.data, - 'base64' - ).toString('utf8'); - expect(decodedData).toEqual('Hello, this is a test.'); + + expect(parsed.Records[0].kinesis.data).toEqual('Hello, this is a test.'); }); it('should parse single kinesis record', () => { - const kinesisStreamEventOneRecord = loadExampleEvent( - 'kinesisStreamEventOneRecord.json' - ); + const kinesisStreamEventOneRecord = TestEvents.kinesisStreamEventOneRecord; const parsed = KinesisDataStreamSchema.parse(kinesisStreamEventOneRecord); - const decodedJson = JSON.parse( - Buffer.from(parsed.Records[0].kinesis.data, 'base64').toString('utf8') - ); - expect(decodedJson).toEqual({ + + expect(parsed.Records[0].kinesis.data).toEqual({ message: 'test message', username: 'test', }); }); it('should parse Firehose event', () => { - const kinesisFirehoseKinesisEvent = loadExampleEvent( - 'kinesisFirehoseKinesisEvent.json' - ); + const kinesisFirehoseKinesisEvent = TestEvents.kinesisFirehoseKinesisEvent; const parsed = KinesisFirehoseSchema.parse(kinesisFirehoseKinesisEvent); expect(parsed.records[0].data).toEqual('Hello World'); }); it('should parse Kinesis Firehose PutEvents event', () => { - const kinesisFirehosePutEvent = loadExampleEvent( - 'kinesisFirehosePutEvent.json' - ); + const kinesisFirehosePutEvent = TestEvents.kinesisFirehosePutEvent; const parsed = KinesisFirehoseSchema.parse(kinesisFirehosePutEvent); expect(JSON.parse(parsed.records[1].data)).toEqual({ Hello: 'World', }); }); it('should parse Firehose event with SQS event', () => { - const kinesisFirehoseSQSEvent = loadExampleEvent( - 'kinesisFirehoseSQSEvent.json' - ); + const kinesisFirehoseSQSEvent = TestEvents.kinesisFirehoseSQSEvent; const parsed = KinesisFirehoseSqsSchema.parse(kinesisFirehoseSQSEvent); expect(parsed.records[0].data).toMatchObject({ messageId: '5ab807d4-5644-4c55-97a3-47396635ac74', body: 'Test message.', }); }); - it('should parse Firehose event with CloudWatch event', () => { - const kinesisStreamCloudWatchLogsEvent = loadExampleEvent( - 'kinesisStreamCloudWatchLogsEvent.json' - ); + it('should parse Kinesis event with CloudWatch event', () => { + const kinesisStreamCloudWatchLogsEvent = + TestEvents.kinesisStreamCloudWatchLogsEvent; const parsed = KinesisDataStreamSchema.parse( kinesisStreamCloudWatchLogsEvent ); - const jsonParsed = extractCloudWatchLogFromEvent( - parsed.Records[0].kinesis.data - ); - expect(jsonParsed).toMatchObject({ + expect(parsed.Records[0].kinesis.data).toMatchObject({ messageType: 'DATA_MESSAGE', owner: '231436140809', logGroup: '/aws/lambda/pt-1488-DummyLogDataFunction-gnWXPvL6jJyG', @@ -80,9 +62,9 @@ describe('Kinesis ', () => { }); }); it('should return original value if cannot parse KinesisFirehoseSqsRecord', () => { - const kinesisFirehoseSQSEvent = loadExampleEvent( - 'kinesisFirehoseSQSEvent.json' - ) as { records: { data: string }[] }; + const kinesisFirehoseSQSEvent = TestEvents.kinesisFirehoseSQSEvent as { + records: { data: string }[]; + }; kinesisFirehoseSQSEvent.records[0].data = 'not a valid json'; const parsed = KinesisFirehoseSqsSchema.parse(kinesisFirehoseSQSEvent); expect(parsed.records[0].data).toEqual('not a valid json'); diff --git a/packages/parser/tests/unit/schema/lambda.test.ts b/packages/parser/tests/unit/schema/lambda.test.ts index 459cd8a32e..cd789704bc 100644 --- a/packages/parser/tests/unit/schema/lambda.test.ts +++ b/packages/parser/tests/unit/schema/lambda.test.ts @@ -4,14 +4,13 @@ * @group unit/parser/schema/ */ -import { loadExampleEvent } from './utils.js'; import { LambdaFunctionUrlSchema } from '../../../src/schemas/lambda.js'; +import { TestEvents } from './utils.js'; describe('Lambda ', () => { it('should parse lambda event', () => { - const lambdaFunctionUrlEvent = loadExampleEvent( - 'apiGatewayProxyV2Event.json' - ); + const lambdaFunctionUrlEvent = TestEvents.apiGatewayProxyV2Event; + expect(LambdaFunctionUrlSchema.parse(lambdaFunctionUrlEvent)).toEqual( lambdaFunctionUrlEvent ); diff --git a/packages/parser/tests/unit/schema/s3.test.ts b/packages/parser/tests/unit/schema/s3.test.ts index 2c5378fdf7..eff1bb6840 100644 --- a/packages/parser/tests/unit/schema/s3.test.ts +++ b/packages/parser/tests/unit/schema/s3.test.ts @@ -10,18 +10,19 @@ import { S3Schema, S3ObjectLambdaEventSchema, } from '../../../src/schemas/s3.js'; -import { loadExampleEvent } from './utils.js'; +import { TestEvents } from './utils.js'; describe('S3 ', () => { it('should parse s3 event', () => { - const s3Event = loadExampleEvent('s3Event.json'); + const s3Event = TestEvents.s3Event; + expect(S3Schema.parse(s3Event)).toEqual(s3Event); }); it('should parse s3 event bridge notification event created', () => { - const s3EventBridgeNotificationObjectCreatedEvent = loadExampleEvent( - 's3EventBridgeNotificationObjectCreatedEvent.json' - ); + const s3EventBridgeNotificationObjectCreatedEvent = + TestEvents.s3EventBridgeNotificationObjectCreatedEvent; + expect( S3EventNotificationEventBridgeSchema.parse( s3EventBridgeNotificationObjectCreatedEvent @@ -30,9 +31,9 @@ describe('S3 ', () => { }); it('should parse s3 event bridge notification event detelted', () => { - const s3EventBridgeNotificationObjectDeletedEvent = loadExampleEvent( - 's3EventBridgeNotificationObjectDeletedEvent.json' - ); + const s3EventBridgeNotificationObjectDeletedEvent = + TestEvents.s3EventBridgeNotificationObjectDeletedEvent; + expect( S3EventNotificationEventBridgeSchema.parse( s3EventBridgeNotificationObjectDeletedEvent @@ -40,9 +41,9 @@ describe('S3 ', () => { ).toEqual(s3EventBridgeNotificationObjectDeletedEvent); }); it('should parse s3 event bridge notification event expired', () => { - const s3EventBridgeNotificationObjectExpiredEvent = loadExampleEvent( - 's3EventBridgeNotificationObjectExpiredEvent.json' - ); + const s3EventBridgeNotificationObjectExpiredEvent = + TestEvents.s3EventBridgeNotificationObjectExpiredEvent; + expect( S3EventNotificationEventBridgeSchema.parse( s3EventBridgeNotificationObjectExpiredEvent @@ -51,27 +52,27 @@ describe('S3 ', () => { }); it('should parse s3 sqs notification event', () => { - const s3SqsEvent = loadExampleEvent('s3SqsEvent.json'); + const s3SqsEvent = TestEvents.s3SqsEvent; expect(S3SqsEventNotificationSchema.parse(s3SqsEvent)).toEqual(s3SqsEvent); }); it('should parse s3 event with decoded key', () => { - const s3EventDecodedKey = loadExampleEvent('s3EventDecodedKey.json'); + const s3EventDecodedKey = TestEvents.s3EventDecodedKey; expect(S3Schema.parse(s3EventDecodedKey)).toEqual(s3EventDecodedKey); }); it('should parse s3 event delete object', () => { - const s3EventDeleteObject = loadExampleEvent('s3EventDeleteObject.json'); + const s3EventDeleteObject = TestEvents.s3EventDeleteObject; expect(S3Schema.parse(s3EventDeleteObject)).toEqual(s3EventDeleteObject); }); it('should parse s3 event glacier', () => { - const s3EventGlacier = loadExampleEvent('s3EventGlacier.json'); + const s3EventGlacier = TestEvents.s3EventGlacier; expect(S3Schema.parse(s3EventGlacier)).toEqual(s3EventGlacier); }); it('should parse s3 object event iam user', () => { - const s3ObjectEventIAMUser = loadExampleEvent('s3ObjectEventIAMUser.json'); + const s3ObjectEventIAMUser = TestEvents.s3ObjectEventIAMUser; expect(S3ObjectLambdaEventSchema.parse(s3ObjectEventIAMUser)).toEqual( s3ObjectEventIAMUser ); @@ -79,9 +80,8 @@ describe('S3 ', () => { it('should parse s3 object event temp credentials', () => { // ignore any because we don't want typed json - const s3ObjectEventTempCredentials = loadExampleEvent( - 's3ObjectEventTempCredentials.json' - ) as any; // eslint-disable-line @typescript-eslint/no-explicit-any + const s3ObjectEventTempCredentials = + TestEvents.s3ObjectEventTempCredentials as any; // eslint-disable-line @typescript-eslint/no-explicit-any const parsed = S3ObjectLambdaEventSchema.parse( s3ObjectEventTempCredentials ); diff --git a/packages/parser/tests/unit/schema/ses.test.ts b/packages/parser/tests/unit/schema/ses.test.ts index eeb29f6a1b..3d714ea074 100644 --- a/packages/parser/tests/unit/schema/ses.test.ts +++ b/packages/parser/tests/unit/schema/ses.test.ts @@ -4,12 +4,12 @@ * @group unit/parser/schema/ */ -import { loadExampleEvent } from './utils.js'; import { SesSchema } from '../../../src/schemas/ses.js'; +import { TestEvents } from './utils.js'; describe('Schema:', () => { - const sesEvent = loadExampleEvent('sesEvent.json'); it('SES should parse ses event', () => { + const sesEvent = TestEvents.sesEvent; expect(SesSchema.parse(sesEvent)).toEqual(sesEvent); }); }); diff --git a/packages/parser/tests/unit/schema/sns.test.ts b/packages/parser/tests/unit/schema/sns.test.ts index 66ec7aa297..1875d20642 100644 --- a/packages/parser/tests/unit/schema/sns.test.ts +++ b/packages/parser/tests/unit/schema/sns.test.ts @@ -4,12 +4,12 @@ * @group unit/parser/schema/ */ -import { loadExampleEvent } from './utils.js'; import { SnsSchema } from '../../../src/schemas/sns.js'; +import { TestEvents } from './utils.js'; describe('Schema:', () => { - const snsEvent = loadExampleEvent('snsEvent.json'); it('SNS should parse sns event', () => { + const snsEvent = TestEvents.snsEvent; expect(SnsSchema.parse(snsEvent)).toEqual(snsEvent); }); }); diff --git a/packages/parser/tests/unit/schema/sqs.test.ts b/packages/parser/tests/unit/schema/sqs.test.ts index 191b843298..802c36da08 100644 --- a/packages/parser/tests/unit/schema/sqs.test.ts +++ b/packages/parser/tests/unit/schema/sqs.test.ts @@ -4,12 +4,12 @@ * @group unit/parser/schema/ */ -import { loadExampleEvent } from './utils.js'; import { SqsSchema } from '../../../src/schemas/sqs.js'; +import { TestEvents } from './utils.js'; describe('SQS ', () => { - const sqsEvent = loadExampleEvent('sqsEvent.json'); it('should parse sqs event', () => { + const sqsEvent = TestEvents.sqsEvent; expect(SqsSchema.parse(sqsEvent)).toEqual(sqsEvent); }); }); diff --git a/packages/parser/tests/unit/schema/utils.ts b/packages/parser/tests/unit/schema/utils.ts index 8101dfefdb..3a17df8570 100644 --- a/packages/parser/tests/unit/schema/utils.ts +++ b/packages/parser/tests/unit/schema/utils.ts @@ -1,7 +1,116 @@ import { readFileSync } from 'node:fs'; +import { z } from 'zod'; -export const loadExampleEvent = (fileName: string): unknown => { - const event = readFileSync(`./tests/events/${fileName}`, 'utf8'); +export const TestSchema = z.object({ + name: z.string(), + age: z.number().min(18).max(99), +}); - return JSON.parse(event); +const filenames = [ + 'activeMQEvent', + 'albEvent', + 'albEventPathTrailingSlash', + 'albMultiValueHeadersEvent', + 'apiGatewayAuthorizerRequestEvent', + 'apiGatewayAuthorizerTokenEvent', + 'apiGatewayAuthorizerV2Event', + 'apiGatewayProxyEvent', + 'apiGatewayProxyEventAnotherPath', + 'apiGatewayProxyEventPathTrailingSlash', + 'apiGatewayProxyEventPrincipalId', + 'apiGatewayProxyEvent_noVersionAuth', + 'apiGatewayProxyOtherEvent', + 'apiGatewayProxyV2Event', + 'apiGatewayProxyV2EventPathTrailingSlash', + 'apiGatewayProxyV2Event_GET', + 'apiGatewayProxyV2IamEvent', + 'apiGatewayProxyV2LambdaAuthorizerEvent', + 'apiGatewayProxyV2OtherGetEvent', + 'apiGatewayProxyV2SchemaMiddlewareInvalidEvent', + 'apiGatewayProxyV2SchemaMiddlewareValidEvent', + 'apiGatewaySchemaMiddlewareInvalidEvent', + 'apiGatewaySchemaMiddlewareValidEvent', + 'appSyncAuthorizerEvent', + 'appSyncAuthorizerResponse', + 'appSyncDirectResolver', + 'appSyncResolverEvent', + 'awsConfigRuleConfigurationChanged', + 'awsConfigRuleOversizedConfiguration', + 'awsConfigRuleScheduled', + 'bedrockAgentEvent', + 'bedrockAgentPostEvent', + 'cloudFormationCustomResourceCreateEvent', + 'cloudFormationCustomResourceDeleteEvent', + 'cloudFormationCustomResourceUpdateEvent', + 'cloudWatchDashboardEvent', + 'cloudWatchLogEvent', + 'codePipelineEvent', + 'codePipelineEventData', + 'codePipelineEventEmptyUserParameters', + 'codePipelineEventWithEncryptionKey', + 'cognitoCreateAuthChallengeEvent', + 'cognitoCustomMessageEvent', + 'cognitoDefineAuthChallengeEvent', + 'cognitoPostAuthenticationEvent', + 'cognitoPostConfirmationEvent', + 'cognitoPreAuthenticationEvent', + 'cognitoPreSignUpEvent', + 'cognitoPreTokenGenerationEvent', + 'cognitoUserMigrationEvent', + 'cognitoVerifyAuthChallengeResponseEvent', + 'connectContactFlowEventAll', + 'connectContactFlowEventMin', + 'dynamoStreamEvent', + 'eventBridgeEvent', + 'kafkaEventMsk', + 'kafkaEventSelfManaged', + 'kinesisFirehoseKinesisEvent', + 'kinesisFirehosePutEvent', + 'kinesisFirehoseSQSEvent', + 'kinesisStreamCloudWatchLogsEvent', + 'kinesisStreamEvent', + 'kinesisStreamEventOneRecord', + 'lambdaFunctionUrlEvent', + 'lambdaFunctionUrlEventPathTrailingSlash', + 'lambdaFunctionUrlIAMEvent', + 'rabbitMQEvent', + 's3Event', + 's3EventBridgeNotificationObjectCreatedEvent', + 's3EventBridgeNotificationObjectDeletedEvent', + 's3EventBridgeNotificationObjectExpiredEvent', + 's3EventBridgeNotificationObjectRestoreCompletedEvent', + 's3EventDecodedKey', + 's3EventDeleteObject', + 's3EventGlacier', + 's3ObjectEventIAMUser', + 's3ObjectEventTempCredentials', + 's3SqsEvent', + 'secretsManagerEvent', + 'sesEvent', + 'snsEvent', + 'snsSqsEvent', + 'snsSqsFifoEvent', + 'sqsEvent', + 'vpcLatticeEvent', + 'vpcLatticeEventPathTrailingSlash', + 'vpcLatticeEventV2PathTrailingSlash', + 'vpcLatticeV2Event', +] as const; + +type TestEvents = { [K in (typeof filenames)[number]]: unknown }; +const loadFileContent = (filename: string): string => + readFileSync(`./tests/events/${filename}.json`, 'utf-8'); + +const createTestEvents = (fileList: readonly string[]): TestEvents => { + const testEvents: Partial = {}; + + fileList.forEach((filename) => { + Object.defineProperty(testEvents, filename, { + get: () => JSON.parse(loadFileContent(filename)), + }); + }); + + return testEvents as TestEvents; }; + +export const TestEvents = createTestEvents(filenames); diff --git a/packages/parser/tests/unit/schema/vpc-lattice.test.ts b/packages/parser/tests/unit/schema/vpc-lattice.test.ts index ea0a0dd4a4..576efa623f 100644 --- a/packages/parser/tests/unit/schema/vpc-lattice.test.ts +++ b/packages/parser/tests/unit/schema/vpc-lattice.test.ts @@ -4,18 +4,17 @@ * @group unit/parser/schema/ */ -import { loadExampleEvent } from './utils.js'; import { VpcLatticeSchema } from '../../../src/schemas/vpc-lattice.js'; +import { TestEvents } from './utils.js'; describe('VPC Lattice ', () => { it('should parse vpc lattice event', () => { - const vpcLatticeEvent = loadExampleEvent('vpcLatticeEvent.json'); + const vpcLatticeEvent = TestEvents.vpcLatticeEvent; expect(VpcLatticeSchema.parse(vpcLatticeEvent)).toEqual(vpcLatticeEvent); }); it('should parse vpc lattice path trailing slash event', () => { - const vpcLatticeEventPathTrailingSlash = loadExampleEvent( - 'vpcLatticeEventPathTrailingSlash.json' - ); + const vpcLatticeEventPathTrailingSlash = + TestEvents.vpcLatticeEventPathTrailingSlash; expect(VpcLatticeSchema.parse(vpcLatticeEventPathTrailingSlash)).toEqual( vpcLatticeEventPathTrailingSlash ); diff --git a/packages/parser/tests/unit/schema/vpc-latticev2.test.ts b/packages/parser/tests/unit/schema/vpc-latticev2.test.ts index da6d7d885e..e93deb24c1 100644 --- a/packages/parser/tests/unit/schema/vpc-latticev2.test.ts +++ b/packages/parser/tests/unit/schema/vpc-latticev2.test.ts @@ -4,20 +4,19 @@ * @group unit/parser/schema/ */ -import { loadExampleEvent } from './utils.js'; import { VpcLatticeV2Schema } from '../../../src/schemas/vpc-latticev2.js'; +import { TestEvents } from './utils.js'; describe('VpcLatticeV2 ', () => { it('should parse VpcLatticeV2 event', () => { - const vpcLatticeV2Event = loadExampleEvent('vpcLatticeV2Event.json'); + const vpcLatticeV2Event = TestEvents.vpcLatticeV2Event; const parsed = VpcLatticeV2Schema.parse(vpcLatticeV2Event); expect(parsed).toEqual(vpcLatticeV2Event); }); it('should parse VpcLatticeV2PathTrailingSlash event', () => { - const vpcLatticeEventV2PathTrailingSlash = loadExampleEvent( - 'vpcLatticeEventV2PathTrailingSlash.json' - ); + const vpcLatticeEventV2PathTrailingSlash = + TestEvents.vpcLatticeEventV2PathTrailingSlash; const parsed = VpcLatticeV2Schema.parse(vpcLatticeEventV2PathTrailingSlash); expect(parsed).toEqual(vpcLatticeEventV2PathTrailingSlash); }); From 9df45af2fb017a7272c8774040418247e4d13f77 Mon Sep 17 00:00:00 2001 From: Alexander Schueren Date: Tue, 12 Dec 2023 13:09:40 +0100 Subject: [PATCH 10/19] simplified check --- packages/parser/src/envelopes/apigw.ts | 6 +++--- packages/parser/src/envelopes/apigwv2.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/parser/src/envelopes/apigw.ts b/packages/parser/src/envelopes/apigw.ts index 0f0f6116fe..2b5768a862 100644 --- a/packages/parser/src/envelopes/apigw.ts +++ b/packages/parser/src/envelopes/apigw.ts @@ -14,10 +14,10 @@ export class ApiGatewayEnvelope extends Envelope { public parse(data: unknown, schema: T): z.infer { const parsedEnvelope: ApiGatewayProxyEvent = APIGatewayProxyEventSchema.parse(data); - if (parsedEnvelope.body === undefined) { + if (parsedEnvelope.body) { + return this._parse(parsedEnvelope.body, schema); + } else { throw new Error('Body field of API Gateway event is undefined'); } - - return this._parse(parsedEnvelope.body, schema); } } diff --git a/packages/parser/src/envelopes/apigwv2.ts b/packages/parser/src/envelopes/apigwv2.ts index dd1b4a5ec6..62e7e4b841 100644 --- a/packages/parser/src/envelopes/apigwv2.ts +++ b/packages/parser/src/envelopes/apigwv2.ts @@ -12,10 +12,10 @@ export class ApiGatwayV2Envelope extends Envelope { public parse(data: unknown, schema: T): z.infer { const parsedEnvelope = APIGatewayProxyEventV2Schema.parse(data); - if (parsedEnvelope.body === undefined) { + if (parsedEnvelope.body) { + return this._parse(parsedEnvelope.body, schema); + } else { throw new Error('Body field of API Gateway V2 event is undefined'); } - - return this._parse(parsedEnvelope.body, schema); } } From a98ea443808959e215af40dbe71275171c5f5c5a Mon Sep 17 00:00:00 2001 From: Alexander Schueren Date: Tue, 12 Dec 2023 13:11:55 +0100 Subject: [PATCH 11/19] remove middleware from this branch --- packages/parser/src/middleware/parser.ts | 56 ------------------------ 1 file changed, 56 deletions(-) delete mode 100644 packages/parser/src/middleware/parser.ts diff --git a/packages/parser/src/middleware/parser.ts b/packages/parser/src/middleware/parser.ts deleted file mode 100644 index ee4336122b..0000000000 --- a/packages/parser/src/middleware/parser.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { MiddyLikeRequest } from '@aws-lambda-powertools/commons/types'; -import { MiddlewareObj } from '@middy/core'; -import { ZodSchema } from 'zod'; -import { Envelope } from '../envelopes/Envelope.js'; - -interface ParserOptions { - schema: S; - envelope?: E; -} - -/** - * A middiy middleware to parse your event. - * - * @exmaple - * ```typescirpt - * import { parser } from '@aws-lambda-powertools/parser/middleware'; - * import middy from '@middy/core'; - * import { SQS_ENVELOPE } from '@aws-lambda-powertools/parser/envelopes;' - * - * const oderSchema = z.object({ - * id: z.number(), - * description: z.string(), - * quantity: z.number() - * } - * - * type Order = z.infer; - * - * export class 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: SQS_ENVELOPE })); - * ``` - * - * @param options - */ -const parser = ( - options: ParserOptions -): MiddlewareObj => { - const before = (request: MiddyLikeRequest): void => { - const { schema, envelope } = options; - if (envelope) { - request.event = envelope.parse(request.event, schema); - } else { - request.event = schema.parse(request.event); - } - }; - - return { - before, - }; -}; - -export { parser }; From c8c8b682b8b16802476f000e457fc69c0a5fa5bb Mon Sep 17 00:00:00 2001 From: Alexander Schueren Date: Tue, 12 Dec 2023 14:07:18 +0100 Subject: [PATCH 12/19] refactored from class to function envelopes --- packages/parser/src/envelopes/Envelope.ts | 27 ---------- packages/parser/src/envelopes/Envelopes.ts | 36 ------------- packages/parser/src/envelopes/apigw.ts | 27 ++++------ packages/parser/src/envelopes/apigwv2.ts | 23 ++++----- packages/parser/src/envelopes/cloudwatch.ts | 23 ++++----- packages/parser/src/envelopes/dynamodb.ts | 32 +++++------- packages/parser/src/envelopes/envelope.ts | 23 +++++++++ .../src/envelopes/eventBridgeEnvelope.ts | 19 +++---- packages/parser/src/envelopes/kafka.ts | 43 ++++++++-------- .../parser/src/envelopes/kinesis-firehose.ts | 23 ++++----- packages/parser/src/envelopes/kinesis.ts | 23 ++++----- packages/parser/src/envelopes/lambda.ts | 24 ++++----- packages/parser/src/envelopes/sns.ts | 50 ++++++++----------- packages/parser/src/envelopes/sqs.ts | 23 ++++----- packages/parser/src/envelopes/vpc-lattice.ts | 19 +++---- .../parser/src/envelopes/vpc-latticev2.ts | 19 +++---- .../tests/unit/envelopes/apigwt.test.ts | 8 +-- .../tests/unit/envelopes/apigwv2.test.ts | 8 ++- .../tests/unit/envelopes/cloudwatch.test.ts | 8 ++- .../tests/unit/envelopes/dynamodb.test.ts | 5 +- .../tests/unit/envelopes/eventbridge.test.ts | 18 +++---- .../parser/tests/unit/envelopes/kafka.test.ts | 8 ++- .../unit/envelopes/kinesis-firehose.test.ts | 11 ++-- .../tests/unit/envelopes/kinesis.test.ts | 5 +- .../tests/unit/envelopes/lambda.test.ts | 8 ++- .../parser/tests/unit/envelopes/sns.test.ts | 12 ++--- .../parser/tests/unit/envelopes/sqs.test.ts | 10 ++-- .../tests/unit/envelopes/vpc-lattice.test.ts | 7 ++- .../unit/envelopes/vpc-latticev2.test.ts | 7 ++- 29 files changed, 218 insertions(+), 331 deletions(-) delete mode 100644 packages/parser/src/envelopes/Envelope.ts delete mode 100644 packages/parser/src/envelopes/Envelopes.ts create mode 100644 packages/parser/src/envelopes/envelope.ts diff --git a/packages/parser/src/envelopes/Envelope.ts b/packages/parser/src/envelopes/Envelope.ts deleted file mode 100644 index 1b2f01f848..0000000000 --- a/packages/parser/src/envelopes/Envelope.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { z, ZodSchema } from 'zod'; - -/** - * Abstract class for envelopes. - */ -export abstract class Envelope { - protected constructor() {} - - public abstract parse( - data: unknown, - _schema: z.ZodSchema - ): z.infer; - - protected _parse( - data: unknown, - schema: T - ): z.infer[] { - if (typeof data === 'string') { - return schema.parse(JSON.parse(data)); - } else if (typeof data === 'object') { - return schema.parse(data); - } else - throw new Error( - `Invalid data type for envelope. Expected string or object, got ${typeof data}` - ); - } -} diff --git a/packages/parser/src/envelopes/Envelopes.ts b/packages/parser/src/envelopes/Envelopes.ts deleted file mode 100644 index 2f0e1d7d65..0000000000 --- a/packages/parser/src/envelopes/Envelopes.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { ApiGatewayEnvelope } from './apigw.js'; -import { ApiGatwayV2Envelope } from './apigwv2.js'; -import { CloudWatchEnvelope } from './cloudwatch.js'; -import { KafkaEnvelope } from './kafka.js'; -import { SqsEnvelope } from './sqs.js'; -import { EventBridgeEnvelope } from './eventBridgeEnvelope.js'; -import { KinesisFirehoseEnvelope } from './kinesis-firehose.js'; -import { LambdaFunctionUrlEnvelope } from './lambda.js'; -import { SnsEnvelope, SnsSqsEnvelope } from './sns.js'; -import { VpcLatticeEnvelope } from './vpc-lattice.js'; -import { VpcLatticeV2Envelope } from './vpc-latticev2.js'; -import { DynamoDBStreamEnvelope } from './dynamodb.js'; -import { KinesisEnvelope } from './kinesis.js'; - -/** - * A collection of envelopes to create new envelopes. - */ -export class Envelopes { - public static readonly API_GW_ENVELOPE = new ApiGatewayEnvelope(); - public static readonly API_GW_V2_ENVELOPE = new ApiGatwayV2Envelope(); - public static readonly CLOUDWATCH_ENVELOPE = new CloudWatchEnvelope(); - public static readonly DYNAMO_DB_STREAM_ENVELOPE = - new DynamoDBStreamEnvelope(); - public static readonly EVENT_BRIDGE_ENVELOPE = new EventBridgeEnvelope(); - public static readonly KAFKA_ENVELOPE = new KafkaEnvelope(); - public static readonly KINESIS_ENVELOPE = new KinesisEnvelope(); - public static readonly KINESIS_FIREHOSE_ENVELOPE = - new KinesisFirehoseEnvelope(); - public static readonly LAMBDA_FUCTION_URL_ENVELOPE = - new LambdaFunctionUrlEnvelope(); - public static readonly SNS_ENVELOPE = new SnsEnvelope(); - public static readonly SNS_SQS_ENVELOPE = new SnsSqsEnvelope(); - public static readonly SQS_ENVELOPE = new SqsEnvelope(); - public static readonly VPC_LATTICE_ENVELOPE = new VpcLatticeEnvelope(); - public static readonly VPC_LATTICE_V2_ENVELOPE = new VpcLatticeV2Envelope(); -} diff --git a/packages/parser/src/envelopes/apigw.ts b/packages/parser/src/envelopes/apigw.ts index 2b5768a862..49d094b405 100644 --- a/packages/parser/src/envelopes/apigw.ts +++ b/packages/parser/src/envelopes/apigw.ts @@ -1,23 +1,18 @@ -import { Envelope } from './Envelope.js'; +import { parse } from './envelope.js'; import { z, ZodSchema } from 'zod'; import { APIGatewayProxyEventSchema } from '../schemas/apigw.js'; -import { type ApiGatewayProxyEvent } from '../types/schema.js'; /** - * API Gateway envelope to extract data within body key" + * API Gateway envelope to extract data within body key */ -export class ApiGatewayEnvelope extends Envelope { - public constructor() { - super(); +export const apiGatewayEnvelope = ( + data: unknown, + schema: T +): z.infer => { + const parsedEnvelope = APIGatewayProxyEventSchema.parse(data); + if (!parsedEnvelope.body) { + throw new Error('Body field of API Gateway event is undefined'); } - public parse(data: unknown, schema: T): z.infer { - const parsedEnvelope: ApiGatewayProxyEvent = - APIGatewayProxyEventSchema.parse(data); - if (parsedEnvelope.body) { - return this._parse(parsedEnvelope.body, schema); - } else { - throw new Error('Body field of API Gateway event is undefined'); - } - } -} + return parse(parsedEnvelope.body, schema); +}; diff --git a/packages/parser/src/envelopes/apigwv2.ts b/packages/parser/src/envelopes/apigwv2.ts index 62e7e4b841..decadfcc57 100644 --- a/packages/parser/src/envelopes/apigwv2.ts +++ b/packages/parser/src/envelopes/apigwv2.ts @@ -1,21 +1,18 @@ -import { Envelope } from './Envelope.js'; +import { parse } from './envelope.js'; import { z, ZodSchema } from 'zod'; import { APIGatewayProxyEventV2Schema } from '../schemas/apigwv2.js'; /** * API Gateway V2 envelope to extract data within body key */ -export class ApiGatwayV2Envelope extends Envelope { - public constructor() { - super(); +export const apiGatewayV2Envelope = ( + data: unknown, + schema: T +): z.infer => { + const parsedEnvelope = APIGatewayProxyEventV2Schema.parse(data); + if (!parsedEnvelope.body) { + throw new Error('Body field of API Gateway event is undefined'); } - public parse(data: unknown, schema: T): z.infer { - const parsedEnvelope = APIGatewayProxyEventV2Schema.parse(data); - if (parsedEnvelope.body) { - return this._parse(parsedEnvelope.body, schema); - } else { - throw new Error('Body field of API Gateway V2 event is undefined'); - } - } -} + return parse(parsedEnvelope.body, schema); +}; diff --git a/packages/parser/src/envelopes/cloudwatch.ts b/packages/parser/src/envelopes/cloudwatch.ts index 5f1c55c463..848e7ab070 100644 --- a/packages/parser/src/envelopes/cloudwatch.ts +++ b/packages/parser/src/envelopes/cloudwatch.ts @@ -1,4 +1,4 @@ -import { Envelope } from './Envelope.js'; +import { parse } from './envelope.js'; import { z, ZodSchema } from 'zod'; import { CloudWatchLogsSchema } from '../schemas/cloudwatch.js'; @@ -11,16 +11,13 @@ import { CloudWatchLogsSchema } from '../schemas/cloudwatch.js'; * * Note: The record will be parsed the same way so if model is str */ -export class CloudWatchEnvelope extends Envelope { - public constructor() { - super(); - } +export const cloudWatchEnvelope = ( + data: unknown, + schema: T +): z.infer => { + const parsedEnvelope = CloudWatchLogsSchema.parse(data); - public parse(data: unknown, schema: T): z.infer[] { - const parsedEnvelope = CloudWatchLogsSchema.parse(data); - - return parsedEnvelope.awslogs.data.logEvents.map((record) => { - return this._parse(record.message, schema); - }); - } -} + return parsedEnvelope.awslogs.data.logEvents.map((record) => { + return parse(record.message, schema); + }); +}; diff --git a/packages/parser/src/envelopes/dynamodb.ts b/packages/parser/src/envelopes/dynamodb.ts index e1dd514a52..bb378b2d0b 100644 --- a/packages/parser/src/envelopes/dynamodb.ts +++ b/packages/parser/src/envelopes/dynamodb.ts @@ -1,4 +1,4 @@ -import { Envelope } from './Envelope.js'; +import { parse } from './envelope.js'; import { z, ZodSchema } from 'zod'; import { DynamoDBStreamSchema } from '../schemas/dynamodb.js'; @@ -13,22 +13,16 @@ type DynamoDBStreamEnvelopeResponse = { * Note: Values are the parsed models. Images' values can also be None, and * length of the list is the record's amount in the original event. */ -export class DynamoDBStreamEnvelope extends Envelope { - public constructor() { - super(); - } +export const dynamoDDStreamEnvelope = ( + data: unknown, + schema: T +): DynamoDBStreamEnvelopeResponse[] => { + const parsedEnvelope = DynamoDBStreamSchema.parse(data); - public parse( - data: unknown, - schema: T - ): DynamoDBStreamEnvelopeResponse[] { - const parsedEnvelope = DynamoDBStreamSchema.parse(data); - - return parsedEnvelope.Records.map((record) => { - return { - NewImage: this._parse(record.dynamodb.NewImage, schema), - OldImage: this._parse(record.dynamodb.OldImage, schema), - }; - }); - } -} + return parsedEnvelope.Records.map((record) => { + return { + NewImage: parse(record.dynamodb.NewImage, schema), + OldImage: parse(record.dynamodb.OldImage, schema), + }; + }); +}; diff --git a/packages/parser/src/envelopes/envelope.ts b/packages/parser/src/envelopes/envelope.ts new file mode 100644 index 0000000000..4c2dd9570d --- /dev/null +++ b/packages/parser/src/envelopes/envelope.ts @@ -0,0 +1,23 @@ +import { z, ZodSchema } from 'zod'; + +/** + * Abstract function to parse the content of the envelope using provided schema. + * Both inputs are provided as unknown by the user. + * We expect the data to be either string that can be parsed to json or object. + * @internal + * @param data data to parse + * @param schema schema + */ +export const parse = ( + data: unknown, + schema: T +): z.infer[] => { + if (typeof data === 'string') { + return schema.parse(JSON.parse(data)); + } else if (typeof data === 'object') { + return schema.parse(data); + } else + throw new Error( + `Invalid data type for envelope. Expected string or object, got ${typeof data}` + ); +}; diff --git a/packages/parser/src/envelopes/eventBridgeEnvelope.ts b/packages/parser/src/envelopes/eventBridgeEnvelope.ts index 786f3d31e8..4484635348 100644 --- a/packages/parser/src/envelopes/eventBridgeEnvelope.ts +++ b/packages/parser/src/envelopes/eventBridgeEnvelope.ts @@ -1,18 +1,13 @@ -import { Envelope } from './Envelope.js'; +import { parse } from './envelope.js'; import { z, ZodSchema } from 'zod'; import { EventBridgeSchema } from '../schemas/eventbridge.js'; /** * Envelope for EventBridge schema that extracts and parses data from the `detail` key. */ -export class EventBridgeEnvelope extends Envelope { - public constructor() { - super(); - } - - public parse(data: unknown, schema: T): z.infer { - const parsedEnvelope = EventBridgeSchema.parse(data); - - return this._parse(parsedEnvelope.detail, schema); - } -} +export const eventBridgeEnvelope = ( + data: unknown, + schema: T +): z.infer => { + return parse(EventBridgeSchema.parse(data).detail, schema); +}; diff --git a/packages/parser/src/envelopes/kafka.ts b/packages/parser/src/envelopes/kafka.ts index 84d573555e..32529d4255 100644 --- a/packages/parser/src/envelopes/kafka.ts +++ b/packages/parser/src/envelopes/kafka.ts @@ -1,5 +1,5 @@ import { z, ZodSchema } from 'zod'; -import { Envelope } from './Envelope.js'; +import { parse } from './envelope.js'; import { KafkaMskEventSchema, KafkaSelfManagedEventSchema, @@ -14,29 +14,26 @@ import { type KafkaRecord } from '../types/schema.js'; * Note: Records will be parsed the same way so if model is str, * all items in the list will be parsed as str and not as JSON (and vice versa) */ -export class KafkaEnvelope extends Envelope { - public constructor() { - super(); - } +export const kafkaEnvelope = ( + data: unknown, + schema: T +): z.infer => { + // manually fetch event source to deside between Msk or SelfManaged - public parse(data: unknown, schema: T): z.infer { - // manually fetch event source to deside between Msk or SelfManaged + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const eventSource = data['eventSource']; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const eventSource = data['eventSource']; + const parsedEnvelope: + | z.infer + | z.infer = + eventSource === 'aws:kafka' + ? KafkaMskEventSchema.parse(data) + : KafkaSelfManagedEventSchema.parse(data); - const parsedEnvelope: - | z.infer - | z.infer = - eventSource === 'aws:kafka' - ? KafkaMskEventSchema.parse(data) - : KafkaSelfManagedEventSchema.parse(data); - - return Object.values(parsedEnvelope.records).map((topicRecord) => { - return topicRecord.map((record: KafkaRecord) => { - return this._parse(record.value, schema); - }); + return Object.values(parsedEnvelope.records).map((topicRecord) => { + return topicRecord.map((record: KafkaRecord) => { + return parse(record.value, schema); }); - } -} + }); +}; diff --git a/packages/parser/src/envelopes/kinesis-firehose.ts b/packages/parser/src/envelopes/kinesis-firehose.ts index 9632351d0b..e51ae47a12 100644 --- a/packages/parser/src/envelopes/kinesis-firehose.ts +++ b/packages/parser/src/envelopes/kinesis-firehose.ts @@ -1,4 +1,4 @@ -import { Envelope } from './Envelope.js'; +import { parse } from './envelope.js'; import { z, ZodSchema } from 'zod'; import { KinesisFirehoseSchema } from '../schemas/kinesis-firehose.js'; @@ -14,16 +14,13 @@ import { KinesisFirehoseSchema } from '../schemas/kinesis-firehose.js'; * * https://docs.aws.amazon.com/lambda/latest/dg/services-kinesisfirehose.html */ -export class KinesisFirehoseEnvelope extends Envelope { - public constructor() { - super(); - } +export const kinesisFirehoseEnvelope = ( + data: unknown, + schema: T +): z.infer => { + const parsedEnvelope = KinesisFirehoseSchema.parse(data); - public parse(data: unknown, schema: T): z.infer { - const parsedEnvelope = KinesisFirehoseSchema.parse(data); - - return parsedEnvelope.records.map((record) => { - return this._parse(record.data, schema); - }); - } -} + return parsedEnvelope.records.map((record) => { + return parse(record.data, schema); + }); +}; diff --git a/packages/parser/src/envelopes/kinesis.ts b/packages/parser/src/envelopes/kinesis.ts index 775adb6a1c..311223042d 100644 --- a/packages/parser/src/envelopes/kinesis.ts +++ b/packages/parser/src/envelopes/kinesis.ts @@ -1,4 +1,4 @@ -import { Envelope } from './Envelope.js'; +import { parse } from './envelope.js'; import { z, ZodSchema } from 'zod'; import { KinesisDataStreamSchema } from '../schemas/kinesis.js'; @@ -12,16 +12,13 @@ import { KinesisDataStreamSchema } from '../schemas/kinesis.js'; * Note: Records will be parsed the same way so if model is str, * all items in the list will be parsed as str and not as JSON (and vice versa) */ -export class KinesisEnvelope extends Envelope { - public constructor() { - super(); - } +export const kinesisEnvelope = ( + data: unknown, + schema: T +): z.infer => { + const parsedEnvelope = KinesisDataStreamSchema.parse(data); - public parse(data: unknown, schema: T): z.infer { - const parsedEnvelope = KinesisDataStreamSchema.parse(data); - - return parsedEnvelope.Records.map((record) => { - return this._parse(record.kinesis.data, schema); - }); - } -} + return parsedEnvelope.Records.map((record) => { + return parse(record.kinesis.data, schema); + }); +}; diff --git a/packages/parser/src/envelopes/lambda.ts b/packages/parser/src/envelopes/lambda.ts index 2608da381e..3ac1f2b8c6 100644 --- a/packages/parser/src/envelopes/lambda.ts +++ b/packages/parser/src/envelopes/lambda.ts @@ -1,22 +1,18 @@ -import { Envelope } from './Envelope.js'; +import { parse } from './envelope.js'; import { z, ZodSchema } from 'zod'; import { LambdaFunctionUrlSchema } from '../schemas/lambda.js'; /** * Lambda function URL envelope to extract data within body key */ -export class LambdaFunctionUrlEnvelope extends Envelope { - public constructor() { - super(); +export const lambdaFunctionUrlEnvelope = ( + data: unknown, + schema: T +): z.infer => { + const parsedEnvelope = LambdaFunctionUrlSchema.parse(data); + if (!parsedEnvelope.body) { + throw new Error('Body field of Lambda function URL event is undefined'); } - public parse(data: unknown, schema: T): z.infer { - const parsedEnvelope = LambdaFunctionUrlSchema.parse(data); - - if (parsedEnvelope.body === undefined) { - throw new Error('Body field of Lambda function URL event is undefined'); - } - - return this._parse(parsedEnvelope.body, schema); - } -} + return parse(parsedEnvelope.body, schema); +}; diff --git a/packages/parser/src/envelopes/sns.ts b/packages/parser/src/envelopes/sns.ts index 0ff391b2cb..3e897a00a8 100644 --- a/packages/parser/src/envelopes/sns.ts +++ b/packages/parser/src/envelopes/sns.ts @@ -1,5 +1,5 @@ import { z, ZodSchema } from 'zod'; -import { Envelope } from './Envelope.js'; +import { parse } from './envelope.js'; import { SnsSchema, SnsSqsNotificationSchema } from '../schemas/sns.js'; import { SqsSchema } from '../schemas/sqs.js'; @@ -12,19 +12,16 @@ import { SqsSchema } from '../schemas/sqs.js'; * 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 SnsEnvelope extends Envelope { - public constructor() { - super(); - } +export const snsEnvelope = ( + data: unknown, + schema: T +): z.infer => { + const parsedEnvelope = SnsSchema.parse(data); - public parse(data: unknown, schema: T): z.infer { - const parsedEnvelope = SnsSchema.parse(data); - - return parsedEnvelope.Records.map((record) => { - return this._parse(record.Sns.Message, schema); - }); - } -} + return parsedEnvelope.Records.map((record) => { + return parse(record.Sns.Message, schema); + }); +}; /** * SNS plus SQS Envelope to extract array of Records @@ -37,20 +34,17 @@ export class SnsEnvelope extends Envelope { * 3. Finally, parse provided model against payload extracted * */ -export class SnsSqsEnvelope extends Envelope { - public constructor() { - super(); - } - - public parse(data: unknown, schema: T): z.infer { - const parsedEnvelope = SqsSchema.parse(data); +export const snsSqsEnvelope = ( + data: unknown, + schema: T +): z.infer => { + const parsedEnvelope = SqsSchema.parse(data); - return parsedEnvelope.Records.map((record) => { - const snsNotification = SnsSqsNotificationSchema.parse( - JSON.parse(record.body) - ); + return parsedEnvelope.Records.map((record) => { + const snsNotification = SnsSqsNotificationSchema.parse( + JSON.parse(record.body) + ); - return this._parse(snsNotification.Message, schema); - }); - } -} + return parse(snsNotification.Message, schema); + }); +}; diff --git a/packages/parser/src/envelopes/sqs.ts b/packages/parser/src/envelopes/sqs.ts index 98e0abaf06..2757663a95 100644 --- a/packages/parser/src/envelopes/sqs.ts +++ b/packages/parser/src/envelopes/sqs.ts @@ -1,6 +1,6 @@ import { z, ZodSchema } from 'zod'; import { SqsSchema } from '../schemas/sqs.js'; -import { Envelope } from './Envelope.js'; +import { parse } from './envelope.js'; /** * SQS Envelope to extract array of Records @@ -11,16 +11,13 @@ import { Envelope } from './Envelope.js'; * 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 constructor() { - super(); - } +export const sqsEnvelope = ( + data: unknown, + schema: T +): z.infer => { + const parsedEnvelope = SqsSchema.parse(data); - public parse(data: unknown, schema: T): z.infer[] { - const parsedEnvelope = SqsSchema.parse(data); - - return parsedEnvelope.Records.map((record) => { - return this._parse(record.body, schema); - }); - } -} + return parsedEnvelope.Records.map((record) => { + return parse(record.body, schema); + }); +}; diff --git a/packages/parser/src/envelopes/vpc-lattice.ts b/packages/parser/src/envelopes/vpc-lattice.ts index e8a8164f37..03d2998757 100644 --- a/packages/parser/src/envelopes/vpc-lattice.ts +++ b/packages/parser/src/envelopes/vpc-lattice.ts @@ -1,18 +1,15 @@ -import { Envelope } from './Envelope.js'; +import { parse } from './envelope.js'; import { z, ZodSchema } from 'zod'; import { VpcLatticeSchema } from '../schemas/vpc-lattice.js'; /** * Amazon VPC Lattice envelope to extract data within body key */ -export class VpcLatticeEnvelope extends Envelope { - public constructor() { - super(); - } +export const vpcLatticeEnvelope = ( + data: unknown, + schema: T +): z.infer => { + const parsedEnvelope = VpcLatticeSchema.parse(data); - public parse(data: unknown, schema: T): z.infer { - const parsedEnvelope = VpcLatticeSchema.parse(data); - - return this._parse(parsedEnvelope.body, schema); - } -} + return parse(parsedEnvelope.body, schema); +}; diff --git a/packages/parser/src/envelopes/vpc-latticev2.ts b/packages/parser/src/envelopes/vpc-latticev2.ts index 0e42762f6c..a3fa4389c0 100644 --- a/packages/parser/src/envelopes/vpc-latticev2.ts +++ b/packages/parser/src/envelopes/vpc-latticev2.ts @@ -1,18 +1,15 @@ -import { Envelope } from './Envelope.js'; +import { parse } from './envelope.js'; import { z, ZodSchema } from 'zod'; import { VpcLatticeV2Schema } from '../schemas/vpc-latticev2.js'; /** * Amazon VPC Lattice envelope to extract data within body key */ -export class VpcLatticeV2Envelope extends Envelope { - public constructor() { - super(); - } +export const vpcLatticeV2Envelope = ( + data: unknown, + schema: T +): z.infer => { + const parsedEnvelope = VpcLatticeV2Schema.parse(data); - public parse(data: unknown, schema: T): z.infer { - const parsedEnvelope = VpcLatticeV2Schema.parse(data); - - return this._parse(parsedEnvelope.body, schema); - } -} + return parse(parsedEnvelope.body, schema); +}; diff --git a/packages/parser/tests/unit/envelopes/apigwt.test.ts b/packages/parser/tests/unit/envelopes/apigwt.test.ts index 7fd07b399a..6c51736b16 100644 --- a/packages/parser/tests/unit/envelopes/apigwt.test.ts +++ b/packages/parser/tests/unit/envelopes/apigwt.test.ts @@ -5,18 +5,18 @@ */ import { generateMock } from '@anatine/zod-mock'; -import { Envelopes } from '../../../src/envelopes/Envelopes.js'; import { TestEvents, TestSchema } from '../schema/utils.js'; import { ApiGatewayProxyEvent } from '../../../src/types/schema.js'; +import { apiGatewayEnvelope } from '../../../src/envelopes/apigw'; describe('ApigwEnvelope ', () => { - const envelope = Envelopes.API_GW_ENVELOPE; it('should parse custom schema in envelope', () => { const testCustomSchemaObject = generateMock(TestSchema); const testEvent = TestEvents.apiGatewayProxyEvent as ApiGatewayProxyEvent; testEvent.body = JSON.stringify(testCustomSchemaObject); - const resp = envelope.parse(testEvent, TestSchema); + + const resp = apiGatewayEnvelope(testEvent, TestSchema); expect(resp).toEqual(testCustomSchemaObject); }); @@ -24,6 +24,6 @@ describe('ApigwEnvelope ', () => { const testEvent = TestEvents.apiGatewayProxyEvent as ApiGatewayProxyEvent; testEvent.body = undefined; - expect(() => envelope.parse(testEvent, TestSchema)).toThrow(); + expect(() => apiGatewayEnvelope(testEvent, TestSchema)).toThrow(); }); }); diff --git a/packages/parser/tests/unit/envelopes/apigwv2.test.ts b/packages/parser/tests/unit/envelopes/apigwv2.test.ts index ba12a566d4..acf7ee815b 100644 --- a/packages/parser/tests/unit/envelopes/apigwv2.test.ts +++ b/packages/parser/tests/unit/envelopes/apigwv2.test.ts @@ -4,14 +4,12 @@ * @group unit/parser/envelopes */ -import { Envelopes } from '../../../src/envelopes/Envelopes.js'; import { TestEvents, TestSchema } from '../schema/utils.js'; import { generateMock } from '@anatine/zod-mock'; import { APIGatewayProxyEventV2 } from 'aws-lambda'; +import { apiGatewayV2Envelope } from '../../../src/envelopes/apigwv2'; describe('ApiGwV2Envelope ', () => { - const envelope = Envelopes.API_GW_V2_ENVELOPE; - it('should parse custom schema in envelope', () => { const testEvent = TestEvents.apiGatewayProxyV2Event as APIGatewayProxyEventV2; @@ -19,7 +17,7 @@ describe('ApiGwV2Envelope ', () => { testEvent.body = JSON.stringify(data); - expect(envelope.parse(testEvent, TestSchema)).toEqual(data); + expect(apiGatewayV2Envelope(testEvent, TestSchema)).toEqual(data); }); it('should throw when no body provided', () => { @@ -27,6 +25,6 @@ describe('ApiGwV2Envelope ', () => { TestEvents.apiGatewayProxyV2Event as APIGatewayProxyEventV2; testEvent.body = undefined; - expect(() => envelope.parse(testEvent, TestSchema)).toThrow(); + expect(() => apiGatewayV2Envelope(testEvent, TestSchema)).toThrow(); }); }); diff --git a/packages/parser/tests/unit/envelopes/cloudwatch.test.ts b/packages/parser/tests/unit/envelopes/cloudwatch.test.ts index b7ee7c747a..3258768b5a 100644 --- a/packages/parser/tests/unit/envelopes/cloudwatch.test.ts +++ b/packages/parser/tests/unit/envelopes/cloudwatch.test.ts @@ -4,7 +4,6 @@ * @group unit/parser/envelopes */ -import { Envelopes } from '../../../src/envelopes/Envelopes.js'; import { generateMock } from '@anatine/zod-mock'; import { gzipSync } from 'node:zlib'; import { @@ -12,6 +11,7 @@ import { CloudWatchLogsDecodeSchema, } from '../../../src/schemas/cloudwatch.js'; import { TestSchema } from '../schema/utils.js'; +import { cloudWatchEnvelope } from '../../../src/envelopes/cloudwatch'; describe('CloudWatch', () => { it('should parse custom schema in envelope', () => { @@ -20,7 +20,6 @@ describe('CloudWatch', () => { data: '', }, }; - const envelope = Envelopes.CLOUDWATCH_ENVELOPE; const data = generateMock(TestSchema); const eventMock = generateMock(CloudWatchLogEventSchema, { @@ -36,7 +35,7 @@ describe('CloudWatch', () => { Buffer.from(JSON.stringify(logMock), 'utf8') ).toString('base64'); - expect(envelope.parse(testEvent, TestSchema)).toEqual([data]); + expect(cloudWatchEnvelope(testEvent, TestSchema)).toEqual([data]); }); it('should throw when schema does not match', () => { @@ -45,7 +44,6 @@ describe('CloudWatch', () => { data: '', }, }; - const envelope = Envelopes.CLOUDWATCH_ENVELOPE; const eventMock = generateMock(CloudWatchLogEventSchema, { stringMap: { @@ -60,6 +58,6 @@ describe('CloudWatch', () => { Buffer.from(JSON.stringify(logMock), 'utf8') ).toString('base64'); - expect(() => envelope.parse(testEvent, TestSchema)).toThrow(); + expect(() => cloudWatchEnvelope(testEvent, TestSchema)).toThrow(); }); }); diff --git a/packages/parser/tests/unit/envelopes/dynamodb.test.ts b/packages/parser/tests/unit/envelopes/dynamodb.test.ts index 65b45d81cb..342ad474e3 100644 --- a/packages/parser/tests/unit/envelopes/dynamodb.test.ts +++ b/packages/parser/tests/unit/envelopes/dynamodb.test.ts @@ -8,7 +8,7 @@ import { generateMock } from '@anatine/zod-mock'; import { TestEvents } from '../schema/utils.js'; import { DynamoDBStreamEvent } from 'aws-lambda'; import { z } from 'zod'; -import { Envelopes } from '../../../src/envelopes/Envelopes.js'; +import { dynamoDDStreamEnvelope } from '../../../src/envelopes/dynamodb'; describe('DynamoDB', () => { const schema = z.object({ @@ -16,7 +16,6 @@ describe('DynamoDB', () => { Id: z.record(z.literal('N'), z.number().min(0).max(100)), }); - const envelope = Envelopes.DYNAMO_DB_STREAM_ENVELOPE; it('should parse dynamodb envelope', () => { const mockOldImage = generateMock(schema); const mockNewImage = generateMock(schema); @@ -31,7 +30,7 @@ describe('DynamoDB', () => { (dynamodbEvent.Records[1].dynamodb!.OldImage as typeof mockOldImage) = mockOldImage; - const parsed = envelope.parse(dynamodbEvent, schema); + const parsed = dynamoDDStreamEnvelope(dynamodbEvent, schema); expect(parsed[0]).toEqual({ OldImage: mockOldImage, NewImage: mockNewImage, diff --git a/packages/parser/tests/unit/envelopes/eventbridge.test.ts b/packages/parser/tests/unit/envelopes/eventbridge.test.ts index d0f6164d76..773545f3bb 100644 --- a/packages/parser/tests/unit/envelopes/eventbridge.test.ts +++ b/packages/parser/tests/unit/envelopes/eventbridge.test.ts @@ -5,13 +5,11 @@ */ import { TestEvents, TestSchema } from '../schema/utils.js'; -import { Envelopes } from '../../../src/envelopes/Envelopes.js'; import { generateMock } from '@anatine/zod-mock'; import { EventBridgeEvent } from 'aws-lambda'; +import { eventBridgeEnvelope } from '../../../src/envelopes/eventBridgeEnvelope'; describe('EventBridgeEnvelope ', () => { - const envelope = Envelopes.EVENT_BRIDGE_ENVELOPE; - it('should parse eventbridge event', () => { const eventBridgeEvent = TestEvents.eventBridgeEvent as EventBridgeEvent< string, @@ -22,7 +20,7 @@ describe('EventBridgeEnvelope ', () => { eventBridgeEvent.detail = data; - expect(envelope.parse(eventBridgeEvent, TestSchema)).toEqual(data); + expect(eventBridgeEnvelope(eventBridgeEvent, TestSchema)).toEqual(data); }); it('should throw error if detail type does not match schema', () => { @@ -31,23 +29,23 @@ describe('EventBridgeEnvelope ', () => { object >; - const envelope = Envelopes.EVENT_BRIDGE_ENVELOPE; - eventBridgeEvent.detail = { foo: 'bar', }; - expect(() => envelope.parse(eventBridgeEvent, TestSchema)).toThrowError(); + expect(() => + eventBridgeEnvelope(eventBridgeEvent, TestSchema) + ).toThrowError(); }); it('should throw when invalid data type provided', () => { - const testEvent = TestEvents.eventBridgeEvent as EventBridgeEvent< + const eventBridgeEvent = TestEvents.eventBridgeEvent as EventBridgeEvent< string, object >; - testEvent.detail = 1 as unknown as object; + eventBridgeEvent.detail = 1 as unknown as object; - expect(() => envelope.parse(testEvent, TestSchema)).toThrow(); + expect(() => eventBridgeEnvelope(eventBridgeEvent, TestSchema)).toThrow(); }); }); diff --git a/packages/parser/tests/unit/envelopes/kafka.test.ts b/packages/parser/tests/unit/envelopes/kafka.test.ts index fff8635d7e..57e43a584f 100644 --- a/packages/parser/tests/unit/envelopes/kafka.test.ts +++ b/packages/parser/tests/unit/envelopes/kafka.test.ts @@ -7,11 +7,9 @@ import { generateMock } from '@anatine/zod-mock'; import { TestEvents, TestSchema } from '../schema/utils.js'; import { MSKEvent, SelfManagedKafkaEvent } from 'aws-lambda'; -import { Envelopes } from '../../../src/envelopes/Envelopes.js'; +import { kafkaEnvelope } from '../../../src/envelopes/kafka'; describe('Kafka', () => { - const envelope = Envelopes.KAFKA_ENVELOPE; - it('should parse MSK kafka envelope', () => { const mock = generateMock(TestSchema); @@ -20,7 +18,7 @@ describe('Kafka', () => { JSON.stringify(mock) ).toString('base64'); - const result = envelope.parse(kafkaEvent, TestSchema); + const result = kafkaEnvelope(kafkaEvent, TestSchema); expect(result).toEqual([[mock]]); }); @@ -34,7 +32,7 @@ describe('Kafka', () => { JSON.stringify(mock) ).toString('base64'); - const result = envelope.parse(kafkaEvent, TestSchema); + const result = kafkaEnvelope(kafkaEvent, TestSchema); expect(result).toEqual([[mock]]); }); diff --git a/packages/parser/tests/unit/envelopes/kinesis-firehose.test.ts b/packages/parser/tests/unit/envelopes/kinesis-firehose.test.ts index b329291b9d..581785d99f 100644 --- a/packages/parser/tests/unit/envelopes/kinesis-firehose.test.ts +++ b/packages/parser/tests/unit/envelopes/kinesis-firehose.test.ts @@ -4,11 +4,11 @@ * @group unit/parser/envelopes */ -import { Envelopes } from '../../../src/envelopes/Envelopes.js'; import { TestEvents, TestSchema } from '../schema/utils.js'; import { generateMock } from '@anatine/zod-mock'; import { KinesisFirehoseSchema } from '../../../src/schemas/kinesis-firehose.js'; import { z } from 'zod'; +import { kinesisFirehoseEnvelope } from '../../../src/envelopes/kinesis-firehose'; describe('Kinesis Firehose Envelope', () => { it('should parse records for PutEvent', () => { @@ -20,9 +20,8 @@ describe('Kinesis Firehose Envelope', () => { testEvent.records.map((record) => { record.data = Buffer.from(JSON.stringify(mock)).toString('base64'); }); - const envelope = Envelopes.KINESIS_FIREHOSE_ENVELOPE; - const resp = envelope.parse(testEvent, TestSchema); + const resp = kinesisFirehoseEnvelope(testEvent, TestSchema); expect(resp).toEqual([mock, mock]); }); @@ -35,9 +34,8 @@ describe('Kinesis Firehose Envelope', () => { testEvent.records.map((record) => { record.data = Buffer.from(JSON.stringify(mock)).toString('base64'); }); - const envelope = Envelopes.KINESIS_FIREHOSE_ENVELOPE; - const resp = envelope.parse(testEvent, TestSchema); + const resp = kinesisFirehoseEnvelope(testEvent, TestSchema); expect(resp).toEqual([mock]); }); @@ -50,9 +48,8 @@ describe('Kinesis Firehose Envelope', () => { testEvent.records.map((record) => { record.data = Buffer.from(JSON.stringify(mock)).toString('base64'); }); - const envelope = Envelopes.KINESIS_FIREHOSE_ENVELOPE; - const resp = envelope.parse(testEvent, TestSchema); + const resp = kinesisFirehoseEnvelope(testEvent, TestSchema); expect(resp).toEqual([mock, mock]); }); }); diff --git a/packages/parser/tests/unit/envelopes/kinesis.test.ts b/packages/parser/tests/unit/envelopes/kinesis.test.ts index 297efc7636..6395cf9803 100644 --- a/packages/parser/tests/unit/envelopes/kinesis.test.ts +++ b/packages/parser/tests/unit/envelopes/kinesis.test.ts @@ -6,11 +6,10 @@ import { generateMock } from '@anatine/zod-mock'; import { KinesisStreamEvent } from 'aws-lambda'; -import { Envelopes } from '../../../src/envelopes/Envelopes.js'; import { TestEvents, TestSchema } from '../schema/utils.js'; +import { kinesisEnvelope } from '../../../src/envelopes/kinesis'; describe('Kinesis', () => { - const envelope = Envelopes.KINESIS_ENVELOPE; it('should parse Kinesis Stream event', () => { const mock = generateMock(TestSchema); const testEvent = TestEvents.kinesisStreamEvent as KinesisStreamEvent; @@ -21,7 +20,7 @@ describe('Kinesis', () => { ); }); - const resp = envelope.parse(testEvent, TestSchema); + const resp = kinesisEnvelope(testEvent, TestSchema); expect(resp).toEqual([mock, mock]); }); }); diff --git a/packages/parser/tests/unit/envelopes/lambda.test.ts b/packages/parser/tests/unit/envelopes/lambda.test.ts index 59a9442460..f00411e8b3 100644 --- a/packages/parser/tests/unit/envelopes/lambda.test.ts +++ b/packages/parser/tests/unit/envelopes/lambda.test.ts @@ -4,14 +4,12 @@ * @group unit/parser/envelopes */ -import { Envelopes } from '../../../src/envelopes/Envelopes.js'; import { TestEvents, TestSchema } from '../schema/utils.js'; import { generateMock } from '@anatine/zod-mock'; import { APIGatewayProxyEventV2 } from 'aws-lambda'; +import { lambdaFunctionUrlEnvelope } from '../../../src/envelopes/lambda'; describe('Lambda Functions Url ', () => { - const envelope = Envelopes.LAMBDA_FUCTION_URL_ENVELOPE; - it('should parse custom schema in envelope', () => { const testEvent = TestEvents.lambdaFunctionUrlEvent as APIGatewayProxyEventV2; @@ -19,7 +17,7 @@ describe('Lambda Functions Url ', () => { testEvent.body = JSON.stringify(data); - expect(envelope.parse(testEvent, TestSchema)).toEqual(data); + expect(lambdaFunctionUrlEnvelope(testEvent, TestSchema)).toEqual(data); }); it('should throw when no body provided', () => { @@ -27,6 +25,6 @@ describe('Lambda Functions Url ', () => { TestEvents.apiGatewayProxyV2Event as APIGatewayProxyEventV2; testEvent.body = undefined; - expect(() => envelope.parse(testEvent, TestSchema)).toThrow(); + expect(() => lambdaFunctionUrlEnvelope(testEvent, TestSchema)).toThrow(); }); }); diff --git a/packages/parser/tests/unit/envelopes/sns.test.ts b/packages/parser/tests/unit/envelopes/sns.test.ts index 9d9bc511b8..3a380303ed 100644 --- a/packages/parser/tests/unit/envelopes/sns.test.ts +++ b/packages/parser/tests/unit/envelopes/sns.test.ts @@ -7,13 +7,12 @@ import { z } from 'zod'; import { generateMock } from '@anatine/zod-mock'; import { SNSEvent, SQSEvent } from 'aws-lambda'; -import { Envelopes } from '../../../src/envelopes/Envelopes.js'; import { TestEvents, TestSchema } from '../schema/utils.js'; +import { snsEnvelope, snsSqsEnvelope } from '../../../src/envelopes/sns'; describe('SNS Envelope', () => { it('should parse custom schema in envelope', () => { const testEvent = TestEvents.snsEvent as SNSEvent; - const envelope = Envelopes.SNS_ENVELOPE; const testRecords = [] as z.infer[]; @@ -23,12 +22,11 @@ describe('SNS Envelope', () => { record.Sns.Message = JSON.stringify(value); }); - expect(envelope.parse(testEvent, TestSchema)).toEqual(testRecords); + expect(snsEnvelope(testEvent, TestSchema)).toEqual(testRecords); }); it('should throw if message does not macht schema', () => { const testEvent = TestEvents.snsEvent as SNSEvent; - const envelope = Envelopes.SNS_ENVELOPE; testEvent.Records.map((record) => { record.Sns.Message = JSON.stringify({ @@ -36,7 +34,7 @@ describe('SNS Envelope', () => { }); }); - expect(() => envelope.parse(testEvent, TestSchema)).toThrowError(); + expect(() => snsEnvelope(testEvent, TestSchema)).toThrowError(); }); it('should parse sqs inside sns envelope', () => { @@ -48,8 +46,6 @@ describe('SNS Envelope', () => { snsSqsTestEvent.Records[0].body = JSON.stringify(snsEvent); - expect( - Envelopes.SNS_SQS_ENVELOPE.parse(snsSqsTestEvent, TestSchema) - ).toEqual([data]); + expect(snsSqsEnvelope(snsSqsTestEvent, TestSchema)).toEqual([data]); }); }); diff --git a/packages/parser/tests/unit/envelopes/sqs.test.ts b/packages/parser/tests/unit/envelopes/sqs.test.ts index 5cbeb86c74..4fb775433f 100644 --- a/packages/parser/tests/unit/envelopes/sqs.test.ts +++ b/packages/parser/tests/unit/envelopes/sqs.test.ts @@ -5,13 +5,11 @@ */ import { generateMock } from '@anatine/zod-mock'; -import { Envelopes } from '../../../src/envelopes/Envelopes.js'; import { TestEvents, TestSchema } from '../schema/utils.js'; import { SQSEvent } from 'aws-lambda'; +import { sqsEnvelope } from '../../../src/envelopes/sqs'; describe('SqsEnvelope ', () => { - const envelope = Envelopes.SQS_ENVELOPE; - it('should parse custom schema in envelope', () => { const mock = generateMock(TestSchema); @@ -19,19 +17,19 @@ describe('SqsEnvelope ', () => { sqsEvent.Records[0].body = JSON.stringify(mock); sqsEvent.Records[1].body = JSON.stringify(mock); - const resp = envelope.parse(sqsEvent, TestSchema); + const resp = sqsEnvelope(sqsEvent, TestSchema); expect(resp).toEqual([mock, mock]); }); it('should throw error if invalid keys for a schema', () => { expect(() => { - envelope.parse({ Records: [{ foo: 'bar' }] }, TestSchema); + sqsEnvelope({ Records: [{ foo: 'bar' }] }, TestSchema); }).toThrow(); }); it('should throw error if invalid values for a schema', () => { expect(() => { - envelope.parse( + sqsEnvelope( { Records: [ { diff --git a/packages/parser/tests/unit/envelopes/vpc-lattice.test.ts b/packages/parser/tests/unit/envelopes/vpc-lattice.test.ts index e754ba9b1a..a3b1b0f6c4 100644 --- a/packages/parser/tests/unit/envelopes/vpc-lattice.test.ts +++ b/packages/parser/tests/unit/envelopes/vpc-lattice.test.ts @@ -4,14 +4,13 @@ * @group unit/parser/envelopes */ -import { Envelopes } from '../../../src/envelopes/Envelopes.js'; import { generateMock } from '@anatine/zod-mock'; import { TestEvents, TestSchema } from '../schema/utils.js'; import { VpcLatticeSchema } from '../../../src/schemas/vpc-lattice.js'; import { z } from 'zod'; +import { vpcLatticeEnvelope } from '../../../src/envelopes/vpc-lattice'; describe('VPC Lattice envelope', () => { - const evnelope = Envelopes.VPC_LATTICE_ENVELOPE; it('should parse VPC Lattice event', () => { const mock = generateMock(TestSchema); const testEvent = TestEvents.vpcLatticeEvent as z.infer< @@ -20,7 +19,7 @@ describe('VPC Lattice envelope', () => { testEvent.body = JSON.stringify(mock); - const resp = evnelope.parse(testEvent, TestSchema); + const resp = vpcLatticeEnvelope(testEvent, TestSchema); expect(resp).toEqual(mock); }); @@ -33,7 +32,7 @@ describe('VPC Lattice envelope', () => { testEvent.body = JSON.stringify(mock); - const resp = evnelope.parse(testEvent, TestSchema); + const resp = vpcLatticeEnvelope(testEvent, TestSchema); expect(resp).toEqual(mock); }); }); diff --git a/packages/parser/tests/unit/envelopes/vpc-latticev2.test.ts b/packages/parser/tests/unit/envelopes/vpc-latticev2.test.ts index 878d15b04c..207dedb2da 100644 --- a/packages/parser/tests/unit/envelopes/vpc-latticev2.test.ts +++ b/packages/parser/tests/unit/envelopes/vpc-latticev2.test.ts @@ -7,11 +7,10 @@ import { generateMock } from '@anatine/zod-mock'; import { VpcLatticeSchema } from '../../../src/schemas/vpc-lattice.js'; import { z } from 'zod'; -import { Envelopes } from '../../../src/envelopes/Envelopes.js'; import { TestEvents, TestSchema } from '../schema/utils.js'; +import { vpcLatticeV2Envelope } from '../../../src/envelopes/vpc-latticev2'; describe('VPC Lattice envelope', () => { - const evnelope = Envelopes.VPC_LATTICE_V2_ENVELOPE; it('should parse VPC Lattice event', () => { const mock = generateMock(TestSchema); const testEvent = TestEvents.vpcLatticeV2Event as z.infer< @@ -20,7 +19,7 @@ describe('VPC Lattice envelope', () => { testEvent.body = JSON.stringify(mock); - const resp = evnelope.parse(testEvent, TestSchema); + const resp = vpcLatticeV2Envelope(testEvent, TestSchema); expect(resp).toEqual(mock); }); @@ -33,7 +32,7 @@ describe('VPC Lattice envelope', () => { testEvent.body = JSON.stringify(mock); - const resp = evnelope.parse(testEvent, TestSchema); + const resp = vpcLatticeV2Envelope(testEvent, TestSchema); expect(resp).toEqual(mock); }); }); From 3bec38a33e36c961a94c861989086463019f39b2 Mon Sep 17 00:00:00 2001 From: Alexander Schueren Date: Tue, 12 Dec 2023 14:43:56 +0100 Subject: [PATCH 13/19] removed parser tests, should be in another branch --- packages/parser/tests/unit/parser.test.ts | 123 ---------------------- 1 file changed, 123 deletions(-) delete mode 100644 packages/parser/tests/unit/parser.test.ts diff --git a/packages/parser/tests/unit/parser.test.ts b/packages/parser/tests/unit/parser.test.ts deleted file mode 100644 index dd0bde0aad..0000000000 --- a/packages/parser/tests/unit/parser.test.ts +++ /dev/null @@ -1,123 +0,0 @@ -/** - * 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 { Envelopes } from '../../src/envelopes/Envelopes.js'; - -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( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - parser({ schema: schema, envelope: Envelopes.SQS_ENVELOPE }) - ); - - it('should parse request body with schema and envelope', async () => { - const bodyMock = generateMock(schema); - - 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: Envelopes.SQS_ENVELOPE }) - ); - - 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: Envelopes.SQS_ENVELOPE }) - ); - - 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 })); - - 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 })); - - 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 ba14813f28cab3e81b4791901bd31b336a691749 Mon Sep 17 00:00:00 2001 From: Alexander Schueren Date: Tue, 12 Dec 2023 14:45:13 +0100 Subject: [PATCH 14/19] add parser to pre push --- .husky/pre-push | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.husky/pre-push b/.husky/pre-push index 3b6d564252..b5f55642f9 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -7,4 +7,5 @@ npm t \ -w packages/metrics \ -w packages/tracer \ -w packages/idempotency \ - -w packages/parameters \ No newline at end of file + -w packages/parameters \ + -w packages/parser \ No newline at end of file From 58ee5fbca5f4b74e8c6c0275c54740a8dd853c4d Mon Sep 17 00:00:00 2001 From: Alexander Schueren Date: Tue, 12 Dec 2023 15:15:04 +0100 Subject: [PATCH 15/19] add middy middleware --- ...eventBridgeEnvelope.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 | 121 ++++++++++++++++++ 5 files changed, 208 insertions(+), 1 deletion(-) rename packages/parser/src/envelopes/{eventBridgeEnvelope.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/eventBridgeEnvelope.ts b/packages/parser/src/envelopes/event-bridge.ts similarity index 100% rename from packages/parser/src/envelopes/eventBridgeEnvelope.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 773545f3bb..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/eventBridgeEnvelope'; +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..24f4bc3322 --- /dev/null +++ b/packages/parser/tests/unit/parser.test.ts @@ -0,0 +1,121 @@ +/** + * 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'; + +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: schema, envelope: sqsEnvelope }) + ); + + it('should parse request body with schema and envelope', async () => { + const bodyMock = generateMock(schema); + + 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 })); + + 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 })); + + 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 057685dd4618ec4ac0eb17a78b7302232c7dd3de Mon Sep 17 00:00:00 2001 From: Alexander Schueren Date: Tue, 12 Dec 2023 15:36:10 +0100 Subject: [PATCH 16/19] use TestSchema --- packages/parser/tests/unit/parser.test.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/parser/tests/unit/parser.test.ts b/packages/parser/tests/unit/parser.test.ts index 24f4bc3322..6adfb256c4 100644 --- a/packages/parser/tests/unit/parser.test.ts +++ b/packages/parser/tests/unit/parser.test.ts @@ -11,13 +11,10 @@ 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; + type schema = z.infer; const handler = async ( event: schema | unknown, _context: Context @@ -27,11 +24,11 @@ describe('Middleware: parser', () => { describe(' when envelope is provided ', () => { const middyfiedHandler = middy(handler).use( - parser({ schema: schema, envelope: sqsEnvelope }) + parser({ schema: TestSchema, envelope: sqsEnvelope }) ); it('should parse request body with schema and envelope', async () => { - const bodyMock = generateMock(schema); + const bodyMock = generateMock(TestSchema); const event = generateMock(SqsSchema, { stringMap: { @@ -97,13 +94,17 @@ describe('Middleware: parser', () => { it('should parse custom event', async () => { const event = { name: 'John', age: 18 }; - const middyfiedHandler = middy(handler).use(parser({ schema })); + 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 })); + const middyfiedHandler = middy(handler).use( + parser({ schema: TestSchema }) + ); await expect(middyfiedHandler(42, {} as Context)).rejects.toThrow(); }); From 6a0da6bff0474ea6dcbb0c5f0b55cd197c567744 Mon Sep 17 00:00:00 2001 From: Alexander Schueren Date: Tue, 19 Dec 2023 14:12:09 +0100 Subject: [PATCH 17/19] feat(parser): add schema envelopes (#1815) * first envelope * add abstract class * add tests * add more tests * fix tests * add envelopes * add middy parser * minor schema changes * add more envelopes and tests, refactored utils to autocomplete event files * simplified check * remove middleware from this branch * refactored from class to function envelopes * removed parser tests, should be in another branch * add parser to pre push * consistent naming --- packages/parser/src/envelopes/eventbridge.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 packages/parser/src/envelopes/eventbridge.ts diff --git a/packages/parser/src/envelopes/eventbridge.ts b/packages/parser/src/envelopes/eventbridge.ts new file mode 100644 index 0000000000..4484635348 --- /dev/null +++ b/packages/parser/src/envelopes/eventbridge.ts @@ -0,0 +1,13 @@ +import { parse } from './envelope.js'; +import { z, ZodSchema } from 'zod'; +import { EventBridgeSchema } from '../schemas/eventbridge.js'; + +/** + * Envelope for EventBridge schema that extracts and parses data from the `detail` key. + */ +export const eventBridgeEnvelope = ( + data: unknown, + schema: T +): z.infer => { + return parse(EventBridgeSchema.parse(data).detail, schema); +}; From fcf762cbb48cfaf1f1897316ca65aea1d9b9349d Mon Sep 17 00:00:00 2001 From: Alexander Schueren Date: Tue, 12 Dec 2023 15:15:04 +0100 Subject: [PATCH 18/19] add middy middleware --- packages/parser/src/envelopes/eventbridge.ts | 13 ---------- packages/parser/tests/unit/parser.test.ts | 26 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 13 deletions(-) delete mode 100644 packages/parser/src/envelopes/eventbridge.ts diff --git a/packages/parser/src/envelopes/eventbridge.ts b/packages/parser/src/envelopes/eventbridge.ts deleted file mode 100644 index 4484635348..0000000000 --- a/packages/parser/src/envelopes/eventbridge.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { parse } from './envelope.js'; -import { z, ZodSchema } from 'zod'; -import { EventBridgeSchema } from '../schemas/eventbridge.js'; - -/** - * Envelope for EventBridge schema that extracts and parses data from the `detail` key. - */ -export const eventBridgeEnvelope = ( - data: unknown, - schema: T -): z.infer => { - return parse(EventBridgeSchema.parse(data).detail, schema); -}; diff --git a/packages/parser/tests/unit/parser.test.ts b/packages/parser/tests/unit/parser.test.ts index 6adfb256c4..d5f3b45e2d 100644 --- a/packages/parser/tests/unit/parser.test.ts +++ b/packages/parser/tests/unit/parser.test.ts @@ -11,10 +11,20 @@ import { generateMock } from '@anatine/zod-mock'; import { SqsSchema } from '../../src/schemas/sqs.js'; import { z, ZodSchema } from 'zod'; import { sqsEnvelope } from '../../src/envelopes/sqs'; +<<<<<<< HEAD import { TestSchema } from './schema/utils'; describe('Middleware: parser', () => { type schema = z.infer; +======= + +describe('Middleware: parser', () => { + const schema = z.object({ + name: z.string(), + age: z.number().min(18).max(99), + }); + type schema = z.infer; +>>>>>>> 8c75dae3 (add middy middleware) const handler = async ( event: schema | unknown, _context: Context @@ -24,11 +34,19 @@ describe('Middleware: parser', () => { describe(' when envelope is provided ', () => { const middyfiedHandler = middy(handler).use( +<<<<<<< HEAD parser({ schema: TestSchema, envelope: sqsEnvelope }) ); it('should parse request body with schema and envelope', async () => { const bodyMock = generateMock(TestSchema); +======= + parser({ schema: schema, envelope: sqsEnvelope }) + ); + + it('should parse request body with schema and envelope', async () => { + const bodyMock = generateMock(schema); +>>>>>>> 8c75dae3 (add middy middleware) const event = generateMock(SqsSchema, { stringMap: { @@ -94,17 +112,25 @@ describe('Middleware: parser', () => { it('should parse custom event', async () => { const event = { name: 'John', age: 18 }; +<<<<<<< HEAD const middyfiedHandler = middy(handler).use( parser({ schema: TestSchema }) ); +======= + const middyfiedHandler = middy(handler).use(parser({ schema })); +>>>>>>> 8c75dae3 (add middy middleware) expect(await middyfiedHandler(event, {} as Context)).toEqual(event); }); it('should throw when the schema does not match', async () => { +<<<<<<< HEAD const middyfiedHandler = middy(handler).use( parser({ schema: TestSchema }) ); +======= + const middyfiedHandler = middy(handler).use(parser({ schema })); +>>>>>>> 8c75dae3 (add middy middleware) await expect(middyfiedHandler(42, {} as Context)).rejects.toThrow(); }); From 65226c2e450d781cd48af05ed719e8a087407a9c Mon Sep 17 00:00:00 2001 From: Alexander Schueren Date: Tue, 19 Dec 2023 15:55:49 +0100 Subject: [PATCH 19/19] fix merge error --- packages/parser/tests/unit/parser.test.ts | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/packages/parser/tests/unit/parser.test.ts b/packages/parser/tests/unit/parser.test.ts index d5f3b45e2d..19bbfdb52d 100644 --- a/packages/parser/tests/unit/parser.test.ts +++ b/packages/parser/tests/unit/parser.test.ts @@ -11,20 +11,14 @@ import { generateMock } from '@anatine/zod-mock'; import { SqsSchema } from '../../src/schemas/sqs.js'; import { z, ZodSchema } from 'zod'; import { sqsEnvelope } from '../../src/envelopes/sqs'; -<<<<<<< HEAD import { TestSchema } from './schema/utils'; -describe('Middleware: parser', () => { - type schema = z.infer; -======= - describe('Middleware: parser', () => { const schema = z.object({ name: z.string(), age: z.number().min(18).max(99), }); type schema = z.infer; ->>>>>>> 8c75dae3 (add middy middleware) const handler = async ( event: schema | unknown, _context: Context @@ -34,19 +28,12 @@ describe('Middleware: parser', () => { describe(' when envelope is provided ', () => { const middyfiedHandler = middy(handler).use( -<<<<<<< HEAD parser({ schema: TestSchema, envelope: sqsEnvelope }) ); it('should parse request body with schema and envelope', async () => { const bodyMock = generateMock(TestSchema); -======= - parser({ schema: schema, envelope: sqsEnvelope }) - ); - - it('should parse request body with schema and envelope', async () => { - const bodyMock = generateMock(schema); ->>>>>>> 8c75dae3 (add middy middleware) + parser({ schema: schema, envelope: sqsEnvelope }); const event = generateMock(SqsSchema, { stringMap: { @@ -112,25 +99,17 @@ describe('Middleware: parser', () => { it('should parse custom event', async () => { const event = { name: 'John', age: 18 }; -<<<<<<< HEAD const middyfiedHandler = middy(handler).use( parser({ schema: TestSchema }) ); -======= - const middyfiedHandler = middy(handler).use(parser({ schema })); ->>>>>>> 8c75dae3 (add middy middleware) expect(await middyfiedHandler(event, {} as Context)).toEqual(event); }); it('should throw when the schema does not match', async () => { -<<<<<<< HEAD const middyfiedHandler = middy(handler).use( parser({ schema: TestSchema }) ); -======= - const middyfiedHandler = middy(handler).use(parser({ schema })); ->>>>>>> 8c75dae3 (add middy middleware) await expect(middyfiedHandler(42, {} as Context)).rejects.toThrow(); });