From 4caea5ccd36584111b461ff19f9216f29599c1d4 Mon Sep 17 00:00:00 2001 From: Steven White Date: Mon, 3 Apr 2017 00:29:29 -0400 Subject: [PATCH] Initial commit --- .babelrc | 3 + .editorconfig | 9 +++ .eslintrc | 45 +++++++++++++++ .gitignore | 32 ++++++++++ .npmignore | 2 + README.md | 1 + package.json | 32 ++++++++++ src/index.js | 157 ++++++++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 281 insertions(+) create mode 100644 .babelrc create mode 100644 .editorconfig create mode 100644 .eslintrc create mode 100644 .gitignore create mode 100644 .npmignore create mode 100644 README.md create mode 100644 package.json create mode 100644 src/index.js diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..b5a260c --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "plugins": ["syntax-async-functions", "transform-async-to-generator"] +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ba02b75 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..235b43d --- /dev/null +++ b/.eslintrc @@ -0,0 +1,45 @@ +{ + "parser": "babel-eslint", + "rules": { + "block-spacing": [ 2 ], + "brace-style": [ 2, "1tbs", { + "allowSingleLine": true + }], + "comma-dangle": [ 2, "never" ], + "comma-spacing": [ 2 ], + "curly": [ 2, "multi-line" ], + "eqeqeq": [ 2 ], + "indent": [ 2, 2, { + "SwitchCase": 1 + }], + "key-spacing": [ 2 ], + "keyword-spacing": [ 2 ], + "linebreak-style": [ 2, "unix" ], + "no-console": [ 0 ], + "no-empty": [ 2, { + "allowEmptyCatch": true + }], + "no-else-return": [ 2 ], + "no-eval": [ 2 ], + "no-mixed-spaces-and-tabs": [ 2 ], + "no-multi-spaces": [ 2 ], + "no-spaced-func": [ 2 ], + "no-trailing-spaces": [ 2 ], + "no-unused-vars": [ 2 ], + "no-whitespace-before-property": [ 2 ], + "one-var": [ 2, { + "initialized": "never", + "uninitialized": "always" + }], + "quotes": [ 2, "single" ], + "semi": [ 2, "always" ], + "space-before-blocks": [ 2 ], + "space-before-function-paren": [ 2, "never" ], + "space-in-parens": [ 2 ] + }, + "env": { + "node": true, + "es6": true + }, + "extends": "eslint:recommended" +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..69dd9b4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# Commenting this out is preferred by some people, see +# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- +node_modules +npm-debug.log + +# Users Environment Variables +.lock-wscript + +.DS_Store +dist diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..a3f930d --- /dev/null +++ b/.npmignore @@ -0,0 +1,2 @@ +src/ +node_modules/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..4e08604 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# lambda-router diff --git a/package.json b/package.json new file mode 100644 index 0000000..011607b --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "lambda-router", + "version": "1.0.0", + "description": "Lambda router for serverless framework", + "main": "index.js", + "scripts": { + "build": "babel -d dist src", + "watch": "babel -w -d dist src", + "prepublish": "npm run build", + "lint": "eslint src/", + "test": "mocha test/**/*.spec.js", + "test:watch": "mocha -w test/**/*.spec.js" + }, + "author": "Steven White ", + "license": "MIT", + "devDependencies": { + "babel-cli": "^6.24.0", + "babel-core": "^6.24.0", + "babel-eslint": "^7.2.1", + "babel-plugin-syntax-async-functions": "^6.13.0", + "babel-plugin-transform-async-to-generator": "^6.22.0", + "babel-runtime": "^6.23.0", + "chai": "^3.5.0", + "eslint": "^3.19.0", + "mocha": "^3.2.0", + "sinon": "^2.1.0", + "sinon-chai": "^2.9.0" + }, + "dependencies": { + "boom": "^4.3.1" + } +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..884760c --- /dev/null +++ b/src/index.js @@ -0,0 +1,157 @@ +/** + * Lambda Router + */ + +const Boom = require('boom'); + +class LambdaRouter { + + /** + * LambdaRouter constructor. + * @param {Object} options + * @param {Object} options.headers + * @param {Function} options.onInvoke + * @param {Function} options.onError + */ + constructor(options = {}) { + this.routes = {}; + this.headers = options.headers || {}; + this.onInvoke = options.onInvoke; + this.onError = options.onError; + } + + /** + * Lambda handler function. + * Maps event details to previously registered route handlers. + * @returns {Function} + */ + handler() { + return async (event, context, cb) => { + // Prevent callback waiting. + // IMPORTANT, otherwise lambda may potentially timeout + // needlessly. + context.callbackWaitsForEmptyEventLoop = false; + + // Setup state object to allow handlers to pass data along + context.state = {}; + + // Find appropriate handlers + const route = this._divineRoute(event); + + try { + // Verify route was found + if (!route || !route.handlers) throw Boom.notFound('Resource not found'); + + // Notify onInvoke handler if provided + if (this.onInvoke) this.onInvoke(event); + + // Invoke handlers for reply + let payload; + for (let handler of route.handlers) { + payload = await handler(event, context); + } + + // Deliver response + return cb(null, { + statusCode: 200, + headers: this.headers, + body: JSON.stringify(Object.assign({ success: true }, payload)) + }); + } catch (err) { + // Capture error details from boom + const details = err.output.payload; + + // Get error body from onError handler if provided + let body; + if (this.onError) { + body = this.onError(err, event); + } else { + body = { + success: false, + error: Object.assign(err.data || {}, { + statusCode: details.statusCode || 400, + message: details.message, + code: details.error + }) + }; + } + + // Deliver response + return cb(null, { + statusCode: details.statusCode || 400, + headers: this.headers, + body: JSON.stringify(body) + }); + } + + }; + } + + /** + * Add handler for get route. + * @param {String} path + * @param {Function} hdlr - list of handlers to call in succession + */ + get(path, ...hdlr) { + this._wrap('GET', path, hdlr); + } + + /** + * Add handler for post route. + * @param {String} path + * @param {Function} hdlr - list of handlers to call in succession + */ + post(path, ...hdlr) { + this._wrap('POST', path, hdlr); + } + + /** + * Add handler for put route. + * @param {String} path + * @param {Function} hdlr - list of handlers to call in succession + */ + put(path, ...hdlr) { + this._wrap('PUT', path, hdlr); + } + + /** + * Add handler for delete route. + * @param {String} path + * @param {Function} hdlr - list of handlers to call in succession + */ + del(path, ...hdlr) { + this._wrap('DELETE', path, hdlr); + } + + /** + * Add handler for options route. + * @param {String} path + * @param {Function} hdlr - list of handlers to call in succession + */ + options(path, ...hdlr) { + this._wrap('OPTIONS', path, hdlr); + } + + /** + * Store route handler reference in routes + * @param {String} method + * @param {String} path + * @param {Function} hdlr - list of handlers to call in succession + */ + _wrap(method, path, handlers) { + if (!this.routes[method]) this.routes[method] = []; + this.routes[method].push({ path, handlers }); + } + + /** + * Deliver handler that matches lambda event. + * @param {Object} event + * @returns {Object} + */ + _divineRoute({ httpMethod, resource }) { + if (!this.routes[httpMethod]) return; + return this.routes[httpMethod].find(r => r.path === resource); + } +} + +module.exports = LambdaRouter;