diff --git a/packages/parser/src/middleware/parser.ts b/packages/parser/src/middleware/parser.ts index 2a79ad0a3f..8a2c26c327 100644 --- a/packages/parser/src/middleware/parser.ts +++ b/packages/parser/src/middleware/parser.ts @@ -1,12 +1,7 @@ import { type MiddyLikeRequest } from '@aws-lambda-powertools/commons/types'; import { type MiddlewareObj } from '@middy/core'; import { type ZodSchema } from 'zod'; -import { type Envelope } from '../types/envelope.js'; - -interface ParserOptions { - schema: S; - envelope?: Envelope; -} +import { type ParserOptions } from '../types/ParserOptions.js'; /** * A middiy middleware to parse your event. diff --git a/packages/parser/src/parser.ts b/packages/parser/src/parser.ts new file mode 100644 index 0000000000..c3ba727ae3 --- /dev/null +++ b/packages/parser/src/parser.ts @@ -0,0 +1,59 @@ +import { HandlerMethodDecorator } from '@aws-lambda-powertools/commons/types'; +import { Context, Handler } from 'aws-lambda'; +import { ZodSchema } from 'zod'; +import { type ParserOptions } from './types/ParserOptions.js'; + +/** + * A decorator to parse your event. + * + * @example + * ```typescript + * + * import { parser } from '@aws-lambda-powertools/parser'; + * import { sqsEnvelope } from '@aws-lambda-powertools/parser/envelopes/sqs'; + * + * + * const Order = z.object({ + * orderId: z.string(), + * description: z.string(), + * } + * + * class Lambda extends LambdaInterface { + * + * @parser({ envelope: sqsEnvelope, schema: OrderSchema }) + * public async handler(event: Order, _context: Context): Promise { + * // sqs event is parsed and the payload is extracted and parsed + * // apply business logic to your Order event + * const res = processOrder(event); + * return res; + * } + * } + * + * @param options + */ +const parser = ( + options: ParserOptions +): HandlerMethodDecorator => { + return (_target, _propertyKey, descriptor) => { + const original = descriptor.value!; + + const { schema, envelope } = options; + + descriptor.value = async function ( + this: Handler, + event: unknown, + context: Context, + callback + ) { + const parsedEvent = envelope + ? envelope(event, schema) + : schema.parse(event); + + return original.call(this, parsedEvent, context, callback); + }; + + return descriptor; + }; +}; + +export { parser }; diff --git a/packages/parser/src/types/ParserOptions.ts b/packages/parser/src/types/ParserOptions.ts new file mode 100644 index 0000000000..efdf91cf95 --- /dev/null +++ b/packages/parser/src/types/ParserOptions.ts @@ -0,0 +1,9 @@ +import type { ZodSchema } from 'zod'; +import { Envelope } from './envelope.js'; + +type ParserOptions = { + schema: S; + envelope?: Envelope; +}; + +export { type ParserOptions }; diff --git a/packages/parser/tests/unit/parser.decorator.test.ts b/packages/parser/tests/unit/parser.decorator.test.ts new file mode 100644 index 0000000000..eba2a3b59b --- /dev/null +++ b/packages/parser/tests/unit/parser.decorator.test.ts @@ -0,0 +1,118 @@ +/** + * Test decorator parser + * + * @group unit/parser + */ + +import { LambdaInterface } from '@aws-lambda-powertools/commons/lib/esm/types'; +import { Context, EventBridgeEvent } from 'aws-lambda'; +import { parser } from '../../src/parser'; +import { TestSchema, TestEvents } from './schema/utils'; +import { generateMock } from '@anatine/zod-mock'; +import { eventBridgeEnvelope } from '../../src/envelopes/event-bridge'; +import { EventBridgeSchema } from '../../src/schemas/eventbridge'; +import { z } from 'zod'; + +describe('Parser Decorator', () => { + const customEventBridgeSchema = EventBridgeSchema.extend({ + detail: TestSchema, + }); + + type TestSchema = z.infer; + + class TestClass implements LambdaInterface { + @parser({ schema: TestSchema }) + public async handler( + event: TestSchema, + _context: Context + ): Promise { + return event; + } + + @parser({ schema: customEventBridgeSchema }) + public async handlerWithCustomSchema( + event: unknown, + _context: Context + ): Promise { + return event; + } + + @parser({ schema: TestSchema, envelope: eventBridgeEnvelope }) + public async handlerWithParserCallsAnotherMethod( + event: unknown, + _context: Context + ): Promise { + return this.anotherMethod(event as TestSchema); + } + + @parser({ envelope: eventBridgeEnvelope, schema: TestSchema }) + public async handlerWithSchemaAndEnvelope( + event: unknown, + _context: Context + ): Promise { + return event; + } + + private async anotherMethod(event: TestSchema): Promise { + return event; + } + } + + const lambda = new TestClass(); + + it('should parse custom schema event', async () => { + const testEvent = generateMock(TestSchema); + + const resp = await lambda.handler(testEvent, {} as Context); + + expect(resp).toEqual(testEvent); + }); + + it('should parse custom schema with envelope event', async () => { + const customPayload = generateMock(TestSchema); + const testEvent = TestEvents.eventBridgeEvent as EventBridgeEvent< + string, + unknown + >; + testEvent.detail = customPayload; + + const resp = await lambda.handlerWithSchemaAndEnvelope( + testEvent, + {} as Context + ); + + expect(resp).toEqual(customPayload); + }); + + it('should parse extended envelope event', async () => { + const customPayload = generateMock(TestSchema); + + const testEvent = generateMock(customEventBridgeSchema); + testEvent.detail = customPayload; + + const resp: z.infer = + (await lambda.handlerWithCustomSchema( + testEvent, + {} as Context + )) as z.infer; + + expect(customEventBridgeSchema.parse(resp)).toEqual(testEvent); + expect(resp.detail).toEqual(customPayload); + }); + + it('should parse and call private async method', async () => { + const customPayload = generateMock(TestSchema); + const testEvent = TestEvents.eventBridgeEvent as EventBridgeEvent< + string, + unknown + >; + testEvent.detail = customPayload; + + const resp = await lambda.handlerWithParserCallsAnotherMethod( + testEvent, + {} as Context + ); + + expect(resp).toEqual(customPayload); + }); +}); diff --git a/packages/parser/tests/unit/parser.test.ts b/packages/parser/tests/unit/parser.middy.test.ts similarity index 100% rename from packages/parser/tests/unit/parser.test.ts rename to packages/parser/tests/unit/parser.middy.test.ts