Merge pull request #1 from strongloop/feature/extract-app-boot

Initial implementation
This commit is contained in:
Miroslav Bajtoš 2014-05-28 15:56:24 +02:00
commit 32ccacfbf7
15 changed files with 985 additions and 1 deletions

1
.gitignore vendored
View File

@ -13,3 +13,4 @@
node_modules node_modules
checkstyle.xml checkstyle.xml
loopback-boot-*.tgz loopback-boot-*.tgz
/test/sandbox/

View File

@ -1,2 +1,3 @@
node_modules/ node_modules/
coverage/ coverage/
test/sandbox/

14
CHANGES.md Normal file
View File

@ -0,0 +1,14 @@
## Changes in version 1.0
- New options: `modelsRootDir`, `dsRootDir`
- Load configuration from files, support dynamic (scripted) options
```sh
app.json, app.local.*, app.{env}.*
datasources.json, datasources.local.*, datasources.{env}.*
```
- Scripts in `models/` and `boot/` can export `function(app)`,
this function is then called by the bootstrapper. The existing code
using `var app = require('../app')` will continue to work.

313
index.js Normal file
View File

@ -0,0 +1,313 @@
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');
/**
* Initialize an application from an options object or
* a set of JSON and JavaScript files.
*
* This function takes an optional argument that is either a string
* or an object.
*
* If the argument is a string, then it sets the application root directory
* based on the string value. Then it:
*
* 1. Creates DataSources from the `datasources.json` file in the application
* root directory.
*
* 2. Creates Models from the `models.json` file in the application
* root directory.
*
* If the argument is an object, then it looks for `model`, `dataSources`,
* and `appRootDir` properties of the object.
* If the object has no `appRootDir` property then it sets the current working
* directory as the application root directory.
* Then it:
*
* 1. Creates DataSources from the `options.dataSources` object.
*
* 2. Creates Models from the `options.models` object.
*
* In both cases, the function loads JavaScript files in the `/models` and
* `/boot` subdirectories of the application root directory with `require()`.
*
* **NOTE:** mixing `app.boot()` and `app.model(name, config)` in multiple
* files may result in models being **undefined** due to race conditions.
* To avoid this when using `app.boot()` make sure all models are passed
* as part of the `models` definition.
*
* 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.
* @property {String} appRootDir Directory to use when loading JSON and
* JavaScript files (optional).
* Defaults to the current directory (`process.cwd()`).
* @property {Object} models Object containing `Model` definitions (optional).
* @property {Object} dataSources Object containing `DataSource`
* definitions (optional).
* @property {String} modelsRootDir Directory to use when loading `models.json`
* and `models/*.js`. Defaults to `appRootDir`.
* @property {String} datasourcesRootDir Directory to use when loading
* `datasources.json`. Defaults to `appRootDir`.
* @end
*
* @header boot(app, [options])
*/
exports = module.exports = function bootLoopBackApp(app, options) {
/*jshint camelcase:false */
options = options || {};
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);
};
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;

154
lib/config-loader.js Normal file
View File

@ -0,0 +1,154 @@
var fs = require('fs');
var path = require('path');
var ConfigLoader = exports;
/**
* Load application config from `app.json` and friends.
* @param {String} rootDir Directory where to look for files.
* @param {String} env Environment, usually `process.env.NODE_ENV`
* @returns {Object}
*/
ConfigLoader.loadAppConfig = function(rootDir, env) {
return loadNamed(rootDir, env, 'app', mergeAppConfig);
};
/**
* Load data-sources config from `datasources.json` and friends.
* @param {String} rootDir Directory where to look for files.
* @param {String} env Environment, usually `process.env.NODE_ENV`
* @returns {Object}
*/
ConfigLoader.loadDataSources = function(rootDir, env) {
return loadNamed(rootDir, env, 'datasources', mergeDataSourceConfig);
};
/**
* Load models config from `models.json` and friends.
* @param {String} rootDir Directory where to look for files.
* @param {String} env Environment, usually `process.env.NODE_ENV`
* @returns {Object}
*/
ConfigLoader.loadModels = function(rootDir, env) {
/*jshint unused:false */
return tryReadJsonConfig(rootDir, 'models') || {};
};
/*-- Implementation --*/
/**
* Load named configuration.
* @param {String} rootDir Directory where to look for files.
* @param {String} env Environment, usually `process.env.NODE_ENV`
* @param {String} name
* @param {function(target:Object, config:Object, filename:String)} mergeFn
* @returns {Object}
*/
function loadNamed(rootDir, env, name, mergeFn) {
var files = findConfigFiles(rootDir, env, name);
var configs = loadConfigFiles(files);
return mergeConfigurations(configs, mergeFn);
}
/**
* Search `appRootDir` for all files containing configuration for `name`.
* @param {String} appRootDir
* @param {String} env Environment, usually `process.env.NODE_ENV`
* @param {String} name
* @returns {Array.<String>} Array of absolute file paths.
*/
function findConfigFiles(appRootDir, env, name) {
var master = ifExists(name + '.json');
if (!master) return [];
var candidates = [
master,
ifExistsWithAnyExt(name + '.local'),
ifExistsWithAnyExt(name + '.' + env)
];
return candidates.filter(function(c) { return c !== undefined; });
function ifExists(fileName) {
var filepath = path.resolve(appRootDir, fileName);
return fs.existsSync(filepath) ? filepath : undefined;
}
function ifExistsWithAnyExt(fileName) {
return ifExists(fileName + '.js') || ifExists(fileName + '.json');
}
}
/**
* Load configuration files into an array of objects.
* Attach non-enumerable `_filename` property to each object.
* @param {Array.<String>} files
* @returns {Array.<Object>}
*/
function loadConfigFiles(files) {
return files.map(function(f) {
var config = require(f);
Object.defineProperty(config, '_filename', {
enumerable: false,
value: f
});
return config;
});
}
/**
* Merge multiple configuration objects into a single one.
* @param {Array.<Object>} configObjects
* @param {function(target:Object, config:Object, filename:String)} mergeFn
*/
function mergeConfigurations(configObjects, mergeFn) {
var result = configObjects.shift() || {};
while(configObjects.length) {
var next = configObjects.shift();
mergeFn(result, next, next._filename);
}
return result;
}
function mergeDataSourceConfig(target, config, fileName) {
for (var ds in target) {
var err = applyCustomConfig(target[ds], config[ds]);
if (err) {
throw new Error('Cannot apply ' + fileName + ' to `' + ds + '`: ' + err);
}
}
}
function mergeAppConfig(target, config, fileName) {
var err = applyCustomConfig(target, config);
if (err) {
throw new Error('Cannot apply ' + fileName + ': ' + err);
}
}
function applyCustomConfig(target, config) {
for (var key in config) {
var value = config[key];
if (typeof value === 'object') {
return 'override for the option `' + key + '` is not a value type.';
}
target[key] = value;
}
return null; // no error
}
/**
* Try to read a config file with .json extension
* @param cwd Dirname of the file
* @param fileName Name of the file without extension
* @returns {Object|undefined} Content of the file, undefined if not found.
*/
function tryReadJsonConfig(cwd, fileName) {
try {
return require(path.join(cwd, fileName + '.json'));
} catch(e) {
if(e.code !== 'MODULE_NOT_FOUND') {
throw e;
}
}
}

View File

@ -14,10 +14,25 @@
}, },
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"pretest": "jshint ." "pretest": "jshint .",
"test": "mocha"
}, },
"license": { "license": {
"name": "Dual MIT/StrongLoop", "name": "Dual MIT/StrongLoop",
"url": "https://github.com/strongloop/loopback-boot/blob/master/LICENSE" "url": "https://github.com/strongloop/loopback-boot/blob/master/LICENSE"
},
"dependencies": {
"underscore": "^1.6.0",
"debug": "^0.8.1"
},
"devDependencies": {
"loopback": "^1.5.0",
"mocha": "^1.19.0",
"must": "^0.11.0",
"supertest": "^0.13.0",
"fs-extra": "^0.9.1"
},
"peerDependencies": {
"loopback": "1.x"
} }
} }

442
test/boot.test.js Normal file
View File

@ -0,0 +1,442 @@
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');
}

4
test/fixtures/simple-app/app.json vendored Normal file
View File

@ -0,0 +1,4 @@
{
"port": 3000,
"host": "127.0.0.1"
}

1
test/fixtures/simple-app/boot/bad.txt vendored Normal file
View File

@ -0,0 +1 @@
this is not a js file!

1
test/fixtures/simple-app/boot/foo.js vendored Normal file
View File

@ -0,0 +1 @@
process.loadedFooJS = true;

View File

@ -0,0 +1,5 @@
{
"db": {
"connector": "memory"
}
}

5
test/fixtures/simple-app/models.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"foo": {
"dataSource": "db"
}
}

View File

@ -0,0 +1 @@
process.loadedBarJS = true;

11
test/global-setup.js Normal file
View File

@ -0,0 +1,11 @@
var loopback = require('loopback');
// bootLoopBackApp() calls loopback.autoAttach
// which attempts to attach all models to default datasources
// one of those models is Email which requires 'email' datasource
loopback.setDefaultDataSourceForType('mail', {
connector: loopback.Mail,
transports: [
{type: 'STUB'}
]
});

16
test/helpers/sandbox.js Normal file
View File

@ -0,0 +1,16 @@
var fs = require('fs-extra');
var path = require('path');
var sandbox = exports;
sandbox.PATH = path.join(__dirname, '..', 'sandbox');
sandbox.reset = function() {
fs.removeSync(sandbox.PATH);
fs.mkdirsSync(sandbox.PATH);
};
sandbox.resolve = function() {
var args = Array.prototype.slice.apply(arguments);
args.unshift(sandbox.PATH);
return path.resolve.apply(path.resolve, args);
};