Skip to content
This repository was archived by the owner on Aug 8, 2024. It is now read-only.

Commit 36a96b1

Browse files
authored
Merge pull request #1 from WeltN24/addsnsrouting
add sns as routing provider
2 parents 117983b + 3034182 commit 36a96b1

File tree

9 files changed

+712
-341
lines changed

9 files changed

+712
-341
lines changed

README.md

Lines changed: 48 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
## aws-lambda-router
77

8-
A small library providing routing for AWS ApiGateway ```any``` method
8+
A small library providing routing for AWS ApiGateway Proxy Integrations and SNS...
99

1010
## Install
1111

@@ -16,40 +16,57 @@ $ npm install aws-lambda-router
1616
## Usage
1717

1818
```js
19-
const httpRouteHandler = require('aws-lambda-router');
20-
21-
exports.handler = httpRouteHandler.handler({
22-
cors: true,
23-
routes: [
24-
{
25-
path: '/graphql',
26-
method: 'POST',
27-
action: request=>doAnything(request.body)
28-
},
29-
{
30-
path: '/article/{id}',
31-
method: 'GET',
32-
action: request=>getArticleInfo(request.body)
33-
},
34-
{
35-
path: '/:sourcepath',
36-
method: 'DELETE',
37-
action: request=>deleteSourcepath(request.paths.sourcepath)
19+
const router = require('aws-lambda-router');
20+
21+
exports.handler = router.handler(
22+
// the router-config contains configs for every type of 'processor'
23+
{
24+
// for handling an http-call from an AWS Apigateway proxyIntegration we provide the following config:
25+
proxyIntegration: {
26+
// activate CORS on all http-methods:
27+
cors: true,
28+
routes: [
29+
{
30+
// the request-path-pattern to match:
31+
path: '/graphql',
32+
// http method to match
33+
method: 'POST',
34+
// provide a function to be called with the propriate data
35+
action: request=>doAnything(request.body)
36+
},
37+
{
38+
// request-path-pattern with a path variable:
39+
path: '/article/:id',
40+
method: 'GET',
41+
// we can use the path param 'id' in the action call:
42+
action: request=>getSomething(request.paths.id)
43+
},
44+
{
45+
path: '/:id',
46+
method: 'DELETE',
47+
action: request=>deleteSomething(request.paths.id)
48+
}
49+
],
50+
debug: true,
51+
errorMapping: {
52+
'NotFound': 404,
53+
'RequestError': 500
3854
}
39-
],
40-
debug: true,
41-
errorMapping: {
42-
'NotFound': 404,
43-
'RequestError': 500
55+
},
56+
// for handling calls initiated from AWS-SNS:
57+
sns: {
58+
routes: [
59+
{
60+
// a regex to match the content of the SNS-Subject:
61+
subject: /.*/,
62+
// Attention: the message is JSON-stringified
63+
action: sns => service.doSomething(JSON.parse(sns.Message))
64+
}
65+
]
4466
}
4567
});
4668
```
4769

48-
## Publish a new version to npmjs.org
49-
50-
51-
52-
5370
## local developement
5471

5572
The best is to work with ```npm link```
@@ -59,5 +76,6 @@ See here: http://vansande.org/2015/03/20/npm-link/
5976

6077
## Release History
6178

79+
* 0.2.0 Attention: breaking changes for configuration; add SNS event process
6280
* 0.1.0 make it work now
6381
* 0.0.1 initial release

index.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"use strict";
2+
3+
function handler(routeConfig) {
4+
const eventProcessorMapping = extractEventProcessorMapping(routeConfig);
5+
return (event, context, callback) => {
6+
for (const eventProcessorName of eventProcessorMapping.keys()) {
7+
8+
try {
9+
// the contract of 'processors' is as follows:
10+
// - their method 'process' is called with (config, event)
11+
// - the method...
12+
// - returns null: the processor does not feel responsible for the event
13+
// - throws Error: the 'error.toString()' is taken as the error message of processing the event
14+
// - returns object: this is taken as the result of processing the event
15+
// - returns promise: when the promise is resolved, this is taken as the result of processing the event
16+
const result = eventProcessorMapping.get(eventProcessorName)(routeConfig[eventProcessorName], event);
17+
if (result) {
18+
// be resilient against a processor returning a value instead of a promise:
19+
return Promise.resolve(result)
20+
.then(result => callback(null, result))
21+
.catch(error => {
22+
console.log(error.stack);
23+
callback(error.toString());
24+
});
25+
}
26+
} catch (error) {
27+
if (error.stack) {
28+
console.log(error.stack);
29+
}
30+
callback(error.toString());
31+
return;
32+
}
33+
}
34+
callback('No event processor found to handle this kind of event!');
35+
}
36+
}
37+
38+
function extractEventProcessorMapping(routeConfig) {
39+
const processorMap = new Map();
40+
for (let key of Object.keys(routeConfig)) {
41+
try {
42+
processorMap.set(key, require(`./lib/${key}`));
43+
} catch (error) {
44+
throw new Error(`The event processor '${key}', that is mentioned in the routerConfig, cannot be instantiated (${error.toString()})`);
45+
}
46+
}
47+
return processorMap;
48+
}
49+
50+
module.exports = {handler: handler};
Lines changed: 44 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,63 @@
11
"use strict";
22

3-
// TODO: req id in log sync u async
4-
5-
const NO_MATCHING_ACTION = request=> {
6-
throw {reason: 'NO_MATCHING_ACTION', message: `Could not find matching action for ${request.path} and method ${request.httpMethod}`}
3+
const NO_MATCHING_ACTION = request => {
4+
throw {
5+
reason: 'NO_MATCHING_ACTION',
6+
message: `Could not find matching action for ${request.path} and method ${request.httpMethod}`
7+
}
78
};
89

9-
function handler(routeConfig) {
10-
return (event, context, callback) => process(event, routeConfig, callback)
11-
}
12-
13-
function process(event, routeConfig, callback) {
14-
if (routeConfig.debug) {
15-
console.log('Event', event);
10+
function process(proxyIntegrationConfig, event) {
11+
//validate config
12+
if (!Array.isArray(proxyIntegrationConfig.routes) || proxyIntegrationConfig.routes.length < 1) {
13+
throw new Error('proxyIntegration.routes must not be empty');
1614
}
1715

18-
const headers = {
16+
// detect if it's an http-call at all:
17+
if (!event.httpMethod || !event.path) {
18+
return null;
19+
}
20+
const headers = Object.assign({
1921
'Content-Type': 'application/json'
20-
};
21-
// assure necessary values have sane defaults:
22-
event.path = event.path || '';
23-
const errorMapping = routeConfig.errorMapping || {};
24-
errorMapping['NO_MATCHING_ACTION'] = 404;
25-
routeConfig.routes = routeConfig.routes || [];
26-
if (routeConfig.cors) {
22+
}, proxyIntegrationConfig.defaultHeaders);
23+
if (proxyIntegrationConfig.cors) {
2724
headers["Access-Control-Allow-Origin"] = "*";
2825
}
29-
// ugly hack: if host is from 'Custom Domain Name Mapping', then event.path has the value '/basepath/resource-path/';
30-
// if host is from amazonaws.com, then event.path is just '/resource-path':
31-
const apiId = event.requestContext ? event.requestContext.apiId : null; // the apiId that is the first part of the amazonaws.com-host
32-
if ((apiId && event.headers && event.headers.Host && event.headers.Host.substring(0, apiId.length) != apiId)) {
33-
// remove first path element:
34-
const groups = /\/[^\/]+(.*)/.exec(event.path) || [null, null];
35-
event.path = groups[1] || '/';
36-
}
26+
27+
// assure necessary values have sane defaults:
28+
const errorMapping = proxyIntegrationConfig.errorMapping || {};
29+
errorMapping['NO_MATCHING_ACTION'] = 404;
30+
31+
event.path = normalizeRequestPath(event);
3732

3833
try {
39-
const actionConfig = findMatchingActionConfig(event.httpMethod, event.path, routeConfig) || {action: NO_MATCHING_ACTION};
34+
const actionConfig = findMatchingActionConfig(event.httpMethod, event.path, proxyIntegrationConfig) || {action: NO_MATCHING_ACTION};
4035
event.paths = actionConfig.paths;
4136
if (event.body) {
4237
event.body = JSON.parse(event.body);
4338
}
44-
const result = actionConfig.action(event);
45-
if (result && result.then) {
46-
return result
47-
.then(res=> {
48-
callback(null, {statusCode: 200, headers: headers, body: JSON.stringify(res)});
49-
})
50-
.catch(err=> {
51-
callback(null, convertError(err, errorMapping, headers))
52-
});
53-
} else {
54-
callback(null, {statusCode: 200, headers: headers, body: JSON.stringify(result)});
55-
}
39+
return Promise.resolve(actionConfig.action(event)).then(res => {
40+
return {statusCode: 200, headers: headers, body: JSON.stringify(res)};
41+
}).catch(err => {
42+
return convertError(err, errorMapping, headers);
43+
});
5644
} catch (error) {
5745
console.log('Error while evaluating matching action handler', error);
58-
callback(null, convertError(error, errorMapping, headers));
46+
return Promise.resolve(convertError(error, errorMapping, headers));
47+
}
48+
}
49+
50+
function normalizeRequestPath(event) {
51+
// ugly hack: if host is from API-Gateway 'Custom Domain Name Mapping', then event.path has the value '/basepath/resource-path/';
52+
// if host is from amazonaws.com, then event.path is just '/resource-path':
53+
const apiId = event.requestContext ? event.requestContext.apiId : null; // the apiId that is the first part of the amazonaws.com-host
54+
if ((apiId && event.headers && event.headers.Host && event.headers.Host.substring(0, apiId.length) != apiId)) {
55+
// remove first path element:
56+
const groups = /\/[^\/]+(.*)/.exec(event.path) || [null, null];
57+
return groups[1] || '/';
5958
}
59+
60+
return event.path;
6061
}
6162

6263
function convertError(error, errorMapping, headers) {
@@ -68,7 +69,7 @@ function convertError(error, errorMapping, headers) {
6869

6970
function findMatchingActionConfig(httpMethod, httpPath, routeConfig) {
7071
const paths = {};
71-
var matchingMethodRoutes = routeConfig.routes.filter(route=>route.method == httpMethod);
72+
const matchingMethodRoutes = routeConfig.routes.filter(route => route.method == httpMethod);
7273
for (let route of matchingMethodRoutes) {
7374
if (routeConfig.debug) {
7475
console.log(`Examining route ${route.path} to match ${httpPath}`);
@@ -104,5 +105,4 @@ function extractPathNames(pathExpression) {
104105
return pathNames && pathNames.length > 0 ? pathNames.slice(1) : null;
105106
}
106107

107-
108-
module.exports = {handler: handler};
108+
module.exports = process;

lib/sns.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"use strict";
2+
3+
function process(snsConfig, event) {
4+
// detect if it's an sns-event at all:
5+
if(snsConfig.debug){
6+
console.log('sns:Event', JSON.stringify(event));
7+
}
8+
9+
if (!Array.isArray(event.Records) || event.Records.length<1 || !event.Records[0].Sns) {
10+
console.log('Event does not look like SNS');
11+
return null;
12+
}
13+
14+
const sns = event.Records[0].Sns;
15+
for(let routeConfig of snsConfig.routes){
16+
if(routeConfig.subject instanceof RegExp){
17+
if(routeConfig.subject.test(sns.Subject)){
18+
const result = routeConfig.action(sns);
19+
return result || {};
20+
}
21+
}else{
22+
console.log(`SNS-Route with subject-regex '${routeConfig.subject}' is not a Regex; it is ignored!`);
23+
}
24+
}
25+
26+
if (snsConfig.debug) {
27+
console.log(`No subject-match for ${sns.Subject}`);
28+
}
29+
30+
return null;
31+
}
32+
33+
module.exports = process;
34+
35+
/*
36+
const cfgExample = {
37+
routes:[
38+
{
39+
subject: /.*\/,
40+
action: sns => articleService.invalidate(JSON.parse(sns.Message).escenicId)
41+
}
42+
]
43+
};
44+
*/
45+
46+
47+
/* this is an example for a standard SNS notification message:
48+
49+
{
50+
"Records": [
51+
{
52+
"EventSource": "aws:sns",
53+
"EventVersion": "1.0",
54+
"EventSubscriptionArn": "arn:aws:sns:eu-west-1:933782373565:production-escenic-updates:2fdd994c-f2b7-4c2f-a2f9-da83b590e0fc",
55+
"Sns": {
56+
"Type": "Notification",
57+
"MessageId": "0629603b-448e-5366-88b4-309d651495c5",
58+
"TopicArn": "arn:aws:sns:eu-west-1:933782373565:production-escenic-updates",
59+
"Subject": null,
60+
"Message": "{\"escenicId\":\"159526803\",\"model\":\"news\",\"status\":\"draft\"}",
61+
"Timestamp": "2016-11-16T08:56:58.227Z",
62+
"SignatureVersion": "1",
63+
"Signature": "dtXM9BlAJJhYkVObnKmzY012kjgl4uYHEPQ1DLUalBHnPNzkDf12YeVcvHmq0SF6QbdgGwSYw0SgtsOkBiW3WSxVosqEb5xKUWIbQhlXwKdZnzekUigsgl3d231RP+9U2Cvd4QUc6klH5P+CuQM/F70LBIIv74UmR2YNMaxWxrv7Q+ETmz/TF6Y5v8Ip3+GLikbu6wQ/F5g3IHO2Lm7cLpV/74odm48SQxoolh94TdgvtYaUnxNjFVlF8Js8trbRkr7DYTogh73cTwuR77Mo+K9GlYn53txiMW5rMl3KhVdw4U3L190gtBJVwgHbqcB60pmNdEAE9f4bEOohizfPhg==",
64+
"SigningCertUrl": "https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-b95095beb82e8f6a046b3aafc7f4149a.pem",
65+
"UnsubscribeUrl": "https://sns.eu-west-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-west-1:933782373565:production-escenic-updates:2fdd994c-f2b7-4c2f-a2f9-da83b590e0fc",
66+
"MessageAttributes": {}
67+
}
68+
}
69+
]
70+
}
71+
72+
*/

package.json

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "aws-lambda-router",
33
"version": "0.1.1",
44
"description": "AWS lambda router",
5-
"main": "httpRouteHandler.js",
5+
"main": "index.js",
66
"scripts": {
77
"test": "gulp test"
88
},
@@ -15,6 +15,7 @@
1515
"lambda",
1616
"apigateway",
1717
"any",
18+
"sns",
1819
"router",
1920
"routing"
2021
],
@@ -25,10 +26,11 @@
2526
},
2627
"homepage": "https://github.com/WeltN24/aws-lambda-router#readme",
2728
"devDependencies": {
28-
"del": "2.2.2",
29-
"gulp": "3.9.1",
30-
"gulp-install": "0.6.0",
31-
"gulp-jasmine": "2.4.2",
32-
"gulp-zip": "3.2.0"
29+
"del": "^2.2.2",
30+
"gulp": "^3.9.1",
31+
"gulp-install": "^0.6.0",
32+
"gulp-jasmine": "^2.4.2",
33+
"gulp-zip": "^3.2.0",
34+
"proxyquire": "^1.7.10"
3335
}
3436
}

0 commit comments

Comments
 (0)