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
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;
|
|
|