├── .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://github.com/emanuelcasco/azure-middleware) [](https://github.com/emanuelcasco/azure-middleware/issues) [](https://github.com/emanuelcasco/azure-middleware) [](https://github.com/emanuelcasco/azure-middleware) [](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 |
--------------------------------------------------------------------------------