diff --git a/docs/utils/MIDDYFY.md b/docs/utils/MIDDYFY.md index e5d0cc6..e064b6a 100644 --- a/docs/utils/MIDDYFY.md +++ b/docs/utils/MIDDYFY.md @@ -127,3 +127,145 @@ export const handle = middyfyAPIGateway({ handler, eventSchema }); When an `eventSchema` is present in the middleware options, the Joi Validator middleware ensures that the event is valid. The middleware throws a ValidationError if it is invalid. The HTTP Error Handler knows how to handle a ValidationError, returning a response with status code 400 and an informative message. Notice that nothing changed with the handler function itself! + +## Creating a Scheduled event handler + +Use a Scheduled event handler when you need to run a Lambda function on a cron schedule. + +To create a Scheduled event handler, you will need to create two modules: the handler function and the middyfy wrapper. + +Create the AWS Lambda Handler function with the usual signature... + +_/handlers/task-scheduled/handler.ts_ + +```ts +import { Context, ScheduledEvent } from 'aws-lambda'; +... + +export const handler = async (event: ScheduledEvent, context: Context): Promise => { + try { + // perform business logic + await TaskService.sendReminders(); + + } catch (error) { + console.error('Failed to send task reminders. Detail:', error); + throw new ServiceError(error); + } +}; +``` + +Next, we need to _"middyfy"_ the handler function. This is just a fancy way of saying we are wrapping the handler function with middleware. + +_/handlers/task-scheduled/index.ts_ + +```ts +import { middyfyScheduled } from '@leanstacks/serverless-common'; + +import { handler } from './handler'; + +export const handle = middyfyScheduled({ handler, logger: console.log }); +``` + +## Creating a SNS event handler + +Use a SNS event handler to process events from an AWS Simple Notification Service (SNS) topic. + +To create a SNS event handler, you will need to create two modules: the handler function and the middyfy wrapper. + +Create the AWS Lambda Handler function with the usual signature... + +_/handlers/task-sns/handler.ts_ + +```ts +import { Context, SNSEvent } from 'aws-lambda'; +... + +export const handler = async (event: SNSEvent, context: Context): Promise => { + try { + // perform business logic + const promises = map(event.Records, (record) => { + const taskToCreate = record.Sns.Message as Task; + return TaskService.create(taskToCreate); + }); + const tasks:Task[] = await Promise.all(promises); + + } catch (error) { + console.error('Failed to create tasks. Detail:', error); + throw new ServiceError(error); + } +}; +``` + +Next, we need to _"middyfy"_ the handler function. This is just a fancy way of saying we are wrapping the handler function with middleware. + +_/handlers/task-sns/index.ts_ + +```ts +import { middyfySNS } from '@leanstacks/serverless-common'; + +import { handler } from './handler'; + +export const handle = middyfySNS({ handler, logger: console.log }); +``` + +The handler is wrapped with two middlewares. + +1. The `event-normalizer` middleware performs a JSON parse on the `Message`. +2. The `validator` middleware will validate the event when an `eventSchema` is provided in the options. + +## Creating a SQS event handler + +Use a SQS event handler to process events from an AWS Simple Queue Service (SQS) topic. + +To create a SQS event handler, you will need to create two modules: the handler function and the middyfy wrapper. You may optionally create a `schema.ts` module which provides a Joi validation schema for the `SQSEvent`. + +Create the AWS Lambda Handler function with the usual signature... + +_/handlers/task-sqs/handler.ts_ + +```ts +import { Context, SQSEvent, SQSBatchResponse } from 'aws-lambda'; +... + +export const handler = async (event: SNSEvent, context: Context): Promise => { + try { + // perform business logic + const promises = map(event.Records, (record) => { + const taskToCreate = record.body as Task; + return TaskService.create(taskToCreate); + }); + + const response:SQSBatchResponse = {}; + Promise.allSettled(promises) + .then((values) => { + response.batchItemFailures = map(values, (value) => { + if (value.status === 'rejected') { + return { itemIdentifier: value.reason } + } + }) + }); + + return response; + } catch (error) { + console.error('Failed to create tasks. Detail:', error); + throw new ServiceError(error); + } +}; +``` + +Next, we need to _"middyfy"_ the handler function. This is just a fancy way of saying we are wrapping the handler function with middleware. + +_/handlers/task-sqs/index.ts_ + +```ts +import { middyfySQS } from '@leanstacks/serverless-common'; + +import { handler } from './handler'; + +export const handle = middyfySQS({ handler, logger: console.log }); +``` + +The handler is wrapped with two middlewares. + +1. The `event-normalizer` middleware performs a JSON parse on the `body`. +2. The `validator` middleware will validate the event when an `eventSchema` is provided in the options. diff --git a/package-lock.json b/package-lock.json index c182d1b..c5a3e1a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@aws-sdk/client-dynamodb": "^3.454.0", "@aws-sdk/lib-dynamodb": "^3.454.0", "@middy/core": "^4.7.0", + "@middy/event-normalizer": "^4.7.0", "@middy/http-event-normalizer": "^4.7.0", "@middy/http-json-body-parser": "^4.7.0", "@rollup/plugin-commonjs": "^25.0.4", @@ -2302,6 +2303,22 @@ "url": "https://github.com/sponsors/willfarrell" } }, + "node_modules/@middy/event-normalizer": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@middy/event-normalizer/-/event-normalizer-4.7.0.tgz", + "integrity": "sha512-M6nmFKqbMS8htbWuZ92Qv/2RdhWBSPduvSgo+9M38/ZBzGifijSILTaaO1yuhcEJH3DwA25MAxtFGCygEUHcjw==", + "dev": true, + "dependencies": { + "@middy/util": "4.7.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/willfarrell" + } + }, "node_modules/@middy/http-event-normalizer": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@middy/http-event-normalizer/-/http-event-normalizer-4.7.0.tgz", diff --git a/package.json b/package.json index dbe7deb..73e268c 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@aws-sdk/client-dynamodb": "^3.454.0", "@aws-sdk/lib-dynamodb": "^3.454.0", "@middy/core": "^4.7.0", + "@middy/event-normalizer": "^4.7.0", "@middy/http-event-normalizer": "^4.7.0", "@middy/http-json-body-parser": "^4.7.0", "@rollup/plugin-commonjs": "^25.0.4", @@ -61,6 +62,7 @@ "@aws-sdk/client-dynamodb": "^3.454.0", "@aws-sdk/lib-dynamodb": "^3.454.0", "@middy/core": "^4.7.0", + "@middy/event-normalizer": "^4.7.0", "@middy/http-event-normalizer": "^4.7.0", "@middy/http-json-body-parser": "^4.7.0", "http-status": "^1.7.3", diff --git a/src/utils/__tests__/middyfy.test.ts b/src/utils/__tests__/middyfy.test.ts index 32524ad..1d0df4b 100644 --- a/src/utils/__tests__/middyfy.test.ts +++ b/src/utils/__tests__/middyfy.test.ts @@ -3,10 +3,12 @@ import { APIGatewayProxyResult, Context, SNSEvent, + SQSBatchResponse, + SQSEvent, ScheduledEvent, } from 'aws-lambda'; -import { middyfyAPIGateway, middyfySNS, middyfyScheduled } from '../middyfy'; +import { middyfyAPIGateway, middyfySNS, middyfySQS, middyfyScheduled } from '../middyfy'; describe('middyfyAPIGateway', () => { const handlerFn = async ( @@ -42,3 +44,17 @@ describe('middyfySNS', () => { expect(handler).toBeDefined(); }); }); + +describe('middyfySQS', () => { + const handlerFn = async (event: SQSEvent, context: Context): Promise => { + return { + batchItemFailures: [{ itemIdentifier: 'messageId' }], + }; + }; + + it('should middyfySQS', () => { + const handler = middyfySQS({ handler: handlerFn }); + + expect(handler).toBeDefined(); + }); +}); diff --git a/src/utils/index.ts b/src/utils/index.ts index f6a0d3d..36f7e66 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -4,11 +4,12 @@ export { middyfyAPIGateway, middyfyScheduled, middyfySNS, + middyfySQS, MiddyfyOptions, ScheduledHandlerFn, ScheduledMiddyfyOptions, - SNSHandlerFn, SNSMiddyfyOptions, + SQSMiddyfyOptions, } from './middyfy'; export { default as ID } from './id'; diff --git a/src/utils/middyfy.ts b/src/utils/middyfy.ts index 1e2b8a8..8b2107a 100644 --- a/src/utils/middyfy.ts +++ b/src/utils/middyfy.ts @@ -1,12 +1,14 @@ import middy from '@middy/core'; +import eventNormalizer from '@middy/event-normalizer'; import httpEventNormalizer from '@middy/http-event-normalizer'; import jsonBodyParser from '@middy/http-json-body-parser'; import { APIGatewayProxyEvent, APIGatewayProxyResult, Context, - SNSEvent, ScheduledEvent, + SQSHandler, + SNSHandler, } from 'aws-lambda'; import { ObjectSchema } from 'joi'; @@ -27,11 +29,6 @@ export type APIGatewayHandlerFn = ( */ export type ScheduledHandlerFn = (event: ScheduledEvent, context: Context) => Promise; -/** - * The AWS Lambda handler function signature for SNS events. - */ -export type SNSHandlerFn = (event: SNSEvent, Context: Context) => Promise; - /** * Base options for `middyfy` functions. */ @@ -59,7 +56,14 @@ export type ScheduledMiddyfyOptions = MiddyfyOptions & { /** * Options for middyfied SNS event handler functions. */ -export type SNSMiddyfyOptions = MiddyfyOptions & { +export type SNSMiddyfyOptions = MiddyfyOptions & { + eventSchema?: ObjectSchema; +}; + +/** + * Options for middyfied SQS event handler functions. + */ +export type SQSMiddyfyOptions = MiddyfyOptions & { eventSchema?: ObjectSchema; }; @@ -102,7 +106,19 @@ export const middyfyScheduled = (options: ScheduledMiddyfyOptions) => { * @returns A middyfied handler function. */ export const middyfySNS = (options: SNSMiddyfyOptions) => { - return middy(options.handler).use( - validator({ eventSchema: options.eventSchema, logger: options.logger }), - ); + return middy(options.handler) + .use(eventNormalizer()) + .use(validator({ eventSchema: options.eventSchema, logger: options.logger })); +}; + +/** + * Wraps a SQS event handler function in middleware, returning the AWS Lambda + * handler function. + * @param options - The `SQSMiddyfyOptions` object. + * @returns A middyfied handler function. + */ +export const middyfySQS = (options: SQSMiddyfyOptions) => { + return middy(options.handler) + .use(eventNormalizer()) + .use(validator({ eventSchema: options.eventSchema, logger: options.logger })); };