Merge branch 'release/2.0.0-beta1' into production

This commit is contained in:
Miroslav Bajtoš 2014-06-26 14:56:29 +02:00
commit fc18561cc7
21 changed files with 907 additions and 222 deletions

200
README.md
View File

@ -26,16 +26,29 @@ app.listen();
See [API docs](http://apidocs.strongloop.com/loopback-boot/#api) for See [API docs](http://apidocs.strongloop.com/loopback-boot/#api) for
complete API reference. complete API reference.
## Versions
The version range `1.x` is backwards compatible with `app.boot` provided
by LoopBack 1.x versions and the project layout scaffolded by `slc lb project`
up to slc version 2.5.
The version range `2.x` supports the new project layout as scaffolded by
`yo loopback`.
This document describes the configuration conventions of the `2.x` versions.
See [Migrating from 1.x to 2.x](http://apidocs.strongloop.com/loopback-boot/#migrating-from-1x-to-2x)
for step-by-step instructions on how to upgrade existing projects.
## Configurations and conventions ## Configurations and conventions
The bootstrapping process takes care of the following tasks: The bootstrapping process takes care of the following tasks:
- Configuration of data-sources. - Configuration of data-sources.
- Definition and configuration of custom Models, attaching models to - Definition of custom Models
data-sources. - Configuration of models, attaching models to data-sources.
- Configuration of app settings like `host`, `port` or `restApiRoot`. - Configuration of app settings like `host`, `port` or `restApiRoot`.
- Running additional boot scripts to keep the custom setup code in multiple - Running additional boot scripts, so that the custom setup code can be kept
small files as opposed to keeping everything in the main app file. in multiple small files as opposed to keeping everything in the main app file.
Below is the typical project layout. See the following sections for description Below is the typical project layout. See the following sections for description
of the project files. of the project files.
@ -43,7 +56,7 @@ of the project files.
``` ```
project/ project/
app.js app.js
app.json config.json
datasources.json datasources.json
models.json models.json
models/ models/
@ -52,13 +65,13 @@ project/
### App settings ### App settings
The settings are loaded from the file `app.json` in the project root directory The settings are loaded from the file `config.json` in the project root directory
and can be accessed via `app.get('option-name')` from the code. and can be accessed via `app.get('option-name')` from the code.
Additionally, the following files can provide values to override `app.json`: Additionally, the following files can provide values to override `config.json`:
- `app.local.js` or `app.local.json` - `config.local.js` or `config.local.json`
- `app.{env}.js` or `app.{env}.json`, where `{env}` is the value of `NODE_ENV` - `config.{env}.js` or `config.{env}.json`, where `{env}` is the value of `NODE_ENV`
(typically `development` or `production`) (typically `development` or `production`)
**NOTE:** The additional files can override the top-level keys with **NOTE:** The additional files can override the top-level keys with
@ -67,7 +80,7 @@ not supported at the moment.
#### Example settings #### Example settings
*app.json* *config.json*
```json ```json
{ {
@ -77,7 +90,7 @@ not supported at the moment.
} }
``` ```
*app.production.js* *config.production.js*
```js ```js
module.exports = { module.exports = {
@ -132,63 +145,67 @@ not supported at the moment.
} }
``` ```
### Models ### Models: definition
App models are loaded from the file `models.json`. Custom models are defined using JSON files in `models/` directory,
one JSON file per model.
#### Example models #### Example models
The following is example JSON for two `Model` definitions: The following are example JSON files for two `Model` definitions:
`Dealership` and `Location`. `Dealership` and `Location`.
*models/dealership.json*
```js ```js
{ {
// the key is the model name // the model name
"Dealership": { "name": "Dealership",
// a reference, by name, to a dataSource definition // the options passed to Model.extend(name, properties, options)
"dataSource": "my-db", "options": {
// the options passed to Model.extend(name, properties, options) "relations": {
"options": { "cars": {
"relations": { "type": "hasMany",
"cars": { "model": "Car",
"type": "hasMany", "foreignKey": "dealerId"
"model": "Car",
"foreignKey": "dealerId"
}
} }
},
// the properties passed to Model.extend(name, properties, options)
"properties": {
"id": {"id": true},
"name": "String",
"zip": "Number",
"address": "String"
} }
}, },
"Car": { // the properties passed to Model.extend(name, properties, options)
"dataSource": "my-db" "properties": {
// options can be specified at the top level too "id": {"id": true},
"relations": { "name": "String",
"dealer": { "zip": "Number",
"type": "belongsTo", "address": "String"
"model": "Dealership", }
"foreignKey": "dealerId" }
}, ```
}
"properties": { *models/car.json*
"id": { ```js
"type": "String", {
"required": true, "name": "Car",
"id": true // options can be specified at the top level too
}, "relations": {
"make": { "dealer": {
"type": "String", "type": "belongsTo",
"required": true "model": "Dealership",
}, "foreignKey": "dealerId"
"model": { },
"type": "String", }
"required": true "properties": {
} "id": {
"type": "String",
"required": true,
"id": true
},
"make": {
"type": "String",
"required": true
},
"model": {
"type": "String",
"required": true
} }
} }
} }
@ -196,26 +213,85 @@ The following is example JSON for two `Model` definitions:
#### Adding custom methods to models #### Adding custom methods to models
The models created from `models.json` come with the set of built-in methods The models created from JSON files come with the set of built-in methods
like `find` and `create`. To implement your custom methods, you should like `find` and `create`. To implement your custom methods, you should
create a javascript file in `models/` directory named after the model create a javascript file in `models/` directory with the same base-name
and define the methods there. as the JSON file containing model definition (e.g. `models/car.js` for
`models/car.json`) and define the methods there.
Example: Example:
*models/car.js* *models/car.js*
```js ```js
module.exports = function(app) { // Car is the model constructor
var Car = app.models.Car; // Base is the parent model (e.g. loopback.PersistedModel)
module.exports = function(Car, Base) {
// Define a static method
Car.customMethod = function(cb) {
// do some work
cb();
};
// Define an instance (prototype) method
Car.prototype.honk = function(duration, cb) { Car.prototype.honk = function(duration, cb) {
// make some noise for `duration` seconds // make some noise for `duration` seconds
cb(); cb();
}; };
// Provide a custom setup method
Car.setup = function() {
Base.setup.call(this);
// configure validations,
// configure remoting for methods, etc.
};
}; };
``` ```
### Models: configuration
Before the models can be used in a loopback application, they have to be
configured - attached to a data-source, exposed via the REST API, and so on.
The configuration is described in the file `models.json`:
```js
{
// the key is the model name
"Dealership": {
// a reference, by name, to a dataSource definition
"dataSource": "my-db"
},
"Car": {
"dataSource": "my-db",
// do not expose Car over the REST API
"public": false
}
}
```
The bootstrapper will automatically load definition of every custom model
configured in `models.json`. By default, the definition files are loaded from
`models/` subdirectory. However, it is possible to specify a different location
(or even multiple locations) via `_meta.sources`:
```js
{
"_meta": {
"sources": [
// all paths are relative to models.json
"./models"
"./node_modules/foobar/models"
]
},
// use the `FooBar` model from the `foobar` module
"FooBar": {
"dataSource": "db"
}
}
```
### Boot scripts ### Boot scripts
When the data sources and models are configured, the bootstrapper invokes When the data sources and models are configured, the bootstrapper invokes

View File

@ -6,6 +6,7 @@
"depth": 2 "depth": 2
}, },
"index.js", "index.js",
"browser.js" "browser.js",
"docs/migrating-from-1x-to-2x.md"
] ]
} }

View File

@ -0,0 +1,133 @@
## Migrating from 1.x to 2.x
**Starting point: a sample 1.x project**
*models.json*
```json
{
"car": {
"properties": {
"color": "string",
},
"dataSource": "db"
}
}
```
*models/car.js*
```js
var app = require('../app');
var Car = app.models.Car;
Car.prototype.honk = function(duration, cb) {
// make some noise for `duration` seconds
cb();
};
```
*app.js*
```js
var loopback = require('loopback');
var boot = require('loopback-boot');
var app = loopback();
boot(app, __dirname);
```
### App settings
The files with applications settings were renamed from `app.*` to `config.*`.
Rename the following files to upgrade a 1.x project for loopback-boot 2.x:
- `app.json` to `config.json`
- `app.local.json` to `config.local.json`
- `app.local.js` to `config.local.js`
- etc.
### Data sources
The configuration of data sources remains the same in both 1.x and 2.x
versions.
### Models
**The 2.x version of loopback-boot no longer creates Models, it's up to the
developer to create them before booting the app.**
The folder `models/` has a different semantincs in 2.x than in 1.x. Instead
of extending Models already defined by `app.boot` and `models.json`,
it provides a set of Model definitions that do not depend on
any application that may use them.
Perform the following steps to update a 1.x project for loopback-boot 2.x.
All code samples are referring to the sample project described above.
1. Move all Model-definition metadata from `models.json`
to new per-model json files in `models/` directory.
*models/car.json*
```json
{
"name": "car",
"properties": {
"color": "string",
}
}
```
*models.json*
```json
{
"car": {
"dataSource": "db"
}
}
```
2. Change per-model javascript files to export a function that adds
custom methods to the model class.
*models/car.js*
```js
module.exports = function(Car, Base) {
Car.prototype.honk = function(duration, cb) {
// make some noise for `duration` seconds
cb();
};
};
```
3. If your model definitions are not in `./models`, then add an entry
to `models.json` to specify the paths where to look for model definitions.
*models.json*
```json
{
"_meta": {
"sources": ["./custom/path/to/models"]
},
"Car": {
"dataSource": "db"
}
}
```
### Attaching built-in models
Models provided by LoopBack, such as `User` or `Role`, are no longer
automatically attached to default data-sources. The data-source configuration
entry `defaultForType` is silently ignored.
You have to explicitly configure all built-in models used by your application
in the `models.json` file.
```
{
"Role": { "dataSource": "db" }
}
```

View File

@ -20,10 +20,10 @@ var addInstructionsToBrowserify = require('./lib/bundler');
* 1. Creates DataSources from the `datasources.json` file in the application * 1. Creates DataSources from the `datasources.json` file in the application
* root directory. * root directory.
* *
* 2. Creates Models from the `models.json` file in the application * 2. Configures Models from the `models.json` file in the application
* root directory. * root directory.
* *
* If the argument is an object, then it looks for `model`, `dataSources`, * If the argument is an object, then it looks for `models`, `dataSources`,
* and `appRootDir` properties of the object. * and `appRootDir` properties of the object.
* If the object has no `appRootDir` property then it sets the current working * If the object has no `appRootDir` property then it sets the current working
* directory as the application root directory. * directory as the application root directory.
@ -31,10 +31,15 @@ var addInstructionsToBrowserify = require('./lib/bundler');
* *
* 1. Creates DataSources from the `options.dataSources` object. * 1. Creates DataSources from the `options.dataSources` object.
* *
* 2. Creates Models from the `options.models` object. * 2. Configures Models from the `options.models` object.
* *
* In both cases, the function loads JavaScript files in the `/models` and * In both cases, the function loads JavaScript files in the
* `/boot` subdirectories of the application root directory with `require()`. * `/boot` subdirectory of the application root directory with `require()`.
*
* **NOTE:** The version 2.0 of loopback-boot changed the way how models
* are created. The `models.json` file contains only configuration options like
* dataSource and extra relations. To define a model, create a per-model
* JSON file in `models/` directory.
* *
* **NOTE:** Mixing `bootLoopBackApp(app, bootConfig)` and * **NOTE:** Mixing `bootLoopBackApp(app, bootConfig)` and
* `app.model(name, modelConfig)` in multiple * `app.model(name, modelConfig)` in multiple
@ -47,19 +52,20 @@ var addInstructionsToBrowserify = require('./lib/bundler');
* @param app LoopBack application created by `loopback()`. * @param app LoopBack application created by `loopback()`.
* @options {String|Object} options Boot options; If String, this is * @options {String|Object} options Boot options; If String, this is
* the application root directory; if object, has below properties. * the application root directory; if object, has below properties.
* @property {String} appRootDir Directory to use when loading JSON and * @property {String} [appRootDir] Directory to use when loading JSON and
* JavaScript files (optional). * JavaScript files.
* Defaults to the current directory (`process.cwd()`). * Defaults to the current directory (`process.cwd()`).
* @property {Object} models Object containing `Model` definitions (optional). * @property {Object} [models] Object containing `Model` configurations.
* @property {Object} dataSources Object containing `DataSource` * @property {Object} [dataSources] Object containing `DataSource` definitions.
* definitions (optional). * @property {String} [modelsRootDir] Directory to use when loading
* @property {String} modelsRootDir Directory to use when loading `models.json` * `models.json`. Defaults to `appRootDir`.
* and `models/*.js`. Defaults to `appRootDir`. * @property {String} [dsRootDir] Directory to use when loading
* @property {String} datasourcesRootDir Directory to use when loading
* `datasources.json`. Defaults to `appRootDir`. * `datasources.json`. Defaults to `appRootDir`.
* @property {String} env Environment type, defaults to `process.env.NODE_ENV` * @property {String} [env] Environment type, defaults to `process.env.NODE_ENV`
* or `development`. Common values are `development`, `staging` and * or `development`. Common values are `development`, `staging` and
* `production`; however the applications are free to use any names. * `production`; however the applications are free to use any names.
* @property {Array.<String>} [modelSources] List of directories where to look
* for files containing model definitions.
* @end * @end
* *
* @header bootLoopBackApp(app, [options]) * @header bootLoopBackApp(app, [options])

View File

@ -9,32 +9,53 @@ var commondir = require('commondir');
*/ */
module.exports = function addInstructionsToBrowserify(instructions, bundler) { module.exports = function addInstructionsToBrowserify(instructions, bundler) {
bundleScripts(instructions.files, bundler); bundleModelScripts(instructions, bundler);
bundleOtherScripts(instructions, bundler);
bundleInstructions(instructions, bundler); bundleInstructions(instructions, bundler);
}; };
function bundleScripts(files, bundler) { function bundleOtherScripts(instructions, bundler) {
for (var key in files) { for (var key in instructions.files) {
var list = files[key]; addScriptsToBundle(key, instructions.files[key], bundler);
if (!list.length) continue; }
}
var root = commondir(files[key].map(path.dirname)); function bundleModelScripts(instructions, bundler) {
var files = instructions.models
.map(function(m) { return m.sourceFile; })
.filter(function(f) { return !!f; });
for (var ix in list) { var modelToFileMapping = instructions.models
var filepath = list[ix]; .map(function(m) { return files.indexOf(m.sourceFile); });
// Build a short unique id that does not expose too much addScriptsToBundle('models', files, bundler);
// information about the file system, but still preserves
// useful information about where is the file coming from.
var fileid = 'loopback-boot#' + key + '#' + path.relative(root, filepath);
// Add the file to the bundle. // Update `sourceFile` properties with the new paths
bundler.require(filepath, { expose: fileid }); modelToFileMapping.forEach(function(fileIx, modelIx) {
if (fileIx === -1) return;
instructions.models[modelIx].sourceFile = files[fileIx];
});
}
// Rewrite the instructions entry with the new id that will be function addScriptsToBundle(name, list, bundler) {
// used to load the file via `require(fileid)`. if (!list.length) return;
list[ix] = fileid;
} var root = commondir(list.map(path.dirname));
for (var ix in list) {
var filepath = list[ix];
// Build a short unique id that does not expose too much
// information about the file system, but still preserves
// useful information about where is the file coming from.
var fileid = 'loopback-boot#' + name + '#' + path.relative(root, filepath);
// Add the file to the bundle.
bundler.require(filepath, { expose: fileid });
// Rewrite the instructions entry with the new id that will be
// used to load the file via `require(fileid)`.
list[ix] = fileid;
} }
} }

View File

@ -1,6 +1,7 @@
var assert = require('assert'); var assert = require('assert');
var fs = require('fs'); var fs = require('fs');
var path = require('path'); var path = require('path');
var toposort = require('toposort');
var ConfigLoader = require('./config-loader'); var ConfigLoader = require('./config-loader');
var debug = require('debug')('loopback:boot:compiler'); var debug = require('debug')('loopback:boot:compiler');
@ -26,13 +27,13 @@ module.exports = function compile(options) {
var appRootDir = options.appRootDir = options.appRootDir || process.cwd(); var appRootDir = options.appRootDir = options.appRootDir || process.cwd();
var env = options.env || process.env.NODE_ENV || 'development'; var env = options.env || process.env.NODE_ENV || 'development';
var appConfig = options.app || ConfigLoader.loadAppConfig(appRootDir, env); var appConfig = options.config || ConfigLoader.loadAppConfig(appRootDir, env);
assertIsValidConfig('app', appConfig); assertIsValidConfig('app', appConfig);
var modelsRootDir = options.modelsRootDir || appRootDir; var modelsRootDir = options.modelsRootDir || appRootDir;
var modelsConfig = options.models || var modelsConfig = options.models ||
ConfigLoader.loadModels(modelsRootDir, env); ConfigLoader.loadModels(modelsRootDir, env);
assertIsValidConfig('model', modelsConfig); assertIsValidModelConfig(modelsConfig);
var dsRootDir = options.dsRootDir || appRootDir; var dsRootDir = options.dsRootDir || appRootDir;
var dataSourcesConfig = options.dataSources || var dataSourcesConfig = options.dataSources ||
@ -40,15 +41,20 @@ module.exports = function compile(options) {
assertIsValidConfig('data source', dataSourcesConfig); assertIsValidConfig('data source', dataSourcesConfig);
// require directories // require directories
var modelsScripts = findScripts(path.join(modelsRootDir, 'models'));
var bootScripts = findScripts(path.join(appRootDir, 'boot')); var bootScripts = findScripts(path.join(appRootDir, 'boot'));
var modelsMeta = modelsConfig._meta || {};
delete modelsConfig._meta;
var modelSources = modelsMeta.sources || ['./models'];
var modelInstructions = buildAllModelInstructions(
modelsRootDir, modelsConfig, modelSources);
return { return {
app: appConfig, config: appConfig,
dataSources: dataSourcesConfig, dataSources: dataSourcesConfig,
models: modelsConfig, models: modelInstructions,
files: { files: {
models: modelsScripts,
boot: bootScripts boot: bootScripts
} }
}; };
@ -61,6 +67,22 @@ function assertIsValidConfig(name, config) {
} }
} }
function assertIsValidModelConfig(config) {
assertIsValidConfig('model', config);
for (var name in config) {
var entry = config[name];
var options = entry.options || {};
var unsupported = entry.properties ||
entry.base || options.base ||
entry.plural || options.plural;
if (unsupported) {
throw new Error(
'The data in models.json is in the unsupported 1.x format.');
}
}
}
/** /**
* Find all javascript files (except for those prefixed with _) * Find all javascript files (except for those prefixed with _)
* and all directories. * and all directories.
@ -124,3 +146,139 @@ function tryReadDir() {
return []; return [];
} }
} }
function buildAllModelInstructions(rootDir, modelsConfig, sources) {
var registry = findModelDefinitions(rootDir, sources);
var modelNamesToBuild = addAllBaseModels(registry, Object.keys(modelsConfig));
var instructions = modelNamesToBuild
.map(function createModelInstructions(name) {
var config = modelsConfig[name];
var definition = registry[name] || {};
debug('Using model "%s"\nConfiguration: %j\nDefinition %j',
name, config, definition.definition);
return {
name: name,
config: config,
definition: definition.definition,
sourceFile: definition.sourceFile
};
});
return sortByInheritance(instructions);
}
function addAllBaseModels(registry, modelNames) {
var result = [];
var visited = {};
while (modelNames.length) {
var name = modelNames.shift();
if (visited[name]) continue;
visited[name] = true;
result.push(name);
var definition = registry[name] && registry[name].definition;
if (!definition) continue;
var base = getBaseModelName(definition);
// ignore built-in models like User
if (!registry[base]) continue;
modelNames.push(base);
}
return result;
}
function getBaseModelName(modelDefinition) {
if (!modelDefinition)
return undefined;
return modelDefinition.base ||
modelDefinition.options && modelDefinition.options.base;
}
function sortByInheritance(instructions) {
// create edges Base name -> Model name
var edges = instructions
.map(function(inst) {
return [getBaseModelName(inst.definition), inst.name];
});
var sortedNames = toposort(edges);
var instructionsByModelName = {};
instructions.forEach(function(inst) {
instructionsByModelName[inst.name] = inst;
});
return sortedNames
// convert to instructions
.map(function(name) {
return instructionsByModelName[name];
})
// remove built-in models
.filter(function(inst) {
return !!inst;
});
}
function findModelDefinitions(rootDir, sources) {
var registry = {};
sources.forEach(function(src) {
var srcDir = path.resolve(rootDir, src);
var files = tryReadDir(srcDir);
files
.filter(function(f) {
return f[0] !== '_' && path.extname(f) === '.json';
})
.forEach(function(f) {
var fullPath = path.resolve(srcDir, f);
var entry = loadModelDefinition(rootDir, fullPath);
var modelName = entry.definition.name;
if (!modelName) {
debug('Skipping model definition without Model name: %s',
path.relative(srcDir, fullPath));
return;
}
registry[modelName] = entry;
});
});
return registry;
}
function loadModelDefinition(rootDir, jsonFile) {
var definition = require(jsonFile);
var sourceFile = path.join(
path.dirname(jsonFile),
path.basename(jsonFile, path.extname(jsonFile)));
try {
// resolve the file to `.js` or any other supported extension like `.coffee`
sourceFile = require.resolve(sourceFile);
} catch (err) {
debug('Model source code not found: %s - %s', sourceFile, err.code || err);
sourceFile = undefined;
}
if (sourceFile === jsonFile)
sourceFile = undefined;
debug('Found model "%s" - %s %s', definition.name,
path.relative(rootDir, jsonFile),
sourceFile ? path.relative(rootDir, sourceFile) : '(no source file)');
return {
definition: definition,
sourceFile: sourceFile
};
}

View File

@ -10,7 +10,7 @@ var ConfigLoader = exports;
* @returns {Object} * @returns {Object}
*/ */
ConfigLoader.loadAppConfig = function(rootDir, env) { ConfigLoader.loadAppConfig = function(rootDir, env) {
return loadNamed(rootDir, env, 'app', mergeAppConfig); return loadNamed(rootDir, env, 'config', mergeAppConfig);
}; };
/** /**

View File

@ -23,7 +23,6 @@ module.exports = function execute(app, instructions) {
setupDataSources(app, instructions); setupDataSources(app, instructions);
setupModels(app, instructions); setupModels(app, instructions);
autoAttach(app);
runBootScripts(app, instructions); runBootScripts(app, instructions);
@ -66,7 +65,7 @@ function setHost(app, instructions) {
process.env.OPENSHIFT_SLS_IP || process.env.OPENSHIFT_SLS_IP ||
process.env.OPENSHIFT_NODEJS_IP || process.env.OPENSHIFT_NODEJS_IP ||
process.env.HOST || process.env.HOST ||
instructions.app.host || instructions.config.host ||
process.env.npm_package_config_host || process.env.npm_package_config_host ||
app.get('host'); app.get('host');
@ -83,7 +82,7 @@ function setPort(app, instructions) {
process.env.OPENSHIFT_SLS_PORT, process.env.OPENSHIFT_SLS_PORT,
process.env.OPENSHIFT_NODEJS_PORT, process.env.OPENSHIFT_NODEJS_PORT,
process.env.PORT, process.env.PORT,
instructions.app.port, instructions.config.port,
process.env.npm_package_config_port, process.env.npm_package_config_port,
app.get('port'), app.get('port'),
3000 3000
@ -99,7 +98,7 @@ function setPort(app, instructions) {
function setApiRoot(app, instructions) { function setApiRoot(app, instructions) {
var restApiRoot = var restApiRoot =
instructions.app.restApiRoot || instructions.config.restApiRoot ||
app.get('restApiRoot') || app.get('restApiRoot') ||
'/api'; '/api';
@ -112,7 +111,7 @@ function setApiRoot(app, instructions) {
} }
function applyAppConfig(app, instructions) { function applyAppConfig(app, instructions) {
var appConfig = instructions.app; var appConfig = instructions.config;
for(var configKey in appConfig) { for(var configKey in appConfig) {
var cur = app.get(configKey); var cur = app.get(configKey);
if(cur === undefined || cur === null) { if(cur === undefined || cur === null) {
@ -128,11 +127,46 @@ function setupDataSources(app, instructions) {
} }
function setupModels(app, instructions) { function setupModels(app, instructions) {
forEachKeyedObject(instructions.models, function(key, obj) { defineModels(app, instructions);
app.model(key, obj);
});
runScripts(app, instructions.files.models); 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);
});
}
function defineModels(app, instructions) {
instructions.models.forEach(function(data) {
var name = data.name;
var model;
if (!data.definition) {
model = app.loopback.getModel(name);
if (!model) {
throw new Error('Cannot configure unknown model ' + name);
}
debug('Configuring existing model %s', name);
} else {
debug('Creating new model %s %j', name, data.definition);
model = app.loopback.createModel(data.definition);
if (data.sourceFile) {
debug('Loading customization script %s', data.sourceFile);
var code = require(data.sourceFile);
if (typeof code === 'function') {
debug('Customizing model %s', name);
// NOTE model.super_ is set by Node's util.inherits
code(model, model.super_);
} else {
debug('Skipping model file %s - `module.exports` is not a function',
data.sourceFile);
}
}
}
data._model = model;
});
} }
function forEachKeyedObject(obj, fn) { function forEachKeyedObject(obj, fn) {
@ -147,16 +181,11 @@ function runScripts(app, list) {
if (!list || !list.length) return; if (!list || !list.length) return;
list.forEach(function(filepath) { list.forEach(function(filepath) {
var exports = tryRequire(filepath); var exports = tryRequire(filepath);
if (isFunctionNotModelCtor(exports, app.loopback.Model)) if (typeof exports === 'function')
exports(app); exports(app);
}); });
} }
function isFunctionNotModelCtor(fn, Model) {
return typeof fn === 'function' &&
!(fn.prototype instanceof Model);
}
function tryRequire(modulePath) { function tryRequire(modulePath) {
try { try {
return require.apply(this, arguments); return require.apply(this, arguments);
@ -170,19 +199,6 @@ function tryRequire(modulePath) {
} }
} }
// Deprecated, will be removed soon
function autoAttach(app) {
try {
app.loopback.autoAttach();
} catch(e) {
if(e.name === 'AssertionError') {
console.warn(e);
} else {
throw e;
}
}
}
function runBootScripts(app, instructions) { function runBootScripts(app, instructions) {
runScripts(app, instructions.files.boot); runScripts(app, instructions.files.boot);
} }
@ -192,7 +208,7 @@ function enableAnonymousSwagger(app, instructions) {
var swagger = app.remotes().exports.swagger; var swagger = app.remotes().exports.swagger;
if (!swagger) return; if (!swagger) return;
var appConfig = instructions.app; var appConfig = instructions.config;
var requireTokenForSwagger = appConfig.swagger && var requireTokenForSwagger = appConfig.swagger &&
appConfig.swagger.requireToken; appConfig.swagger.requireToken;
swagger.requireToken = requireTokenForSwagger || false; swagger.requireToken = requireTokenForSwagger || false;

View File

@ -1,6 +1,6 @@
{ {
"name": "loopback-boot", "name": "loopback-boot",
"version": "1.1.0", "version": "2.0.0-beta1",
"description": "Convention-based bootstrapper for LoopBack applications", "description": "Convention-based bootstrapper for LoopBack applications",
"keywords": [ "keywords": [
"StrongLoop", "StrongLoop",
@ -26,6 +26,7 @@
"commondir": "0.0.1", "commondir": "0.0.1",
"debug": "^0.8.1", "debug": "^0.8.1",
"semver": "^2.3.0", "semver": "^2.3.0",
"toposort": "^0.2.10",
"underscore": "^1.6.0" "underscore": "^1.6.0"
}, },
"devDependencies": { "devDependencies": {

View File

@ -17,6 +17,9 @@ describe('browser support', function() {
// configured in fixtures/browser-app/boot/configure.js // configured in fixtures/browser-app/boot/configure.js
expect(app.settings).to.have.property('custom-key', 'custom-value'); expect(app.settings).to.have.property('custom-key', 'custom-value');
expect(Object.keys(app.models)).to.include('Customer');
expect(app.models.Customer.settings)
.to.have.property('_customized', 'Customer');
done(); done();
}); });
@ -53,12 +56,17 @@ function executeBundledApp(bundlePath) {
} }
function createBrowserLikeContext() { function createBrowserLikeContext() {
return vm.createContext({ var context = {
// required by browserify // required by browserify
XMLHttpRequest: function() { throw new Error('not implemented'); }, XMLHttpRequest: function() { throw new Error('not implemented'); },
// used by loopback to detect browser runtime localStorage: {
window: {}, // used by `debug` module
debug: process.env.DEBUG
},
// used by `debug` module
document: { documentElement: { style: {} } },
// allow the browserified code to log messages // allow the browserified code to log messages
// call `printContextLogs(context)` to print the accumulated messages // call `printContextLogs(context)` to print the accumulated messages
@ -78,7 +86,12 @@ function createBrowserLikeContext() {
error: [] error: []
}, },
} }
}); };
// `window` is used by loopback to detect browser runtime
context.window = context;
return vm.createContext(context);
} }
function printContextLogs(context) { function printContextLogs(context) {

View File

@ -1,7 +1,6 @@
var boot = require('../'); var boot = require('../');
var fs = require('fs-extra'); var fs = require('fs-extra');
var path = require('path'); var path = require('path');
var assert = require('assert');
var expect = require('must'); var expect = require('must');
var sandbox = require('./helpers/sandbox'); var sandbox = require('./helpers/sandbox');
var appdir = require('./helpers/appdir'); var appdir = require('./helpers/appdir');
@ -16,7 +15,7 @@ describe('compiler', function() {
var options, instructions, appConfig; var options, instructions, appConfig;
beforeEach(function() { beforeEach(function() {
options = { options = {
app: { config: {
port: 3000, port: 3000,
host: '127.0.0.1', host: '127.0.0.1',
restApiRoot: '/rest-api', restApiRoot: '/rest-api',
@ -25,9 +24,6 @@ describe('compiler', function() {
}, },
models: { models: {
'foo-bar-bat-baz': { 'foo-bar-bat-baz': {
options: {
plural: 'foo-bar-bat-bazzies'
},
dataSource: 'the-db' dataSource: 'the-db'
} }
}, },
@ -39,7 +35,7 @@ describe('compiler', function() {
} }
}; };
instructions = boot.compile(options); instructions = boot.compile(options);
appConfig = instructions.app; appConfig = instructions.config;
}); });
it('has port setting', function() { it('has port setting', function() {
@ -62,7 +58,15 @@ describe('compiler', function() {
}); });
it('has models definition', function() { it('has models definition', function() {
expect(instructions.models).to.eql(options.models); expect(instructions.models).to.have.length(1);
expect(instructions.models[0]).to.eql({
name: 'foo-bar-bat-baz',
config: {
dataSource: 'the-db'
},
definition: undefined,
sourceFile: undefined
});
}); });
it('has datasources definition', function() { it('has datasources definition', function() {
@ -73,8 +77,16 @@ describe('compiler', function() {
describe('from directory', function() { describe('from directory', function() {
it('loads config files', function() { it('loads config files', function() {
var instructions = boot.compile(SIMPLE_APP); var instructions = boot.compile(SIMPLE_APP);
assert(instructions.models.foo);
assert(instructions.models.foo.dataSource); expect(instructions.models).to.have.length(1);
expect(instructions.models[0]).to.eql({
name: 'User',
config: {
dataSource: 'db'
},
definition: undefined,
sourceFile: undefined
});
}); });
it('merges datasource configs from multiple files', function() { it('merges datasource configs from multiple files', function() {
@ -136,13 +148,14 @@ describe('compiler', function() {
it('merges app configs from multiple files', function() { it('merges app configs from multiple files', function() {
appdir.createConfigFilesSync(); appdir.createConfigFilesSync();
appdir.writeConfigFileSync('app.local.json', { cfgLocal: 'applied' }); appdir.writeConfigFileSync('config.local.json', { cfgLocal: 'applied' });
var env = process.env.NODE_ENV || 'development'; var env = process.env.NODE_ENV || 'development';
appdir.writeConfigFileSync('app.' + env + '.json', { cfgEnv: 'applied' }); appdir.writeConfigFileSync('config.' + env + '.json',
{ cfgEnv: 'applied' });
var instructions = boot.compile(appdir.PATH); var instructions = boot.compile(appdir.PATH);
var appConfig = instructions.app; var appConfig = instructions.config;
expect(appConfig).to.have.property('cfgLocal', 'applied'); expect(appConfig).to.have.property('cfgLocal', 'applied');
expect(appConfig).to.have.property('cfgEnv', 'applied'); expect(appConfig).to.have.property('cfgEnv', 'applied');
@ -157,11 +170,11 @@ describe('compiler', function() {
it('supports .js for custom app config files', function() { it('supports .js for custom app config files', function() {
appdir.createConfigFilesSync(); appdir.createConfigFilesSync();
appdir.writeFileSync('app.local.js', appdir.writeFileSync('config.local.js',
'module.exports = { fromJs: true };'); 'module.exports = { fromJs: true };');
var instructions = boot.compile(appdir.PATH); var instructions = boot.compile(appdir.PATH);
var appConfig = instructions.app; var appConfig = instructions.config;
expect(appConfig).to.have.property('fromJs', true); expect(appConfig).to.have.property('fromJs', true);
}); });
@ -189,15 +202,13 @@ describe('compiler', function() {
foo: { dataSource: 'db' } foo: { dataSource: 'db' }
}); });
var fooJs = appdir.writeFileSync('custom/models/foo.js', '');
var instructions = boot.compile({ var instructions = boot.compile({
appRootDir: appdir.PATH, appRootDir: appdir.PATH,
modelsRootDir: path.resolve(appdir.PATH, 'custom') modelsRootDir: path.resolve(appdir.PATH, 'custom')
}); });
expect(instructions.models).to.have.property('foo'); expect(instructions.models).to.have.length(1);
expect(instructions.files.models).to.eql([fooJs]); expect(instructions.models[0]).to.have.property('name', 'foo');
}); });
it('includes boot/*.js scripts', function() { it('includes boot/*.js scripts', function() {
@ -208,13 +219,192 @@ describe('compiler', function() {
expect(instructions.files.boot).to.eql([initJs]); expect(instructions.files.boot).to.eql([initJs]);
}); });
it('supports models/ subdirectires that are not require()able', function() { it('ignores models/ subdirectory', function() {
appdir.createConfigFilesSync(); appdir.createConfigFilesSync();
appdir.writeFileSync('models/test/model.test.js', appdir.writeFileSync('models/my-model.js', '');
'throw new Error("should not been called");');
var instructions = boot.compile(appdir.PATH); var instructions = boot.compile(appdir.PATH);
expect(instructions.files.models).to.eql([]); expect(instructions.files).to.not.have.property('models');
});
it('throws when models.json contains `properties` from 1.x', function() {
appdir.createConfigFilesSync({}, {}, {
foo: { properties: { name: 'string' } }
});
expect(function() { boot.compile(appdir.PATH); })
.to.throw(/unsupported 1\.x format/);
});
it('throws when models.json contains `options.base` from 1.x', function() {
appdir.createConfigFilesSync({}, {}, {
Customer: { options: { base: 'User' } }
});
expect(function() { boot.compile(appdir.PATH); })
.to.throw(/unsupported 1\.x format/);
});
it('loads models from `./models`', function() {
appdir.createConfigFilesSync({}, {}, {
Car: { dataSource: 'db' }
});
appdir.writeConfigFileSync('models/car.json', { name: 'Car' });
appdir.writeFileSync('models/car.js', '');
var instructions = boot.compile(appdir.PATH);
expect(instructions.models).to.have.length(1);
expect(instructions.models[0]).to.eql({
name: 'Car',
config: {
dataSource: 'db'
},
definition: {
name: 'Car'
},
sourceFile: path.resolve(appdir.PATH, 'models', 'car.js')
});
});
it('supports `sources` option in `models.json`', function() {
appdir.createConfigFilesSync({}, {}, {
_meta: {
sources: ['./custom-models']
},
Car: { dataSource: 'db' }
});
appdir.writeConfigFileSync('custom-models/car.json', { name: 'Car' });
appdir.writeFileSync('custom-models/car.js', '');
var instructions = boot.compile(appdir.PATH);
expect(instructions.models).to.have.length(1);
expect(instructions.models[0]).to.eql({
name: 'Car',
config: {
dataSource: 'db'
},
definition: {
name: 'Car'
},
sourceFile: path.resolve(appdir.PATH, 'custom-models', 'car.js')
});
});
it('handles model definitions with no code', function() {
appdir.createConfigFilesSync({}, {}, {
Car: { dataSource: 'db' }
});
appdir.writeConfigFileSync('models/car.json', { name: 'Car' });
var instructions = boot.compile(appdir.PATH);
expect(instructions.models).to.eql([{
name: 'Car',
config: {
dataSource: 'db'
},
definition: {
name: 'Car'
},
sourceFile: undefined
}]);
});
it('excludes models not listed in `models.json`', function() {
appdir.createConfigFilesSync({}, {}, {
Car: { dataSource: 'db' }
});
appdir.writeConfigFileSync('models/car.json', { name: 'Car' });
appdir.writeConfigFileSync('models/bar.json', { name: 'Bar' });
var instructions = boot.compile(appdir.PATH);
var models = instructions.models.map(getNameProperty);
expect(models).to.eql(['Car']);
});
it('includes models used as Base models', function() {
appdir.createConfigFilesSync({}, {}, {
Car: { dataSource: 'db' }
});
appdir.writeConfigFileSync('models/car.json', {
name: 'Car',
base: 'Vehicle'
});
appdir.writeConfigFileSync('models/vehicle.json', {
name: 'Vehicle'
});
var instructions = boot.compile(appdir.PATH);
var models = instructions.models;
var modelNames = models.map(getNameProperty);
expect(modelNames).to.eql(['Vehicle', 'Car']);
expect(models[0].config).to.equal(undefined);
});
it('excludes pre-built base models', function() {
appdir.createConfigFilesSync({}, {}, {
Car: { dataSource: 'db' }
});
appdir.writeConfigFileSync('models/car.json', {
name: 'Car',
base: 'Model'
});
var instructions = boot.compile(appdir.PATH);
var modelNames = instructions.models.map(getNameProperty);
expect(modelNames).to.eql(['Car']);
});
it('sorts models, base models first', function() {
appdir.createConfigFilesSync({}, {}, {
Vehicle: { dataSource: 'db' },
FlyingCar: { dataSource: 'db' },
Car: { dataSource: 'db' }
});
appdir.writeConfigFileSync('models/car.json', {
name: 'Car',
base: 'Vehicle'
});
appdir.writeConfigFileSync('models/vehicle.json', {
name: 'Vehicle'
});
appdir.writeConfigFileSync('models/flying-car.json', {
name: 'FlyingCar',
base: 'Car'
});
var instructions = boot.compile(appdir.PATH);
var modelNames = instructions.models.map(getNameProperty);
expect(modelNames).to.eql(['Vehicle', 'Car', 'FlyingCar']);
});
it('detects circular Model dependencies', function() {
appdir.createConfigFilesSync({}, {}, {
Vehicle: { dataSource: 'db' },
Car: { dataSource: 'db' }
});
appdir.writeConfigFileSync('models/car.json', {
name: 'Car',
base: 'Vehicle'
});
appdir.writeConfigFileSync('models/vehicle.json', {
name: 'Vehicle',
base: 'Car'
});
expect(function() { boot.compile(appdir.PATH); })
.to.throw(/cyclic dependency/i);
}); });
}); });
}); });
function getNameProperty(obj) {
return obj.name;
}

View File

@ -10,7 +10,6 @@ var SIMPLE_APP = path.join(__dirname, 'fixtures', 'simple-app');
var app; var app;
describe('executor', function() { describe('executor', function() {
beforeEach(sandbox.reset); beforeEach(sandbox.reset);
@ -21,21 +20,21 @@ describe('executor', function() {
}); });
var dummyInstructions = someInstructions({ var dummyInstructions = someInstructions({
app: { config: {
port: 3000, port: 3000,
host: '127.0.0.1', host: '127.0.0.1',
restApiRoot: '/rest-api', restApiRoot: '/rest-api',
foo: { bar: 'bat' }, foo: { bar: 'bat' },
baz: true baz: true
}, },
models: { models: [
'foo-bar-bat-baz': { {
options: { name: 'User',
plural: 'foo-bar-bat-bazzies' config: {
}, dataSource: 'the-db'
dataSource: 'the-db' }
} }
}, ],
dataSources: { dataSources: {
'the-db': { 'the-db': {
connector: 'memory', connector: 'memory',
@ -44,19 +43,101 @@ describe('executor', function() {
} }
}); });
it('instantiates models', function() { it('configures models', function() {
boot.execute(app, dummyInstructions); boot.execute(app, dummyInstructions);
assert(app.models); assert(app.models);
assert(app.models.FooBarBatBaz); assert(app.models.User);
assert(app.models.fooBarBatBaz); assert.equal(app.models.User, loopback.User,
assertValidDataSource(app.models.FooBarBatBaz.dataSource); 'Boot should not have extended loopback.User model');
assert.isFunc(app.models.FooBarBatBaz, 'find'); assertValidDataSource(app.models.User.dataSource);
assert.isFunc(app.models.FooBarBatBaz, 'create'); assert.isFunc(app.models.User, 'find');
assert.isFunc(app.models.User, 'create');
});
it('defines and customizes models', function() {
appdir.writeFileSync('models/Customer.js', 'module.exports = ' +
function(Customer, Base) {
Customer.settings._customized = 'Customer';
Base.settings._customized = 'Base';
}.toString());
boot.execute(app, someInstructions({
models: [
{
name: 'Customer',
config: { dataSource: 'db' },
definition: {
name: 'Customer',
base: 'User',
},
sourceFile: path.resolve(appdir.PATH, 'models', 'Customer.js')
}
]
}));
expect(app.models.Customer).to.exist();
expect(app.models.Customer.settings._customized).to.be.equal('Customer');
expect(loopback.User.settings._customized).to.equal('Base');
});
it('defines model without attaching it', function() {
boot.execute(app, someInstructions({
models: [
{
name: 'Vehicle',
config: undefined,
definition: {
name: 'Vehicle'
},
sourceFile: undefined
},
{
name: 'Car',
config: { dataSource: 'db' },
definition: {
name: 'Car',
base: 'Vehicle',
},
sourceFile: undefined
},
]
}));
expect(Object.keys(app.models)).to.eql(['Car']);
}); });
it('attaches models to data sources', function() { it('attaches models to data sources', function() {
boot.execute(app, dummyInstructions); boot.execute(app, dummyInstructions);
assert.equal(app.models.FooBarBatBaz.dataSource, app.dataSources.theDb); assert.equal(app.models.User.dataSource, app.dataSources.theDb);
});
it('defines all models first before running the config phase', function() {
appdir.writeFileSync('models/Customer.js', 'module.exports = ' +
function(Customer/*, Base*/) {
Customer.on('attached', function() {
Customer._modelsWhenAttached =
Object.keys(Customer.modelBuilder.models);
});
}.toString());
boot.execute(app, someInstructions({
models: [
{
name: 'Customer',
config: { dataSource: 'db' },
definition: { name: 'Customer' },
sourceFile: path.resolve(appdir.PATH, 'models', 'Customer.js')
},
{
name: 'UniqueName',
config: { dataSource: 'db' },
definition: { name: 'UniqueName' },
sourceFile: undefined
}
]
}));
expect(app.models.Customer._modelsWhenAttached).to.include('UniqueName');
}); });
it('instantiates data sources', function() { it('instantiates data sources', function() {
@ -67,6 +148,17 @@ describe('executor', function() {
assert(app.dataSources.TheDb); assert(app.dataSources.TheDb);
}); });
it('does not call autoAttach', function() {
boot.execute(app, dummyInstructions);
// loopback-datasource-juggler quirk:
// Model.dataSources has modelBuilder as the default value,
// therefore it's not enough to assert a false-y value
var actual = loopback.Email.dataSource instanceof loopback.DataSource ?
'attached' : 'not attached';
expect(actual).to.equal('not attached');
});
describe('with boot and models files', function() { describe('with boot and models files', function() {
beforeEach(function() { beforeEach(function() {
boot.execute(app, simpleAppInstructions()); boot.execute(app, simpleAppInstructions());
@ -76,18 +168,13 @@ describe('executor', function() {
assert(process.loadedFooJS); assert(process.loadedFooJS);
delete 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() { describe('with PaaS and npm env variables', function() {
function bootWithDefaults() { function bootWithDefaults() {
app = loopback(); app = loopback();
boot.execute(app, someInstructions({ boot.execute(app, someInstructions({
app: { config: {
port: undefined, port: undefined,
host: undefined host: undefined
} }
@ -155,25 +242,16 @@ describe('executor', function() {
} }
it('should honor 0 for free port', function() { it('should honor 0 for free port', function() {
boot.execute(app, someInstructions({ app: { port: 0 } })); boot.execute(app, someInstructions({ config: { port: 0 } }));
assert.equal(app.get('port'), 0); assert.equal(app.get('port'), 0);
}); });
it('should default to port 3000', function() { it('should default to port 3000', function() {
boot.execute(app, someInstructions({ app: { port: undefined } })); boot.execute(app, someInstructions({ config: { port: undefined } }));
assert.equal(app.get('port'), 3000); 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() { it('calls function exported by boot/init.js', function() {
var file = appdir.writeFileSync('boot/init.js', var file = appdir.writeFileSync('boot/init.js',
'module.exports = function(app) { app.fnCalled = true; };'); 'module.exports = function(app) { app.fnCalled = true; };');
@ -182,19 +260,6 @@ describe('executor', function() {
boot.execute(app, someInstructions({ files: { boot: [ file ] } })); boot.execute(app, someInstructions({ files: { boot: [ file ] } }));
expect(app.fnCalled, 'exported fn was called').to.be.true(); 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();
});
}); });
@ -217,11 +282,10 @@ assert.isFunc = function (obj, name) {
function someInstructions(values) { function someInstructions(values) {
var result = { var result = {
app: values.app || {}, config: values.config || {},
models: values.models || {}, models: values.models || [],
dataSources: values.dataSources || {}, dataSources: values.dataSources || { db: { connector: 'memory' } },
files: { files: {
models: [],
boot: [] boot: []
} }
}; };

View File

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

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

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

View File

@ -0,0 +1,4 @@
module.exports = function(Customer, Base) {
Customer.settings._customized = 'Customer';
Base.settings._customized = 'Base';
};

View File

@ -0,0 +1,4 @@
{
"name": "Customer",
"base": "User"
}

View File

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

View File

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

View File

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

View File

@ -22,7 +22,7 @@ appdir.init = function(cb) {
appdir.createConfigFilesSync = function(appConfig, dataSources, models) { appdir.createConfigFilesSync = function(appConfig, dataSources, models) {
appConfig = extend({ appConfig = extend({
}, appConfig); }, appConfig);
appdir.writeConfigFileSync ('app.json', appConfig); appdir.writeConfigFileSync ('config.json', appConfig);
dataSources = extend({ dataSources = extend({
db: { db: {