Merge pull request #5 from strongloop/feature/two-step-boot-for-browserify
Feature/two step boot for browserify
This commit is contained in:
commit
f3207f6451
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"content": [
|
||||
{
|
||||
"title": "Bootstrap API",
|
||||
"depth": 2
|
||||
},
|
||||
"index.js",
|
||||
"lib/compiler.js",
|
||||
"lib/executor.js",
|
||||
"docs/configuration.md",
|
||||
"docs/instructions.md"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
## Configuration and conventions
|
||||
|
||||
### Model Definitions
|
||||
|
||||
The following is example JSON for two `Model` definitions:
|
||||
"dealership" and "location".
|
||||
|
||||
```js
|
||||
{
|
||||
"dealership": {
|
||||
// a reference, by name, to a dataSource definition
|
||||
"dataSource": "my-db",
|
||||
// the options passed to Model.extend(name, properties, options)
|
||||
"options": {
|
||||
"relations": {
|
||||
"cars": {
|
||||
"type": "hasMany",
|
||||
"model": "Car",
|
||||
"foreignKey": "dealerId"
|
||||
}
|
||||
}
|
||||
},
|
||||
// the properties passed to Model.extend(name, properties, options)
|
||||
"properties": {
|
||||
"id": {"id": true},
|
||||
"name": "String",
|
||||
"zip": "Number",
|
||||
"address": "String"
|
||||
}
|
||||
},
|
||||
"car": {
|
||||
"dataSource": "my-db"
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "String",
|
||||
"required": true,
|
||||
"id": true
|
||||
},
|
||||
"make": {
|
||||
"type": "String",
|
||||
"required": true
|
||||
},
|
||||
"model": {
|
||||
"type": "String",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
|
@ -0,0 +1,33 @@
|
|||
## Two-step boot
|
||||
|
||||
The methods `compile` and `execute` can be used to split the bootstrap
|
||||
process into two steps, the first one run by a build script before calling
|
||||
`browserify`, the second one run in the browser by the browserified app.
|
||||
|
||||
The first method - `compile` - loads all configuration files, applies any
|
||||
values specified in environmental variable and produces one JSON object
|
||||
containing all instructions needed by `execute` to bootstrap the application.
|
||||
|
||||
```js
|
||||
{
|
||||
app: {
|
||||
/* application config from app.json & friends */
|
||||
},
|
||||
models: {
|
||||
/* model configuration from models.json */
|
||||
},
|
||||
dataSources: {
|
||||
/* datasources configuration from datasources.json & friends*/
|
||||
},
|
||||
files: {
|
||||
models: [
|
||||
'/project/models/customer.js',
|
||||
/* ... */
|
||||
],
|
||||
boot: [
|
||||
'/project/boot/authentication.js',
|
||||
/* ... */
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
265
index.js
265
index.js
|
@ -1,10 +1,6 @@
|
|||
var assert = require('assert');
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var _ = require('underscore');
|
||||
var loopback = require('loopback');
|
||||
var ConfigLoader = require('./lib/config-loader');
|
||||
var debug = require('debug')('loopback:boot');
|
||||
var compile = require('./lib/compiler');
|
||||
var execute = require('./lib/executor');
|
||||
|
||||
/**
|
||||
* Initialize an application from an options object or
|
||||
|
@ -42,56 +38,6 @@ var debug = require('debug')('loopback:boot');
|
|||
*
|
||||
* Throws an error if the config object is not valid or if boot fails.
|
||||
*
|
||||
* <a name="model-definition"></a>
|
||||
* **Model Definitions**
|
||||
*
|
||||
* The following is example JSON for two `Model` definitions:
|
||||
* "dealership" and "location".
|
||||
*
|
||||
* ```js
|
||||
* {
|
||||
* "dealership": {
|
||||
* // a reference, by name, to a dataSource definition
|
||||
* "dataSource": "my-db",
|
||||
* // the options passed to Model.extend(name, properties, options)
|
||||
* "options": {
|
||||
* "relations": {
|
||||
* "cars": {
|
||||
* "type": "hasMany",
|
||||
* "model": "Car",
|
||||
* "foreignKey": "dealerId"
|
||||
* }
|
||||
* }
|
||||
* },
|
||||
* // the properties passed to Model.extend(name, properties, options)
|
||||
* "properties": {
|
||||
* "id": {"id": true},
|
||||
* "name": "String",
|
||||
* "zip": "Number",
|
||||
* "address": "String"
|
||||
* }
|
||||
* },
|
||||
* "car": {
|
||||
* "dataSource": "my-db"
|
||||
* "properties": {
|
||||
* "id": {
|
||||
* "type": "String",
|
||||
* "required": true,
|
||||
* "id": true
|
||||
* },
|
||||
* "make": {
|
||||
* "type": "String",
|
||||
* "required": true
|
||||
* },
|
||||
* "model": {
|
||||
* "type": "String",
|
||||
* "required": true
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param app LoopBack application created by `loopback()`.
|
||||
* @options {String|Object} options Boot options; If String, this is
|
||||
* the application root directory; if object, has below properties.
|
||||
|
@ -105,209 +51,22 @@ var debug = require('debug')('loopback:boot');
|
|||
* and `models/*.js`. Defaults to `appRootDir`.
|
||||
* @property {String} datasourcesRootDir Directory to use when loading
|
||||
* `datasources.json`. Defaults to `appRootDir`.
|
||||
* @property {String} env Environment type, defaults to `process.env.NODE_ENV`
|
||||
* or `development`. Common values are `development`, `staging` and
|
||||
* `production`; however the applications are free to use any names.
|
||||
* @end
|
||||
*
|
||||
* @header boot(app, [options])
|
||||
* @header bootLoopBackApp(app, [options])
|
||||
*/
|
||||
|
||||
exports = module.exports = function bootLoopBackApp(app, options) {
|
||||
/*jshint camelcase:false */
|
||||
options = options || {};
|
||||
// backwards compatibility with loopback's app.boot
|
||||
options.env = options.env || app.get('env');
|
||||
|
||||
if(typeof options === 'string') {
|
||||
options = { appRootDir: options };
|
||||
}
|
||||
var appRootDir = options.appRootDir = options.appRootDir || process.cwd();
|
||||
var env = app.get('env');
|
||||
|
||||
var appConfig = options.app || ConfigLoader.loadAppConfig(appRootDir, env);
|
||||
|
||||
var modelsRootDir = options.modelsRootDir || appRootDir;
|
||||
var modelConfig = options.models ||
|
||||
ConfigLoader.loadModels(modelsRootDir, env);
|
||||
|
||||
var dsRootDir = options.dsRootDir || appRootDir;
|
||||
var dataSourceConfig = options.dataSources ||
|
||||
ConfigLoader.loadDataSources(dsRootDir, env);
|
||||
|
||||
assertIsValidConfig('app', appConfig);
|
||||
assertIsValidConfig('model', modelConfig);
|
||||
assertIsValidConfig('data source', dataSourceConfig);
|
||||
|
||||
appConfig.host =
|
||||
process.env.npm_config_host ||
|
||||
process.env.OPENSHIFT_SLS_IP ||
|
||||
process.env.OPENSHIFT_NODEJS_IP ||
|
||||
process.env.HOST ||
|
||||
appConfig.host ||
|
||||
process.env.npm_package_config_host ||
|
||||
app.get('host');
|
||||
|
||||
appConfig.port = _.find([
|
||||
process.env.npm_config_port,
|
||||
process.env.OPENSHIFT_SLS_PORT,
|
||||
process.env.OPENSHIFT_NODEJS_PORT,
|
||||
process.env.PORT,
|
||||
appConfig.port,
|
||||
process.env.npm_package_config_port,
|
||||
app.get('port'),
|
||||
3000
|
||||
], _.isFinite);
|
||||
|
||||
appConfig.restApiRoot =
|
||||
appConfig.restApiRoot ||
|
||||
app.get('restApiRoot') ||
|
||||
'/api';
|
||||
|
||||
if(appConfig.host !== undefined) {
|
||||
assert(typeof appConfig.host === 'string', 'app.host must be a string');
|
||||
app.set('host', appConfig.host);
|
||||
}
|
||||
|
||||
if(appConfig.port !== undefined) {
|
||||
var portType = typeof appConfig.port;
|
||||
assert(portType === 'string' || portType === 'number',
|
||||
'app.port must be a string or number');
|
||||
app.set('port', appConfig.port);
|
||||
}
|
||||
|
||||
assert(appConfig.restApiRoot !== undefined, 'app.restBasePath is required');
|
||||
assert(typeof appConfig.restApiRoot === 'string',
|
||||
'app.restBasePath must be a string');
|
||||
assert(/^\//.test(appConfig.restApiRoot),
|
||||
'app.restBasePath must start with "/"');
|
||||
app.set('restApiRoot', appConfig.restBasePath);
|
||||
|
||||
for(var configKey in appConfig) {
|
||||
var cur = app.get(configKey);
|
||||
if(cur === undefined || cur === null) {
|
||||
app.set(configKey, appConfig[configKey]);
|
||||
}
|
||||
}
|
||||
|
||||
// instantiate data sources
|
||||
forEachKeyedObject(dataSourceConfig, function(key, obj) {
|
||||
app.dataSource(key, obj);
|
||||
});
|
||||
|
||||
// instantiate models
|
||||
forEachKeyedObject(modelConfig, function(key, obj) {
|
||||
app.model(key, obj);
|
||||
});
|
||||
|
||||
// try to attach models to dataSources by type
|
||||
try {
|
||||
loopback.autoAttach();
|
||||
} catch(e) {
|
||||
if(e.name === 'AssertionError') {
|
||||
console.warn(e);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// disable token requirement for swagger, if available
|
||||
var swagger = app.remotes().exports.swagger;
|
||||
var requireTokenForSwagger = appConfig.swagger &&
|
||||
appConfig.swagger.requireToken;
|
||||
if(swagger) {
|
||||
swagger.requireToken = requireTokenForSwagger || false;
|
||||
}
|
||||
|
||||
// require directories
|
||||
requireDir(path.join(modelsRootDir, 'models'), app);
|
||||
requireDir(path.join(appRootDir, 'boot'), app);
|
||||
var instructions = compile(options);
|
||||
execute(app, instructions);
|
||||
};
|
||||
|
||||
function assertIsValidConfig(name, config) {
|
||||
if(config) {
|
||||
assert(typeof config === 'object',
|
||||
name + ' config must be a valid JSON object');
|
||||
}
|
||||
}
|
||||
|
||||
function forEachKeyedObject(obj, fn) {
|
||||
if(typeof obj !== 'object') return;
|
||||
|
||||
Object.keys(obj).forEach(function(key) {
|
||||
fn(key, obj[key]);
|
||||
});
|
||||
}
|
||||
|
||||
function requireDir(dir, app) {
|
||||
assert(dir, 'cannot require directory contents without directory name');
|
||||
|
||||
var requires = {};
|
||||
|
||||
// require all javascript files (except for those prefixed with _)
|
||||
// and all directories
|
||||
|
||||
var files = tryReadDir(dir);
|
||||
|
||||
// sort files in lowercase alpha for linux
|
||||
files.sort(function(a, b) {
|
||||
a = a.toLowerCase();
|
||||
b = b.toLowerCase();
|
||||
|
||||
if (a < b) {
|
||||
return -1;
|
||||
} else if (b < a) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
files.forEach(function(filename) {
|
||||
// ignore index.js and files prefixed with underscore
|
||||
if ((filename === 'index.js') || (filename[0] === '_')) {
|
||||
return;
|
||||
}
|
||||
|
||||
var filepath = path.resolve(path.join(dir, filename));
|
||||
var ext = path.extname(filename);
|
||||
var stats = fs.statSync(filepath);
|
||||
|
||||
// only require files supported by require.extensions (.txt .md etc.)
|
||||
if (stats.isFile() && !(ext in require.extensions)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var exports = tryRequire(filepath);
|
||||
if (isFunctionNotModelCtor(exports))
|
||||
exports(app);
|
||||
|
||||
var basename = path.basename(filename, ext);
|
||||
requires[basename] = exports;
|
||||
});
|
||||
|
||||
return requires;
|
||||
}
|
||||
|
||||
function tryRequire(modulePath) {
|
||||
try {
|
||||
return require.apply(this, arguments);
|
||||
} catch(e) {
|
||||
if(e.code === 'MODULE_NOT_FOUND') {
|
||||
debug('Warning: cannot require %s - module not found.', modulePath);
|
||||
return undefined;
|
||||
}
|
||||
console.error('failed to require "%s"', modulePath);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
function tryReadDir() {
|
||||
try {
|
||||
return fs.readdirSync.apply(fs, arguments);
|
||||
} catch(e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function isFunctionNotModelCtor(fn) {
|
||||
return typeof fn === 'function' &&
|
||||
!(fn.prototype instanceof loopback.Model);
|
||||
}
|
||||
|
||||
exports.ConfigLoader = ConfigLoader;
|
||||
exports.compile = compile;
|
||||
exports.execute = execute;
|
||||
|
|
|
@ -0,0 +1,126 @@
|
|||
var assert = require('assert');
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var ConfigLoader = require('./config-loader');
|
||||
var debug = require('debug')('loopback:boot:compiler');
|
||||
|
||||
/**
|
||||
* Gather all bootstrap-related configuration data and compile it into
|
||||
* a single object containing instruction for `boot.execute`.
|
||||
*
|
||||
* @options {String|Object} options Boot options; If String, this is
|
||||
* the application root directory; if object, has the properties
|
||||
* described in `bootLoopBackApp` options above.
|
||||
* @return {Object}
|
||||
*
|
||||
* @header boot.compile(options)
|
||||
*/
|
||||
|
||||
module.exports = function compile(options) {
|
||||
options = options || {};
|
||||
|
||||
if(typeof options === 'string') {
|
||||
options = { appRootDir: options };
|
||||
}
|
||||
|
||||
var appRootDir = options.appRootDir = options.appRootDir || process.cwd();
|
||||
var env = options.env || process.env.NODE_ENV || 'development';
|
||||
|
||||
var appConfig = options.app || ConfigLoader.loadAppConfig(appRootDir, env);
|
||||
assertIsValidConfig('app', appConfig);
|
||||
|
||||
var modelsRootDir = options.modelsRootDir || appRootDir;
|
||||
var modelsConfig = options.models ||
|
||||
ConfigLoader.loadModels(modelsRootDir, env);
|
||||
assertIsValidConfig('model', modelsConfig);
|
||||
|
||||
var dsRootDir = options.dsRootDir || appRootDir;
|
||||
var dataSourcesConfig = options.dataSources ||
|
||||
ConfigLoader.loadDataSources(dsRootDir, env);
|
||||
assertIsValidConfig('data source', dataSourcesConfig);
|
||||
|
||||
// require directories
|
||||
var modelsScripts = findScripts(path.join(modelsRootDir, 'models'));
|
||||
var bootScripts = findScripts(path.join(appRootDir, 'boot'));
|
||||
|
||||
return {
|
||||
app: appConfig,
|
||||
dataSources: dataSourcesConfig,
|
||||
models: modelsConfig,
|
||||
files: {
|
||||
models: modelsScripts,
|
||||
boot: bootScripts
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
function assertIsValidConfig(name, config) {
|
||||
if(config) {
|
||||
assert(typeof config === 'object',
|
||||
name + ' config must be a valid JSON object');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all javascript files (except for those prefixed with _)
|
||||
* and all directories.
|
||||
* @param {String} dir Full path of the directory to enumerate.
|
||||
* @return {Array.<String>} A list of absolute paths to pass to `require()`.
|
||||
* @private
|
||||
*/
|
||||
|
||||
function findScripts(dir) {
|
||||
assert(dir, 'cannot require directory contents without directory name');
|
||||
|
||||
var files = tryReadDir(dir);
|
||||
|
||||
// sort files in lowercase alpha for linux
|
||||
files.sort(function(a, b) {
|
||||
a = a.toLowerCase();
|
||||
b = b.toLowerCase();
|
||||
|
||||
if (a < b) {
|
||||
return -1;
|
||||
} else if (b < a) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
var results = [];
|
||||
files.forEach(function(filename) {
|
||||
// ignore index.js and files prefixed with underscore
|
||||
if ((filename === 'index.js') || (filename[0] === '_')) {
|
||||
return;
|
||||
}
|
||||
|
||||
var filepath = path.resolve(path.join(dir, filename));
|
||||
var ext = path.extname(filename);
|
||||
var stats = fs.statSync(filepath);
|
||||
|
||||
// only require files supported by require.extensions (.txt .md etc.)
|
||||
if (stats.isFile()) {
|
||||
if (ext in require.extensions)
|
||||
results.push(filepath);
|
||||
else
|
||||
debug('Skipping file %s - unknown extension', filepath);
|
||||
} else {
|
||||
try {
|
||||
path.join(require.resolve(filepath));
|
||||
} catch(err) {
|
||||
debug('Skipping directory %s - %s', filepath, err.code || err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
function tryReadDir() {
|
||||
try {
|
||||
return fs.readdirSync.apply(fs, arguments);
|
||||
} catch(e) {
|
||||
return [];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,167 @@
|
|||
var assert = require('assert');
|
||||
var _ = require('underscore');
|
||||
var loopback = require('loopback');
|
||||
var debug = require('debug')('loopback:boot:executor');
|
||||
|
||||
/**
|
||||
* Execute bootstrap instructions gathered by `boot.compile`.
|
||||
*
|
||||
* @options {Object} app The loopback app to boot.
|
||||
* @options {Object} instructions Boot instructions.
|
||||
*
|
||||
* @header boot.execute(instructions)
|
||||
*/
|
||||
|
||||
module.exports = function execute(app, instructions) {
|
||||
setHost(app, instructions);
|
||||
setPort(app, instructions);
|
||||
setApiRoot(app, instructions);
|
||||
applyAppConfig(app, instructions);
|
||||
|
||||
setupDataSources(app, instructions);
|
||||
setupModels(app, instructions);
|
||||
autoAttach();
|
||||
|
||||
runBootScripts(app, instructions);
|
||||
|
||||
enableAnonymousSwagger(app, instructions);
|
||||
};
|
||||
|
||||
function setHost(app, instructions) {
|
||||
//jshint camelcase:false
|
||||
var host =
|
||||
process.env.npm_config_host ||
|
||||
process.env.OPENSHIFT_SLS_IP ||
|
||||
process.env.OPENSHIFT_NODEJS_IP ||
|
||||
process.env.HOST ||
|
||||
instructions.app.host ||
|
||||
process.env.npm_package_config_host ||
|
||||
app.get('host');
|
||||
|
||||
if(host !== undefined) {
|
||||
assert(typeof host === 'string', 'app.host must be a string');
|
||||
app.set('host', host);
|
||||
}
|
||||
}
|
||||
|
||||
function setPort(app, instructions) {
|
||||
//jshint camelcase:false
|
||||
var port = _.find([
|
||||
process.env.npm_config_port,
|
||||
process.env.OPENSHIFT_SLS_PORT,
|
||||
process.env.OPENSHIFT_NODEJS_PORT,
|
||||
process.env.PORT,
|
||||
instructions.app.port,
|
||||
process.env.npm_package_config_port,
|
||||
app.get('port'),
|
||||
3000
|
||||
], _.isFinite);
|
||||
|
||||
if(port !== undefined) {
|
||||
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 =
|
||||
instructions.app.restApiRoot ||
|
||||
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) {
|
||||
var appConfig = instructions.app;
|
||||
for(var configKey in appConfig) {
|
||||
var cur = app.get(configKey);
|
||||
if(cur === undefined || cur === null) {
|
||||
app.set(configKey, appConfig[configKey]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setupDataSources(app, instructions) {
|
||||
forEachKeyedObject(instructions.dataSources, function(key, obj) {
|
||||
app.dataSource(key, obj);
|
||||
});
|
||||
}
|
||||
|
||||
function setupModels(app, instructions) {
|
||||
forEachKeyedObject(instructions.models, function(key, obj) {
|
||||
app.model(key, obj);
|
||||
});
|
||||
|
||||
runScripts(app, instructions.files.models);
|
||||
}
|
||||
|
||||
function forEachKeyedObject(obj, fn) {
|
||||
if(typeof obj !== 'object') return;
|
||||
|
||||
Object.keys(obj).forEach(function(key) {
|
||||
fn(key, obj[key]);
|
||||
});
|
||||
}
|
||||
|
||||
function runScripts(app, list) {
|
||||
if (!list || !list.length) return;
|
||||
list.forEach(function(filepath) {
|
||||
var exports = tryRequire(filepath);
|
||||
if (isFunctionNotModelCtor(exports))
|
||||
exports(app);
|
||||
});
|
||||
}
|
||||
|
||||
function isFunctionNotModelCtor(fn) {
|
||||
return typeof fn === 'function' &&
|
||||
!(fn.prototype instanceof loopback.Model);
|
||||
}
|
||||
|
||||
function tryRequire(modulePath) {
|
||||
try {
|
||||
return require.apply(this, arguments);
|
||||
} catch(e) {
|
||||
if(e.code === 'MODULE_NOT_FOUND') {
|
||||
debug('Warning: cannot require %s - module not found.', modulePath);
|
||||
return undefined;
|
||||
}
|
||||
console.error('failed to require "%s"', modulePath);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// Deprecated, will be removed soon
|
||||
function autoAttach() {
|
||||
try {
|
||||
loopback.autoAttach();
|
||||
} catch(e) {
|
||||
if(e.name === 'AssertionError') {
|
||||
console.warn(e);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function runBootScripts(app, instructions) {
|
||||
runScripts(app, instructions.files.boot);
|
||||
}
|
||||
|
||||
function enableAnonymousSwagger(app, instructions) {
|
||||
// disable token requirement for swagger, if available
|
||||
var swagger = app.remotes().exports.swagger;
|
||||
if (!swagger) return;
|
||||
|
||||
var appConfig = instructions.app;
|
||||
var requireTokenForSwagger = appConfig.swagger &&
|
||||
appConfig.swagger.requireToken;
|
||||
swagger.requireToken = requireTokenForSwagger || false;
|
||||
}
|
|
@ -1,442 +0,0 @@
|
|||
var boot = require('../');
|
||||
var fs = require('fs-extra');
|
||||
var extend = require('util')._extend;
|
||||
var path = require('path');
|
||||
var loopback = require('loopback');
|
||||
var assert = require('assert');
|
||||
var expect = require('must');
|
||||
var sandbox = require('./helpers/sandbox');
|
||||
|
||||
var SIMPLE_APP = path.join(__dirname, 'fixtures', 'simple-app');
|
||||
|
||||
var appDir;
|
||||
|
||||
describe('bootLoopBackApp', function() {
|
||||
beforeEach(sandbox.reset);
|
||||
|
||||
beforeEach(function makeUniqueAppDir(done) {
|
||||
// Node's module loader has a very aggressive caching, therefore
|
||||
// we can't reuse the same path for multiple tests
|
||||
// The code here is used to generate a random string
|
||||
require('crypto').randomBytes(5, function(ex, buf) {
|
||||
var randomStr = buf.toString('hex');
|
||||
appDir = sandbox.resolve(randomStr);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('from options', function () {
|
||||
var app;
|
||||
beforeEach(function () {
|
||||
app = loopback();
|
||||
boot(app, {
|
||||
app: {
|
||||
port: 3000,
|
||||
host: '127.0.0.1',
|
||||
restApiRoot: '/rest-api',
|
||||
foo: {bar: 'bat'},
|
||||
baz: true
|
||||
},
|
||||
models: {
|
||||
'foo-bar-bat-baz': {
|
||||
options: {
|
||||
plural: 'foo-bar-bat-bazzies'
|
||||
},
|
||||
dataSource: 'the-db'
|
||||
}
|
||||
},
|
||||
dataSources: {
|
||||
'the-db': {
|
||||
connector: 'memory',
|
||||
defaultForType: 'db'
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should have port setting', function () {
|
||||
assert.equal(app.get('port'), 3000);
|
||||
});
|
||||
|
||||
it('should have host setting', function() {
|
||||
assert.equal(app.get('host'), '127.0.0.1');
|
||||
});
|
||||
|
||||
it('should have restApiRoot setting', function() {
|
||||
assert.equal(app.get('restApiRoot'), '/rest-api');
|
||||
});
|
||||
|
||||
it('should have other settings', function () {
|
||||
expect(app.get('foo')).to.eql({
|
||||
bar: 'bat'
|
||||
});
|
||||
expect(app.get('baz')).to.eql(true);
|
||||
});
|
||||
|
||||
it('Instantiate models', function () {
|
||||
assert(app.models);
|
||||
assert(app.models.FooBarBatBaz);
|
||||
assert(app.models.fooBarBatBaz);
|
||||
assertValidDataSource(app.models.FooBarBatBaz.dataSource);
|
||||
assert.isFunc(app.models.FooBarBatBaz, 'find');
|
||||
assert.isFunc(app.models.FooBarBatBaz, 'create');
|
||||
});
|
||||
|
||||
it('Attach models to data sources', function () {
|
||||
assert.equal(app.models.FooBarBatBaz.dataSource, app.dataSources.theDb);
|
||||
});
|
||||
|
||||
it('Instantiate data sources', function () {
|
||||
assert(app.dataSources);
|
||||
assert(app.dataSources.theDb);
|
||||
assertValidDataSource(app.dataSources.theDb);
|
||||
assert(app.dataSources.TheDb);
|
||||
});
|
||||
|
||||
describe('boot and models directories', function() {
|
||||
beforeEach(function() {
|
||||
boot(app, SIMPLE_APP);
|
||||
});
|
||||
|
||||
it('should run all modules in the boot directory', function () {
|
||||
assert(process.loadedFooJS);
|
||||
delete process.loadedFooJS;
|
||||
});
|
||||
|
||||
it('should run all modules in the models directory', function () {
|
||||
assert(process.loadedBarJS);
|
||||
delete process.loadedBarJS;
|
||||
});
|
||||
});
|
||||
|
||||
describe('PaaS and npm env variables', function() {
|
||||
function bootWithDefaults() {
|
||||
app = loopback();
|
||||
boot(app, {
|
||||
app: {
|
||||
port: undefined,
|
||||
host: undefined
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
it('should be honored', function() {
|
||||
function assertHonored(portKey, hostKey) {
|
||||
process.env[hostKey] = randomPort();
|
||||
process.env[portKey] = randomHost();
|
||||
bootWithDefaults();
|
||||
assert.equal(app.get('port'), process.env[portKey]);
|
||||
assert.equal(app.get('host'), process.env[hostKey]);
|
||||
delete process.env[portKey];
|
||||
delete process.env[hostKey];
|
||||
}
|
||||
|
||||
assertHonored('OPENSHIFT_SLS_PORT', 'OPENSHIFT_NODEJS_IP');
|
||||
assertHonored('npm_config_port', 'npm_config_host');
|
||||
assertHonored('npm_package_config_port', 'npm_package_config_host');
|
||||
assertHonored('OPENSHIFT_SLS_PORT', 'OPENSHIFT_SLS_IP');
|
||||
assertHonored('PORT', 'HOST');
|
||||
});
|
||||
|
||||
it('should be honored in order', function() {
|
||||
/*jshint camelcase:false */
|
||||
process.env.npm_config_host = randomHost();
|
||||
process.env.OPENSHIFT_SLS_IP = randomHost();
|
||||
process.env.OPENSHIFT_NODEJS_IP = randomHost();
|
||||
process.env.HOST = randomHost();
|
||||
process.env.npm_package_config_host = randomHost();
|
||||
|
||||
bootWithDefaults();
|
||||
assert.equal(app.get('host'), process.env.npm_config_host);
|
||||
|
||||
delete process.env.npm_config_host;
|
||||
delete process.env.OPENSHIFT_SLS_IP;
|
||||
delete process.env.OPENSHIFT_NODEJS_IP;
|
||||
delete process.env.HOST;
|
||||
delete process.env.npm_package_config_host;
|
||||
|
||||
process.env.npm_config_port = randomPort();
|
||||
process.env.OPENSHIFT_SLS_PORT = randomPort();
|
||||
process.env.OPENSHIFT_NODEJS_PORT = randomPort();
|
||||
process.env.PORT = randomPort();
|
||||
process.env.npm_package_config_port = randomPort();
|
||||
|
||||
bootWithDefaults();
|
||||
assert.equal(app.get('host'), process.env.npm_config_host);
|
||||
assert.equal(app.get('port'), process.env.npm_config_port);
|
||||
|
||||
delete process.env.npm_config_port;
|
||||
delete process.env.OPENSHIFT_SLS_PORT;
|
||||
delete process.env.OPENSHIFT_NODEJS_PORT;
|
||||
delete process.env.PORT;
|
||||
delete process.env.npm_package_config_port;
|
||||
});
|
||||
|
||||
function randomHost() {
|
||||
return Math.random().toString().split('.')[1];
|
||||
}
|
||||
|
||||
function randomPort() {
|
||||
return Math.floor(Math.random() * 10000);
|
||||
}
|
||||
|
||||
it('should honor 0 for free port', function () {
|
||||
boot(app, {app: {port: 0}});
|
||||
assert.equal(app.get('port'), 0);
|
||||
});
|
||||
|
||||
it('should default to port 3000', function () {
|
||||
boot(app, {app: {port: undefined}});
|
||||
assert.equal(app.get('port'), 3000);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('from directory', function () {
|
||||
it('Load config files', function () {
|
||||
var app = loopback();
|
||||
|
||||
boot(app, SIMPLE_APP);
|
||||
|
||||
assert(app.models.foo);
|
||||
assert(app.models.Foo);
|
||||
assert(app.models.Foo.dataSource);
|
||||
assert.isFunc(app.models.Foo, 'find');
|
||||
assert.isFunc(app.models.Foo, 'create');
|
||||
});
|
||||
|
||||
it('merges datasource configs from multiple files', function() {
|
||||
givenAppInSandbox();
|
||||
|
||||
writeAppConfigFile('datasources.local.json', {
|
||||
db: { local: 'applied' }
|
||||
});
|
||||
|
||||
var env = process.env.NODE_ENV || 'development';
|
||||
writeAppConfigFile('datasources.' + env + '.json', {
|
||||
db: { env: 'applied' }
|
||||
});
|
||||
|
||||
var app = loopback();
|
||||
boot(app, appDir);
|
||||
|
||||
var db = app.datasources.db.settings;
|
||||
expect(db).to.have.property('local', 'applied');
|
||||
expect(db).to.have.property('env', 'applied');
|
||||
|
||||
var expectedLoadOrder = ['local', 'env'];
|
||||
var actualLoadOrder = Object.keys(db).filter(function(k) {
|
||||
return expectedLoadOrder.indexOf(k) !== -1;
|
||||
});
|
||||
|
||||
expect(actualLoadOrder, 'load order').to.eql(expectedLoadOrder);
|
||||
});
|
||||
|
||||
it('supports .js for custom datasource config files', function() {
|
||||
givenAppInSandbox();
|
||||
fs.writeFileSync(
|
||||
path.resolve(appDir, 'datasources.local.js'),
|
||||
'module.exports = { db: { fromJs: true } };');
|
||||
|
||||
var app = loopback();
|
||||
boot(app, appDir);
|
||||
|
||||
var db = app.datasources.db.settings;
|
||||
expect(db).to.have.property('fromJs', true);
|
||||
});
|
||||
|
||||
it('refuses to merge Object properties', function() {
|
||||
givenAppInSandbox();
|
||||
writeAppConfigFile('datasources.local.json', {
|
||||
db: { nested: { key: 'value' } }
|
||||
});
|
||||
|
||||
var app = loopback();
|
||||
expect(function() { boot(app, appDir); })
|
||||
.to.throw(/`nested` is not a value type/);
|
||||
});
|
||||
|
||||
it('refuses to merge Array properties', function() {
|
||||
givenAppInSandbox();
|
||||
writeAppConfigFile('datasources.local.json', {
|
||||
db: { nested: ['value'] }
|
||||
});
|
||||
|
||||
var app = loopback();
|
||||
expect(function() { boot(app, appDir); })
|
||||
.to.throw(/`nested` is not a value type/);
|
||||
});
|
||||
|
||||
it('merges app configs from multiple files', function() {
|
||||
givenAppInSandbox();
|
||||
|
||||
writeAppConfigFile('app.local.json', { cfgLocal: 'applied' });
|
||||
|
||||
var env = process.env.NODE_ENV || 'development';
|
||||
writeAppConfigFile('app.' + env + '.json', { cfgEnv: 'applied' });
|
||||
|
||||
var app = loopback();
|
||||
boot(app, appDir);
|
||||
|
||||
expect(app.settings).to.have.property('cfgLocal', 'applied');
|
||||
expect(app.settings).to.have.property('cfgEnv', 'applied');
|
||||
|
||||
var expectedLoadOrder = ['cfgLocal', 'cfgEnv'];
|
||||
var actualLoadOrder = Object.keys(app.settings).filter(function(k) {
|
||||
return expectedLoadOrder.indexOf(k) !== -1;
|
||||
});
|
||||
|
||||
expect(actualLoadOrder, 'load order').to.eql(expectedLoadOrder);
|
||||
});
|
||||
|
||||
it('supports .js for custom app config files', function() {
|
||||
givenAppInSandbox();
|
||||
fs.writeFileSync(
|
||||
path.resolve(appDir, 'app.local.js'),
|
||||
'module.exports = { fromJs: true };');
|
||||
|
||||
var app = loopback();
|
||||
boot(app, appDir);
|
||||
|
||||
expect(app.settings).to.have.property('fromJs', true);
|
||||
});
|
||||
|
||||
it('supports `dsRootDir` option', function() {
|
||||
givenAppInSandbox();
|
||||
|
||||
var customDir = path.resolve(appDir, 'custom');
|
||||
fs.mkdirsSync(customDir);
|
||||
fs.renameSync(
|
||||
path.resolve(appDir, 'datasources.json'),
|
||||
path.resolve(customDir, 'datasources.json'));
|
||||
|
||||
var app = loopback();
|
||||
|
||||
// workaround for https://github.com/strongloop/loopback/pull/283
|
||||
app.datasources = app.dataSources = {};
|
||||
|
||||
boot(app, {
|
||||
appRootDir: appDir,
|
||||
dsRootDir: path.resolve(appDir, 'custom')
|
||||
});
|
||||
|
||||
expect(app.datasources).to.have.property('db');
|
||||
});
|
||||
|
||||
it('supports `modelsRootDir` option', function() {
|
||||
givenAppInSandbox();
|
||||
|
||||
writeAppConfigFile('custom/models.json', {
|
||||
foo: { dataSource: 'db' }
|
||||
});
|
||||
|
||||
global.testData = {};
|
||||
writeAppFile('custom/models/foo.js', 'global.testData.foo = "loaded";');
|
||||
|
||||
var app = loopback();
|
||||
boot(app, {
|
||||
appRootDir: appDir,
|
||||
modelsRootDir: path.resolve(appDir, 'custom')
|
||||
});
|
||||
|
||||
expect(app.models).to.have.property('foo');
|
||||
expect(global.testData).to.have.property('foo', 'loaded');
|
||||
});
|
||||
|
||||
it('calls function exported by models/model.js', function() {
|
||||
givenAppInSandbox();
|
||||
writeAppFile('models/model.js',
|
||||
'module.exports = function(app) { app.fnCalled = true; };');
|
||||
|
||||
var app = loopback();
|
||||
delete app.fnCalled;
|
||||
boot(app, appDir);
|
||||
expect(app.fnCalled, 'exported fn was called').to.be.true();
|
||||
});
|
||||
|
||||
it('calls function exported by boot/init.js', function() {
|
||||
givenAppInSandbox();
|
||||
writeAppFile('boot/init.js',
|
||||
'module.exports = function(app) { app.fnCalled = true; };');
|
||||
|
||||
var app = loopback();
|
||||
delete app.fnCalled;
|
||||
boot(app, appDir);
|
||||
expect(app.fnCalled, 'exported fn was called').to.be.true();
|
||||
});
|
||||
|
||||
it('does not call Model ctor exported by models/model.json', function() {
|
||||
givenAppInSandbox();
|
||||
writeAppFile('models/model.js',
|
||||
'var loopback = require("loopback");\n' +
|
||||
'module.exports = loopback.Model.extend("foo");\n' +
|
||||
'module.exports.prototype._initProperties = function() {\n' +
|
||||
' global.fnCalled = true;\n' +
|
||||
'};');
|
||||
|
||||
var app = loopback();
|
||||
delete global.fnCalled;
|
||||
boot(app, appDir);
|
||||
expect(global.fnCalled, 'exported fn was called').to.be.undefined();
|
||||
});
|
||||
|
||||
it('supports models/ subdirectires that are not require()able', function() {
|
||||
givenAppInSandbox();
|
||||
writeAppFile('models/test/model.test.js',
|
||||
'throw new Error("should not been called");');
|
||||
|
||||
var app = loopback();
|
||||
boot(app, appDir);
|
||||
|
||||
// no assert, the test passed when we got here
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
function assertValidDataSource(dataSource) {
|
||||
// has methods
|
||||
assert.isFunc(dataSource, 'createModel');
|
||||
assert.isFunc(dataSource, 'discoverModelDefinitions');
|
||||
assert.isFunc(dataSource, 'discoverSchema');
|
||||
assert.isFunc(dataSource, 'enableRemote');
|
||||
assert.isFunc(dataSource, 'disableRemote');
|
||||
assert.isFunc(dataSource, 'defineOperation');
|
||||
assert.isFunc(dataSource, 'operations');
|
||||
}
|
||||
|
||||
assert.isFunc = function (obj, name) {
|
||||
assert(obj, 'cannot assert function ' + name +
|
||||
' on object that does not exist');
|
||||
assert(typeof obj[name] === 'function', name + ' is not a function');
|
||||
};
|
||||
|
||||
function givenAppInSandbox(appConfig, dataSources, models) {
|
||||
fs.mkdirsSync(appDir);
|
||||
|
||||
appConfig = extend({
|
||||
}, appConfig);
|
||||
writeAppConfigFile('app.json', appConfig);
|
||||
|
||||
dataSources = extend({
|
||||
db: {
|
||||
connector: 'memory',
|
||||
defaultForType: 'db'
|
||||
}
|
||||
}, dataSources);
|
||||
writeAppConfigFile('datasources.json', dataSources);
|
||||
|
||||
models = extend({
|
||||
}, models);
|
||||
writeAppConfigFile('models.json', models);
|
||||
}
|
||||
|
||||
function writeAppConfigFile(name, json) {
|
||||
writeAppFile(name, JSON.stringify(json, null, 2));
|
||||
}
|
||||
|
||||
function writeAppFile(name, content) {
|
||||
var filePath = path.resolve(appDir, name);
|
||||
fs.mkdirsSync(path.dirname(filePath));
|
||||
fs.writeFileSync(filePath, content, 'utf-8');
|
||||
}
|
|
@ -0,0 +1,220 @@
|
|||
var boot = require('../');
|
||||
var fs = require('fs-extra');
|
||||
var path = require('path');
|
||||
var assert = require('assert');
|
||||
var expect = require('must');
|
||||
var sandbox = require('./helpers/sandbox');
|
||||
var appdir = require('./helpers/appdir');
|
||||
|
||||
var SIMPLE_APP = path.join(__dirname, 'fixtures', 'simple-app');
|
||||
|
||||
describe('compiler', function() {
|
||||
beforeEach(sandbox.reset);
|
||||
beforeEach(appdir.init);
|
||||
|
||||
describe('from options', function() {
|
||||
var options, instructions, appConfig;
|
||||
beforeEach(function() {
|
||||
options = {
|
||||
app: {
|
||||
port: 3000,
|
||||
host: '127.0.0.1',
|
||||
restApiRoot: '/rest-api',
|
||||
foo: {bar: 'bat'},
|
||||
baz: true
|
||||
},
|
||||
models: {
|
||||
'foo-bar-bat-baz': {
|
||||
options: {
|
||||
plural: 'foo-bar-bat-bazzies'
|
||||
},
|
||||
dataSource: 'the-db'
|
||||
}
|
||||
},
|
||||
dataSources: {
|
||||
'the-db': {
|
||||
connector: 'memory',
|
||||
defaultForType: 'db'
|
||||
}
|
||||
}
|
||||
};
|
||||
instructions = boot.compile(options);
|
||||
appConfig = instructions.app;
|
||||
});
|
||||
|
||||
it('has port setting', function() {
|
||||
expect(appConfig).to.have.property('port', 3000);
|
||||
});
|
||||
|
||||
it('has host setting', function() {
|
||||
expect(appConfig).to.have.property('host', '127.0.0.1');
|
||||
});
|
||||
|
||||
it('has restApiRoot setting', function() {
|
||||
expect(appConfig).to.have.property('restApiRoot', '/rest-api');
|
||||
});
|
||||
|
||||
it('has other settings', function() {
|
||||
expect(appConfig).to.have.property('baz', true);
|
||||
expect(appConfig.foo, 'appConfig.foo').to.eql({
|
||||
bar: 'bat'
|
||||
});
|
||||
});
|
||||
|
||||
it('has models definition', function() {
|
||||
expect(instructions.models).to.eql(options.models);
|
||||
});
|
||||
|
||||
it('has datasources definition', function() {
|
||||
expect(instructions.dataSources).to.eql(options.dataSources);
|
||||
});
|
||||
});
|
||||
|
||||
describe('from directory', function() {
|
||||
it('loads config files', function() {
|
||||
var instructions = boot.compile(SIMPLE_APP);
|
||||
assert(instructions.models.foo);
|
||||
assert(instructions.models.foo.dataSource);
|
||||
});
|
||||
|
||||
it('merges datasource configs from multiple files', function() {
|
||||
appdir.createConfigFilesSync();
|
||||
appdir.writeConfigFileSync('datasources.local.json', {
|
||||
db: { local: 'applied' }
|
||||
});
|
||||
|
||||
var env = process.env.NODE_ENV || 'development';
|
||||
appdir.writeConfigFileSync('datasources.' + env + '.json', {
|
||||
db: { env: 'applied' }
|
||||
});
|
||||
|
||||
var instructions = boot.compile(appdir.PATH);
|
||||
|
||||
var db = instructions.dataSources.db;
|
||||
expect(db).to.have.property('local', 'applied');
|
||||
expect(db).to.have.property('env', 'applied');
|
||||
|
||||
var expectedLoadOrder = ['local', 'env'];
|
||||
var actualLoadOrder = Object.keys(db).filter(function(k) {
|
||||
return expectedLoadOrder.indexOf(k) !== -1;
|
||||
});
|
||||
|
||||
expect(actualLoadOrder, 'load order').to.eql(expectedLoadOrder);
|
||||
});
|
||||
|
||||
it('supports .js for custom datasource config files', function() {
|
||||
appdir.createConfigFilesSync();
|
||||
appdir.writeFileSync('datasources.local.js',
|
||||
'module.exports = { db: { fromJs: true } };');
|
||||
|
||||
var instructions = boot.compile(appdir.PATH);
|
||||
|
||||
var db = instructions.dataSources.db;
|
||||
expect(db).to.have.property('fromJs', true);
|
||||
});
|
||||
|
||||
it('refuses to merge Object properties', function() {
|
||||
appdir.createConfigFilesSync();
|
||||
appdir.writeConfigFileSync('datasources.local.json', {
|
||||
db: { nested: { key: 'value' } }
|
||||
});
|
||||
|
||||
expect(function() { boot.compile(appdir.PATH); })
|
||||
.to.throw(/`nested` is not a value type/);
|
||||
});
|
||||
|
||||
it('refuses to merge Array properties', function() {
|
||||
appdir.createConfigFilesSync();
|
||||
appdir.writeConfigFileSync('datasources.local.json', {
|
||||
db: { nested: ['value'] }
|
||||
});
|
||||
|
||||
expect(function() { boot.compile(appdir.PATH); })
|
||||
.to.throw(/`nested` is not a value type/);
|
||||
});
|
||||
|
||||
it('merges app configs from multiple files', function() {
|
||||
appdir.createConfigFilesSync();
|
||||
|
||||
appdir.writeConfigFileSync('app.local.json', { cfgLocal: 'applied' });
|
||||
|
||||
var env = process.env.NODE_ENV || 'development';
|
||||
appdir.writeConfigFileSync('app.' + env + '.json', { cfgEnv: 'applied' });
|
||||
|
||||
var instructions = boot.compile(appdir.PATH);
|
||||
var appConfig = instructions.app;
|
||||
|
||||
expect(appConfig).to.have.property('cfgLocal', 'applied');
|
||||
expect(appConfig).to.have.property('cfgEnv', 'applied');
|
||||
|
||||
var expectedLoadOrder = ['cfgLocal', 'cfgEnv'];
|
||||
var actualLoadOrder = Object.keys(appConfig).filter(function(k) {
|
||||
return expectedLoadOrder.indexOf(k) !== -1;
|
||||
});
|
||||
|
||||
expect(actualLoadOrder, 'load order').to.eql(expectedLoadOrder);
|
||||
});
|
||||
|
||||
it('supports .js for custom app config files', function() {
|
||||
appdir.createConfigFilesSync();
|
||||
appdir.writeFileSync('app.local.js',
|
||||
'module.exports = { fromJs: true };');
|
||||
|
||||
var instructions = boot.compile(appdir.PATH);
|
||||
var appConfig = instructions.app;
|
||||
|
||||
expect(appConfig).to.have.property('fromJs', true);
|
||||
});
|
||||
|
||||
it('supports `dsRootDir` option', function() {
|
||||
appdir.createConfigFilesSync();
|
||||
|
||||
var customDir = path.resolve(appdir.PATH, 'custom');
|
||||
fs.mkdirsSync(customDir);
|
||||
fs.renameSync(
|
||||
path.resolve(appdir.PATH, 'datasources.json'),
|
||||
path.resolve(customDir, 'datasources.json'));
|
||||
|
||||
var instructions = boot.compile({
|
||||
appRootDir: appdir.PATH,
|
||||
dsRootDir: path.resolve(appdir.PATH, 'custom')
|
||||
});
|
||||
|
||||
expect(instructions.dataSources).to.have.property('db');
|
||||
});
|
||||
|
||||
it('supports `modelsRootDir` option', function() {
|
||||
appdir.createConfigFilesSync();
|
||||
appdir.writeConfigFileSync('custom/models.json', {
|
||||
foo: { dataSource: 'db' }
|
||||
});
|
||||
|
||||
var fooJs = appdir.writeFileSync('custom/models/foo.js', '');
|
||||
|
||||
var instructions = boot.compile({
|
||||
appRootDir: appdir.PATH,
|
||||
modelsRootDir: path.resolve(appdir.PATH, 'custom')
|
||||
});
|
||||
|
||||
expect(instructions.models).to.have.property('foo');
|
||||
expect(instructions.files.models).to.eql([fooJs]);
|
||||
});
|
||||
|
||||
it('includes boot/*.js scripts', function() {
|
||||
appdir.createConfigFilesSync();
|
||||
var initJs = appdir.writeFileSync('boot/init.js',
|
||||
'module.exports = function(app) { app.fnCalled = true; };');
|
||||
var instructions = boot.compile(appdir.PATH);
|
||||
expect(instructions.files.boot).to.eql([initJs]);
|
||||
});
|
||||
|
||||
it('supports models/ subdirectires that are not require()able', function() {
|
||||
appdir.createConfigFilesSync();
|
||||
appdir.writeFileSync('models/test/model.test.js',
|
||||
'throw new Error("should not been called");');
|
||||
var instructions = boot.compile(appdir.PATH);
|
||||
|
||||
expect(instructions.files.models).to.eql([]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,239 @@
|
|||
var boot = require('../');
|
||||
var path = require('path');
|
||||
var loopback = require('loopback');
|
||||
var assert = require('assert');
|
||||
var expect = require('must');
|
||||
var sandbox = require('./helpers/sandbox');
|
||||
var appdir = require('./helpers/appdir');
|
||||
|
||||
var SIMPLE_APP = path.join(__dirname, 'fixtures', 'simple-app');
|
||||
|
||||
var app;
|
||||
|
||||
|
||||
describe('executor', function() {
|
||||
beforeEach(sandbox.reset);
|
||||
|
||||
beforeEach(appdir.init);
|
||||
|
||||
beforeEach(function() {
|
||||
app = loopback();
|
||||
});
|
||||
|
||||
var dummyInstructions = someInstructions({
|
||||
app: {
|
||||
port: 3000,
|
||||
host: '127.0.0.1',
|
||||
restApiRoot: '/rest-api',
|
||||
foo: { bar: 'bat' },
|
||||
baz: true
|
||||
},
|
||||
models: {
|
||||
'foo-bar-bat-baz': {
|
||||
options: {
|
||||
plural: 'foo-bar-bat-bazzies'
|
||||
},
|
||||
dataSource: 'the-db'
|
||||
}
|
||||
},
|
||||
dataSources: {
|
||||
'the-db': {
|
||||
connector: 'memory',
|
||||
defaultForType: 'db'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('instantiates models', function() {
|
||||
boot.execute(app, dummyInstructions);
|
||||
assert(app.models);
|
||||
assert(app.models.FooBarBatBaz);
|
||||
assert(app.models.fooBarBatBaz);
|
||||
assertValidDataSource(app.models.FooBarBatBaz.dataSource);
|
||||
assert.isFunc(app.models.FooBarBatBaz, 'find');
|
||||
assert.isFunc(app.models.FooBarBatBaz, 'create');
|
||||
});
|
||||
|
||||
it('attaches models to data sources', function() {
|
||||
boot.execute(app, dummyInstructions);
|
||||
assert.equal(app.models.FooBarBatBaz.dataSource, app.dataSources.theDb);
|
||||
});
|
||||
|
||||
it('instantiates data sources', function() {
|
||||
boot.execute(app, dummyInstructions);
|
||||
assert(app.dataSources);
|
||||
assert(app.dataSources.theDb);
|
||||
assertValidDataSource(app.dataSources.theDb);
|
||||
assert(app.dataSources.TheDb);
|
||||
});
|
||||
|
||||
describe('with boot and models files', function() {
|
||||
beforeEach(function() {
|
||||
boot.execute(app, simpleAppInstructions());
|
||||
});
|
||||
|
||||
it('should run `boot/*` files', function() {
|
||||
assert(process.loadedFooJS);
|
||||
delete process.loadedFooJS;
|
||||
});
|
||||
|
||||
it('should run `models/*` files', function() {
|
||||
assert(process.loadedBarJS);
|
||||
delete process.loadedBarJS;
|
||||
});
|
||||
});
|
||||
|
||||
describe('with PaaS and npm env variables', function() {
|
||||
function bootWithDefaults() {
|
||||
app = loopback();
|
||||
boot.execute(app, someInstructions({
|
||||
app: {
|
||||
port: undefined,
|
||||
host: undefined
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
it('should honor host and port', function() {
|
||||
function assertHonored(portKey, hostKey) {
|
||||
process.env[hostKey] = randomPort();
|
||||
process.env[portKey] = randomHost();
|
||||
bootWithDefaults();
|
||||
assert.equal(app.get('port'), process.env[portKey], portKey);
|
||||
assert.equal(app.get('host'), process.env[hostKey], hostKey);
|
||||
delete process.env[portKey];
|
||||
delete process.env[hostKey];
|
||||
}
|
||||
|
||||
assertHonored('OPENSHIFT_SLS_PORT', 'OPENSHIFT_NODEJS_IP');
|
||||
assertHonored('npm_config_port', 'npm_config_host');
|
||||
assertHonored('npm_package_config_port', 'npm_package_config_host');
|
||||
assertHonored('OPENSHIFT_SLS_PORT', 'OPENSHIFT_SLS_IP');
|
||||
assertHonored('PORT', 'HOST');
|
||||
});
|
||||
|
||||
it('should prioritize sources', function() {
|
||||
/*jshint camelcase:false */
|
||||
process.env.npm_config_host = randomHost();
|
||||
process.env.OPENSHIFT_SLS_IP = randomHost();
|
||||
process.env.OPENSHIFT_NODEJS_IP = randomHost();
|
||||
process.env.HOST = randomHost();
|
||||
process.env.npm_package_config_host = randomHost();
|
||||
|
||||
bootWithDefaults();
|
||||
assert.equal(app.get('host'), process.env.npm_config_host);
|
||||
|
||||
delete process.env.npm_config_host;
|
||||
delete process.env.OPENSHIFT_SLS_IP;
|
||||
delete process.env.OPENSHIFT_NODEJS_IP;
|
||||
delete process.env.HOST;
|
||||
delete process.env.npm_package_config_host;
|
||||
|
||||
process.env.npm_config_port = randomPort();
|
||||
process.env.OPENSHIFT_SLS_PORT = randomPort();
|
||||
process.env.OPENSHIFT_NODEJS_PORT = randomPort();
|
||||
process.env.PORT = randomPort();
|
||||
process.env.npm_package_config_port = randomPort();
|
||||
|
||||
bootWithDefaults();
|
||||
assert.equal(app.get('host'), process.env.npm_config_host);
|
||||
assert.equal(app.get('port'), process.env.npm_config_port);
|
||||
|
||||
delete process.env.npm_config_port;
|
||||
delete process.env.OPENSHIFT_SLS_PORT;
|
||||
delete process.env.OPENSHIFT_NODEJS_PORT;
|
||||
delete process.env.PORT;
|
||||
delete process.env.npm_package_config_port;
|
||||
});
|
||||
|
||||
function randomHost() {
|
||||
return Math.random().toString().split('.')[1];
|
||||
}
|
||||
|
||||
function randomPort() {
|
||||
return Math.floor(Math.random() * 10000);
|
||||
}
|
||||
|
||||
it('should honor 0 for free port', function() {
|
||||
boot.execute(app, someInstructions({ app: { port: 0 } }));
|
||||
assert.equal(app.get('port'), 0);
|
||||
});
|
||||
|
||||
it('should default to port 3000', function() {
|
||||
boot.execute(app, someInstructions({ app: { port: undefined } }));
|
||||
assert.equal(app.get('port'), 3000);
|
||||
});
|
||||
});
|
||||
|
||||
it('calls function exported by models/model.js', function() {
|
||||
var file = appdir.writeFileSync('models/model.js',
|
||||
'module.exports = function(app) { app.fnCalled = true; };');
|
||||
|
||||
delete app.fnCalled;
|
||||
boot.execute(app, someInstructions({ files: { models: [ file ] } }));
|
||||
expect(app.fnCalled, 'exported fn was called').to.be.true();
|
||||
});
|
||||
|
||||
it('calls function exported by boot/init.js', function() {
|
||||
var file = appdir.writeFileSync('boot/init.js',
|
||||
'module.exports = function(app) { app.fnCalled = true; };');
|
||||
|
||||
delete app.fnCalled;
|
||||
boot.execute(app, someInstructions({ files: { boot: [ file ] } }));
|
||||
expect(app.fnCalled, 'exported fn was called').to.be.true();
|
||||
});
|
||||
|
||||
it('does not call Model ctor exported by models/model.json', function() {
|
||||
var file = appdir.writeFileSync('models/model.js',
|
||||
'var loopback = require("loopback");\n' +
|
||||
'module.exports = loopback.Model.extend("foo");\n' +
|
||||
'module.exports.prototype._initProperties = function() {\n' +
|
||||
' global.fnCalled = true;\n' +
|
||||
'};');
|
||||
|
||||
delete global.fnCalled;
|
||||
boot.execute(app, someInstructions({ files: { models: [ file ] } }));
|
||||
expect(global.fnCalled, 'exported fn was called').to.be.undefined();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
function assertValidDataSource(dataSource) {
|
||||
// has methods
|
||||
assert.isFunc(dataSource, 'createModel');
|
||||
assert.isFunc(dataSource, 'discoverModelDefinitions');
|
||||
assert.isFunc(dataSource, 'discoverSchema');
|
||||
assert.isFunc(dataSource, 'enableRemote');
|
||||
assert.isFunc(dataSource, 'disableRemote');
|
||||
assert.isFunc(dataSource, 'defineOperation');
|
||||
assert.isFunc(dataSource, 'operations');
|
||||
}
|
||||
|
||||
assert.isFunc = function (obj, name) {
|
||||
assert(obj, 'cannot assert function ' + name +
|
||||
' on object that does not exist');
|
||||
assert(typeof obj[name] === 'function', name + ' is not a function');
|
||||
};
|
||||
|
||||
function someInstructions(values) {
|
||||
var result = {
|
||||
app: values.app || {},
|
||||
models: values.models || {},
|
||||
dataSources: values.dataSources || {},
|
||||
files: {
|
||||
models: [],
|
||||
boot: []
|
||||
}
|
||||
};
|
||||
|
||||
if (values.files) {
|
||||
for (var k in values.files)
|
||||
result.files[k] = values.files[k];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function simpleAppInstructions() {
|
||||
return boot.compile(SIMPLE_APP);
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
var path = require('path');
|
||||
var fs = require('fs-extra');
|
||||
var extend = require('util')._extend;
|
||||
var sandbox = require('./sandbox');
|
||||
|
||||
var appdir = exports;
|
||||
|
||||
var PATH = appdir.PATH = null;
|
||||
|
||||
appdir.init = function(cb) {
|
||||
// Node's module loader has a very aggressive caching, therefore
|
||||
// we can't reuse the same path for multiple tests
|
||||
// The code here is used to generate a random string
|
||||
require('crypto').randomBytes(5, function(err, buf) {
|
||||
if (err) return cb(err);
|
||||
var randomStr = buf.toString('hex');
|
||||
PATH = appdir.PATH = sandbox.resolve(randomStr);
|
||||
cb(null, appdir.PATH);
|
||||
});
|
||||
};
|
||||
|
||||
appdir.createConfigFilesSync = function(appConfig, dataSources, models) {
|
||||
appConfig = extend({
|
||||
}, appConfig);
|
||||
appdir.writeConfigFileSync ('app.json', appConfig);
|
||||
|
||||
dataSources = extend({
|
||||
db: {
|
||||
connector: 'memory',
|
||||
defaultForType: 'db'
|
||||
}
|
||||
}, dataSources);
|
||||
appdir.writeConfigFileSync ('datasources.json', dataSources);
|
||||
|
||||
models = extend({
|
||||
}, models);
|
||||
appdir.writeConfigFileSync ('models.json', models);
|
||||
};
|
||||
|
||||
appdir.writeConfigFileSync = function(name, json) {
|
||||
return appdir.writeFileSync(name, JSON.stringify(json, null, 2));
|
||||
};
|
||||
|
||||
appdir.writeFileSync = function(name, content) {
|
||||
var filePath = path.resolve(PATH, name);
|
||||
fs.mkdirsSync(path.dirname(filePath));
|
||||
fs.writeFileSync(filePath, content, 'utf-8');
|
||||
return filePath;
|
||||
};
|
Loading…
Reference in New Issue