UNPKG

@balderdash/sails-edge

Version:

API-driven framework for building realtime apps, using MVC conventions (based on Express and Socket.io)

459 lines (345 loc) 11.8 kB
/** * Module dependencies. */ var Readable = require('stream').Readable; var QS = require('querystring'); var _ = require('lodash'); var Express = require('express'); var buildReq = require('./req'); var buildRes = require('./res'); var defaultHandlers = require('./bindDefaultHandlers'); /** * Expose new instance of `Router` * * @api private */ module.exports = function(sails) { return new Router({sails: sails}); }; /** * Initialize a new `Router` * * @param {Object} options * @api private */ function Router(options) { options = options || {}; this.sails = options.sails; this.defaultHandlers = defaultHandlers(this.sails); // Expose router on `sails` object this.sails.router = this; // Required for dynamic NODE_ENV setting via command line args // TODO: // instead, use: https://www.npmjs.org/package/path-to-regexp // (or: https://www.npmjs.org/package/path-match) this._privateRouter = Express(); // Bind the context of all instance methods this.load = _.bind(this.load, this); this.bind = _.bind(this.bind, this); this.unbind = _.bind(this.unbind, this); this.reset = _.bind(this.reset, this); this.flush = _.bind(this.flush, this); this.route = _.bind(this.route, this); } /** * _privateRouter * * This internal "private" instance of an Express app object * is used only for routing. (i.e. it will not be used for * listening to actual HTTP requests; instead, one or more * delegate servers can be attached- see the `http` or * `sockets` hooks for examples of attaching a server to * Sails) * * NOTE: Requires calling `load()` before use in order to * provide access to the proper NODE_ENV, since Express * uses that to determine its environment (development vs. * production.) */ // Router.prototype._privateRouter; /** * `sails.router.load()` * * Expose the router, create the Express private router, * then call flush(), which will bind configured routes * and emit the appropriate events. * * @api public */ Router.prototype.load = function(cb) { var sails = this.sails; sails.log.verbose('Loading router...'); // Maintain a reference to the static route config this.explicitRoutes = sails.config.routes; // Save reference to sails logger this.log = sails.log; // If a session store is configured, hook it up as `req.session` by passing // it down to the session middleware if (sails.hooks.session) { sails.after('hook:session:loaded', function (){ // if (!sails.config.session || !sails.config.session.store || !sails.config.session.secret) { // return cb(new Error('Consistency violation: expected session store+secret config to exist if the session hook is enabled. Is `sails.config.session` valid?')); // } sails._privateCpMware = Express.cookieParser(sails.config.session.secret); sails._privateSessionMiddleware = Express.session(sails.config.session); }); } // Wipe any existing routes and bind them anew this.flush(); // Listen for requests sails.on('router:request', this.route); // Listen for unhandled errors and unmatched routes sails.on('router:request:500', this.defaultHandlers[500]); sails.on('router:request:404', this.defaultHandlers[404]); cb(); }; /** * `sails.router.route(partialReq, partialRes)` * * Interpret the specified (usually partial) request and response objects into * streams with all of the expected methods, then routes the fully-formed request * using the built-in private router. Useful for creating virtual request/response * streams from non-HTTP sources, like Socket.io or unit tests. * * This method is not always helpful-- it is not called for HTTP requests, for instance, * since the true HTTP req/res streams already exist. In that case, at lift-time, Sails * calls `router:bind`, which loads Sails' routes as normal middleware/routes in the http hook. * stack will run as usual. * * On the other hand, Socket.io needs to use this method (i.e. the `router:request` event) * to simulate a connect-style router since it can't bind dynamic routes ahead of time. * * Keep in mind that, if `route` is not used, the implementing server is responsible * for routing to Sails' default `next(foo)` handler. * * @param {Request} req * @param {Response} res * @api private */ Router.prototype.route = function(req, res) { var sails = this.sails; var _privateRouter = this._privateRouter; // If sails is `_exiting`, ignore the request. if (sails._exiting) { return; } // Provide access to `sails` object req._sails = req._sails || sails; // Use base req and res definitions to ensure the specified // objects are at least ducktype-able as standard node HTTP // req and req streams. // // Make sure request and response objects have reasonable defaults // (will use the supplied definitions if possible) req = buildReq(req, res); res = buildRes(req, res); // console.log('\n\n\n\n=======================\nReceived request to %s %s\nwith req.body:\n',req.method,req.url, req.body); // Deprecation error: res._cb = function _cbIsDeprecated(err) { throw new Error('As of v0.10, `_cb()` shim is no longer supported in the Sails router.'); }; // Run some basic middleware sails.log.silly('Handling virtual request :: Running virtual querystring parser...'); qsParser(req,res, function (err) { if (err) { return res.send(400, err && err.stack); } // Parse cookies parseCookies(req, res, function(err){ if (err) { return res.send(400, err && err.stack); } // console.log('Ran cookie parser'); // console.log('res.writeHead= ',res.writeHead); // Load session (if relevant) loadSession(req, res, function (err) { if (err) { return res.send(400, err && err.stack); } // console.log('res is now:\n',res); // console.log('\n\n'); // console.log('Ran session middleware'); // console.log('req.sessionID= ',req.sessionID); // console.log('The loaded req.session= ',req.session); sails.log.silly('Handling virtual request :: Running virtual body parser...'); bodyParser(req,res, function (err) { if (err) { return res.send(400, err && err.stack); } // Use our private router to route the request _privateRouter.router(req, res, function handleUnmatchedNext(err) { // // In the event of an unmatched `next()`, `next('foo')`, // or `next('foo', errorCode)`... // // Use the default server error handler if (err) { sails.log.silly('Handling virtual request :: Running final "error" handler...'); sails.emit('router:request:500', err, req, res); return; } // Or the default not found handler sails.log.silly('Handling virtual request :: Running final "not found" handler...'); sails.emit('router:request:404', req, res); return; }); }); }); }); }); }; /** * `sails.router.bind()` * * Bind new route(s) * * @param {String|RegExp} path * @param {String|Object|Array|Function} bindTo * @param {String} verb * @api private */ Router.prototype.bind = require('./bind'); /** * `sails.router.unbind()` * * Unbind existing route * * @param {Object} route * @api private */ Router.prototype.unbind = function(route) { var sails = this.sails; // Inform attached servers that route should be unbound sails.emit('router:unbind', route); // Remove route in internal router var newRoutes = []; _.each(this._privateRouter.routes[route.method], function(expressRoute) { if (expressRoute.path != route.path) { newRoutes.push(expressRoute); } }); this._privateRouter.routes[route.method] = newRoutes; }; /** * `sails.router.reset()` * * Unbind all routes currently attached to the router * * @api private */ Router.prototype.reset = function() { var sails = this.sails; // Unbind everything _.each(this._privateRouter.routes, function(routes, httpMethod) { // Unbind each route for the specified HTTP verb var routesToUnbind = this._privateRouter.routes[httpMethod] || []; _.each(routesToUnbind, this.unbind, this); }, this); // Emit reset event to allow attached servers to // unbind all of their routes as well sails.emit('router:reset'); }; /** * `sails.router.flush()` * * Unbind all current routes, then re-bind everything, re-emitting the routing * lifecycle events (e.g. `router:before` and `router:after`) * * @param {Object} routes - (optional) * If specified, replaces `this.explicitRoutes` before flushing. * * @api private */ Router.prototype.flush = function(routes) { var sails = this.sails; // Wipe routes this.reset(); // Fired before static routes are bound sails.emit('router:before'); // If specified, replace `this.explicitRoutes` if (routes) { this.explicitRoutes = routes; } // Use specified path to bind static routes _.each(this.explicitRoutes, function(target, path) { this.bind(path, target); }, this); // Fired after static routes are bound sails.emit('router:after'); }; //////////////////////////////////////////////////////////////////////////////////////////////////// // // || Private functions // \/ // //////////////////////////////////////////////////////////////////////////////////////////////////// // Extremely simple query string parser (`req.query`) function qsParser(req,res,next) { var queryStringPos = req.url.indexOf('?'); if (queryStringPos !== -1) { req.query = _.merge(req.query, QS.parse(req.url.substr(queryStringPos + 1))); } else { req.query = req.query || {}; } next(); } // Extremely simple body parser (`req.body`) function bodyParser (req, res, next) { var bodyBuffer=''; if (req.method === 'GET' || req.method === 'HEAD' || req.method === 'DELETE'){ req.body = _.extend({}, req.body); return next(); } // Ensure that `req` is a readable stream at this point if ( ! req instanceof Readable ) { return next(new Error('Sails Internal Error: `req` should be a Readable stream by the time `route()` is called')); } req.on('readable', function() { var chunk; while (null !== (chunk = req.read())) { bodyBuffer += chunk; } }); req.on('end', function() { var parsedBody; try { parsedBody = JSON.parse(bodyBuffer); } catch (e) {} req.body = _.merge(req.body, parsedBody); next(); }); } /** * [parseCookies description] * @param {[type]} req [description] * @param {[type]} res [description] * @param {Function} next [description] * @return {[type]} [description] */ function parseCookies (req, res, next){ // req._sails.log.verbose('Parsing cookie:',req.headers.cookie); if (req._sails._privateCpMware) { // Run the middleware return req._sails._privateCpMware(req, res, next); } // Otherwise don't even worry about it. return next(); } /** * [loadSession description] * @param {[type]} req [description] * @param {[type]} res [description] * @param {Function} next [description] * @return {[type]} [description] */ function loadSession (req, res, next){ // If a session store is configured, hook it up as `req.session` by passing // it down to the session middleware if (req._sails._privateSessionMiddleware) { // Access store preconfigured session middleware as a private property on the app instance. return req._sails._privateSessionMiddleware(req, res, next); } // Otherwise don't even worry about it. return next(); }