Skip to content

Commit dd1cc5c

Browse files
jpavekconnystrecker
authored andcommitted
Added SQS route handler (spring-media#16)
added sqs route handler
1 parent 752743c commit dd1cc5c

File tree

8 files changed

+218
-17
lines changed

8 files changed

+218
-17
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
node_modules
2-
.idea
2+
.idea
3+
*.iml

README.md

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ A small library for [AWS Lambda](https://aws.amazon.com/lambda/details) providin
1414
* Lambda Proxy Resource support for AWS API Gateway
1515
* Enable CORS for requests
1616
* No external dependencies
17-
* Currently there are two `processors` (callers for Lambda) implemented: API Gateway ANY method (called proxyIntegration) and SNS.
17+
* Currently there are two `processors` (callers for Lambda) implemented: API Gateway ANY method (called proxyIntegration), SNS and SQS.
1818

1919
## Installation
2020
Install via npm
@@ -141,6 +141,38 @@ exports.handler = router.handler({
141141
});
142142
```
143143
144+
## SQS to Lambda Integrations
145+
146+
For handling calls in Lambdas initiated from AWS-SQS you can use the following code snippet:
147+
148+
```js
149+
const router = require('aws-lambda-router');
150+
151+
exports.handler = router.handler({
152+
sqs: {
153+
routes: [
154+
{
155+
// match complete SQS ARN:
156+
source: 'arn:aws:sqs:us-west-2:594035263019:aticle-import',
157+
// Attention: the messages Array is JSON-stringified
158+
action: (messages, context) => messages.forEach(message => console.log(JSON.parse(message)))
159+
},
160+
{
161+
// a regex to match the source SQS ARN:
162+
source: /.*notification/,
163+
// Attention: the messages array is JSON-stringified
164+
action: (messages, context) => service.doNotify(messages)
165+
}
166+
]
167+
}
168+
});
169+
```
170+
171+
An SQS message always contains an array of records. In each SQS record there is the message in the body JSON key.
172+
The `action` method gets all body elements from the router as an array.
173+
174+
If more than one route matches, only the **first** is used!
175+
144176
### Custom response
145177
146178
Per default a status code 200 will be returned. This behavior can be overridden.
@@ -174,6 +206,7 @@ See here: https://yarnpkg.com/en/docs/cli/link
174206
175207
## Release History
176208
209+
* 0.5.0 new feature: SQS route integration now available; bugfix: SNS integration now works woth Array of message instead of single message
177210
* 0.4.0 now [the Context Object](https://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-handler.html) pass through
178211
* 0.3.1 proxyIntegration: avoid error if response object is not set; add some debug logging
179212
* 0.3.0 proxyIntegration: add PATCH method; allow for custom status codes from route (thanks to [@mintuz](https://github.com/mintuz))

index.d.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export function handler(routeConfig: RouteConfig): any;
66
export interface ProxyIntegrationRoute {
77
path: string;
88
method: string;
9-
action?: (request: any, context: any) => any;
9+
action: (request: any, context: any) => any;
1010
}
1111

1212
export interface ProxyIntegrationConfig {
@@ -18,16 +18,27 @@ export interface ProxyIntegrationConfig {
1818
}
1919

2020
export interface SnsRoute {
21-
subject: any;
22-
action?: (sns: any, context: any) => any;
21+
subject: RegExp;
22+
action: (sns: any, context: any) => any;
2323
}
2424

2525
export interface SnsConfig {
2626
routes: SnsRoute[];
2727
debug?: boolean;
2828
}
2929

30+
export interface SqsRoute {
31+
source: string | RegExp;
32+
action: (messages: any[], context: any) => any;
33+
}
34+
35+
export interface SqsConfig {
36+
routes: SqsRoute[];
37+
debug?: boolean;
38+
}
39+
3040
export interface RouteConfig {
3141
proxyIntegration?: ProxyIntegrationConfig;
3242
sns?: SnsConfig;
43+
sqs?: SqsConfig;
3344
}

lib/sqs.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"use strict";
2+
3+
function process(sqsConfig, event, context) {
4+
// detect if it's an sqs-event at all:
5+
if (sqsConfig.debug) {
6+
console.log('sqs:Event', JSON.stringify(event));
7+
console.log('sqs:context', context);
8+
}
9+
10+
if (!Array.isArray(event.Records) || event.Records.length < 1 || event.Records[0].eventSource !== 'aws:sqs') {
11+
console.log('Event does not look like SQS');
12+
return null;
13+
}
14+
15+
const records = event.Records;
16+
const recordSourceArn = records[0].eventSourceARN;
17+
for (let routeConfig of sqsConfig.routes) {
18+
if (routeConfig.source instanceof RegExp) {
19+
if (routeConfig.source.test(recordSourceArn)) {
20+
const result = routeConfig.action(records.map(record => record.body) , context);
21+
return result || {};
22+
}
23+
} else {
24+
if (routeConfig.source === recordSourceArn) {
25+
const result = routeConfig.action(records.map(record => record.body) , context);
26+
return result || {};
27+
}
28+
}
29+
}
30+
31+
if (sqsConfig.debug) {
32+
console.log(`No source-match for ${recordSourceArn}`);
33+
}
34+
35+
return null;
36+
}
37+
38+
module.exports = process;
39+
40+
/*
41+
const cfgExample = {
42+
routes:[
43+
{
44+
source: /.*\/,
45+
action: (record, context) => service.import(JSON.parse(record.body), context)
46+
}
47+
]
48+
};
49+
*/
50+
51+
52+
/* this is an example for a standard SQS notification message:
53+
54+
{
55+
"Records": [
56+
{
57+
"messageId": "c80e8021-a70a-42c7-a470-796e1186f753",
58+
"receiptHandle": "AQEBJQ+/u6NsnT5t8Q/VbVxgdUl4TMKZ5FqhksRdIQvLBhwNvADoBxYSOVeCBXdnS9P+erlTtwEALHsnBXynkfPLH3BOUqmgzP25U8kl8eHzq6RAlzrSOfTO8ox9dcp6GLmW33YjO3zkq5VRYyQlJgLCiAZUpY2D4UQcE5D1Vm8RoKfbE+xtVaOctYeINjaQJ1u3mWx9T7tork3uAlOe1uyFjCWU5aPX/1OHhWCGi2EPPZj6vchNqDOJC/Y2k1gkivqCjz1CZl6FlZ7UVPOx3AMoszPuOYZ+Nuqpx2uCE2MHTtMHD8PVjlsWirt56oUr6JPp9aRGo6bitPIOmi4dX0FmuMKD6u/JnuZCp+AXtJVTmSHS8IXt/twsKU7A+fiMK01NtD5msNgVPoe9JbFtlGwvTQ==",
59+
"body": "{\"foo\":\"bar\"}",
60+
"attributes": {
61+
"ApproximateReceiveCount": "3",
62+
"SentTimestamp": "1529104986221",
63+
"SenderId": "594035263019",
64+
"ApproximateFirstReceiveTimestamp": "1529104986230"
65+
},
66+
"messageAttributes": {},
67+
"md5OfBody": "9bb58f26192e4ba00f01e2e7b136bbd8",
68+
"eventSource": "aws:sqs",
69+
"eventSourceARN": "arn:aws:sqs:eu-central-1:594035263019:article-import",
70+
"awsRegion": "eu-central-1"
71+
}
72+
]
73+
}
74+
75+
*/

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "aws-lambda-router",
3-
"version": "0.4.1",
3+
"version": "0.5.0",
44
"description": "AWS lambda router",
55
"main": "index.js",
66
"types": "index.d.ts",

test/sns.spec.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,5 +55,18 @@ describe('sns.processor', () => {
5555
expect(sns(snsCfg, event)).toBe(2);
5656
});
5757

58+
it('should not fail on missing subject', () => {
59+
const snsCfg = {routes: [{action: () => 1}]};
60+
sns(snsCfg, {Records: [{Sns: {Subject: 'Subject'}}]});
61+
});
62+
63+
it('should fail on missing action', () => {
64+
const snsCfg = {routes: [{subject: /.*/}]};
65+
try {
66+
sns(snsCfg, {Records: [{Sns: {Subject: 'Subject'}}]});
67+
fail();
68+
} catch (e) {
69+
}
70+
});
5871

5972
});

test/sqs.spec.js

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"use strict";
2+
3+
describe('sqs.processor', () => {
4+
5+
const sqs = require('../lib/sqs');
6+
7+
it('context should be passed through', () => {
8+
const actionSpy = jasmine.createSpy('action');
9+
10+
const context = {bla: "blup"};
11+
const sqsCfg = {routes: [{source: /.*/, action: actionSpy}]};
12+
const event = {Records: [{eventSource: 'aws:sqs', body: 'B'}]};
13+
14+
sqs(sqsCfg, event, context);
15+
16+
expect(actionSpy).toHaveBeenCalledWith([event.Records[0].body], context);
17+
});
18+
19+
it('should ignore event if it is no SQS event', () => {
20+
const sqsCfg = {routes: [{source: /.*/, action: () => 1}]};
21+
expect(sqs(sqsCfg, {})).toBe(null);
22+
expect(sqs(sqsCfg, {Records: 1})).toBe(null);
23+
expect(sqs(sqsCfg, {Records: []})).toBe(null);
24+
});
25+
26+
it('should match null source for ".*"', () => {
27+
const sqsCfg = {routes: [{source: /.*/, action: () => 1}]};
28+
expect(sqs(sqsCfg, {Records: [{eventSource: 'aws:sqs', eventSourceARN: null}]})).toBe(1);
29+
});
30+
31+
it('should match empty subject for ".*"', () => {
32+
const sqsCfg = {routes: [{subject: /.*/, action: () => 1}]};
33+
expect(sqs(sqsCfg, {Records: [{eventSource: 'aws:sqs', body: 'B'}]})).toBe(1);
34+
});
35+
36+
it('should match source for "/porter/"', () => {
37+
const sqsCfg = {routes: [{source: /porter/, action: () => 1}]};
38+
expect(sqs(sqsCfg, {Records: [{eventSource: 'aws:sqs', eventSourceARN: 'importer'}]})).toBe(1);
39+
});
40+
41+
it('should call action with sqs-message', () => {
42+
const sqsCfg = {routes: [{source: /porter/, action: (events) => events}]};
43+
const event = {Records: [{eventSource: 'aws:sqs', eventSourceARN: 'importer', body: 'B'}]};
44+
45+
expect(sqs(sqsCfg, event)).toEqual([event.Records[0].body]);
46+
});
47+
48+
it('should call first action with matching subject', () => {
49+
const sqsCfg = {
50+
routes: [
51+
{source: /^123$/, action: () => 1},
52+
{source: /123/, action: () => 2},
53+
{source: /1234/, action: () => 3}
54+
]
55+
};
56+
const event = {Records: [{eventSource: 'aws:sqs', eventSourceARN: '1234', body: 'B'}]};
57+
expect(sqs(sqsCfg, event)).toBe(2);
58+
});
59+
60+
it('should match complete source', () => {
61+
const sqsCfg = {routes: [{source: 'aws:123:importer', action: () => 1}]};
62+
expect(sqs(sqsCfg, {Records: [{eventSource: 'aws:sqs', eventSourceARN: 'aws:123:importer'}]})).toBe(1);
63+
});
64+
65+
it('should not throw error on missing source', () => {
66+
const sqsCfg = {routes: [{action: () => 1}]};
67+
sqs(sqsCfg, {Records: [{eventSource: 'aws:sqs', eventSourceARN: 'importer'}]});
68+
});
69+
70+
it('should fail on missing action', () => {
71+
const sqsCfg = {routes: [{source: /.*/}]};
72+
try {
73+
sqs(sqsCfg, {Records: [{eventSource: 'aws:sqs', eventSourceARN: 'importer'}]});
74+
fail();
75+
} catch (e) {
76+
}
77+
});
78+
79+
});

yarn.lock

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1241,13 +1241,6 @@ jasmine-core@~3.2.0:
12411241
version "3.2.1"
12421242
resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.2.1.tgz#8e4ff5b861603ee83343f2b49eee6a0ffe9650ce"
12431243

1244-
1245-
version "2.3.2"
1246-
resolved "https://registry.yarnpkg.com/jasmine-reporters/-/jasmine-reporters-2.3.2.tgz#898818ffc234eb8b3f635d693de4586f95548d43"
1247-
dependencies:
1248-
mkdirp "^0.5.1"
1249-
xmldom "^0.1.22"
1250-
12511244
jasmine-terminal-reporter@^1.0.3:
12521245
version "1.0.3"
12531246
resolved "https://registry.yarnpkg.com/jasmine-terminal-reporter/-/jasmine-terminal-reporter-1.0.3.tgz#896f1ec8fdf4bf6aecdd41c503eda7347f61526b"
@@ -2478,10 +2471,6 @@ wrappy@1:
24782471
version "1.0.2"
24792472
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
24802473

2481-
xmldom@^0.1.22:
2482-
version "0.1.27"
2483-
resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9"
2484-
24852474
xtend@~4.0.0, xtend@~4.0.1:
24862475
version "4.0.1"
24872476
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"

0 commit comments

Comments
 (0)