Skip to content

Commit

Permalink
SLSCMN-12 middyfy SQS events (#9)
Browse files Browse the repository at this point in the history
* SLSCMN-12 middyfy util for SQS events

* SLSCMN-12 docs

* SLSCMN-12 docs

* SLSCMN-12 middyfy SNS cleanup
  • Loading branch information
mwarman authored Dec 17, 2023
1 parent 331c2cc commit 0b37064
Show file tree
Hide file tree
Showing 6 changed files with 206 additions and 12 deletions.
142 changes: 142 additions & 0 deletions docs/utils/MIDDYFY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> => {
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<void> => {
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<SQSBatchResponse | void> => {
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.
17 changes: 17 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
18 changes: 17 additions & 1 deletion src/utils/__tests__/middyfy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -42,3 +44,17 @@ describe('middyfySNS', () => {
expect(handler).toBeDefined();
});
});

describe('middyfySQS', () => {
const handlerFn = async (event: SQSEvent, context: Context): Promise<SQSBatchResponse> => {
return {
batchItemFailures: [{ itemIdentifier: 'messageId' }],
};
};

it('should middyfySQS', () => {
const handler = middyfySQS({ handler: handlerFn });

expect(handler).toBeDefined();
});
});
3 changes: 2 additions & 1 deletion src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ export {
middyfyAPIGateway,
middyfyScheduled,
middyfySNS,
middyfySQS,
MiddyfyOptions,
ScheduledHandlerFn,
ScheduledMiddyfyOptions,
SNSHandlerFn,
SNSMiddyfyOptions,
SQSMiddyfyOptions,
} from './middyfy';

export { default as ID } from './id';
36 changes: 26 additions & 10 deletions src/utils/middyfy.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -27,11 +29,6 @@ export type APIGatewayHandlerFn = (
*/
export type ScheduledHandlerFn = (event: ScheduledEvent, context: Context) => Promise<void>;

/**
* The AWS Lambda handler function signature for SNS events.
*/
export type SNSHandlerFn = (event: SNSEvent, Context: Context) => Promise<void>;

/**
* Base options for `middyfy` functions.
*/
Expand Down Expand Up @@ -59,7 +56,14 @@ export type ScheduledMiddyfyOptions = MiddyfyOptions<ScheduledHandlerFn> & {
/**
* Options for middyfied SNS event handler functions.
*/
export type SNSMiddyfyOptions = MiddyfyOptions<SNSHandlerFn> & {
export type SNSMiddyfyOptions = MiddyfyOptions<SNSHandler> & {
eventSchema?: ObjectSchema;
};

/**
* Options for middyfied SQS event handler functions.
*/
export type SQSMiddyfyOptions = MiddyfyOptions<SQSHandler> & {
eventSchema?: ObjectSchema;
};

Expand Down Expand Up @@ -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 }));
};

0 comments on commit 0b37064

Please sign in to comment.