UNPKG

@root/async-router

Version:

Write Express middleware and route handlers using async/await

156 lines (139 loc) 4.33 kB
'use strict'; const express = require('express'); const { findNext, findResponse } = require('./utils'); module.exports = { /** * @param {import('express').RouterOptions} [options] * @returns import('express').Router */ Router: function asyncRouter(options) { return wrapRouter(express.Router(options)); }, wrap: wrapRouter, // exported for tests _wrapFunction: function (fn) { return wrapMiddleware('', '', fn); } }; function wrapRouter(app, methods = ['use', 'get', 'post', 'put', 'patch', 'delete', 'head']) { const routeFn = app.route; /** * @param {Array<any>} options * @returns import('express').Router */ app.route = function route(...options) { return wrapRouter(routeFn.apply(app, options)); }; methods.forEach(function (method) { const fn = app[method]; /** * @returns import('express').Router */ app[method] = function () { const args = wrapArgs(method, arguments); return fn.apply(app, args); }; }); return app; } function typeOf(arg) { if (arg instanceof RegExp) { return 'regexp'; } if (Array.isArray(arg)) { return 'array'; } return typeof arg; } function wrapArgs(method, args) { const ret = []; for (let i = 0; i < args.length; i += 1) { let arg = args[i]; let typ = typeOf(arg); switch (typ) { case 'regex': ret.push(arg); continue; case 'string': ret.push(arg); continue; case 'array': ret.push(wrapArgs(method, arg)); return ret; case 'function': ret.push(wrapMiddleware(method, args[0], arg)); continue; default: throw new Error(`argument ${i} has the unexpected type '${typ}': ` + JSON.stringify(arg)); } } return ret; } function wrapMiddleware(method, path, fn) { // response handler: function (req, res) { ... } if (fn.length === 2) { return async function asyncWrapper2(req, res) { await handleResult(method, path, fn, arguments, res, arguments[2]); }; } // middleware: function (req, res, next) { ... } if (fn.length === 3) { return async function asyncWrapper3(req, res, next) { await handleResult(method, path, fn, arguments, res, next); }; } // error handler: function (req, res, next) { ... } if (fn.length === 4) { /* eslint-disable node/handle-callback-err */ return async function asyncWrapper4(err, req, res, next) { await handleResult(method, path, fn, arguments, res, next); }; } // other return async function asyncWrapperN(...args) { const next = findNext(args); const res = findResponse(args); await handleResult(method, path, fn, args, res, next); }; } async function handleResult(method, path, fn, args, res, next) { if (path !== 'string') { // implicit app.use(fn) or app.get(fn) rather than app.get('/', fn) path = '/'; } try { const response = await fn.apply(fn, args); if (res.headersSent) { return; } if (response) { res.send(response); return; } // otherwise expect that the middleware should call next() } catch (e) { let err = e; if (!err) { let errType = typeof err; err = new Error(`'${err}' (${errType}) was thrown`); } let extra = ''; if (method) { extra = ` '${method} ${path}'`; } if (res.headersSent) { // no sense in re-throwing at this point console.error(`@root/async-router: Unhandled Error in Async Route${extra} after Headers Sent:`); console.error(err); return; } if (typeof next === 'function') { next(err); return; } console.error(`@root/async-router: Unhandled Error in Async Route${extra} without 'next':`); console.error(err); res.statusCode = 500; res.end(); } }