UNPKG

compound-ex4

Version:

Compound-ex4 - MVC framework for NodeJS (ExpressJs 4 version), fork compoundjs(https://github.com/1602/compound)

352 lines (319 loc) 10.4 kB
module.exports = FlowControl; function FlowControl() { } FlowControl.prototype.perform = function perform(actionName, req, res, next) { if (this.occupied) { throw new Error('Controller is not ready to handle another request'); } var requestedActionName = actionName; var actions = this.constructor.actions; if (!(actionName in actions) && '__missingAction' in actions) { actionName = '__missingAction'; } this.occupied = true; this.initialize(req, res, next); this.call(actionName, requestedActionName); }; /** * Initialize controller instance before request handling * * this is a point where controller gives some information from outside env * (from router for example) * * @param {IncomingMessage} req - incoming http request * @param {ServerResponse} res - http server response * @param {Function} next - called when request could not handled in current action */ FlowControl.prototype.initialize = function (req, res, next) { this.context.apply(req, res, next); this.locals = req && req.locals || {}; }; /** * Internal request handler. Serves request using railway env: * * - run before hooks * - run action * - run after hooks * * @param {String} actionName */ FlowControl.prototype.call = function call(actionName, requestedActionName) { var f, ctl = this, prevHook, index = 0; if (f = this.compiledAction[actionName]) { return f(); } // handling request queue var queue = []; // index for tracking uniqueness of hooks by name var hookDone = []; if (ctl.logger) { queue.push(function () { ctl.logger.beforeProcessing(); run(); }); } var action = this.constructor.actions[actionName]; if (!action) { enqueue([[function (c) { return c.next(new Error('Undefined action ' + c.controllerName + '#' + c.actionName + '(' + c.req.url + ')')); }, null, actionName]]); } else { enqueue(this.constructor.before); enqueue([[action, null, actionName]]); enqueue(this.constructor.after); } // if (ctl.logger) { // queue.push(function () { // ctl.logger.afterProcessing(); // run(); // }); // } this.context.innerNext = run; var len = queue.length; if (actionName !== '__missingAction') { this.compiledAction[actionName] = function () { ctl.context.applyAction(actionName, requestedActionName, run); index = 0; run(); }; this.call(actionName); } else { ctl.context.applyAction(actionName, requestedActionName, run); index = 0; run(); } function run() { var res; if (ctl.logger && prevHook) { // log afterHook ctl.logger.afterHook(prevHook); prevHook = ''; } if (index < len) { try { res = queue[index++](); if (res && 'function' === typeof res.then) { res.then(function(data) { if (ctl.req.param('format') === 'json' || ctl.req.headers['expect-format'] === 'json') { ctl.send(data); } else { if (data && 'object' === typeof data) { Object.keys(data).forEach(function(key) { ctl.locals[key] = data[key]; }); } ctl.render(); } }, function(err) { ctl.next(err); }); } } catch (e) { if (ctl.context.inAction) { e.message += ' in ' + ctl.controllerName + ' controller during "' + actionName + '" action'; } else { e.message += ' in ' + ctl.controllerName + ' controller during "' + prevHook + '" hook'; } ctl.occupied = false; ctl.context.outerNext(e); } } else { ctl.occupied = false; if (typeof ctl.context.outerNext === 'function') { ctl.context.outerNext(); } } } function enqueue(collection) { collection.forEach(function (f) { var fn = f[0]; if (!fn) { throw new Error('Trying to queue undefined function'); } var params = f[1]; var hookName = f[2]; if (!params) { ok(); } else if (params.only && params.only.indexOf(actionName) !== -1 && (!params.except || params.except.indexOf(actionName) === -1)) { ok(); } else if (params.except && params.except.indexOf(actionName) === -1) { ok(); } function ok() { if (hookName) { if (hookDone.indexOf(hookName) !== -1) return; hookDone.push(hookName); } if (ctl.logger) { // log beforeHook and perform action queue.push(function () { ctl.logger.beforeHook(hookName); prevHook = hookName; ctl.context.inAction = fn.isAction; return fn.call(ctl.locals, ctl); }); } else { // just do action queue.push(function () { ctl.context.inAction = fn.isAction; prevHook = hookName; return fn.call(ctl.locals, ctl); }); } } }); } }; /** * Define controller action * * @param name String - optional (if missed, named function required as first param) * @param action Function - required, should be named function if first arg missed * * @example * ``` * action(function index() { * Post.all(function (err, posts) { * render({posts: posts}); * }); * }); * ``` * */ FlowControl.prototype.action = function action(name, fn) { if (typeof name === 'function') { fn = name; name = fn.name; if (!name) { throw new Error('Named function required when `name` param omitted'); } } fn.isAction = true; fn.customName = name; this.constructor.actions[name] = fn; }; /** * Schedule before hook to the end of queue. This method can be called * with named function as single param, or with two params: name and * anonimous function * * Examples: * ``` * before('some named hook', function () {}); * before(function namedMethod() {}); * ``` * * This hooks can be skipped using this names in future. Examples: * ``` * skipBefore('some named hook'); * skipBefore('namedMethod'); * ``` * * Please note, that every named hook only can be scheduled once: * ``` * before(function myMethod() { * // some code * }); * before(function myMethod() { * // another code * }); * ``` * This will only schedule first method! * * @param {Function} f * @param {Object} params */ FlowControl.prototype.before = function before(f, params) { this.constructor.before.push(filter(arguments)); }; /** * Schedule hook to the end of 'after'-queue * * @param {Function} f * @param {Object} params */ FlowControl.prototype.after = function after(f, params) { this.constructor.after.push(filter(arguments)); }; /** * Schedule before hook to the start of queue. This method can be called * with named function as single param, or with two params: name and * anonimous function. * * @alias prependBeforeFilter * @param {Function} f * @param {Object} params */ FlowControl.prototype.prependBefore = function prependBefore(f, params) { this.constructor.before.unshift(filter(arguments)); }; /** * @param {String} name - name of hook to skip * @param {Array} or {String} only - choose actions to skip hook */ FlowControl.prototype.skipBefore = function (name, only) { this.constructor.before.forEach(function (hook, i) { if (hook[0] && hook[0].customName && name === hook[0].customName) { skipFilter(this.constructor.before, i, only ? only.only : null); } }.bind(this)); }; /** * @param {String} name - name of hook to skip * @param {Array} or {String} only - choose actions to skip hook */ FlowControl.prototype.skipAfter = function (name, only) { this.constructor.after.forEach(function (hook, i) { if (hook[0] && hook[0].customName && name === hook[0].customName) { skipFilter(this.constructor.after, i, only ? only.only : null); } }.bind(this)); }; /** * @param {Array} hooks collection * @param {Number} index of hook to skip * @param {Array} or {String} only - choose actions to skip hook * @private */ function skipFilter(hooks, index, only) { if (!only) { delete hooks[index]; } else if (hooks[index][0]) { if (!hooks[index][1]) { hooks[index][1] = {except: []}; } if (!hooks[index][1].except) { hooks[index][1].except = []; } else if (typeof hooks[index][1].except === 'string') { hooks[index][1].except = [hooks[index][1].except]; } if (typeof only === 'string') { hooks[index][1].except.push(only); } else if (only && only.constructor.name === 'Array') { only.forEach(function (name) { hooks[index][1].except.push(name); }); } } } function filter(args) { if (typeof args[0] === 'string' && typeof args[1] === 'function') { // change order args[1].customName = args[0]; return [args[1], args[2], args[0]]; } else { // normal order args[0].customName = args[0].name; return [args[0], args[1], args[0].name]; } } /** * @param {String} name - name of action * @returns whether controller responds to action */ FlowControl.prototype.respondsTo = function respondTo(name) { return typeof this.constructor.actions[name] == 'function'; };