2014-06-02 16:47:46 +00:00
|
|
|
var assert = require('assert');
|
2014-12-03 06:10:50 +00:00
|
|
|
var _ = require('lodash');
|
2014-06-16 13:23:32 +00:00
|
|
|
var semver = require('semver');
|
2014-06-02 16:47:46 +00:00
|
|
|
var debug = require('debug')('loopback:boot:executor');
|
2014-10-09 19:18:36 +00:00
|
|
|
var async = require('async');
|
2014-11-10 16:22:57 +00:00
|
|
|
var path = require('path');
|
2015-01-12 14:48:28 +00:00
|
|
|
var format = require('util').format;
|
2014-06-02 16:47:46 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Execute bootstrap instructions gathered by `boot.compile`.
|
|
|
|
*
|
2014-10-30 20:34:59 +00:00
|
|
|
* @param {Object} app The loopback app to boot.
|
2014-06-02 16:47:46 +00:00
|
|
|
* @options {Object} instructions Boot instructions.
|
2014-10-30 20:34:59 +00:00
|
|
|
* @param {Function} [callback] Callback function.
|
2014-06-02 16:47:46 +00:00
|
|
|
*
|
|
|
|
* @header boot.execute(instructions)
|
|
|
|
*/
|
|
|
|
|
2014-10-09 19:18:36 +00:00
|
|
|
module.exports = function execute(app, instructions, callback) {
|
2014-12-03 08:31:40 +00:00
|
|
|
callback = callback || function() {};
|
|
|
|
|
|
|
|
app.booting = true;
|
|
|
|
|
2014-06-25 12:10:16 +00:00
|
|
|
patchAppLoopback(app);
|
2014-06-16 13:23:32 +00:00
|
|
|
assertLoopBackVersion(app);
|
|
|
|
|
2014-06-02 16:47:46 +00:00
|
|
|
setHost(app, instructions);
|
|
|
|
setPort(app, instructions);
|
|
|
|
setApiRoot(app, instructions);
|
|
|
|
applyAppConfig(app, instructions);
|
|
|
|
|
|
|
|
setupDataSources(app, instructions);
|
|
|
|
setupModels(app, instructions);
|
2014-11-12 15:56:01 +00:00
|
|
|
setupMiddleware(app, instructions);
|
2015-01-06 13:00:26 +00:00
|
|
|
setupComponents(app, instructions);
|
2014-06-02 16:47:46 +00:00
|
|
|
|
2014-10-09 19:18:36 +00:00
|
|
|
// Run the boot scripts in series synchronously or asynchronously
|
|
|
|
// Please note async supports both styles
|
|
|
|
async.series([
|
|
|
|
function(done) {
|
|
|
|
runBootScripts(app, instructions, done);
|
|
|
|
},
|
|
|
|
function(done) {
|
|
|
|
enableAnonymousSwagger(app, instructions);
|
|
|
|
done();
|
2014-12-03 08:31:40 +00:00
|
|
|
}], function(err) {
|
|
|
|
app.booting = false;
|
|
|
|
|
|
|
|
if (err) return callback(err);
|
|
|
|
|
|
|
|
app.emit('booted');
|
|
|
|
|
|
|
|
callback();
|
|
|
|
});
|
2014-06-02 16:47:46 +00:00
|
|
|
};
|
|
|
|
|
2014-06-25 12:10:16 +00:00
|
|
|
function patchAppLoopback(app) {
|
|
|
|
if (app.loopback) return;
|
|
|
|
// app.loopback was introduced in 1.9.0
|
|
|
|
// patch the app object to make loopback-boot work with older versions too
|
|
|
|
try {
|
|
|
|
app.loopback = require('loopback');
|
2014-11-13 14:54:59 +00:00
|
|
|
} catch (err) {
|
2014-06-25 12:10:16 +00:00
|
|
|
if (err.code === 'MODULE_NOT_FOUND') {
|
|
|
|
console.error(
|
2014-11-13 14:54:59 +00:00
|
|
|
'When using loopback-boot with loopback <1.9, ' +
|
2014-06-25 12:10:16 +00:00
|
|
|
'the loopback module must be available for `require(\'loopback\')`.');
|
|
|
|
}
|
|
|
|
throw err;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-06-16 13:23:32 +00:00
|
|
|
function assertLoopBackVersion(app) {
|
|
|
|
var RANGE = '1.x || 2.x';
|
|
|
|
|
2014-06-25 12:10:16 +00:00
|
|
|
var loopback = app.loopback;
|
2015-01-12 14:48:28 +00:00
|
|
|
// remove any pre-release tag from the version string,
|
|
|
|
// because semver has special treatment of pre-release versions,
|
|
|
|
// while loopback-boot treats pre-releases the same way as regular versions
|
|
|
|
var version = (loopback.version || '1.0.0').replace(/-.*$/, '');
|
|
|
|
if (!semver.satisfies(version, RANGE)) {
|
|
|
|
var msg = format(
|
|
|
|
'The `app` is powered by an incompatible loopback version %s. ' +
|
|
|
|
'Supported versions: %s',
|
|
|
|
loopback.version || '(unknown)',
|
2014-06-16 13:23:32 +00:00
|
|
|
RANGE);
|
2015-01-12 14:48:28 +00:00
|
|
|
throw new Error(msg);
|
2014-06-16 13:23:32 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-06-02 16:47:46 +00:00
|
|
|
function setHost(app, instructions) {
|
2014-11-13 14:54:59 +00:00
|
|
|
// jscs:disable requireCamelCaseOrUpperCaseIdentifiers
|
2014-06-02 16:47:46 +00:00
|
|
|
var host =
|
|
|
|
process.env.npm_config_host ||
|
|
|
|
process.env.OPENSHIFT_SLS_IP ||
|
|
|
|
process.env.OPENSHIFT_NODEJS_IP ||
|
|
|
|
process.env.HOST ||
|
2014-06-25 06:09:24 +00:00
|
|
|
instructions.config.host ||
|
2014-06-02 16:47:46 +00:00
|
|
|
process.env.npm_package_config_host ||
|
|
|
|
app.get('host');
|
|
|
|
|
2014-11-13 14:54:59 +00:00
|
|
|
if (host !== undefined) {
|
2014-06-02 16:47:46 +00:00
|
|
|
assert(typeof host === 'string', 'app.host must be a string');
|
|
|
|
app.set('host', host);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function setPort(app, instructions) {
|
2014-11-13 14:54:59 +00:00
|
|
|
// jscs:disable requireCamelCaseOrUpperCaseIdentifiers
|
2014-06-02 16:47:46 +00:00
|
|
|
var port = _.find([
|
|
|
|
process.env.npm_config_port,
|
|
|
|
process.env.OPENSHIFT_SLS_PORT,
|
|
|
|
process.env.OPENSHIFT_NODEJS_PORT,
|
|
|
|
process.env.PORT,
|
2014-06-25 06:09:24 +00:00
|
|
|
instructions.config.port,
|
2014-06-02 16:47:46 +00:00
|
|
|
process.env.npm_package_config_port,
|
|
|
|
app.get('port'),
|
|
|
|
3000
|
|
|
|
], _.isFinite);
|
|
|
|
|
2014-11-13 14:54:59 +00:00
|
|
|
if (port !== undefined) {
|
2014-06-02 16:47:46 +00:00
|
|
|
var portType = typeof port;
|
|
|
|
assert(portType === 'string' || portType === 'number',
|
|
|
|
'app.port must be a string or number');
|
|
|
|
app.set('port', port);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function setApiRoot(app, instructions) {
|
|
|
|
var restApiRoot =
|
2014-06-25 06:09:24 +00:00
|
|
|
instructions.config.restApiRoot ||
|
2014-06-02 16:47:46 +00:00
|
|
|
app.get('restApiRoot') ||
|
|
|
|
'/api';
|
|
|
|
|
|
|
|
assert(restApiRoot !== undefined, 'app.restBasePath is required');
|
|
|
|
assert(typeof restApiRoot === 'string',
|
|
|
|
'app.restApiRoot must be a string');
|
|
|
|
assert(/^\//.test(restApiRoot),
|
|
|
|
'app.restApiRoot must start with "/"');
|
|
|
|
app.set('restApiRoot', restApiRoot);
|
|
|
|
}
|
|
|
|
|
|
|
|
function applyAppConfig(app, instructions) {
|
2014-06-25 06:09:24 +00:00
|
|
|
var appConfig = instructions.config;
|
2014-11-13 14:54:59 +00:00
|
|
|
for (var configKey in appConfig) {
|
2014-06-02 16:47:46 +00:00
|
|
|
var cur = app.get(configKey);
|
2014-11-13 14:54:59 +00:00
|
|
|
if (cur === undefined || cur === null) {
|
2014-06-02 16:47:46 +00:00
|
|
|
app.set(configKey, appConfig[configKey]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function setupDataSources(app, instructions) {
|
|
|
|
forEachKeyedObject(instructions.dataSources, function(key, obj) {
|
|
|
|
app.dataSource(key, obj);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function setupModels(app, instructions) {
|
2014-06-26 12:53:47 +00:00
|
|
|
defineModels(app, instructions);
|
2014-06-16 08:56:55 +00:00
|
|
|
|
|
|
|
instructions.models.forEach(function(data) {
|
|
|
|
// Skip base models that are not exported to the app
|
|
|
|
if (!data.config) return;
|
|
|
|
|
|
|
|
app.model(data._model, data.config);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2014-06-26 12:53:47 +00:00
|
|
|
function defineModels(app, instructions) {
|
2015-03-19 07:54:49 +00:00
|
|
|
var registry = app.registry || app.loopback;
|
2014-06-13 11:14:43 +00:00
|
|
|
instructions.models.forEach(function(data) {
|
|
|
|
var name = data.name;
|
|
|
|
var model;
|
|
|
|
|
|
|
|
if (!data.definition) {
|
2015-03-19 07:54:49 +00:00
|
|
|
model = registry.getModel(name);
|
2014-06-13 11:14:43 +00:00
|
|
|
if (!model) {
|
|
|
|
throw new Error('Cannot configure unknown model ' + name);
|
|
|
|
}
|
|
|
|
debug('Configuring existing model %s', name);
|
2014-10-21 08:06:28 +00:00
|
|
|
} else if (isBuiltinLoopBackModel(app, data)) {
|
2015-03-19 07:54:49 +00:00
|
|
|
model = registry.getModel(name);
|
|
|
|
assert(model, 'Built-in model ' + name + ' should have been defined');
|
2014-10-21 08:06:28 +00:00
|
|
|
debug('Configuring built-in LoopBack model %s', name);
|
2014-06-13 11:14:43 +00:00
|
|
|
} else {
|
|
|
|
debug('Creating new model %s %j', name, data.definition);
|
2015-03-19 07:54:49 +00:00
|
|
|
model = registry.createModel(data.definition);
|
2014-06-13 11:14:43 +00:00
|
|
|
if (data.sourceFile) {
|
|
|
|
debug('Loading customization script %s', data.sourceFile);
|
|
|
|
var code = require(data.sourceFile);
|
|
|
|
if (typeof code === 'function') {
|
|
|
|
debug('Customizing model %s', name);
|
2014-07-22 08:58:18 +00:00
|
|
|
code(model);
|
2014-06-13 11:14:43 +00:00
|
|
|
} else {
|
|
|
|
debug('Skipping model file %s - `module.exports` is not a function',
|
|
|
|
data.sourceFile);
|
|
|
|
}
|
|
|
|
}
|
2014-06-09 12:43:44 +00:00
|
|
|
}
|
2014-06-13 11:14:43 +00:00
|
|
|
|
2014-06-16 08:56:55 +00:00
|
|
|
data._model = model;
|
2014-06-02 16:47:46 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2014-11-10 16:22:57 +00:00
|
|
|
// Regular expression to match built-in loopback models
|
|
|
|
var LOOPBACK_MODEL_REGEXP = new RegExp(
|
|
|
|
['', 'node_modules', 'loopback', '[^\\/\\\\]+', 'models', '[^\\/\\\\]+\\.js$']
|
|
|
|
.join('\\' + path.sep));
|
|
|
|
|
2014-10-21 08:06:28 +00:00
|
|
|
function isBuiltinLoopBackModel(app, data) {
|
|
|
|
// 1. Built-in models are exposed on the loopback object
|
|
|
|
if (!app.loopback[data.name]) return false;
|
|
|
|
|
|
|
|
// 2. Built-in models have a script file `loopback/{facet}/models/{name}.js`
|
2014-11-10 16:22:57 +00:00
|
|
|
var srcFile = data.sourceFile;
|
|
|
|
return srcFile &&
|
|
|
|
LOOPBACK_MODEL_REGEXP.test(srcFile);
|
2014-10-21 08:06:28 +00:00
|
|
|
}
|
|
|
|
|
2014-06-02 16:47:46 +00:00
|
|
|
function forEachKeyedObject(obj, fn) {
|
2014-11-13 14:54:59 +00:00
|
|
|
if (typeof obj !== 'object') return;
|
2014-06-02 16:47:46 +00:00
|
|
|
|
|
|
|
Object.keys(obj).forEach(function(key) {
|
|
|
|
fn(key, obj[key]);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2014-10-09 19:18:36 +00:00
|
|
|
function runScripts(app, list, callback) {
|
|
|
|
list = list || [];
|
|
|
|
var functions = [];
|
2014-06-02 16:47:46 +00:00
|
|
|
list.forEach(function(filepath) {
|
2014-10-09 19:18:36 +00:00
|
|
|
debug('Requiring script %s', filepath);
|
2015-01-08 21:07:23 +00:00
|
|
|
var exports = require(filepath);
|
2014-10-09 19:18:36 +00:00
|
|
|
if (typeof exports === 'function') {
|
|
|
|
debug('Exported function detected %s', filepath);
|
|
|
|
functions.push({
|
|
|
|
path: filepath,
|
|
|
|
func: exports
|
|
|
|
});
|
|
|
|
}
|
2014-06-02 16:47:46 +00:00
|
|
|
});
|
2014-10-09 19:18:36 +00:00
|
|
|
|
|
|
|
async.eachSeries(functions, function(f, done) {
|
|
|
|
debug('Running script %s', f.path);
|
|
|
|
if (f.func.length >= 2) {
|
|
|
|
debug('Starting async function %s', f.path);
|
|
|
|
f.func(app, function(err) {
|
|
|
|
debug('Async function finished %s', f.path);
|
|
|
|
done(err);
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
debug('Starting sync function %s', f.path);
|
|
|
|
f.func(app);
|
|
|
|
debug('Sync function finished %s', f.path);
|
|
|
|
done();
|
|
|
|
}
|
|
|
|
}, callback);
|
2014-06-02 16:47:46 +00:00
|
|
|
}
|
|
|
|
|
2014-11-12 15:56:01 +00:00
|
|
|
function setupMiddleware(app, instructions) {
|
|
|
|
if (!instructions.middleware) {
|
|
|
|
// the browserified client does not support middleware
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2014-11-21 21:56:28 +00:00
|
|
|
// Phases can be empty
|
|
|
|
var phases = instructions.middleware.phases || [];
|
2014-11-12 15:56:01 +00:00
|
|
|
assert(Array.isArray(phases),
|
|
|
|
'instructions.middleware.phases must be an array');
|
|
|
|
|
|
|
|
var middleware = instructions.middleware.middleware;
|
|
|
|
assert(Array.isArray(middleware),
|
|
|
|
'instructions.middleware.middleware must be an object');
|
|
|
|
|
|
|
|
debug('Defining middleware phases %j', phases);
|
|
|
|
app.defineMiddlewarePhases(phases);
|
|
|
|
|
|
|
|
middleware.forEach(function(data) {
|
2014-11-21 21:56:28 +00:00
|
|
|
debug('Configuring middleware %j%s', data.sourceFile,
|
|
|
|
data.fragment ? ('#' + data.fragment) : '');
|
2014-11-12 15:56:01 +00:00
|
|
|
var factory = require(data.sourceFile);
|
2014-11-21 21:56:28 +00:00
|
|
|
if (data.fragment) {
|
2015-01-01 09:57:09 +00:00
|
|
|
factory = factory[data.fragment].bind(factory);
|
2014-11-21 21:56:28 +00:00
|
|
|
}
|
|
|
|
assert(typeof factory === 'function',
|
|
|
|
'Middleware factory must be a function');
|
2014-11-12 15:56:01 +00:00
|
|
|
app.middlewareFromConfig(factory, data.config);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2015-01-06 13:00:26 +00:00
|
|
|
function setupComponents(app, instructions) {
|
|
|
|
instructions.components.forEach(function(data) {
|
|
|
|
debug('Configuring component %j', data.sourceFile);
|
|
|
|
var configFn = require(data.sourceFile);
|
|
|
|
configFn(app, data.config);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2014-10-09 19:18:36 +00:00
|
|
|
function runBootScripts(app, instructions, callback) {
|
|
|
|
runScripts(app, instructions.files.boot, callback);
|
2014-06-02 16:47:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function enableAnonymousSwagger(app, instructions) {
|
|
|
|
// disable token requirement for swagger, if available
|
|
|
|
var swagger = app.remotes().exports.swagger;
|
|
|
|
if (!swagger) return;
|
|
|
|
|
2014-06-25 06:09:24 +00:00
|
|
|
var appConfig = instructions.config;
|
2014-06-02 16:47:46 +00:00
|
|
|
var requireTokenForSwagger = appConfig.swagger &&
|
|
|
|
appConfig.swagger.requireToken;
|
|
|
|
swagger.requireToken = requireTokenForSwagger || false;
|
|
|
|
}
|