├── .eslintignore ├── .eslintrc ├── .gitignore ├── .nsprc ├── .nvmrc ├── .travis.yml ├── LICENSE.md ├── README.md ├── assets └── logo.png ├── index.js ├── package-lock.json ├── package.json ├── src └── index.js └── test └── index.test.js /.eslintignore: -------------------------------------------------------------------------------- 1 | app/dist 2 | .nyc_output 3 | coverage 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", // https://github.com/babel/babel-eslint 3 | "parserOptions": { 4 | "ecmaFeatures": { 5 | "experimentalObjectRestSpread": true 6 | }, 7 | "sourceType": "module" 8 | }, 9 | "env": { // http://eslint.org/docs/user-guide/configuring.html#specifying-environments 10 | "node": true, // node global variables 11 | "mocha": true, // mocha keywords 12 | "es6": true 13 | }, 14 | "plugins": [ 15 | "prettier" 16 | ], 17 | "extends": [ 18 | "eslint:recommended", 19 | "airbnb-base", 20 | "prettier" 21 | ], 22 | "rules": { 23 | "accessor-pairs": 0, // http://eslint.org/docs/rules/accessor-pairs 24 | "arrow-body-style": 0, // http://eslint.org/docs/rules/arrow-body-style 25 | "callback-return": 0, // http://eslint.org/docs/rules/callback-return 26 | "consistent-return": 0, // http://eslint.org/docs/rules/consistent-return 27 | "default-case": 0, // http://eslint.org/docs/rules/default-case 28 | "func-names": 0, // http://eslint.org/docs/rules/func-names 29 | "global-require": 0, // http://eslint.org/docs/rules/global-require 30 | "guard-for-in": 0, // http://eslint.org/docs/rules/guard-for-in 31 | "handle-callback-err": 0, // http://eslint.org/docs/rules/handle-callback-err 32 | "id-length": 0, // http://eslint.org/docs/rules/id-length 33 | "id-match": 0, // http://eslint.org/docs/rules/id-match 34 | "import/no-dynamic-require": 0, // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-dynamic-require.md 35 | "import/no-extraneous-dependencies": 0, // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-extraneous-dependencies.md 36 | "init-declarations": 0, // http://eslint.org/docs/rules/init-declarations 37 | "jsx-quotes": 0, // http://eslint.org/docs/rules/jsx-quotes 38 | "key-spacing": 0, // http://eslint.org/docs/rules/key-spacing 39 | "linebreak-style": 0, // http://eslint.org/docs/rules/linebreak-style 40 | "lines-around-comment": 0, // http://eslint.org/docs/rules/lines-around-comment 41 | "max-depth": 0, // http://eslint.org/docs/rules/max-depth 42 | "max-nested-callbacks": 0, // http://eslint.org/docs/rules/max-nested-callbacks 43 | "max-params": 0, // http://eslint.org/docs/rules/max-params 44 | "max-statements": 0, // http://eslint.org/docs/rules/max-statements 45 | "newline-after-var": 0, // http://eslint.org/docs/rules/newline-after-var 46 | "no-array-constructor": 0, // http://eslint.org/docs/rules/no-array-constructor 47 | "no-arrow-condition": 0, // http://eslint.org/docs/rules/no-arrow-condition 48 | "no-caller": 0, // http://eslint.org/docs/rules/no-caller 49 | "no-case-declarations": 0, // http://eslint.org/docs/rules/no-case-declarations 50 | "no-control-regex": 0, // http://eslint.org/docs/rules/no-control-regex 51 | "no-else-return": 0, // http://eslint.org/docs/rules/no-else-return 52 | "no-empty-character-class": 0, // http://eslint.org/docs/rules/no-empty-character-class 53 | "no-extend-native": 0, // http://eslint.org/docs/rules/no-extend-native 54 | "no-implicit-coercion": 0, // http://eslint.org/docs/rules/no-implicit-coercion 55 | "no-inline-comments": 0, // http://eslint.org/docs/rules/no-inline-comments 56 | "no-inner-declarations": 0, // http://eslint.org/docs/rules/no-inner-declarations 57 | "no-invalid-this": 0, // http://eslint.org/docs/rules/no-invalid-this 58 | "no-iterator": 0, // http://eslint.org/docs/rules/no-iterator 59 | "no-lonely-if": 0, // http://eslint.org/docs/rules/no-lonely-if 60 | "no-loop-func": 0, // http://eslint.org/docs/rules/no-loop-func 61 | "no-magic-numbers": 0, // http://eslint.org/docs/rules/no-magic-numbers 62 | "no-mixed-requires": 0, // http://eslint.org/docs/rules/no-mixed-requires 63 | "no-multi-str": 0, // http://eslint.org/docs/rules/no-multi-str 64 | "no-nested-ternary": 0, // http://eslint.org/docs/rules/no-nested-ternary 65 | "no-param-reassign": 0, // https://eslint.org/docs/rules/no-param-reassign 66 | "no-plusplus": 0, // http://eslint.org/docs/rules/no-plusplus 67 | "no-process-env": 0, // http://eslint.org/docs/rules/no-process-env 68 | "no-process-exit": 0, // http://eslint.org/docs/rules/no-process-exit 69 | "no-proto": 0, // http://eslint.org/docs/rules/no-proto 70 | "no-regex-spaces": 0, // http://eslint.org/docs/rules/no-regex-spaces 71 | "no-restricted-imports": 0, // http://eslint.org/docs/rules/no-restricted-imports 72 | "no-restricted-modules": 0, // http://eslint.org/docs/rules/no-restricted-modules 73 | "no-restricted-syntax": 0, // http://eslint.org/docs/rules/no-restricted-syntax 74 | "no-sequences": 0, // http://eslint.org/docs/rules/no-sequences 75 | "no-spaced-func": 0, // http://eslint.org/docs/rules/no-spaced-func 76 | "no-sync": 0, // http://eslint.org/docs/rules/no-sync 77 | "no-ternary": 0, // http://eslint.org/docs/rules/no-ternary 78 | "no-throw-literal": 0, // http://eslint.org/docs/rules/no-ternary 79 | "no-undefined": 0, // http://eslint.org/docs/rules/no-undefined 80 | "no-underscore-dangle": 0, // http://eslint.org/docs/rules/no-underscore-dangle 81 | "no-unexpected-multiline": 0, // http://eslint.org/docs/rules/no-unexpected-multiline 82 | "no-unused-expressions": 0, // http://eslint.org/docs/rules/no-unused-expressions 83 | "no-unused-vars": 0, // http://eslint.org/docs/rules/no-unused-vars 84 | "no-useless-escape": 0, // http://eslint.org/docs/rules/no-useless-escape 85 | "one-var": 0, // http://eslint.org/docs/rules/one-var 86 | "operator-linebreak": 0, // http://eslint.org/docs/rules/operator-linebreak 87 | "padded-blocks": 0, // http://eslint.org/docs/rules/padded-blocks 88 | "prefer-arrow-callback": 0, // http://eslint.org/docs/rules/prefer-arrow-callback 89 | "prefer-destructuring": 0, // https://eslint.org/docs/rules/prefer-destructuring 90 | "prefer-promise-reject-errors": 0, // https://eslint.org/docs/rules/prefer-promise-reject-errors 91 | "prefer-reflect": 0, // http://eslint.org/docs/rules/prefer-reflect 92 | "prefer-rest-params": 0, // http://eslint.org/docs/rules/prefer-rest-params 93 | "radix": 0, // http://eslint.org/docs/rules/radix 94 | "require-jsdoc": 0, // http://eslint.org/docs/rules/require-jsdoc 95 | "require-yield": 0, // http://eslint.org/docs/rules/require-yield 96 | "sort-vars": 0, // http://eslint.org/docs/rules/sort-vars 97 | "strict": 0, // http://eslint.org/docs/rules/strict 98 | "valid-jsdoc": 0, // http://eslint.org/docs/rules/valid-jsdoc 99 | "vars-on-top": 0, // http://eslint.org/docs/rules/vars-on-top 100 | "wrap-regex": 0, // http://eslint.org/docs/rules/wrap-regex 101 | 102 | 103 | 104 | "prettier/prettier": ["error", { "printWidth": 110, "singleQuote": true }] // https://github.com/prettier/prettier 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Frontend Dist Files 2 | app/dist 3 | 4 | # Mac Files 5 | .DS_Store 6 | */DS_Store 7 | 8 | # Logs 9 | logs 10 | *.log 11 | npm-debug.log* 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | 24 | # Coverage directory used by tools like nyc 25 | .nyc_output 26 | 27 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 28 | .grunt 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (http://nodejs.org/api/addons.html) 34 | build/Release 35 | 36 | # Dependency directory 37 | node_modules 38 | 39 | # Optional npm cache directory 40 | .npm 41 | 42 | # Optional REPL history 43 | .node_repl_history 44 | 45 | # Code coverage 46 | ./coverage 47 | 48 | # dotenv 49 | .env 50 | 51 | # VS Code 52 | .vscode/ 53 | -------------------------------------------------------------------------------- /.nsprc: -------------------------------------------------------------------------------- 1 | { 2 | "exceptions": ["https://nodesecurity.io/advisories/118"] 3 | } 4 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/* -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | dist: trusty 5 | install: 6 | - npm i -g npm@6.5.0 7 | - npm ci 8 | scripts: 9 | - npm test -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Emanuel Casco 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![](https://img.shields.io/travis/emanuelcasco/azure-middleware.svg)](https://github.com/emanuelcasco/azure-middleware) [![](https://img.shields.io/github/issues/emanuelcasco/azure-middleware.svg)](https://github.com/emanuelcasco/azure-middleware/issues) [![](https://img.shields.io/github/stars/emanuelcasco/azure-middleware.svg)](https://github.com/emanuelcasco/azure-middleware) [![](https://img.shields.io/github/license/emanuelcasco/azure-middleware.svg)](https://github.com/emanuelcasco/azure-middleware) [![](https://img.shields.io/node/v/azure-middleware-engine.svg)](https://www.npmjs.com/package/azure-middleware) 2 | 3 |

4 | 5 |

6 | 7 | # Azure Middleware Engine 🔗 8 | 9 | Azure Middleware Engine is developed inspired in web framworks like [express](http://expressjs.com/), [fastify](http://fastify.io/), [hapi](https://hapijs.com/), etc. to provide an easy-to-use api to use middleware patter in [Azure Functions](https://azure.microsoft.com/en-us/services/functions/). 10 | 11 | But, less talk and let see some code. 12 | 13 | For example: 14 | 15 | ```js 16 | // index.js 17 | const { someFunctionHandler } = require('./handlers'); 18 | const schema = require('../schemas'); 19 | 20 | const ChainedFunction = new MiddlewareHandler() 21 | .validate(schema) 22 | .use(someFunctionHandler) 23 | .use(ctx => { 24 | Promise.resolve(1).then(() => { 25 | ctx.log.info('Im called second'); 26 | ctx.next(); 27 | }); 28 | }) 29 | .use(ctx => { 30 | ctx.log.info('Im called third'); 31 | ctx.done(null, { status: 200 }); 32 | }) 33 | .listen(); 34 | 35 | module.exports = ChainedFunction; 36 | ``` 37 | 38 | ## Install 39 | 40 | Simply run: 41 | 42 | ```bash 43 | npm install azure-middleware 44 | ``` 45 | 46 | 47 | ## Motivation 48 | 49 | Biggest benefit of serverless arquitectures is that you can focus on implementing business logic. The problem is that when you are writing a function handler, you have to deal with some common technical concerns outside business logic, like input parsing and validation, output serialization, error handling, api calls, and more. 50 | 51 | Very often, all this necessary code ends up polluting the pure business logic code in your handlers, making the code harder to read and to maintain. 52 | 53 | Web frameworks, like [express](http://expressjs.com/), [fastify](http://fastify.io/) or [hapi](https://hapijs.com/), has solved this problem using the [middleware pattern](https://www.packtpub.com/mapt/book/web_development/9781783287314/4/ch04lvl1sec33/middleware). 54 | 55 | This pattern allows developers to isolate these common technical concerns into *"steps"* that *decorate* the main business logic code. 56 | 57 | Separating the business logic in smaller steps allows you to keep your code clean, readable and easy to maintain. 58 | 59 | Having not found an option already developed, I decided to create my own middleware engine for Azure Functions. 60 | 61 | ## Usage 62 | 63 | If you are familiar with Functional programming you will notice that behavior is similar to a pipeline. You can attach function handlers to the chain and them will be executed sequentially, 64 | 65 | #### middlewareHandler.use 66 | 67 | You can add a middleware using `use`. The order which handlers are added to the handler determines the order in which they'll be executed in the runtime. 68 | 69 | ```javascript 70 | const ChainedFunction = new MiddlewareHandler() 71 | .use(context => { 72 | myPromise(1, () => { 73 | context.log.info('Im called second'); 74 | context.next(); 75 | }); 76 | }) 77 | .use(context => { 78 | context.log.info('Im called third'); 79 | context.done(null, { status: 200 }); 80 | }) 81 | .listen(); 82 | 83 | module.exports = ChainedFunction; 84 | ``` 85 | 86 | #### middlewareHandler.useIf 87 | 88 | Similar to `use`, but you can define a predicate as first argument. If predicates resolves in a `false` then function handler won't be executed. 89 | 90 | ```javascript 91 | const OptionalFunction = new MiddlewareHandler() 92 | .use(ctx => { 93 | ctx.log.info('I will be called'); 94 | ctx.next(); 95 | }) 96 | .useIf( 97 | (ctx, msg) => false, // function won't be executed 98 | ctx => { 99 | ctx.log.info('I won\'t be called'); 100 | ctx.next(); 101 | } 102 | ) 103 | .catch((err, ctx) => { 104 | ctx.done(err); 105 | }) 106 | .listen() 107 | 108 | module.exports = OptionalFunction; 109 | ``` 110 | 111 | #### middlewareHandler.iterate 112 | 113 | Allows you to iterate over an array of elements that will be passed to an iterator function. 114 | 115 | ```javascript 116 | const IterateFunction = new MiddlewareHandler() 117 | .iterate([1,2,3,4], index => ctx => { 118 | ctx.log.info(index); 119 | ctx.next(); 120 | }) 121 | .catch((err, ctx) => { 122 | ctx.done(err); 123 | }) 124 | .listen() 125 | 126 | module.exports = IterateFunction; 127 | ``` 128 | 129 | #### middlewareHandler.validate 130 | 131 | You can define a schema validation to your function input. We use [Joi](https://www.npmjs.com/package/azure-middleware) to create and validate schemas. 132 | 133 | ```javascript 134 | const SchemaFunction = new MiddlewareHandler() 135 | .validate(JoiSchema) 136 | .use(context => { 137 | context.log.info('Im called only if message is valid'); 138 | context.done(); 139 | }) 140 | .catch((err, context) => { 141 | context.log.error(err); 142 | context.done(); 143 | }) 144 | .listen(); 145 | ``` 146 | 147 | #### middlewareHandler.catch 148 | 149 | Error handling functions will only be executed if there an error has been thrown or returned to the context.next method, described later, at which point normal Function Handler methods will stop being executed. 150 | 151 | ```javascript 152 | const CatchedFunction = new FunctionMiddlewareHandler() 153 | .validate(EventSchema) 154 | .use(() => { 155 | throw 'This is an error'; 156 | }) 157 | .catch((err, context) => { 158 | context.log.error(err); 159 | context.done(err); 160 | }) 161 | .listen(); 162 | ``` 163 | 164 | ##### middlewareHandler.listen 165 | 166 | Creates a function which can be exported as an Azure Function module. 167 | 168 | ## API 169 | 170 | #### Function types 171 | 172 | ##### FunctionHandler - (context, input): any 173 | 174 | A Function Handler is the normal syntax for an Azure Function. Any existing Node.js Functions could be used in this place. Note that you have to use the `context.next` method to trigger the next piece of middleware, which would require changes to any existing code that was used with func-middleware. 175 | 176 | ##### ErrorFunctionHandler - (err, context, input): any 177 | 178 | Same as a normal Function Handler, but the first parameter is instead a context object. 179 | 180 | ##### Predicate - (input): boolean 181 | 182 | Predicates are functions that have to return a boolean value. They are used to define a condition by which a FunctionHandler is executed or not. 183 | 184 | #### Next & Done 185 | 186 | ##### context.next(err?: Error) 187 | 188 | The `context.next` method triggers the next middleware to start. If an error is passed as a parameter, it will trigger the next ErrorFunctionHandler or, if there is none, call context.done with the error passed along. 189 | 190 | ##### context.done(err?: Error, output: any) 191 | 192 | The `context.done` method works the same as normal, but it's been wrapped by the library to prevent multiple calls. 193 | 194 | 195 | ## About 196 | 197 | This project is maintained by [Emanuel Casco](https://github.com/emanuelcasco). 198 | 199 | ## License 200 | 201 | **azure-middleware-engine** is available under the MIT [license](https://github.com/emanuelcasco/azure-middleware/blob/HEAD/LICENSE.md). 202 | 203 | ``` 204 | Copyright (c) 2019 Emanuel Casco 205 | 206 | Permission is hereby granted, free of charge, to any person obtaining a copy 207 | of this software and associated documentation files (the "Software"), to deal 208 | in the Software without restriction, including without limitation the rights 209 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 210 | copies of the Software, and to permit persons to whom the Software is 211 | furnished to do so, subject to the following conditions: 212 | 213 | The above copyright notice and this permission notice shall be included in 214 | all copies or substantial portions of the Software. 215 | 216 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 217 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 218 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 219 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 220 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 221 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 222 | THE SOFTWARE. 223 | ``` 224 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emanuelcasco/azure-middleware/00b31ff19a6b3b26793dc7260d04852cce220c39/assets/logo.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const AzureMiddleware = require('./src'); 4 | 5 | module.exports = AzureMiddleware; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "azure-middleware", 3 | "version": "1.0.1", 4 | "description": "Node.js middleware engine for Azure Functions", 5 | "keywords": [ 6 | "azure", 7 | "middlewares", 8 | "middleware handler", 9 | "middleware engine", 10 | "azure functions" 11 | ], 12 | "engines": { 13 | "node": ">=8.9.4", 14 | "npm": ">=6.5.0" 15 | }, 16 | "scripts": { 17 | "eslint-check": "eslint --print-config .eslintrc.js | eslint-config-prettier-check", 18 | "lint": "./node_modules/eslint/bin/eslint.js \"**/*.js\"", 19 | "lint-diff": "git diff --name-only --cached --relative | grep \\\\.js$ | xargs ./node_modules/eslint/bin/eslint.js", 20 | "lint-fix": "./node_modules/eslint/bin/eslint.js \"**/*.js\" --fix", 21 | "precommit": "npm run lint-diff", 22 | "outdated": "npm outdated --depth 0", 23 | "pretest": "npm run lint", 24 | "cover": "nyc --reporter=text npm test", 25 | "test": "NODE_ENV=testing ./node_modules/mocha/bin/_mocha test/index.test.js --timeout 6000 --exit", 26 | "test-inspect": "NODE_ENV=testing node --inspect --debug-brk ./node_modules/mocha/bin/_mocha \"**/*.test.js\"" 27 | }, 28 | "cacheDirectories": [ 29 | "node_modules" 30 | ], 31 | "main": "app.js", 32 | "author": "Emanuel Casco", 33 | "homepage": "https://github.com/emanuelcasco/azure-middleware", 34 | "license": "MIT", 35 | "repository": { 36 | "type": "git", 37 | "url": "https://github.com/emanuelcasco/azure-middleware.git" 38 | }, 39 | "bugs": { 40 | "url": "https://github.com/emanuelcasco/azure-middleware/issues" 41 | }, 42 | "devDependencies": { 43 | "babel": "6.23.0", 44 | "babel-core": "6.26.0", 45 | "babel-eslint": "^8.2.2", 46 | "babel-preset-es2015": "6.24.1", 47 | "chai": "^4.1.2", 48 | "chai-http": "^4.2.0", 49 | "chai-spies": "^1.0.0", 50 | "coveralls": "^3.0.0", 51 | "eslint": "^4.8.0", 52 | "eslint-config-airbnb-base": "^12.0.2", 53 | "eslint-config-prettier": "^2.3.1", 54 | "eslint-plugin-import": "^2.6.1", 55 | "eslint-plugin-prettier": "^2.1.1", 56 | "husky": "^0.14.3", 57 | "istanbul": "^0.4.3", 58 | "mocha": "^5.0.1", 59 | "mocha-lcov-reporter": "^1.2.0", 60 | "nyc": "^14.1.1", 61 | "prettier": "^1.8.2", 62 | "prettier-eslint": "^8.2.1" 63 | }, 64 | "dependencies": { 65 | "joi": "^14.3.0" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi'); 2 | 3 | const validateSchema = schema => (ctx, input) => { 4 | const { error } = Joi.validate(input, schema); 5 | return ctx.next( 6 | error 7 | ? { 8 | message: `Invalid input, ${error.message}`, 9 | details: JSON.stringify(error.details), 10 | input: JSON.stringify(input) 11 | } 12 | : null 13 | ); 14 | }; 15 | 16 | class FunctionMiddlewareHandler { 17 | constructor() { 18 | this.stack = []; 19 | } 20 | 21 | validate(schema) { 22 | if (!schema) { 23 | throw Error('schema should not be empty!'); 24 | } 25 | this.stack = [{ fn: validateSchema(schema) }, ...this.stack]; 26 | return this; 27 | } 28 | 29 | use(fn) { 30 | this.stack.push({ fn }); 31 | return this; 32 | } 33 | 34 | iterate(args, iterator) { 35 | args.forEach(arg => { 36 | this.stack.push({ fn: iterator(arg) }); 37 | }); 38 | return this; 39 | } 40 | 41 | useIf(predicate, fn) { 42 | this.stack.push({ fn, predicate, optional: true }); 43 | return this; 44 | } 45 | 46 | catch(fn) { 47 | this.stack.push({ fn, error: true }); 48 | return this; 49 | } 50 | 51 | listen() { 52 | const self = this; 53 | return (context, inputs, ...args) => self._handle(context, inputs, ...args); 54 | } 55 | 56 | _handle(ctx, input, ...args) { 57 | const originalDoneImplementation = ctx.done; 58 | const stack = this.stack; 59 | let index = 0; 60 | let doneWasCalled = false; 61 | 62 | ctx.done = (...params) => { 63 | if (doneWasCalled) return; 64 | doneWasCalled = true; 65 | originalDoneImplementation(...params); 66 | }; 67 | 68 | ctx.next = err => { 69 | try { 70 | const layer = stack[index++]; 71 | // No more layers to evaluate 72 | // Call DONE 73 | if (!layer) return ctx.done(err); 74 | // Both next called with err AND layers is error handler 75 | // Call error handler 76 | if (err && layer.error) return layer.fn(err, ctx, input, ...args); 77 | // Next called with err OR layers is error handler, but not both 78 | // Next layer 79 | if (err || layer.error) return ctx.next(err); 80 | // Layer is optional and predicate resolves to false 81 | // Next layer 82 | if (layer.optional && !layer.predicate(ctx, input)) return ctx.next(); 83 | 84 | // Call function handler 85 | return layer.fn(ctx, input, ...args); 86 | } catch (e) { 87 | return ctx.next(e); 88 | } 89 | }; 90 | ctx.next(); 91 | } 92 | } 93 | 94 | module.exports = FunctionMiddlewareHandler; 95 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | const Joi = require('joi'); 3 | const spies = require('chai-spies'); 4 | 5 | const MiddlewareHandler = require('../index'); 6 | 7 | const { expect } = chai; 8 | chai.use(spies); 9 | 10 | const EventSchema = Joi.object({ 11 | event: Joi.string() 12 | .valid('example') 13 | .required(), 14 | payload: Joi.object() 15 | .keys({ 16 | text: Joi.string() 17 | }) 18 | .required() 19 | }).required(); 20 | 21 | const handler = ctx => { 22 | ctx.log.info('Im called first'); 23 | ctx.next(); 24 | }; 25 | 26 | const defaultctx = { 27 | log: chai.spy.interface('log', ['info', 'error', 'warn']), 28 | bindings: () => ({}) 29 | }; 30 | 31 | it('should handle chained functions', done => { 32 | const ChainedFunction = new MiddlewareHandler() 33 | .use(handler) 34 | .use(ctx => { 35 | Promise.resolve(1).then(() => { 36 | ctx.log.info('Im called second'); 37 | ctx.next(); 38 | }); 39 | }) 40 | .use(ctx => { 41 | ctx.log.info('Im called third'); 42 | ctx.done(null, { status: 200 }); 43 | }) 44 | .listen(); 45 | 46 | const message = { 47 | event: 'example', 48 | payload: { text: 'holamundo' } 49 | }; 50 | const mockCtx = { 51 | ...defaultctx, 52 | done: (err, res) => { 53 | try { 54 | expect(err).to.equal(null); 55 | expect(res).to.deep.equal({ status: 200 }); 56 | expect(mockCtx.log.info).to.have.been.called.with('Im called first'); 57 | expect(mockCtx.log.info).to.have.been.called.with('Im called second'); 58 | expect(mockCtx.log.info).to.have.been.called.with('Im called third'); 59 | done(); 60 | } catch (error) { 61 | done(error); 62 | } 63 | } 64 | }; 65 | ChainedFunction(mockCtx, message); 66 | }); 67 | 68 | it('should handle error in catch', done => { 69 | const CatchedFunction = new MiddlewareHandler() 70 | .use(() => { 71 | throw 'This is an error'; 72 | }) 73 | .use(ctx => { 74 | ctx.log.info('Im not called'); 75 | ctx.done(); 76 | }) 77 | .catch((err, ctx) => { 78 | ctx.log.error(err); 79 | ctx.done(err); 80 | }) 81 | .listen(); 82 | 83 | const message = { 84 | event: 'example', 85 | payload: { text: 'holamundo' } 86 | }; 87 | 88 | const mockCtx = { 89 | ...defaultctx, 90 | done: err => { 91 | try { 92 | expect(err).to.equal('This is an error'); 93 | expect(mockCtx.log.error).to.have.been.called.with('This is an error'); 94 | done(); 95 | } catch (error) { 96 | done(error); 97 | } 98 | } 99 | }; 100 | 101 | CatchedFunction(mockCtx, message); 102 | }); 103 | 104 | it('should handle valid schema inputs', done => { 105 | const message = { 106 | event: 'example', 107 | payload: { text: 'holamundo' } 108 | }; 109 | 110 | const ValidSchemaFunction = new MiddlewareHandler() 111 | .validate(EventSchema) 112 | .use(ctx => { 113 | ctx.log.info('Im called'); 114 | ctx.done(); 115 | }) 116 | .catch((err, ctx) => { 117 | ctx.log.error(err); 118 | ctx.done(err); 119 | }) 120 | .listen(); 121 | 122 | const mockCtx = { 123 | ...defaultctx, 124 | done: err => { 125 | try { 126 | expect(err).to.equal(undefined); 127 | expect(mockCtx.log.info).to.have.been.called.with('Im called'); 128 | done(); 129 | } catch (error) { 130 | done(error); 131 | } 132 | } 133 | }; 134 | 135 | ValidSchemaFunction(mockCtx, message); 136 | }); 137 | 138 | it('should handle invalid schema inputs', done => { 139 | const message = {}; 140 | 141 | const InvalidSchemaFunction = new MiddlewareHandler() 142 | .validate(EventSchema) 143 | .use(ctx => { 144 | ctx.log.info('Im not called'); 145 | ctx.done(); 146 | }) 147 | .catch((err, ctx) => { 148 | ctx.log.error(err); 149 | ctx.done(err); 150 | }) 151 | .listen(); 152 | 153 | const mockCtx = { 154 | ...defaultctx, 155 | done: err => { 156 | try { 157 | expect(err.message).to.include('Invalid input'); 158 | expect(err.input).to.equal(JSON.stringify(message)); 159 | expect(mockCtx.log.info).to.have.not.been.called.with('Im not called'); 160 | expect(mockCtx.log.error).to.have.been.called(); 161 | done(); 162 | } catch (error) { 163 | done(error); 164 | } 165 | } 166 | }; 167 | 168 | InvalidSchemaFunction(mockCtx, message); 169 | }); 170 | 171 | it('should handle when done in called early', done => { 172 | const message = { 173 | event: 'example', 174 | payload: { text: 'holamundo' } 175 | }; 176 | 177 | const DoneEarlyFunction = new MiddlewareHandler() 178 | .use(ctx => { 179 | const predicate = true; 180 | if (predicate) { 181 | ctx.log.info('Im called'); 182 | ctx.done(null); 183 | } 184 | ctx.next(); 185 | }) 186 | .use(ctx => { 187 | ctx.log.info('Im not called'); 188 | ctx.done(); 189 | }) 190 | .catch((err, ctx) => { 191 | ctx.log.error(err); 192 | ctx.done(err); 193 | }) 194 | .listen(); 195 | 196 | const mockCtx = { 197 | ...defaultctx, 198 | done: err => { 199 | try { 200 | expect(err).to.equal(null); 201 | expect(mockCtx.log.info).to.have.been.called.with('Im called'); 202 | expect(mockCtx.log.info).to.have.not.been.called.with('Im not called'); 203 | done(); 204 | } catch (error) { 205 | done(error); 206 | } 207 | } 208 | }; 209 | 210 | DoneEarlyFunction(mockCtx, message); 211 | }); 212 | 213 | it('should handle data spreading', done => { 214 | const message = { 215 | event: 'example', 216 | payload: { text: 'holamundo' } 217 | }; 218 | 219 | const SpreadDataFunction = new MiddlewareHandler() 220 | .use(ctx => { 221 | ctx.myData = 'some info'; 222 | ctx.next(); 223 | }) 224 | .use(ctx => { 225 | ctx.log.info('Im not called'); 226 | ctx.done(null, { data: ctx.myData }); 227 | }) 228 | .catch((err, ctx) => { 229 | ctx.log.error(err); 230 | ctx.done(err); 231 | }) 232 | .listen(); 233 | 234 | const mockCtx = { 235 | ...defaultctx, 236 | done: (err, response) => { 237 | try { 238 | expect(err).to.equal(null); 239 | expect(response.data).to.equal('some info'); 240 | done(); 241 | } catch (error) { 242 | done(error); 243 | } 244 | } 245 | }; 246 | 247 | SpreadDataFunction(mockCtx, message); 248 | }); 249 | 250 | it('should handle when optional chaining function handlers', done => { 251 | const message = { 252 | event: 'example', 253 | payload: { text: 'holamundo' } 254 | }; 255 | 256 | const OptionalFunction = new MiddlewareHandler() 257 | .use(ctx => { 258 | ctx.data = []; 259 | ctx.next(); 260 | }) 261 | .useIf( 262 | (ctx, msg) => msg.event === 'example', 263 | ctx => { 264 | ctx.data.push(1); 265 | ctx.next(); 266 | } 267 | ) 268 | .use(ctx => { 269 | ctx.data.push(2); 270 | ctx.next(); 271 | }) 272 | .useIf( 273 | (ctx, msg) => msg.event !== 'example', 274 | ctx => { 275 | ctx.data.push(3); 276 | ctx.next(); 277 | } 278 | ) 279 | .use(ctx => { 280 | ctx.data.push(4); 281 | ctx.done(null, ctx.data); 282 | }) 283 | .catch((err, ctx) => { 284 | ctx.done(err); 285 | }) 286 | .listen(); 287 | 288 | const mockCtx = { 289 | ...defaultctx, 290 | done: (err, data) => { 291 | try { 292 | expect(err).to.equal(null); 293 | expect(data).to.deep.equal([1, 2, 4]); 294 | done(); 295 | } catch (error) { 296 | done(error); 297 | } 298 | } 299 | }; 300 | 301 | OptionalFunction(mockCtx, message); 302 | }); 303 | 304 | it('should handle empty argument in validation and throw error', done => { 305 | try { 306 | new MiddlewareHandler() 307 | .validate() 308 | .use(handler) 309 | .catch((err, ctx) => ctx.done(err)) 310 | .listen(); 311 | done('should throw!'); 312 | } catch (e) { 313 | expect(e.message).to.equal('schema should not be empty!'); 314 | done(); 315 | } 316 | }); 317 | 318 | it('should handle when optional chaining function handlers', done => { 319 | const message = { 320 | event: 'example', 321 | payload: { text: 'holamundo' } 322 | }; 323 | 324 | const NextFunction = new MiddlewareHandler() 325 | .use(ctx => { 326 | ctx.log.info('Im called first'); 327 | ctx.next(); 328 | }) 329 | .use(ctx => { 330 | ctx.log.info('Im called second'); 331 | ctx.next(null); 332 | }) 333 | .listen(); 334 | 335 | const mockCtx = { 336 | ...defaultctx, 337 | done: err => { 338 | try { 339 | expect(err).to.equal(null); 340 | expect(mockCtx.log.info).to.have.been.called.with('Im called first'); 341 | expect(mockCtx.log.info).to.have.been.called.with('Im called second'); 342 | done(); 343 | } catch (error) { 344 | done(error); 345 | } 346 | } 347 | }; 348 | 349 | NextFunction(mockCtx, message); 350 | }); 351 | --------------------------------------------------------------------------------