UNPKG

6.59 kBJavaScriptView Raw
1'use strict';
2
3// Small Express router that runs request handlers written as ES6 generators
4// using Node co. On top of that the router does a few useful things, including
5// some logging and error handling using a Node domain.
6
7const _ = require('underscore');
8const express = require('express');
9const domain = require('domain');
10const url = require('url');
11const yieldable = require('abacus-yieldable');
12const transform = require('abacus-transform');
13
14const toArray = _.toArray;
15const map = _.map;
16const extend = _.extend;
17const omit = _.omit;
18const pick = _.pick;
19const without = _.without;
20const allKeys = _.allKeys;
21
22// Setup debug log
23const debug = require('abacus-debug')('abacus-router');
24const edebug = require('abacus-debug')('e-abacus-router');
25
26// Convert a middleware function which can be a regular Express middleware or a
27// generator to a regular Express middleware function.
28const callbackify = (m, trusted) => {
29
30 // If the callback is a regular function just use it as-is, otherwise it's
31 // a generator and we need to wrap it using the co module
32 const mfunc = yieldable.functioncb(m);
33
34 // Return a middleware function. Middleware functions can be of the form
35 // function(req, res, next) or function(err, req, res, next) so we need to
36 // support both forms
37 return function() {
38 const next = arguments[arguments.length - 1];
39 const res = arguments[1];
40 const params = toArray(arguments).slice(0, arguments.length - 1);
41
42 // Pass errors down the middleware stack, if the middleware is
43 // un-trusted then we mark the error with bailout flag to trigger our
44 // server bailout logic
45 const error = (err, type) => {
46 edebug('Route error - %s - %o', type, err);
47 debug('Route error - %s - %o', type, err);
48 if(!trusted && !err.status && !err.statusCode)
49 err.bailout = true;
50 next(err);
51 };
52
53 // Call the middleware function
54 try {
55 mfunc.apply(undefined, params.concat([(err, value) => {
56 if(err) error(err, 'generator error');
57 else if(value)
58 // Store the returned value in the response, it'll be sent
59 // by one of our Express middleware later down the
60 // middleware stack
61 res.value = value;
62 next();
63 }]));
64 }
65 catch (exc) {
66 error(exc, 'exception');
67 }
68 };
69};
70
71// Return an implementation of the router.use(path, middleware) function that
72// supports middleware implemented as generators in addition to regular
73// callbacks
74const use = (original, trusted) => {
75 return function(path, m) {
76 return typeof path === 'function' ?
77 original.call(this, callbackify(path, trusted)) :
78 original.call(this, path, callbackify(m, trusted));
79 };
80};
81
82// Return an implementation of the router.route() function that supports
83// middleware implemented as generators in addition to regular callbacks
84const route = (original, trusted) => {
85 return function() {
86 // Get the route
87 const r = original.apply(this, arguments);
88
89 // Monkey patch its HTTP methods
90 map(['get', 'head', 'post', 'put', 'patch', 'delete', 'options', 'all',
91 'use'
92 ], (method) => {
93 const f = r[method];
94 r[method] = function() {
95 const middleware = map(toArray(arguments), function(m) {
96 return callbackify(m, trusted);
97 });
98 return f.apply(this, middleware);
99 };
100 });
101 return r;
102 };
103};
104
105// Return an Express middleware that uses a Node domain to run the middleware
106// stack and handle any errors not caught in async callbacks
107const catchall = (trusted) => {
108 return (req, res, next) => {
109 const d = domain.create();
110 d.on('error', (err) => {
111 debug('Route domain error %o', err);
112
113 // Pass the error down the middleware stack, if the router runs
114 // un-trusted middleware then mark it with a bailout flag to
115 // trigger our server bailout logic
116 // Warning: mutating variable err
117 if(!trusted && !err.status && !err.statusCode)
118 err.bailout = true;
119 next(err);
120 });
121
122 // Because req and res were created before this domain existed,
123 // we need to explicitly add them. See the explanation of implicit
124 // vs explicit binding in the Node domain docs.
125 d.add(req);
126 d.add(res);
127
128 // Run the middleware stack in our new domain
129 d.run(next);
130 };
131};
132
133// Return an Express router middleware that works with generators
134const router = (trusted) => {
135 const r = express.Router();
136
137 // Catch all errors down the middleware stack using a Node domain
138 r.use(catchall(trusted));
139
140 // Monkey patch the router function with our implementation of the use
141 // and route functions
142 r.use = use(r.use, trusted);
143 r.route = route(r.route, trusted);
144
145 return r;
146};
147
148// Return an express middleware that runs a batch of requests through the
149// given router
150const batch = (routes) => {
151 return (req, res, next) => {
152 if(req.method !== 'POST' || req.url !== '/batch')
153 return next();
154 debug('Handling batch request %o', req.body);
155
156 // Run the batch of requests found in the body through the router
157 transform.map(req.body, (r, i, reqs, rcb) => {
158 // Setup an Express request representing the batched request
159 const path = url.resolve(req.url, r.uri);
160 const rreq = extend({}, pick(req, without(allKeys(req), 'host')), {
161 method: r.method,
162 url: path,
163 path: path,
164 body: r.body
165 });
166
167 // Setup an Express response object that will capture the response
168 const rres = extend({}, res, {
169 status: (s) => {
170 rres.statusCode = s;
171 return rres;
172 },
173 header: (k, v) => {
174 if (!rres.header) rres.header = {};
175 rres.header[k] = v;
176 return rres;
177 },
178 send: (b) => {
179 rres.body = b;
180 return rres.end();
181 },
182 json: (b) => {
183 rres.body = b;
184 return rres.end();
185 },
186 end: () => {
187 rcb(undefined, {
188 statusCode: rres.statusCode,
189 header: omit(rres.header, 'setHeader'),
190 body: rres.body
191 });
192 return rres;
193 }
194 });
195
196 // Call the given router
197 routes(rreq, rres, (err) => {
198 if (err) return rcb(err);
199
200 if(rres.value) return rcb(undefined, rres.value);
201 next();
202 });
203
204 }, (err, bres) => {
205 if(err) return res.status(500).end();
206
207 // Return the batch of results
208 res.send(bres);
209 });
210 };
211};
212
213// Export our public functions
214module.exports = router;
215module.exports.batch = batch;
216