├── tslint.json ├── .gitignore ├── SECURITY.md ├── .editorconfig ├── .travis.yml ├── tsconfig.json ├── LICENSE ├── README.md ├── package.json └── src ├── index.ts └── index.spec.ts /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint-config-standard", "tslint-config-prettier"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | coverage/ 3 | node_modules/ 4 | npm-debug.log 5 | lib/ 6 | typings/ 7 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Security contact information 4 | 5 | To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_size = 2 7 | indent_style = space 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | 4 | notifications: 5 | email: 6 | on_success: never 7 | on_failure: change 8 | 9 | node_js: 10 | - "6" 11 | - "stable" 12 | 13 | after_script: "npm install coveralls@2 && cat ./coverage/lcov.info | coveralls" 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "rootDir": "src", 5 | "outDir": "lib", 6 | "module": "commonjs", 7 | "strict": true, 8 | "declaration": true, 9 | "sourceMap": true, 10 | "inlineSources": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com) 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 | # Compose Middleware 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | [![NPM downloads][downloads-image]][downloads-url] 5 | [![Build status][travis-image]][travis-url] 6 | [![Test coverage][coveralls-image]][coveralls-url] 7 | 8 | > Compose an array of middleware into a single function for use in Express, Connect, router, etc. 9 | 10 | ## Installation 11 | 12 | ```sh 13 | npm install compose-middleware --save 14 | ``` 15 | 16 | ## Usage 17 | 18 | Compose multiple middleware functions into a single request middleware handler, with support for inline error handling middleware. 19 | 20 | ```js 21 | var express = require("express"); 22 | var compose = require("compose-middleware").compose; 23 | 24 | var app = express(); 25 | 26 | app.use( 27 | compose([ 28 | function(req, res, next) {}, 29 | function(err, req, res, next) {}, 30 | function(req, res, next) {} 31 | ]) 32 | ); 33 | ``` 34 | 35 | **P.S.** The composed function takes three arguments. Express.js (and Connect, router) only accept error handlers of four arguments. If you want to return an error handler from `compose` instead, try the `errors` export - it works exactly the same, but exposes the four argument middleware pattern. 36 | 37 | ## License 38 | 39 | MIT 40 | 41 | [npm-image]: https://img.shields.io/npm/v/compose-middleware.svg?style=flat 42 | [npm-url]: https://npmjs.org/package/compose-middleware 43 | [downloads-image]: https://img.shields.io/npm/dm/compose-middleware.svg?style=flat 44 | [downloads-url]: https://npmjs.org/package/compose-middleware 45 | [travis-image]: https://img.shields.io/travis/blakeembrey/compose-middleware.svg?style=flat 46 | [travis-url]: https://travis-ci.org/blakeembrey/compose-middleware 47 | [coveralls-image]: https://img.shields.io/coveralls/blakeembrey/compose-middleware.svg?style=flat 48 | [coveralls-url]: https://coveralls.io/r/blakeembrey/compose-middleware?branch=master 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "compose-middleware", 3 | "version": "5.0.1", 4 | "description": "Compose an array of middleware into a single function for use in Express, Connect, router, etc.", 5 | "main": "lib/index.js", 6 | "typings": "lib/index.d.ts", 7 | "files": [ 8 | "lib/", 9 | "LICENSE" 10 | ], 11 | "scripts": { 12 | "prettier": "prettier --write", 13 | "lint": "tslint \"src/**/*\" --project tsconfig.json", 14 | "format": "npm run prettier -- \"*.{json,md,yml}\" \"src/**/*.{js,jsx,ts,tsx}\"", 15 | "build": "rimraf dist/ && tsc", 16 | "specs": "jest --coverage", 17 | "test": "npm run build && npm run lint && npm run specs", 18 | "prepare": "npm run build" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git://github.com/blakeembrey/compose-middleware.git" 23 | }, 24 | "keywords": [ 25 | "middleware", 26 | "express", 27 | "compose", 28 | "flatten", 29 | "function" 30 | ], 31 | "author": { 32 | "name": "Blake Embrey", 33 | "email": "hello@blakeembrey.com", 34 | "url": "http://blakeembrey.me" 35 | }, 36 | "license": "MIT", 37 | "bugs": { 38 | "url": "https://github.com/blakeembrey/compose-middleware/issues" 39 | }, 40 | "homepage": "https://github.com/blakeembrey/compose-middleware", 41 | "jest": { 42 | "roots": [ 43 | "/src/" 44 | ], 45 | "transform": { 46 | "\\.tsx?$": "ts-jest" 47 | }, 48 | "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(tsx?|jsx?)$", 49 | "moduleFileExtensions": [ 50 | "ts", 51 | "tsx", 52 | "js", 53 | "jsx", 54 | "json", 55 | "node" 56 | ] 57 | }, 58 | "husky": { 59 | "hooks": { 60 | "pre-commit": "lint-staged" 61 | } 62 | }, 63 | "lint-staged": { 64 | "*.{js,jsx,ts,tsx,json,css,md,yml,yaml,gql,graphql}": [ 65 | "npm run prettier", 66 | "git add" 67 | ] 68 | }, 69 | "publishConfig": { 70 | "access": "public" 71 | }, 72 | "devDependencies": { 73 | "@types/jest": "^24.0.21", 74 | "@types/node": "^12.12.5", 75 | "husky": "^3.0.9", 76 | "jest": "^24.9.0", 77 | "lint-staged": "^9.4.2", 78 | "prettier": "^1.18.2", 79 | "rimraf": "^3.0.0", 80 | "ts-jest": "^24.1.0", 81 | "tslint": "^5.20.0", 82 | "tslint-config-prettier": "^1.18.0", 83 | "tslint-config-standard": "^9.0.0", 84 | "typescript": "^3.6.4" 85 | }, 86 | "dependencies": { 87 | "@types/debug": "^4.1.5", 88 | "array-flatten": "^3.0.0", 89 | "debug": "^4.1.0" 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import debug = require("debug"); 2 | import { flatten } from "array-flatten"; 3 | 4 | const log = debug("compose-middleware"); 5 | 6 | export type Next = (err?: Error | null) => T; 7 | export type RequestHandler = ( 8 | req: T, 9 | res: U, 10 | next: Next 11 | ) => V; 12 | export type ErrorHandler = ( 13 | err: Error, 14 | req: T, 15 | res: U, 16 | next: Next 17 | ) => V; 18 | export type Middleware = 19 | | RequestHandler 20 | | ErrorHandler; 21 | 22 | export type Handler = 23 | | Middleware 24 | | NestedMiddleware; 25 | 26 | export interface NestedMiddleware 27 | extends ReadonlyArray> {} 28 | 29 | /** 30 | * Compose an array of middleware handlers into a single handler. 31 | */ 32 | export function compose( 33 | ...handlers: Handler[] 34 | ): RequestHandler { 35 | const middleware = generate(handlers); 36 | 37 | return function requestMiddleware(req: T, res: U, done: Next) { 38 | return middleware(null, req, res, done); 39 | }; 40 | } 41 | 42 | /** 43 | * Wrap middleware handlers. 44 | */ 45 | export function errors( 46 | ...handlers: Handler[] 47 | ): ErrorHandler { 48 | return generate(handlers); 49 | } 50 | 51 | /** 52 | * Generate a composed middleware function. 53 | */ 54 | function generate(handlers: Array>) { 55 | const stack = flatten(handlers) as Middleware[]; 56 | 57 | for (const handler of stack) { 58 | if ((typeof handler as any) !== "function") { 59 | throw new TypeError("Handlers must be a function"); 60 | } 61 | } 62 | 63 | return function middleware( 64 | err: Error | null, 65 | req: T, 66 | res: U, 67 | done: Next 68 | ): V { 69 | let index = -1; 70 | 71 | function dispatch(pos: number, err?: Error | null): V { 72 | const handler = stack[pos]; 73 | 74 | index = pos; 75 | 76 | if (index === stack.length) return done(err); 77 | 78 | function next(err?: Error | null) { 79 | if (pos < index) { 80 | throw new TypeError("`next()` called multiple times"); 81 | } 82 | 83 | return dispatch(pos + 1, err); 84 | } 85 | 86 | try { 87 | if (handler.length === 4) { 88 | if (err) { 89 | log("handle(err)", (handler as any).name || ""); 90 | 91 | return (handler as ErrorHandler)(err, req, res, next); 92 | } 93 | } else { 94 | if (!err) { 95 | log("handle()", (handler as any).name || ""); 96 | 97 | return (handler as RequestHandler)(req, res, next); 98 | } 99 | } 100 | } catch (e) { 101 | // Avoid future errors that could diverge stack execution. 102 | if (index > pos) throw e; 103 | 104 | log("try..catch", e); 105 | 106 | return next(e); 107 | } 108 | 109 | return next(err); 110 | } 111 | 112 | return dispatch(0, err); 113 | }; 114 | } 115 | -------------------------------------------------------------------------------- /src/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { compose, Next, Middleware } from "./index"; 2 | 3 | describe("compose middleware", () => { 4 | it("should be assignable to array of middleware", done => { 5 | const pipeline = (...middlewares: Array>) => 6 | compose(middlewares); 7 | const middleware = pipeline( 8 | (req: undefined, res: undefined, next: () => void) => next() 9 | ); 10 | 11 | return middleware(undefined, undefined, done); 12 | }); 13 | 14 | it("should compose middleware", done => { 15 | const middleware = compose([ 16 | function(req: any, res: any, next: Next) { 17 | req.one = true; 18 | next(); 19 | }, 20 | function(req: any, res: any, next: Next) { 21 | req.two = true; 22 | next(); 23 | } 24 | ]); 25 | 26 | const req: any = {}; 27 | const res: any = {}; 28 | 29 | middleware(req, res, function(err) { 30 | expect(err).toEqual(undefined); 31 | expect(req.one).toEqual(true); 32 | expect(req.two).toEqual(true); 33 | 34 | return done(); 35 | }); 36 | }); 37 | 38 | it("should exit with an error", done => { 39 | const middleware = compose([ 40 | function(req: any, res: any, next: Next) { 41 | req.one = true; 42 | next(new Error("test")); 43 | }, 44 | function(req: any, res: any, next: Next) { 45 | req.two = true; 46 | next(); 47 | } 48 | ]); 49 | 50 | const req: any = {}; 51 | const res: any = {}; 52 | 53 | middleware(req, res, function(err) { 54 | expect(err).toBeInstanceOf(Error); 55 | expect(req.one).toEqual(true); 56 | expect(req.two).toEqual(undefined); 57 | 58 | return done(); 59 | }); 60 | }); 61 | 62 | it("should short-cut handler with a single function", done => { 63 | const middleware = compose([ 64 | function(req: any, res: any, next: Next) { 65 | req.one = true; 66 | next(); 67 | } 68 | ]); 69 | 70 | const req: any = {}; 71 | const res: any = {}; 72 | 73 | middleware(req, res, function(err) { 74 | expect(err).toEqual(undefined); 75 | expect(req.one).toEqual(true); 76 | 77 | return done(); 78 | }); 79 | }); 80 | 81 | it("should accept a single function", done => { 82 | const middleware = compose(function( 83 | req: any, 84 | res: any, 85 | next: Next 86 | ) { 87 | req.one = true; 88 | next(); 89 | }); 90 | 91 | const req: any = {}; 92 | 93 | middleware(req, {}, function(err?: Error | null) { 94 | expect(err).toEqual(undefined); 95 | expect(req.one).toEqual(true); 96 | 97 | return done(); 98 | }); 99 | }); 100 | 101 | it("should noop with no middleware", done => { 102 | const middleware = compose([]); 103 | 104 | middleware({}, {}, done); 105 | }); 106 | 107 | it("should validate all handlers are functions", () => { 108 | expect(() => compose(["foo"] as any)).toThrow( 109 | new TypeError("Handlers must be a function") 110 | ); 111 | }); 112 | 113 | it("should support error handlers", done => { 114 | const middleware = compose( 115 | function(req: any, res: any, next: Next) { 116 | return next(new Error("test")); 117 | }, 118 | function(_: Error, req: any, res: any, next: Next) { 119 | return next(); 120 | }, 121 | function(req: any, res: any, next: Next) { 122 | req.success = true; 123 | return next(); 124 | }, 125 | function(_: Error, req: any, res: any, next: Next) { 126 | req.fail = true; 127 | return next(); 128 | } 129 | ); 130 | 131 | const req: any = {}; 132 | 133 | middleware(req, {} as any, function(err) { 134 | expect(req.fail).toEqual(undefined); 135 | expect(req.success).toEqual(true); 136 | 137 | return done(err); 138 | }); 139 | }); 140 | 141 | it("should error when calling `next()` multiple times", done => { 142 | const middleware = compose(function( 143 | req: any, 144 | res: any, 145 | next: Next 146 | ) { 147 | next(); 148 | next(); 149 | }); 150 | 151 | try { 152 | middleware({}, {}, function() { 153 | /* */ 154 | }); 155 | } catch (err) { 156 | expect(err.message).toEqual("`next()` called multiple times"); 157 | 158 | return done(); 159 | } 160 | }); 161 | 162 | it("should forward thrown errors", done => { 163 | const middleware = compose(function( 164 | req: any, 165 | res: any, 166 | next: Next 167 | ) { 168 | throw new Error("Boom!"); 169 | }); 170 | 171 | middleware({}, {}, function(err) { 172 | expect(err).toBeInstanceOf(Error); 173 | expect(err!.message).toEqual("Boom!"); 174 | 175 | return done(); 176 | }); 177 | }); 178 | 179 | it("should not cascade errors from `done()`", done => { 180 | const request = { 181 | done: 0, 182 | first: 0, 183 | second: 0, 184 | third: 0 185 | }; 186 | 187 | const middleware = compose( 188 | function(req: typeof request, res: any, next: Next) { 189 | req.first++; 190 | 191 | return next(); 192 | }, 193 | function(req: typeof request, res: any, next: Next) { 194 | req.second++; 195 | 196 | throw new TypeError("Boom!"); 197 | }, 198 | function(req: typeof request, res: any, next: Next) { 199 | req.third++; 200 | 201 | return next(); 202 | } 203 | ); 204 | 205 | try { 206 | middleware(request, {}, function() { 207 | request.done++; 208 | 209 | throw new TypeError("This is the end"); 210 | }); 211 | } catch (err) { 212 | expect(request.done).toEqual(1); 213 | expect(request.first).toEqual(1); 214 | expect(request.second).toEqual(1); 215 | expect(request.third).toEqual(0); 216 | 217 | expect(err).toBeInstanceOf(TypeError); 218 | expect(err.message).toEqual("This is the end"); 219 | 220 | return done(); 221 | } 222 | 223 | return done(new TypeError("Missed thrown error")); 224 | }); 225 | 226 | it("should avoid handling post-next thrown errors", function(done) { 227 | const middleware = compose( 228 | function(req: any, res: any, next: Next) { 229 | return next(); 230 | }, 231 | function(req: any, res: any, next: Next) { 232 | next(); 233 | throw new TypeError("Boom!"); 234 | }, 235 | function(req: any, res: any, next: Next) { 236 | return setTimeout(next); 237 | } 238 | ); 239 | 240 | try { 241 | middleware({}, {}, function(err) { 242 | return done(err); 243 | }); 244 | } catch (err) { 245 | expect(err).toBeInstanceOf(TypeError); 246 | expect(err.message).toEqual("Boom!"); 247 | return; 248 | } 249 | 250 | return done(new TypeError("Missed thrown error")); 251 | }); 252 | 253 | it("should compose functions without all arguments", function(done) { 254 | const middleware = compose( 255 | function(req: any, res: any, next: Next) { 256 | return next(); 257 | }, 258 | function() { 259 | return done(); 260 | } 261 | ); 262 | 263 | middleware({}, {}, function(err) { 264 | return done(err || new Error("Middleware should not have finished")); 265 | }); 266 | }); 267 | }); 268 | --------------------------------------------------------------------------------