UNPKG

@akala/core

Version:
505 lines 16.4 kB
"use strict"; /*! * router * Copyright(c) 2013 Roman Shtylman * Copyright(c) 2014 Douglas Christopher Wilson * MIT Licensed */ Object.defineProperty(exports, "__esModule", { value: true }); /** * Module dependencies. * @private */ var debug = require('debug')('router'); const layer_1 = require("./layer"); exports.Layer = layer_1.Layer; // import * as methods from 'methods'; const helpers_1 = require("../helpers"); const url_1 = require("url"); const route_1 = require("./route"); exports.Route = route_1.Route; var slice = Array.prototype.slice; /* istanbul ignore next */ var defer = typeof setImmediate === 'function' ? setImmediate : function (fn, ...args) { process.nextTick(fn.bind.apply(fn, arguments)); }; class Router { constructor(options) { this.params = {}; this.stack = []; this.router = this.handle.bind(this); var opts = options || {}; this.caseSensitive = opts.caseSensitive; this.mergeParams = opts.mergeParams; this.separator = opts.separator || '/'; this.strict = opts.strict; this.length = opts.length || 2; } /** * Map the given param placeholder `name`(s) to the given callback. * * Parameter mapping is used to provide pre-conditions to routes * which use normalized placeholders. For example a _:user_id_ parameter * could automatically load a user's information from the database without * any additional code. * * The callback uses the same signature as middleware, the only difference * being that the value of the placeholder is passed, in this case the _id_ * of the user. Once the `next()` function is invoked, just like middleware * it will continue on to execute the route, or subsequent parameter functions. * * Just like in middleware, you must either respond to the request or call next * to avoid stalling the request. * * router.param('user_id', function(req, res, next, id){ * User.find(id, function(err, user){ * if (err) { * return next(err) * } else if (!user) { * return next(new Error('failed to load user')) * } * req.user = user * next() * }) * }) * * @param {string} name * @param {function} fn * @public */ param(name, fn) { if (!name) { throw new TypeError('argument name is required'); } if (typeof name !== 'string') { throw new TypeError('argument name must be a string'); } if (!fn) { throw new TypeError('argument fn is required'); } if (typeof fn !== 'function') { throw new TypeError('argument fn must be a function'); } var params = this.params[name]; if (!params) { params = this.params[name] = []; } params.push(fn); return this; } /** * Dispatch a req, res into the router. * * @private */ handle(req, ...rest) { return this.internalHandle.apply(this, [{}, req].concat(rest)); } internalHandle(options, req, ...rest) { var callback = rest[rest.length - 1]; if (options && !options.ensureCleanStart) { options.ensureCleanStart = function () { if (req.url[0] !== separator) { req.url = separator + req.url; slashAdded = true; } }; } if (!callback) { throw new TypeError('argument callback is required'); } debug('dispatching %s %s', req['method'] || '', req.url); var idx = 0; var removed = ''; var self = this; var slashAdded = false; var paramcalled = {}; var separator = this.separator; // middleware and routes var stack = this.stack; // manage inter-router variables var parentParams = req.params; var parentUrl = req.baseUrl || ''; var done = Router.restore(callback, req, 'baseUrl', 'next', 'params'); // setup next layer req.next = next; if (options && options.preHandle) { done = options.preHandle(done); } // setup basic req values req.baseUrl = parentUrl; req.originalUrl = req.originalUrl || req.url; next(); function next(err) { var layerError = err === 'route' ? null : err; // remove added slash if (slashAdded && req.url) { req.url = req.url.substr(1); slashAdded = false; } // restore altered req.url if (removed.length !== 0) { self.unshift(req, removed, parentUrl); removed = ''; } // signal to exit router if (layerError === 'router') { defer(done, null); return; } // no more matching layers if (idx >= stack.length) { defer(done, layerError); return; } // get pathname of request var path = self.getPathname(req); if (path == null) { return done(layerError); } // find next matching layer var layer; var match; var route; while (match !== true && idx < stack.length) { layer = stack[idx++]; match = Router.matchLayer(layer, path); route = layer.route; if (typeof match !== 'boolean') { // hold on to layerError layerError = layerError || match; } if (match !== true) { continue; } if (!route) { // process non-route handlers normally continue; } if (layerError) { // routes do not match with a pending error match = false; continue; } var isApplicable = route.isApplicable(req); // build up automatic options response if (!isApplicable) { if (options && options.notApplicableRoute) { if (options.notApplicableRoute(route) === false) { match = false; continue; } } } } // no match if (match !== true) { return done(layerError); } // store route for dispatch on change if (route) { req.route = route; } // Capture one-time layer values req.params = self.mergeParams ? Router.mergeParams(layer.params, parentParams) : layer.params; var layerPath = layer.path; var args = [req]; args = args.concat(rest.slice(0, rest.length - 1)); ; // this should be done for the layer self.process_params.apply(self, [layer, paramcalled].concat(args).concat(function (err) { if (err) { return next(layerError || err); } if (route) { return layer.handle_request.apply(layer, args.concat(next)); } trim_prefix(layer, layerError, layerPath, path); })); } function trim_prefix(layer, layerError, layerPath, path) { if (layerPath.length !== 0) { // Validate path breaks on a path separator var c = path[layerPath.length]; if (c && c !== separator) { next(layerError); return; } // Trim off the part of the url that matches the route // middleware (.use stuff) needs to have the path stripped debug('trim prefix (%s) from url %s', layerPath, req.url); removed = layerPath; self.shift(req, removed); // Ensure leading slash options.ensureCleanStart(req); // Setup base URL (no trailing slash) req.baseUrl = parentUrl + (removed[removed.length - 1] === separator ? removed.substring(0, removed.length - 1) : removed); } debug('%s %s : %s', layer.name, layerPath, req.originalUrl); var args = [req].concat(rest.slice(0, rest.length - 1)); args.push(next); if (layerError) { layer.handle_error.apply(layer, [layerError].concat(args)); } else { layer.handle_request.apply(layer, args); } } } shift(req, removed) { req.url = req.url.substring(removed.length); } unshift(req, removed, parentUrl) { req.baseUrl = parentUrl; req.url = removed + req.url; } process_params(layer, called, req, ...rest) { var done = rest[rest.length - 1]; var params = this.params; // captured parameters from the layer, keys and values var keys = layer.keys; // fast track if (!keys || keys.length === 0) { return done(); } var i = 0; var name; var paramIndex = 0; var key; var paramVal; var paramCallbacks; var paramCalled; // process params in order // param callbacks can be async function param(err) { if (err) { return done(err); } if (i >= keys.length) { return done(); } paramIndex = 0; key = keys[i++]; name = key.name; paramVal = req.params[name]; paramCallbacks = params[name]; paramCalled = called[name]; if (paramVal === undefined || !paramCallbacks) { return param(); } // param previously called with same value or error occurred if (paramCalled && (paramCalled.match === paramVal || (paramCalled.error && paramCalled.error !== 'route'))) { // restore value req.params[name] = paramCalled.value; // next param return param(paramCalled.error); } called[name] = paramCalled = { error: null, match: paramVal, value: paramVal }; paramCallback(); } // single param callbacks function paramCallback(err) { var fn = paramCallbacks[paramIndex++]; // store updated value paramCalled.value = req.params[key.name]; if (err) { // store error paramCalled.error = err; param(err); return; } if (!fn) return param(); try { fn(req, paramCallback, paramVal, key.name, rest.slice(0, rest.length - 1)); } catch (e) { paramCallback(e); } } param(); } use(...handlers) { var offset = 0; var path = this.separator; // default path to *separator* // disambiguate router.use([handler]) if (typeof handlers[0] !== 'function') { // first arg is the path if (typeof handlers[0] == 'string') { offset = 1; path = handlers.shift(); } } var callbacks = handlers; if (callbacks.length === 0) { throw new TypeError('argument handler is required'); } for (var i = 0; i < callbacks.length; i++) { this.layer(path, callbacks[i]); } return this; } layer(path, fn) { if (typeof fn !== 'function') { throw new TypeError('argument handler must be a function'); } // add the middleware debug('use %o %s', path, fn.name || '<anonymous>'); var layer = this.buildLayer(path, { sensitive: this.caseSensitive, strict: false, end: false, length: this.length }, fn); layer.route = undefined; this.stack.push(layer); return layer; } /** * Create a new Route for the given path. * * Each route contains a separate middleware stack and VERB handlers. * * See the Route api documentation for details on adding handlers * and middleware to routes. * * @param {string} path * @return {Route} * @public */ route(path) { var route = this.buildRoute(path); var layer = this.buildLayer(path, { sensitive: this.caseSensitive, strict: this.strict, end: true, length: this.length }, route.dispatch.bind(route)); layer.route = route; this.stack.push(layer); return route; } /** * Get pathname of request. * * @param {IncomingMessage} req * @private */ getPathname(req) { try { return url_1.parse(req.url).pathname; } catch (err) { return undefined; } } /** * Match path to a layer. * * @param {Layer} layer * @param {string} path * @private */ static matchLayer(layer, path) { try { return layer.match(path); } catch (err) { return err; } } /** * Merge params with parent params * * @private */ static mergeParams(params, parent) { if (typeof parent !== 'object' || !parent) { return params; } // make copy of parent for base var obj = helpers_1.extend({}, parent); // simple non-numeric merging if (!(0 in params) || !(0 in parent)) { return helpers_1.extend(obj, params); } var i = 0; var o = 0; // determine numeric gap in params while (i in params) { i++; } // determine numeric gap in parent while (o in parent) { o++; } // offset numeric indices in params before merge for (i--; i >= 0; i--) { params[i + o] = params[i]; // create holes for the merge when necessary if (i < o) { delete params[i]; } } return helpers_1.extend(obj, params); } static restore(fn, obj, ...props) { var vals = new Array(arguments.length - 2); for (var i = 0; i < props.length; i++) { vals[i] = obj[props[i]]; } return function (...args) { // restore vals for (var i = 0; i < props.length; i++) { obj[props[i]] = vals[i]; } return fn.apply(this, arguments); }; } static wrap(old, fn) { return function proxy() { var args = new Array(arguments.length + 1); args[0] = old; for (var i = 0, len = arguments.length; i < len; i++) { args[i + 1] = arguments[i]; } fn.apply(this, args); }; } } exports.Router = Router; class Router1 extends Router { constructor(options) { super(options); } } exports.Router1 = Router1; class Router2 extends Router { constructor(options) { super(options); } } exports.Router2 = Router2; // // create Router#VERB functions // methods.concat('all').forEach(function (method) // { // Router.prototype[method] = function (path) // { // var route = this.route(path) // route[method].apply(route, slice.call(arguments, 1)) // return this // } // }) //# sourceMappingURL=index.js.map