├── tsconfig.json ├── .gitignore ├── package.json ├── tslint.json ├── Gruntfile.js ├── README.md └── index.ts /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2015", 7 | "es2015.promise", 8 | "es2015.symbol" 9 | ] 10 | } 11 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | 4 | # Typings 5 | typings/ 6 | 7 | # Build results 8 | [T|t]emp/ 9 | [B|b]uild/ 10 | [B|b]in/ 11 | [O|o]bj/ 12 | [D|d]ist/ 13 | *.tmp.txt 14 | 15 | # Development Javascript 16 | *.js 17 | *.js.map 18 | !Gruntfile.js 19 | 20 | # Visual Studio 21 | .vs/ 22 | *.psess 23 | *.vsp 24 | *.vspx 25 | *.dat 26 | *.tmp 27 | *.suo 28 | *.user 29 | *.sln.docstates 30 | 31 | # IntelliJ 32 | .idea/ 33 | 34 | # Logs 35 | *.log 36 | 37 | # Windows image file caches 38 | Thumbs.db 39 | ehthumbs.db 40 | 41 | # Folder config file 42 | Desktop.ini 43 | 44 | # Recycle Bin used on file shares 45 | $RECYCLE.BIN/ 46 | 47 | # Mac desktop service store files 48 | .DS_Store 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-async-router", 3 | "version": "0.1.15", 4 | "description": "Express Async Router - An Express Router wrapper which automatically manage Promise.", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "homepage": "https://github.com/spatools/express-async-router#readme", 8 | "author": { 9 | "name": "SPATools", 10 | "url": "https://github.com/spatools", 11 | "email": "spa@touchify.co" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/spatools/express-async-router.git" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/spatools/express-async-router/issues" 19 | }, 20 | "keywords": [ 21 | "express", 22 | "async", 23 | "router", 24 | "promise", 25 | "es6" 26 | ], 27 | "scripts": { 28 | "test": "grunt" 29 | }, 30 | "dependencies": { 31 | "@types/express": "^4.16.0", 32 | "@types/node": "^8.10.36", 33 | "express": "^4.16.4" 34 | }, 35 | "devDependencies": { 36 | "grunt": "^1.0.3", 37 | "grunt-build-control": "^0.7.1", 38 | "grunt-contrib-clean": "^2.0.0", 39 | "grunt-contrib-copy": "^1.0.0", 40 | "grunt-ts": "^6.0.0-beta.21", 41 | "grunt-tslint": "^5.0.2", 42 | "time-grunt": "^1.4.0", 43 | "tslint": "^5.11.0", 44 | "typescript": "^3.1.3" 45 | } 46 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "class-name": true, 4 | "curly": false, 5 | "eofline": true, 6 | "forin": true, 7 | "indent": [ 8 | true, 9 | 4 10 | ], 11 | "label-position": true, 12 | "max-line-length": false, 13 | "no-arg": false, 14 | "no-bitwise": false, 15 | "no-console": [ 16 | true, 17 | "debug", 18 | "info", 19 | "time", 20 | "timeEnd", 21 | "trace" 22 | ], 23 | "no-construct": true, 24 | "no-debugger": true, 25 | "no-duplicate-variable": true, 26 | "no-empty": true, 27 | "no-eval": true, 28 | "no-string-literal": false, 29 | "no-trailing-whitespace": true, 30 | "one-line": [ 31 | true, 32 | "check-open-brace", 33 | "check-whitespace" 34 | ], 35 | "quotemark": [ 36 | true, 37 | "double" 38 | ], 39 | "radix": true, 40 | "semicolon": true, 41 | "triple-equals": [ 42 | true, 43 | "allow-null-check" 44 | ], 45 | "variable-name": false, 46 | "whitespace": [ 47 | true, 48 | "check-branch", 49 | "check-decl", 50 | "check-operator", 51 | "check-separator", 52 | "check-type" 53 | ] 54 | } 55 | } -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = function (grunt) { 4 | require("time-grunt")(grunt); // Time how long tasks take. Can help when optimizing build times 5 | 6 | //#region Initialization 7 | 8 | var config = { 9 | 10 | pkg: grunt.file.readJSON("package.json"), 11 | 12 | options: { 13 | dev: grunt.option("dev"), 14 | env: grunt.option("env") || "preprod", 15 | }, 16 | 17 | paths: { 18 | build: "dist", 19 | temp: "temp" 20 | }, 21 | }; 22 | 23 | //#endregion 24 | 25 | //#region Typescript 26 | 27 | config.ts = { 28 | options: { 29 | target: "es5", 30 | module: "commonjs", 31 | sourceMap: true, 32 | declaration: false, 33 | comments: false, 34 | disallowbool: true, 35 | disallowimportmodule: true, 36 | fast: "never" 37 | }, 38 | src: { 39 | src: ["typings/index.d.ts", "*.ts"], 40 | tsconfig: "tsconfig.json" 41 | }, 42 | dist: { 43 | src: "<%= ts.src.src %>", 44 | dest: "<%= paths.build %>", 45 | tsconfig: "tsconfig.json", 46 | options: { 47 | rootDir: ".", 48 | declaration: true, 49 | sourceMap: false 50 | } 51 | } 52 | }; 53 | 54 | //#endregion 55 | 56 | //#region Static 57 | 58 | config.copy = { 59 | dist: { 60 | files: [ 61 | { 62 | expand: true, 63 | src: ["README.md"], 64 | dest: "<%= paths.build %>" 65 | } 66 | ] 67 | } 68 | }; 69 | 70 | grunt.registerTask("packagejson", "", function () { 71 | var pkg = grunt.file.readJSON("package.json"); 72 | 73 | delete pkg.scripts; 74 | delete pkg.devDependencies; 75 | delete pkg.optionalDependencies; 76 | 77 | grunt.file.write(config.paths.build + "/package.json", JSON.stringify(pkg, null, 2)); 78 | }); 79 | 80 | //#endregion 81 | 82 | //#region Tests 83 | 84 | config.tslint = { 85 | options: { 86 | configuration: grunt.file.readJSON("tslint.json") 87 | }, 88 | 89 | src: ["*.ts"] 90 | }; 91 | 92 | //#endregion 93 | 94 | //#region Publish 95 | 96 | config.buildcontrol = { 97 | options: { 98 | dir: "<%= paths.build %>", 99 | tag: "<%= pkg.version %>", 100 | commit: true, 101 | push: true, 102 | config: { 103 | "user.name": "buildcontrol", 104 | "user.email": "robot@touchify.co" 105 | } 106 | }, 107 | release: { 108 | options: { 109 | remote: "<%= pkg.repository.url %>", 110 | branch: "release" 111 | } 112 | } 113 | }; 114 | 115 | //#endregion 116 | 117 | //#region Cleanup 118 | 119 | config.clean = { 120 | dist: "<%= paths.build %>/", 121 | src: [ 122 | "*.{js,js.map}", 123 | "!Gruntfile.js" 124 | ] 125 | }; 126 | 127 | //#endregion 128 | 129 | //#region Aliases 130 | 131 | grunt.initConfig(config); 132 | 133 | grunt.loadNpmTasks("grunt-build-control"); 134 | grunt.loadNpmTasks("grunt-contrib-clean"); 135 | grunt.loadNpmTasks("grunt-contrib-copy"); 136 | grunt.loadNpmTasks("grunt-ts"); 137 | grunt.loadNpmTasks("grunt-tslint"); 138 | 139 | grunt.registerTask("src", ["clean:src", "tslint:src", "ts:src"]); 140 | grunt.registerTask("build", ["clean:dist", "tslint:src", "ts:dist", "copy:dist", "packagejson"]); 141 | 142 | grunt.registerTask("default", ["build"]); 143 | grunt.registerTask("publish", ["build", "buildcontrol"]); 144 | 145 | //#endregion 146 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # express-async-router 2 | 3 | `express-async-router` is an [Express] [Router] wrapper which automatically manage `Promise`. 4 | 5 | ## Getting Started 6 | 7 | `express-async-router` works exactly as Express [Router]. 8 | If you're not family with Express Router API, please see [Router] documentation. 9 | 10 | ### Installation 11 | 12 | `express-async-router` can be installed using NPM: 13 | 14 | ```shell 15 | $ npm install express-async-router --save 16 | ``` 17 | 18 | ### Usage 19 | 20 | First import `express-async-router` in your project: 21 | 22 | ```javascript 23 | var AsyncRouter = require("express-async-router").AsyncRouter; 24 | ``` 25 | 26 | Then instanciate `AsyncRouter`: 27 | 28 | ```javascript 29 | var router = AsyncRouter(); 30 | ``` 31 | 32 | You're ready to use `AsyncRouter` the same way as Express [Router] but without worrying about `Promise`. 33 | 34 | ```javascript 35 | router.get("/", function (req, res) { 36 | return myGetOperation() 37 | .then(myOtherOperation); 38 | }); 39 | 40 | router.post("/:test", function (req, res) { 41 | return myParametrizedOperation(req.params.test) 42 | .then(myOtherOperation); 43 | }); 44 | 45 | router.use(function (req, res) { 46 | return myMiddlewareOperation() 47 | .then(myOtherOperation); 48 | }); 49 | ``` 50 | 51 | ## Options 52 | 53 | `express-async-router` works exactly as Express [Router] so it can take the same options plus some additionnals to manage how request is sent. 54 | 55 | By default, `express-async-router` sends the Promise result by using `res.send(result)` if headers was not already sent. You can customize this behavior by passing `sender` option when creating `AsyncRouter`. 56 | 57 | ### options.send 58 | 59 | **Type**: `boolean` | **Default**: `true` 60 | 61 | If set to `false`, `AsyncRouter` will never try to send `Promise` result. 62 | 63 | ### options.sender 64 | 65 | **Type**: `(req, res, value) => Thenable` | **Default**: `function (req, res, value) { res.send(value); }` 66 | 67 | If set, it will override the default `AsyncRouter` `sender` function. 68 | 69 | 70 | Examples: 71 | 72 | ```javascript 73 | var router = AsyncRouter({ send: false }); 74 | ``` 75 | Or 76 | 77 | ```javascript 78 | var router = AsyncRouter({ sender: mySender }); 79 | 80 | function mySender(req, res, value) { 81 | res.rend(value.template, value.data); 82 | } 83 | 84 | router.get("/", function () { 85 | return myOperation().then(function (data) { 86 | return { 87 | template: "index", 88 | data: data 89 | }; 90 | }); 91 | }); 92 | ``` 93 | 94 | ### send 95 | 96 | ## Promise handling 97 | 98 | `express-async-router` automatically handles Promises when it can. 99 | 100 | ### param(name: string, handler: (req, res, param) => Thenable) 101 | 102 | A special `Router.param` override which automatically calls `next` function when returned `Promise` resolves. 103 | If returned `Promise` rejects, rejected `Error` is transfered to `next` function. 104 | If result is not a `Promise`, `next` function is immediatelly called. 105 | 106 | Example: 107 | ```javascript 108 | router.param("test", function (req, res, param) { 109 | return getTestEntity(param) 110 | .then(function(entity) { 111 | req.test = entity; 112 | }); 113 | }); 114 | ``` 115 | 116 | ### _[method]_(name: string, handler: (req, res) => Thenable) 117 | 118 | A `Router[method]` wrapper which automatically calls `next` function when returned `Promise` resolves. 119 | If returned `Promise` rejects, rejected `Error` is transfered to `next` function. 120 | If result is not a `Promise`, `next` function is immediatelly called. 121 | 122 | Examples: 123 | ```javascript 124 | router.get("/", function () { 125 | return getTestEntities(); 126 | }); 127 | 128 | router.post("/:test", function (req) { 129 | return getTestEntity(req.params.test); 130 | }); 131 | ``` 132 | 133 | ### use(...handlers[]: (req, res) => Thenable) 134 | ### use(name: string | RegExp | string[], ...handlers[]: (req, res) => Thenable) 135 | 136 | A `Router.use` wrapper which automatically calls `next` function when returned `Promise` resolves. 137 | If returned `Promise` rejects, rejected `Error` is transfered to `next` function. 138 | If result is not a `Promise`, `next` function is immediatelly called. 139 | 140 | __NOTE: If you declare 3 arguments in your function, `next` will only be called when an error occured.__ 141 | 142 | Examples: 143 | ```javascript 144 | router.use(function (req) { 145 | return validateToken(req.header("MyCustomToken")) 146 | .then(function (user) { 147 | req.user = user; 148 | }); 149 | }); 150 | 151 | router.use("/test", function (req) { 152 | return validateToken(req.header("MyCustomToken")) 153 | .then(function (user) { 154 | req.user = user; 155 | }); 156 | }); 157 | 158 | router.use(myCustomAuth, serveStatic(__dirname + "/public"), function (req) { 159 | return logToServer(req) 160 | .then(function () { 161 | console.log(req); 162 | }); 163 | }); 164 | 165 | ``` 166 | 167 | ### use(...handlers[]: (err, req, res, next) => Thenable) 168 | ### use(name: string | RegExp | string[], ...handlers[]: (err, req, res, next) => Thenable) 169 | 170 | A `Router.use` wrapper for Error handling which automatically calls `next` function when returned `Promise` resolves. 171 | If returned `Promise` rejects, rejected `Error` is transfered to `next` function. 172 | If result is not a `Promise`, `next` function is immediatelly called. 173 | 174 | __WARNING: You must declare the 4 arguments to your function to be recognized as an Error handler. This is for compatibility with Native Middlewares.__ 175 | 176 | Examples: 177 | ```javascript 178 | router.use(function (err, req, res, next) { 179 | return logError(err) 180 | .then(function () { 181 | console.error(err); 182 | res.send(500, "An error occured!"); 183 | }); 184 | }); 185 | 186 | router.use("/test", function (err, req, res, next) { 187 | return logError(err) 188 | .then(function () { 189 | console.error(err); 190 | res.send(500, "An error occured!"); 191 | }); 192 | }); 193 | 194 | router.use(function (err, req, res, next) { 195 | return logError(err) 196 | .then(function () { 197 | console.error(err); 198 | res.send(500, "An error occured!"); 199 | }); 200 | }); 201 | ``` 202 | 203 | ## Contribute 204 | 205 | ### Install Global Dependencies 206 | 207 | `express-router-async` needs some development dependencies: 208 | 209 | * [Grunt](http://gruntjs.com) 210 | * [tsd](http://definitelytyped.org/tsd/) 211 | 212 | ```shell 213 | $ npm install -g grunt-cli tsd 214 | ``` 215 | 216 | ### Install Project dependencies 217 | 218 | ```shell 219 | $ npm install 220 | ``` 221 | 222 | ### Build project 223 | 224 | ```shell 225 | $ grunt 226 | ``` 227 | 228 | 229 | [Express]: http://expressjs.com/ 230 | [Router]: http://expressjs.com/4x/api.html#router -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | //#region Express Members 2 | 3 | import * as express from "express"; 4 | 5 | export type Router = express.Router; 6 | export type Request = express.Request; 7 | export type Response = express.Response; 8 | 9 | export type RequestHandler = express.RequestHandler; 10 | export type RequestParamHandler = express.RequestParamHandler; 11 | export type ErrorRequestHandler = express.ErrorRequestHandler; 12 | export type ParamHandler = RequestParamHandler; 13 | export type ErrorHandler = ErrorRequestHandler; 14 | 15 | export type NextFunction = express.NextFunction; 16 | 17 | //#endregion 18 | 19 | //#region Types and Constants 20 | 21 | const 22 | DEFAULT_SENDER = (req, res, val) => { res.send(val); }, 23 | SHORTCUTS_METHODS = ["all", "get", "post", "put", "delete", "patch", "options", "head"]; 24 | 25 | export type AsyncRouterParamHandler = (req: Request, res: Response, param: any) => any; 26 | export type AsyncRouterSender = (req: Request, res: Response, value: any) => any; 27 | 28 | export interface AsyncRouterOptions { 29 | caseSensitive?: boolean; 30 | mergeParams?: boolean; 31 | strict?: boolean; 32 | 33 | send?: boolean; 34 | sender?: AsyncRouterSender; 35 | } 36 | 37 | export interface AsyncRouter { 38 | (): AsyncRouterInstance; 39 | (options: AsyncRouterOptions): AsyncRouterInstance; 40 | 41 | new(): AsyncRouterInstance; 42 | new(options: AsyncRouterOptions): AsyncRouterInstance; 43 | } 44 | 45 | export interface AsyncRouterInstance extends express.Router { 46 | param(name: string, handler: ParamHandler): this; 47 | param(name: string, matcher: RegExp): this; 48 | param(name: string, mapper: (param: any) => any): this; 49 | param(callback: (name: string, matcher: RegExp) => ParamHandler): this; 50 | 51 | /** Async param function. */ 52 | param(name: string, handler: AsyncRouterParamHandler): void; 53 | } 54 | 55 | //#endregion 56 | 57 | //#region Public 58 | 59 | const ASYNC_MARKER = typeof Symbol !== "undefined" ? Symbol("ASYNC_MARKER") : "__ASYNC_MARKER__"; 60 | 61 | export function AsyncRouter(options?: AsyncRouterOptions): AsyncRouterInstance { 62 | const 63 | sender = getSender(options), 64 | innerRouter = express.Router(options), 65 | 66 | asyncRouter: AsyncRouterInstance = function () { 67 | return innerRouter.apply(this, arguments); 68 | } as any; 69 | 70 | wrapAllMatchers(asyncRouter, sender, innerRouter); 71 | 72 | asyncRouter[ASYNC_MARKER] = true; 73 | asyncRouter.param = function param(): AsyncRouterInstance { 74 | if (typeof arguments[1] === "function" && arguments[1].length === 3) { 75 | innerRouter.param(arguments[0], wrapParamHandler(arguments[1])); 76 | return this; 77 | } 78 | 79 | innerRouter.param.apply(innerRouter, arguments); 80 | return this; 81 | }; 82 | 83 | asyncRouter.route = function route(path: string) { 84 | const r = innerRouter.route(path); 85 | wrapAllMatchers(r as any, sender); 86 | return r; 87 | }; 88 | 89 | asyncRouter.use = function use(...args: any[]): AsyncRouterInstance { 90 | innerRouter.use.apply(innerRouter, args.map(arg => { 91 | if (Array.isArray(arg)) { 92 | return arg.map(a => isHandlerOrErrorHandler(a) ? wrapHandlerOrErrorHandler(a) : a); 93 | } 94 | 95 | if (isHandlerOrErrorHandler(arg)) { 96 | return wrapHandlerOrErrorHandler(arg); 97 | } 98 | 99 | return arg; 100 | })); 101 | 102 | return this; 103 | }; 104 | 105 | return asyncRouter; 106 | } 107 | 108 | 109 | /** Returns a new AsyncRouter instance with default options. */ 110 | export function create(): AsyncRouterInstance; 111 | /** 112 | * Returns a new AsyncRouter instance with default options. 113 | * @param {Object} options - options to pass to express Router plus AsyncRouter options. 114 | */ 115 | export function create(options: AsyncRouterOptions): AsyncRouterInstance; 116 | export function create(options?: AsyncRouterOptions): AsyncRouterInstance { 117 | return AsyncRouter(options); 118 | } 119 | 120 | //#endregion 121 | 122 | //#region Private Methods 123 | 124 | function getSender(options: AsyncRouterOptions): AsyncRouterSender { 125 | if (!options) { 126 | return DEFAULT_SENDER; 127 | } 128 | 129 | const 130 | send = options.send, 131 | sender = options.sender; 132 | 133 | delete options.send; 134 | delete options.sender; 135 | 136 | if (send !== false) { 137 | return sender || DEFAULT_SENDER; 138 | } 139 | } 140 | 141 | function wrapAllMatchers(route: Router, sender: AsyncRouterSender, router?: Router): void { 142 | router = router || route as Router; 143 | 144 | SHORTCUTS_METHODS.forEach(method => { 145 | route[method] = wrapMatcher(router, router[method], sender); 146 | }); 147 | } 148 | 149 | function wrapMatcher(router: Router, routerMatcher: Function, sender: AsyncRouterSender): Function { 150 | return (name: any, ...args: RequestHandler[]) => { 151 | const 152 | last = args.length - 1, 153 | mappedArgs = args.map((arg, i) => { 154 | if (i === last) { 155 | return wrapHandler(arg, sender); 156 | } 157 | 158 | if (Array.isArray(arg)) { 159 | return arg.map(a => isHandlerOrErrorHandler(a) ? wrapHandlerOrErrorHandler(a) : a); 160 | } 161 | 162 | if (isHandlerOrErrorHandler(arg)) { 163 | return wrapHandlerOrErrorHandler(arg); 164 | } 165 | 166 | return arg; 167 | }); 168 | 169 | routerMatcher.apply(router, [name].concat(mappedArgs)); 170 | 171 | return this; 172 | }; 173 | } 174 | 175 | function wrapHandler(handler: RequestHandler, sender: AsyncRouterSender): RequestHandler { 176 | return function (req, res, next): void { 177 | try { 178 | next = once(next); 179 | toCallback(handler.call(this, req, res, next), next, req, res, result => { 180 | if (sender && !res.headersSent) { 181 | return sender(req, res, result); 182 | } 183 | }); 184 | } 185 | catch (err) { 186 | next(err); 187 | } 188 | }; 189 | } 190 | 191 | function wrapParamHandler(handler: AsyncRouterParamHandler): ParamHandler { 192 | return function (req, res, next, param): void { 193 | try { 194 | next = once(next); 195 | toCallback(handler.call(this, req, res, param), next, req, res); 196 | } 197 | catch (err) { 198 | next(err); 199 | } 200 | }; 201 | } 202 | 203 | function wrapHandlerOrErrorHandler(handler: RequestHandler | ErrorHandler): RequestHandler | ErrorHandler { 204 | if (handler.length === 4) { 205 | return function (err, req, res, next): void { 206 | try { 207 | next = once(next); 208 | toCallback(handler.call(this, err, req, res, next), next, req, res); 209 | } 210 | catch (err) { 211 | next(err); 212 | } 213 | }; 214 | } 215 | 216 | return function (req, res, next): void { 217 | try { 218 | next = once(next); 219 | toCallback(handler.call(this, req, res, next), next, req, res, handler.length === 3); 220 | } 221 | catch (err) { 222 | next(err); 223 | } 224 | }; 225 | } 226 | 227 | function toCallback(thenable: PromiseLike, next: Function, req: Request, res: Response, end?: boolean | ((res) => any)): void { 228 | if (!thenable || typeof thenable.then !== "function") { 229 | thenable = Promise.resolve(thenable); 230 | } 231 | 232 | if (typeof end === "function") { 233 | thenable = thenable.then(end); 234 | } 235 | 236 | thenable.then( 237 | () => { 238 | if (next && !end && !res.headersSent) { 239 | next(); 240 | } 241 | }, 242 | err => { 243 | if (typeof err === "string") { 244 | err = new Error(err); 245 | } 246 | 247 | next(err); 248 | } 249 | ); 250 | } 251 | 252 | function isHandlerOrErrorHandler(handler: any): handler is RequestHandler | ErrorHandler { 253 | return typeof handler === "function" && handler[ASYNC_MARKER] !== true; 254 | } 255 | 256 | function once(fn: NextFunction): NextFunction { 257 | let called = false; 258 | 259 | return function () { 260 | if (called) { 261 | return; 262 | } 263 | 264 | called = true; 265 | fn.apply(this, arguments); 266 | }; 267 | } 268 | 269 | //#endregion 270 | --------------------------------------------------------------------------------