sails
Version:
API-driven framework for building realtime apps, using MVC conventions (based on Express and Socket.io)
258 lines (207 loc) • 8.5 kB
JavaScript
/**
* Dependencies
*/
var _ = require('@sailshq/lodash');
var chalk = require('chalk');
var STRINGFILE = require('sails-stringfile');
var Err = require('../../../errors/fatal');
var onRoute = require('./onRoute');
/**
* Expose hook definition
*/
module.exports = function(sails) {
return {
defaults: {},
configure: function() {
// Legacy (< v0.10) support for configured handlers
if (typeof sails.config[500] === 'function') {
sails.after('lifted', function() {
STRINGFILE.logDeprecationNotice('sails.config[500]',
STRINGFILE.get('links.docs.migrationGuide.responses'),
sails.log.debug);
sails.log.debug('sails.config[500] (i.e. `config/500.js`) has been superceded in Sails v0.10.');
sails.log.debug('Please define a "response" instead. (i.e. api/responses/serverError.js)');
sails.log.debug('Your old handler is being ignored. (the format has been upgraded in v0.10)');
sails.log.debug('If you\'d like to use the default handler, just remove this configuration option.');
});
}
if (typeof sails.config[404] === 'function') {
sails.after('lifted', function() {
STRINGFILE.logDeprecationNotice('sails.config[404]',
STRINGFILE.get('links.docs.migrationGuide.responses'),
sails.log.debug);
sails.log.debug('Please define a "response" instead. (i.e. api/responses/notFound.js)');
sails.log.debug('Your old handler is being ignored. (the format has been upgraded in v0.10)');
sails.log.debug('If you\'d like to use the default handler, just remove this configuration option.');
});
}
},
/**
* When this hook is loaded...
*/
initialize: function(cb) {
// Register route syntax that allows explicit routes
// to be bound directly to custom responses by name.
// (e.g. {response: 'foo'})
sails.on('route:typeUnknown', onRoute(sails));
cb();
},
/**
* Fetch relevant modules, exposing them on `sails` subglobal if necessary,
*/
loadModules: function(cb) {
var hook = this;
sails.log.silly('Loading runtime custom response definitions...');
sails.modules.loadResponses(function loadedRuntimeErrorModules(err, responseDefs) {
if (err) { return cb(err); }
// Check that none of the custom responses provided from userland collie with
// reserved response methods/properties.
//
// Note: this could be made more flexible in the future-- I've found it to be
// helpful to sometimes override res.view() in apps. That said, in those circumstances,
// I've been able to accomplish this by manually overriding res.view in a custom hook.
// That said, if that won't work for your use case, please let me know (tweet @mikermcneil).
var reservedResKeys = [
'view',
'status', 'set', 'get', 'cookie', 'clearCookie', 'redirect',
'location', 'charset', 'send', 'json', 'jsonp', 'type', 'format',
'attachment', 'sendfile', 'download', 'links', 'locals', 'render'
];
_.each(responseDefs, function (responseDef, customResponseKey) {
if ( _.contains(reservedResKeys, customResponseKey) ) {
Err.invalidCustomResponse(customResponseKey);
}
});
// Mix in the built-in default definitions for custom responses.
_.defaults(responseDefs, {
ok: require('./defaults/ok'),
negotiate: require('./defaults/negotiate'),
notFound: require('./defaults/notFound'),
serverError: require('./defaults/serverError'),
forbidden: require('./defaults/forbidden'),
badRequest: require('./defaults/badRequest')
});
// Expose combined custom/default response method definitions on the hook.
// (e.g. `serverError`, `notFound`, `ok`, etc.)
// TODO: use this instead of exposing as "middleware", since that's confusing naming.
// Register blueprint actions as middleware of this hook.
hook.middleware = responseDefs;
return cb();
});
},
/**
* Shadow route bindings
* @type {Object}
*/
routes: {
before: {
/**
* Add custom response methods to `res`.
*
* @param {Request} req
* @param {Response} res
* @param {Function} next
* @api private
*/
'all /*': function addResponseMethods(req, res, next) {
// Attach res.jsonx to `res` object
_mixinJsonxMethod(req, res);
// Attach custom responses to `res` object
// Provide access to `req` and `res` in each of their `this` contexts.
_.each(sails.middleware.responses, function eachMethod(responseFn, name) {
res[name] = responseFn.bind({
req: req,
res: res
});
});
// Proceed!
return next();
}
}
}
};
};
/**
* [_mixinJsonxMethod description]
* @param {[type]} req [description]
* @param {[type]} res [description]
* @return {[type]} [description]
*/
function _mixinJsonxMethod(req, res) {
function _stringifyJsonxError(err) {
var plainObject = {};
Object.getOwnPropertyNames(err).forEach(function (key) {
plainObject[key] = err[key];
});
return JSON.stringify(plainObject);
}
function _handleJsonxError(err){
var serializedErr;
var jsonSerializedErr;
try {
serializedErr = _stringifyJsonxError(err);
jsonSerializedErr = JSON.parse(serializedErr);
if (!jsonSerializedErr.stack || !jsonSerializedErr.message) {
jsonSerializedErr.message = err.message;
jsonSerializedErr.stack = err.stack;
}
return jsonSerializedErr;
}
catch (unusedErr){
return {name: err.name, message: err.message, stack: err.stack};
}
}
/**
* res.jsonx(data)
*
* Serve JSON (and allow JSONP if enabled in `req.options`)
*
* @param {Object} data
*
* - - - - - - - - - - - - - - - - - -
* WARNING: THIS IS DEPRECATED!
* - - - - - - - - - - - - - - - - - -
*/
res.jsonx = res.jsonx || function jsonx (data){
var caller = jsonx.caller && jsonx.caller.name;
// Get easy access to Sails app instance.
var sails = req._sails;
// Log a deprecation notice.
sails.log.debug('***********************************************************************************');
sails.log.debug('`res.jsonx()` is deprecated in Sails v1.0 and will be removed in a future release.');
sails.log.debug(chalk.bold('Any files in `api/responses/` that haven\'t been customized can simply be removed.'));
sails.log.debug(chalk.gray('Otherwise see http://sailsjs.com/upgrading for options to replace `res.jsonx()`.'));
if (caller) {
sails.log.debug(chalk.gray('(jsonx was called from `' + caller + '`)'));
}
sails.log.debug('***********************************************************************************');
// Send conventional status message if no data was provided
// (see http://expressjs.com/en/api.html#res.send)
if (_.isUndefined(data)) {
return res.status(res.statusCode).send();
}
else if (typeof data !== 'object') {
// (note that this guard includes arrays, functions, and even `null`)
return res.send(data);
}
// When responding with an Error instance, if it's going to get sringified into
// a dictionary with no `.stack` or `.message` properties, add them in.
if (data instanceof Error) {
data = _handleJsonxError(data);
}
if ( req.options.jsonp && !req.isSocket ) {
return res.jsonp(data);
}
else {
return res.json(data);
}
};//ƒ
}//ƒ
// Note for later
// We could differentiate between 500 (generic error message)
// and 504 (gateway did not receive response from upstream server) which could describe an IO problem
// This is worth having a think about, since there are 2 fundamentally different kinds of "server errors":
// (a) An infrastructural issue, or 504 (e.g. MySQL database randomly crashed or Twitter is down)
// (b) Unexpected bug in app code, or 500 (e.g. `req.session.user.id`, but `req.session.user` doesn't exist)
//
// See the Sails project roadmap (sailsjs.com/roadmap) for future plans to better account for this.