四好公路
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

210 lines
6.4 KiB

'use strict';
var _ = require('lodash'),
Endpoint = require('../Endpoint'),
Promise = require('bluebird'),
errors = require('../Errors');
var Controller = function(args) {
this.initialize(args);
};
Controller.prototype.initialize = function(options) {
options = options || {};
this.endpoint = new Endpoint(options.endpoint);
this.model = options.model;
this.app = options.app;
this.resource = options.resource;
this.include = options.include;
if (options.include.length) {
var includeAttributes = [], includeModels = [];
options.include.forEach(function(include) {
includeModels.push(!!include.model ? include.model : include);
});
_.forEach(this.model.associations, function(association) {
if (_.includes(includeModels, association.target))
includeAttributes.push(association.identifier);
});
this.includeAttributes = includeAttributes;
}
this.route();
};
Controller.milestones = [
'start',
'auth',
'fetch',
'data',
'write',
'send',
'complete'
];
Controller.hooks = Controller.milestones.reduce(function(hooks, milestone) {
['_before', '', '_after'].forEach(function(modifier) {
hooks.push(milestone + modifier);
});
return hooks;
}, []);
Controller.prototype.error = function(req, res, err) {
res.status(err.status);
res.json({
message: err.message,
errors: err.errors
});
};
Controller.prototype.send = function(req, res, context) {
res.json(context.instance);
return context.continue;
};
Controller.prototype.route = function() {
var app = this.app,
endpoint = this.endpoint,
self = this;
// NOTE: is there a better place to do this mapping?
if (app.name === 'restify' && self.method === 'delete')
self.method = 'del';
app[self.method](endpoint.string, function(req, res) {
self._control(req, res);
});
};
Controller.prototype._control = function(req, res) {
var hookChain = Promise.resolve(false),
self = this,
context = {
instance: undefined,
criteria: {},
attributes: {},
options: {}
};
if (['PATCH', 'POST', 'PUT'].indexOf(req.method) !== -1) {
if (this.resource.readOnlyAttributes && this.resource.readOnlyAttributes.length) {
// better to delete attributes from req.body, user can later set them in hooks
this.resource.readOnlyAttributes.filter(function (attr) {
delete req.body[attr];
});
}
}
Controller.milestones.forEach(function(milestone) {
if (!self[milestone])
return;
[milestone + '_before', milestone, milestone + '_after'].forEach(function(hook) {
if (!self[hook])
return;
hookChain = hookChain.then(function runHook(skip) {
if (skip) return true;
var functions = Array.isArray(self[hook]) ? self[hook] : [self[hook]];
// return the function chain. This means if the function chain resolved
// to skip then all the remaining hooks on this milestone will also be
// skipped and we will go to the next milestone
return functions.reduce(function(prev, current) {
return prev.then(function runHookFunction(skipNext) {
// if any asked to skip keep returning true to avoid calling further
// functions inside this hook
if (skipNext) return true;
var decisionPromise = new Promise(function(resolve) {
_.assign(context, {
skip: function() {
resolve(context.skip);
},
stop: function() {
resolve(new errors.RequestCompleted());
},
continue: function() {
resolve(context.continue);
},
error: function(status, message, errorList, cause) {
// if the second parameter is undefined then we are being
// passed an error to rethrow, otherwise build an EpilogueError
if (_.isUndefined(message) || status instanceof errors.EpilogueError) {
resolve(status);
} else {
resolve(new errors.EpilogueError(status, message, errorList, cause));
}
}
});
});
return Promise.resolve(current.call(self, req, res, context))
.then(function(result) {
// if they were returned directly or as a result of a promise
if (_.includes([context.skip, context.continue, context.stop], result)) {
// call it to resolve the decision
result();
}
return decisionPromise.then(function(decision) {
if (decision === context.continue) return false;
if (decision === context.skip) return true;
// must be an error/context.stop, throw the decision for error handling
if (process.domain) {
// restify wraps the server in domain and sets error handlers that get in the way of mocha
// https://github.com/dchester/epilogue/issues/83
return Promise.reject(decision);
}
throw decision;
});
});
});
}, Promise.resolve(false));
});
});
hookChain = hookChain.then(function() {
// clear any passed results so the next milestone will run even if a
// _after said to skip
return false;
});
});
hookChain
.catch(errors.RequestCompleted, _.noop)
.catch(self.model.sequelize.ValidationError, function(err) {
var errorList = _.reduce(err.errors, function(result, error) {
result.push({ field: error.path, message: error.message });
return result;
}, []);
self.error(req, res, new errors.BadRequestError(err.message, errorList, err));
})
.catch(errors.EpilogueError, function(err) {
self.error(req, res, err);
})
.catch(function(err) {
self.error(req, res, new errors.EpilogueError(500, 'internal error', [err.message], err));
});
};
Controller.prototype.milestone = function(name, callback) {
if (!_.includes(Controller.hooks, name))
throw new Error('invalid milestone: ' + name);
if (!this[name]) {
this[name] = [];
} else if (!Array.isArray(this[name])) {
this[name] = [ this[name] ];
}
this[name].push(callback);
};
module.exports = Controller;