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

Commit

Permalink
Merge pull request #1 from WeltN24/addsnsrouting
Browse files Browse the repository at this point in the history
add sns as routing provider
  • Loading branch information
chgohlke authored Nov 21, 2016
2 parents 117983b + 3034182 commit 36a96b1
Show file tree
Hide file tree
Showing 9 changed files with 712 additions and 341 deletions.
78 changes: 48 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

## aws-lambda-router

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

## Install

Expand All @@ -16,40 +16,57 @@ $ npm install aws-lambda-router
## Usage

```js
const httpRouteHandler = require('aws-lambda-router');

exports.handler = httpRouteHandler.handler({
cors: true,
routes: [
{
path: '/graphql',
method: 'POST',
action: request=>doAnything(request.body)
},
{
path: '/article/{id}',
method: 'GET',
action: request=>getArticleInfo(request.body)
},
{
path: '/:sourcepath',
method: 'DELETE',
action: request=>deleteSourcepath(request.paths.sourcepath)
const router = require('aws-lambda-router');

exports.handler = router.handler(
// the router-config contains configs for every type of 'processor'
{
// for handling an http-call from an AWS Apigateway proxyIntegration we provide the following config:
proxyIntegration: {
// activate CORS on all http-methods:
cors: true,
routes: [
{
// the request-path-pattern to match:
path: '/graphql',
// http method to match
method: 'POST',
// provide a function to be called with the propriate data
action: request=>doAnything(request.body)
},
{
// request-path-pattern with a path variable:
path: '/article/:id',
method: 'GET',
// we can use the path param 'id' in the action call:
action: request=>getSomething(request.paths.id)
},
{
path: '/:id',
method: 'DELETE',
action: request=>deleteSomething(request.paths.id)
}
],
debug: true,
errorMapping: {
'NotFound': 404,
'RequestError': 500
}
],
debug: true,
errorMapping: {
'NotFound': 404,
'RequestError': 500
},
// for handling calls initiated from AWS-SNS:
sns: {
routes: [
{
// a regex to match the content of the SNS-Subject:
subject: /.*/,
// Attention: the message is JSON-stringified
action: sns => service.doSomething(JSON.parse(sns.Message))
}
]
}
});
```

## Publish a new version to npmjs.org




## local developement

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

## Release History

* 0.2.0 Attention: breaking changes for configuration; add SNS event process
* 0.1.0 make it work now
* 0.0.1 initial release
50 changes: 50 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"use strict";

function handler(routeConfig) {
const eventProcessorMapping = extractEventProcessorMapping(routeConfig);
return (event, context, callback) => {
for (const eventProcessorName of eventProcessorMapping.keys()) {

try {
// the contract of 'processors' is as follows:
// - their method 'process' is called with (config, event)
// - the method...
// - returns null: the processor does not feel responsible for the event
// - throws Error: the 'error.toString()' is taken as the error message of processing the event
// - returns object: this is taken as the result of processing the event
// - returns promise: when the promise is resolved, this is taken as the result of processing the event
const result = eventProcessorMapping.get(eventProcessorName)(routeConfig[eventProcessorName], event);
if (result) {
// be resilient against a processor returning a value instead of a promise:
return Promise.resolve(result)
.then(result => callback(null, result))
.catch(error => {
console.log(error.stack);
callback(error.toString());
});
}
} catch (error) {
if (error.stack) {
console.log(error.stack);
}
callback(error.toString());
return;
}
}
callback('No event processor found to handle this kind of event!');
}
}

function extractEventProcessorMapping(routeConfig) {
const processorMap = new Map();
for (let key of Object.keys(routeConfig)) {
try {
processorMap.set(key, require(`./lib/${key}`));
} catch (error) {
throw new Error(`The event processor '${key}', that is mentioned in the routerConfig, cannot be instantiated (${error.toString()})`);
}
}
return processorMap;
}

module.exports = {handler: handler};
88 changes: 44 additions & 44 deletions httpRouteHandler.js → lib/proxyIntegration.js
Original file line number Diff line number Diff line change
@@ -1,62 +1,63 @@
"use strict";

// TODO: req id in log sync u async

const NO_MATCHING_ACTION = request=> {
throw {reason: 'NO_MATCHING_ACTION', message: `Could not find matching action for ${request.path} and method ${request.httpMethod}`}
const NO_MATCHING_ACTION = request => {
throw {
reason: 'NO_MATCHING_ACTION',
message: `Could not find matching action for ${request.path} and method ${request.httpMethod}`
}
};

function handler(routeConfig) {
return (event, context, callback) => process(event, routeConfig, callback)
}

function process(event, routeConfig, callback) {
if (routeConfig.debug) {
console.log('Event', event);
function process(proxyIntegrationConfig, event) {
//validate config
if (!Array.isArray(proxyIntegrationConfig.routes) || proxyIntegrationConfig.routes.length < 1) {
throw new Error('proxyIntegration.routes must not be empty');
}

const headers = {
// detect if it's an http-call at all:
if (!event.httpMethod || !event.path) {
return null;
}
const headers = Object.assign({
'Content-Type': 'application/json'
};
// assure necessary values have sane defaults:
event.path = event.path || '';
const errorMapping = routeConfig.errorMapping || {};
errorMapping['NO_MATCHING_ACTION'] = 404;
routeConfig.routes = routeConfig.routes || [];
if (routeConfig.cors) {
}, proxyIntegrationConfig.defaultHeaders);
if (proxyIntegrationConfig.cors) {
headers["Access-Control-Allow-Origin"] = "*";
}
// ugly hack: if host is from 'Custom Domain Name Mapping', then event.path has the value '/basepath/resource-path/';
// if host is from amazonaws.com, then event.path is just '/resource-path':
const apiId = event.requestContext ? event.requestContext.apiId : null; // the apiId that is the first part of the amazonaws.com-host
if ((apiId && event.headers && event.headers.Host && event.headers.Host.substring(0, apiId.length) != apiId)) {
// remove first path element:
const groups = /\/[^\/]+(.*)/.exec(event.path) || [null, null];
event.path = groups[1] || '/';
}

// assure necessary values have sane defaults:
const errorMapping = proxyIntegrationConfig.errorMapping || {};
errorMapping['NO_MATCHING_ACTION'] = 404;

event.path = normalizeRequestPath(event);

try {
const actionConfig = findMatchingActionConfig(event.httpMethod, event.path, routeConfig) || {action: NO_MATCHING_ACTION};
const actionConfig = findMatchingActionConfig(event.httpMethod, event.path, proxyIntegrationConfig) || {action: NO_MATCHING_ACTION};
event.paths = actionConfig.paths;
if (event.body) {
event.body = JSON.parse(event.body);
}
const result = actionConfig.action(event);
if (result && result.then) {
return result
.then(res=> {
callback(null, {statusCode: 200, headers: headers, body: JSON.stringify(res)});
})
.catch(err=> {
callback(null, convertError(err, errorMapping, headers))
});
} else {
callback(null, {statusCode: 200, headers: headers, body: JSON.stringify(result)});
}
return Promise.resolve(actionConfig.action(event)).then(res => {
return {statusCode: 200, headers: headers, body: JSON.stringify(res)};
}).catch(err => {
return convertError(err, errorMapping, headers);
});
} catch (error) {
console.log('Error while evaluating matching action handler', error);
callback(null, convertError(error, errorMapping, headers));
return Promise.resolve(convertError(error, errorMapping, headers));
}
}

function normalizeRequestPath(event) {
// ugly hack: if host is from API-Gateway 'Custom Domain Name Mapping', then event.path has the value '/basepath/resource-path/';
// if host is from amazonaws.com, then event.path is just '/resource-path':
const apiId = event.requestContext ? event.requestContext.apiId : null; // the apiId that is the first part of the amazonaws.com-host
if ((apiId && event.headers && event.headers.Host && event.headers.Host.substring(0, apiId.length) != apiId)) {
// remove first path element:
const groups = /\/[^\/]+(.*)/.exec(event.path) || [null, null];
return groups[1] || '/';
}

return event.path;
}

function convertError(error, errorMapping, headers) {
Expand All @@ -68,7 +69,7 @@ function convertError(error, errorMapping, headers) {

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


module.exports = {handler: handler};
module.exports = process;
72 changes: 72 additions & 0 deletions lib/sns.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"use strict";

function process(snsConfig, event) {
// detect if it's an sns-event at all:
if(snsConfig.debug){
console.log('sns:Event', JSON.stringify(event));
}

if (!Array.isArray(event.Records) || event.Records.length<1 || !event.Records[0].Sns) {
console.log('Event does not look like SNS');
return null;
}

const sns = event.Records[0].Sns;
for(let routeConfig of snsConfig.routes){
if(routeConfig.subject instanceof RegExp){
if(routeConfig.subject.test(sns.Subject)){
const result = routeConfig.action(sns);
return result || {};
}
}else{
console.log(`SNS-Route with subject-regex '${routeConfig.subject}' is not a Regex; it is ignored!`);
}
}

if (snsConfig.debug) {
console.log(`No subject-match for ${sns.Subject}`);
}

return null;
}

module.exports = process;

/*
const cfgExample = {
routes:[
{
subject: /.*\/,
action: sns => articleService.invalidate(JSON.parse(sns.Message).escenicId)
}
]
};
*/


/* this is an example for a standard SNS notification message:
{
"Records": [
{
"EventSource": "aws:sns",
"EventVersion": "1.0",
"EventSubscriptionArn": "arn:aws:sns:eu-west-1:933782373565:production-escenic-updates:2fdd994c-f2b7-4c2f-a2f9-da83b590e0fc",
"Sns": {
"Type": "Notification",
"MessageId": "0629603b-448e-5366-88b4-309d651495c5",
"TopicArn": "arn:aws:sns:eu-west-1:933782373565:production-escenic-updates",
"Subject": null,
"Message": "{\"escenicId\":\"159526803\",\"model\":\"news\",\"status\":\"draft\"}",
"Timestamp": "2016-11-16T08:56:58.227Z",
"SignatureVersion": "1",
"Signature": "dtXM9BlAJJhYkVObnKmzY012kjgl4uYHEPQ1DLUalBHnPNzkDf12YeVcvHmq0SF6QbdgGwSYw0SgtsOkBiW3WSxVosqEb5xKUWIbQhlXwKdZnzekUigsgl3d231RP+9U2Cvd4QUc6klH5P+CuQM/F70LBIIv74UmR2YNMaxWxrv7Q+ETmz/TF6Y5v8Ip3+GLikbu6wQ/F5g3IHO2Lm7cLpV/74odm48SQxoolh94TdgvtYaUnxNjFVlF8Js8trbRkr7DYTogh73cTwuR77Mo+K9GlYn53txiMW5rMl3KhVdw4U3L190gtBJVwgHbqcB60pmNdEAE9f4bEOohizfPhg==",
"SigningCertUrl": "https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-b95095beb82e8f6a046b3aafc7f4149a.pem",
"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",
"MessageAttributes": {}
}
}
]
}
*/
14 changes: 8 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "aws-lambda-router",
"version": "0.1.1",
"description": "AWS lambda router",
"main": "httpRouteHandler.js",
"main": "index.js",
"scripts": {
"test": "gulp test"
},
Expand All @@ -15,6 +15,7 @@
"lambda",
"apigateway",
"any",
"sns",
"router",
"routing"
],
Expand All @@ -25,10 +26,11 @@
},
"homepage": "https://github.com/WeltN24/aws-lambda-router#readme",
"devDependencies": {
"del": "2.2.2",
"gulp": "3.9.1",
"gulp-install": "0.6.0",
"gulp-jasmine": "2.4.2",
"gulp-zip": "3.2.0"
"del": "^2.2.2",
"gulp": "^3.9.1",
"gulp-install": "^0.6.0",
"gulp-jasmine": "^2.4.2",
"gulp-zip": "^3.2.0",
"proxyquire": "^1.7.10"
}
}
Loading

0 comments on commit 36a96b1

Please sign in to comment.