loopback-phase
Version:
Hook into a LoopBack application's phases
304 lines (255 loc) • 7.26 kB
JavaScript
// Copyright IBM Corp. 2014,2016. All Rights Reserved.
// Node module: loopback-phase
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
var g = require('strong-globalize')();
var Phase = require('./phase');
var zipMerge = require('./merge-name-lists');
var async = require('async');
module.exports = PhaseList;
/**
* An ordered list of phases.
*
* ```js
* var PhaseList = require('loopback-phase').PhaseList;
* var phases = new PhaseList();
* phases.add('my-phase');
* ```
*
* @class PhaseList
*/
function PhaseList() {
this._phases = [];
this._phaseMap = {};
}
/**
* Get the first `Phase` in the list.
*
* @returns {Phase} The first phase.
*/
PhaseList.prototype.first = function() {
return this._phases[0];
};
/**
* Get the last `Phase` in the list.
*
* @returns {Phase} The last phase.
*/
PhaseList.prototype.last = function() {
return this._phases[this._phases.length - 1];
};
/**
* Add one or more phases to the list.
*
* @param {Phase|String|String[]} phase The phase (or phases) to be added.
* @returns {Phase|Phase[]} The added phase or phases.
*/
PhaseList.prototype.add = function(phase) {
var phaseList = this;
var phaseArray = Array.isArray(phase) ? phase : null;
if(phaseArray) {
return phaseArray.map(phaseList.add.bind(phaseList));
}
phase = this._resolveNameAndAddToMap(phase);
this._phases.push(phase);
return phase;
};
PhaseList.prototype._resolveNameAndAddToMap = function(phaseOrName) {
var phase = phaseOrName;
if(typeof phase === 'string') {
phase = new Phase(phase);
}
if (phase.id in this._phaseMap) {
throw new Error(g.f('Phase "%s" already exists.', phase.id));
}
if(!phase.__isPhase__) {
throw new Error(g.f('Cannot add a non phase object to a {{PhaseList}}'));
}
this._phaseMap[phase.id] = phase;
return phase;
};
/**
* Add a new phase at the specified index.
* @param {Number} index The zero-based index.
* @param {String|String[]} phase The name of the phase to add.
* @returns {Phase} The added phase.
*/
PhaseList.prototype.addAt = function(index, phase) {
phase = this._resolveNameAndAddToMap(phase);
this._phases.splice(index, 0, phase);
return phase;
};
/**
* Add a new phase as the next one after the given phase.
* @param {String} after The referential phase.
* @param {String|String[]} phase The name of the phase to add.
* @returns {Phase} The added phase.
*/
PhaseList.prototype.addAfter = function(after, phase) {
var ix = this.getPhaseNames().indexOf(after);
if (ix === -1) {
throw new Error(g.f('Unknown phase: %s', after));
}
return this.addAt(ix+1, phase);
};
/**
* Add a new phase as the previous one before the given phase.
* @param {String} before The referential phase.
* @param {String|String[]} phase The name of the phase to add.
* @returns {Phase} The added phase.
*/
PhaseList.prototype.addBefore = function(before, phase) {
var ix = this.getPhaseNames().indexOf(before);
if (ix === -1) {
throw new Error(g.f('Unknown phase: %s', before));
}
return this.addAt(ix, phase);
};
/**
* Remove a `Phase` from the list.
*
* @param {Phase|String} phase The phase to be removed.
* @returns {Phase} The removed phase.
*/
PhaseList.prototype.remove = function(phase) {
var phases = this._phases;
var phaseMap = this._phaseMap;
var phaseId;
if(!phase) return null;
if(typeof phase === 'object') {
phaseId = phase.id;
} else {
phaseId = phase;
phase = phaseMap[phaseId];
}
if(!phase || !phase.__isPhase__) return null;
phases.splice(phases.indexOf(phase), 1);
delete this._phaseMap[phaseId];
return phase;
};
/**
* Merge the provided list of names with the existing phases
* in such way that the order of phases is preserved.
*
* **Example**
*
* ```js
* // Initial list of phases
* phaseList.add(['initial', 'session', 'auth', 'routes', 'files', 'final']);
*
* // zip-merge more phases
* phaseList.zipMerge([
* 'initial', 'postinit', 'preauth', 'auth',
* 'routes', 'subapps', 'final', 'last'
* ]);
*
* // print the result
* console.log('Result:', phaseList.getPhaseNames());
* // Result: [
* // 'initial', 'postinit', 'preauth', 'session', 'auth',
* // 'routes', 'subapps', 'files', 'final', 'last'
* // ]
* ```
*
* @param {String[]} names List of phase names to zip-merge
*/
PhaseList.prototype.zipMerge = function(names) {
if (!names.length) return;
var mergedNames = zipMerge(this.getPhaseNames(), names);
this._phases = mergedNames.map(function(name) {
var existing = this.find(name);
return existing ?
existing :
this._resolveNameAndAddToMap(name);
}, this);
};
/**
* Find a `Phase` from the list.
*
* @param {String} id The phase identifier
* @returns {Phase} The `Phase` with the given `id`.
*/
PhaseList.prototype.find = function(id) {
return this._phaseMap[id] || null;
};
/**
* Find or add a `Phase` from/into the list.
*
* @param {String} id The phase identifier
* @returns {Phase} The `Phase` with the given `id`.
*/
PhaseList.prototype.findOrAdd = function(id) {
var phase = this.find(id);
if(phase) return phase;
return this.add(id);
};
/**
* Get the list of phases as an array of `Phase` objects.
*
* @returns {Phase[]} An array of phases.
*/
PhaseList.prototype.toArray = function() {
return this._phases.slice(0);
};
/**
* Launch the phases contained in the list. If there are no phases
* in the list `process.nextTick` is called with the provided callback.
*
* @param {Object} [context] The context of each `Phase` handler.
* @callback {Function} cb
* @param {Error} err Any error that occured during a phase contained
* in the list.
*/
PhaseList.prototype.run = function(ctx, cb) {
var phases = this._phases;
if(typeof ctx === 'function') {
cb = ctx;
ctx = undefined;
}
if(phases.length) {
async.eachSeries(phases, function(phase, next) {
phase.run(ctx, next);
}, cb);
} else {
process.nextTick(cb);
}
};
/**
* Get an array of phase identifiers.
* @returns {String[]} phaseNames
*/
PhaseList.prototype.getPhaseNames = function() {
return this._phases.map(function(phase) {
return phase.id;
});
};
/**
* Register a phase handler for the given phase (and sub-phase).
*
* **Example**
*
* ```js
* // register via phase.use()
* phaseList.registerHandler('routes', function(ctx, next) { next(); });
* // register via phase.before()
* phaseList.registerHandler('auth:before', function(ctx, next) { next(); });
* // register via phase.after()
* phaseList.registerHandler('auth:after', function(ctx, next) { next(); });
* ```
*
* @param {String} phaseName Name of an existing phase, optionally with
* ":before" or ":after" suffix.
* @param {Function(Object, Function)} handler The handler function to register
* with the given phase.
*/
PhaseList.prototype.registerHandler = function(phaseName, handler) {
var subphase = 'use';
var m = phaseName.match(/^(.+):(before|after)$/);
if (m) {
phaseName = m[1];
subphase = m[2];
}
var phase = this.find(phaseName);
if (!phase) throw new Error(g.f('Unknown phase %s', phaseName));
phase[subphase](handler);
};