Merge branch 'release/2.0.0-beta1' into production
This commit is contained in:
commit
fc18561cc7
200
README.md
200
README.md
|
@ -26,16 +26,29 @@ app.listen();
|
|||
See [API docs](http://apidocs.strongloop.com/loopback-boot/#api) for
|
||||
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
|
||||
|
||||
The bootstrapping process takes care of the following tasks:
|
||||
|
||||
- Configuration of data-sources.
|
||||
- Definition and configuration of custom Models, attaching models to
|
||||
data-sources.
|
||||
- Definition of custom Models
|
||||
- Configuration of models, attaching models to data-sources.
|
||||
- Configuration of app settings like `host`, `port` or `restApiRoot`.
|
||||
- Running additional boot scripts to keep the custom setup code in multiple
|
||||
small files as opposed to keeping everything in the main app file.
|
||||
- Running additional boot scripts, so that the custom setup code can be kept
|
||||
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
|
||||
of the project files.
|
||||
|
@ -43,7 +56,7 @@ of the project files.
|
|||
```
|
||||
project/
|
||||
app.js
|
||||
app.json
|
||||
config.json
|
||||
datasources.json
|
||||
models.json
|
||||
models/
|
||||
|
@ -52,13 +65,13 @@ project/
|
|||
|
||||
### 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.
|
||||
|
||||
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`
|
||||
- `app.{env}.js` or `app.{env}.json`, where `{env}` is the value of `NODE_ENV`
|
||||
- `config.local.js` or `config.local.json`
|
||||
- `config.{env}.js` or `config.{env}.json`, where `{env}` is the value of `NODE_ENV`
|
||||
(typically `development` or `production`)
|
||||
|
||||
**NOTE:** The additional files can override the top-level keys with
|
||||
|
@ -67,7 +80,7 @@ not supported at the moment.
|
|||
|
||||
#### Example settings
|
||||
|
||||
*app.json*
|
||||
*config.json*
|
||||
|
||||
```json
|
||||
{
|
||||
|
@ -77,7 +90,7 @@ not supported at the moment.
|
|||
}
|
||||
```
|
||||
|
||||
*app.production.js*
|
||||
*config.production.js*
|
||||
|
||||
```js
|
||||
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
|
||||
|
||||
The following is example JSON for two `Model` definitions:
|
||||
The following are example JSON files for two `Model` definitions:
|
||||
`Dealership` and `Location`.
|
||||
|
||||
*models/dealership.json*
|
||||
|
||||
```js
|
||||
{
|
||||
// the key is the model name
|
||||
"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 model name
|
||||
"name": "Dealership",
|
||||
// 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"
|
||||
// options can be specified at the top level too
|
||||
"relations": {
|
||||
"dealer": {
|
||||
"type": "belongsTo",
|
||||
"model": "Dealership",
|
||||
"foreignKey": "dealerId"
|
||||
},
|
||||
}
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "String",
|
||||
"required": true,
|
||||
"id": true
|
||||
},
|
||||
"make": {
|
||||
"type": "String",
|
||||
"required": true
|
||||
},
|
||||
"model": {
|
||||
"type": "String",
|
||||
"required": true
|
||||
}
|
||||
// the properties passed to Model.extend(name, properties, options)
|
||||
"properties": {
|
||||
"id": {"id": true},
|
||||
"name": "String",
|
||||
"zip": "Number",
|
||||
"address": "String"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
*models/car.json*
|
||||
```js
|
||||
{
|
||||
"name": "Car",
|
||||
// options can be specified at the top level too
|
||||
"relations": {
|
||||
"dealer": {
|
||||
"type": "belongsTo",
|
||||
"model": "Dealership",
|
||||
"foreignKey": "dealerId"
|
||||
},
|
||||
}
|
||||
"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
|
||||
|
||||
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
|
||||
create a javascript file in `models/` directory named after the model
|
||||
and define the methods there.
|
||||
create a javascript file in `models/` directory with the same base-name
|
||||
as the JSON file containing model definition (e.g. `models/car.js` for
|
||||
`models/car.json`) and define the methods there.
|
||||
|
||||
Example:
|
||||
|
||||
*models/car.js*
|
||||
|
||||
```js
|
||||
module.exports = function(app) {
|
||||
var Car = app.models.Car;
|
||||
// Car is the model constructor
|
||||
// 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) {
|
||||
// make some noise for `duration` seconds
|
||||
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
|
||||
|
||||
When the data sources and models are configured, the bootstrapper invokes
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
"depth": 2
|
||||
},
|
||||
"index.js",
|
||||
"browser.js"
|
||||
"browser.js",
|
||||
"docs/migrating-from-1x-to-2x.md"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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" }
|
||||
}
|
||||
```
|
34
index.js
34
index.js
|
@ -20,10 +20,10 @@ var addInstructionsToBrowserify = require('./lib/bundler');
|
|||
* 1. Creates DataSources from the `datasources.json` file in the application
|
||||
* 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.
|
||||
*
|
||||
* 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.
|
||||
* If the object has no `appRootDir` property then it sets the current working
|
||||
* directory as the application root directory.
|
||||
|
@ -31,10 +31,15 @@ var addInstructionsToBrowserify = require('./lib/bundler');
|
|||
*
|
||||
* 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
|
||||
* `/boot` subdirectories of the application root directory with `require()`.
|
||||
* In both cases, the function loads JavaScript files in the
|
||||
* `/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
|
||||
* `app.model(name, modelConfig)` in multiple
|
||||
|
@ -47,19 +52,20 @@ var addInstructionsToBrowserify = require('./lib/bundler');
|
|||
* @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).
|
||||
* @property {String} [appRootDir] Directory to use when loading JSON and
|
||||
* JavaScript files.
|
||||
* 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
|
||||
* @property {Object} [models] Object containing `Model` configurations.
|
||||
* @property {Object} [dataSources] Object containing `DataSource` definitions.
|
||||
* @property {String} [modelsRootDir] Directory to use when loading
|
||||
* `models.json`. Defaults to `appRootDir`.
|
||||
* @property {String} [dsRootDir] Directory to use when loading
|
||||
* `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
|
||||
* `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
|
||||
*
|
||||
* @header bootLoopBackApp(app, [options])
|
||||
|
|
|
@ -9,32 +9,53 @@ var commondir = require('commondir');
|
|||
*/
|
||||
|
||||
module.exports = function addInstructionsToBrowserify(instructions, bundler) {
|
||||
bundleScripts(instructions.files, bundler);
|
||||
bundleModelScripts(instructions, bundler);
|
||||
bundleOtherScripts(instructions, bundler);
|
||||
bundleInstructions(instructions, bundler);
|
||||
};
|
||||
|
||||
function bundleScripts(files, bundler) {
|
||||
for (var key in files) {
|
||||
var list = files[key];
|
||||
if (!list.length) continue;
|
||||
function bundleOtherScripts(instructions, bundler) {
|
||||
for (var key in instructions.files) {
|
||||
addScriptsToBundle(key, instructions.files[key], bundler);
|
||||
}
|
||||
}
|
||||
|
||||
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 filepath = list[ix];
|
||||
var modelToFileMapping = instructions.models
|
||||
.map(function(m) { return files.indexOf(m.sourceFile); });
|
||||
|
||||
// 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#' + key + '#' + path.relative(root, filepath);
|
||||
addScriptsToBundle('models', files, bundler);
|
||||
|
||||
// Add the file to the bundle.
|
||||
bundler.require(filepath, { expose: fileid });
|
||||
// Update `sourceFile` properties with the new paths
|
||||
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
|
||||
// used to load the file via `require(fileid)`.
|
||||
list[ix] = fileid;
|
||||
}
|
||||
function addScriptsToBundle(name, list, bundler) {
|
||||
if (!list.length) return;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
170
lib/compiler.js
170
lib/compiler.js
|
@ -1,6 +1,7 @@
|
|||
var assert = require('assert');
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var toposort = require('toposort');
|
||||
var ConfigLoader = require('./config-loader');
|
||||
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 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);
|
||||
|
||||
var modelsRootDir = options.modelsRootDir || appRootDir;
|
||||
var modelsConfig = options.models ||
|
||||
ConfigLoader.loadModels(modelsRootDir, env);
|
||||
assertIsValidConfig('model', modelsConfig);
|
||||
assertIsValidModelConfig(modelsConfig);
|
||||
|
||||
var dsRootDir = options.dsRootDir || appRootDir;
|
||||
var dataSourcesConfig = options.dataSources ||
|
||||
|
@ -40,15 +41,20 @@ module.exports = function compile(options) {
|
|||
assertIsValidConfig('data source', dataSourcesConfig);
|
||||
|
||||
// require directories
|
||||
var modelsScripts = findScripts(path.join(modelsRootDir, 'models'));
|
||||
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 {
|
||||
app: appConfig,
|
||||
config: appConfig,
|
||||
dataSources: dataSourcesConfig,
|
||||
models: modelsConfig,
|
||||
models: modelInstructions,
|
||||
files: {
|
||||
models: modelsScripts,
|
||||
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 _)
|
||||
* and all directories.
|
||||
|
@ -124,3 +146,139 @@ function tryReadDir() {
|
|||
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
|
||||
};
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ var ConfigLoader = exports;
|
|||
* @returns {Object}
|
||||
*/
|
||||
ConfigLoader.loadAppConfig = function(rootDir, env) {
|
||||
return loadNamed(rootDir, env, 'app', mergeAppConfig);
|
||||
return loadNamed(rootDir, env, 'config', mergeAppConfig);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -23,7 +23,6 @@ module.exports = function execute(app, instructions) {
|
|||
|
||||
setupDataSources(app, instructions);
|
||||
setupModels(app, instructions);
|
||||
autoAttach(app);
|
||||
|
||||
runBootScripts(app, instructions);
|
||||
|
||||
|
@ -66,7 +65,7 @@ function setHost(app, instructions) {
|
|||
process.env.OPENSHIFT_SLS_IP ||
|
||||
process.env.OPENSHIFT_NODEJS_IP ||
|
||||
process.env.HOST ||
|
||||
instructions.app.host ||
|
||||
instructions.config.host ||
|
||||
process.env.npm_package_config_host ||
|
||||
app.get('host');
|
||||
|
||||
|
@ -83,7 +82,7 @@ function setPort(app, instructions) {
|
|||
process.env.OPENSHIFT_SLS_PORT,
|
||||
process.env.OPENSHIFT_NODEJS_PORT,
|
||||
process.env.PORT,
|
||||
instructions.app.port,
|
||||
instructions.config.port,
|
||||
process.env.npm_package_config_port,
|
||||
app.get('port'),
|
||||
3000
|
||||
|
@ -99,7 +98,7 @@ function setPort(app, instructions) {
|
|||
|
||||
function setApiRoot(app, instructions) {
|
||||
var restApiRoot =
|
||||
instructions.app.restApiRoot ||
|
||||
instructions.config.restApiRoot ||
|
||||
app.get('restApiRoot') ||
|
||||
'/api';
|
||||
|
||||
|
@ -112,7 +111,7 @@ function setApiRoot(app, instructions) {
|
|||
}
|
||||
|
||||
function applyAppConfig(app, instructions) {
|
||||
var appConfig = instructions.app;
|
||||
var appConfig = instructions.config;
|
||||
for(var configKey in appConfig) {
|
||||
var cur = app.get(configKey);
|
||||
if(cur === undefined || cur === null) {
|
||||
|
@ -128,11 +127,46 @@ function setupDataSources(app, instructions) {
|
|||
}
|
||||
|
||||
function setupModels(app, instructions) {
|
||||
forEachKeyedObject(instructions.models, function(key, obj) {
|
||||
app.model(key, obj);
|
||||
});
|
||||
defineModels(app, instructions);
|
||||
|
||||
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) {
|
||||
|
@ -147,16 +181,11 @@ function runScripts(app, list) {
|
|||
if (!list || !list.length) return;
|
||||
list.forEach(function(filepath) {
|
||||
var exports = tryRequire(filepath);
|
||||
if (isFunctionNotModelCtor(exports, app.loopback.Model))
|
||||
if (typeof exports === 'function')
|
||||
exports(app);
|
||||
});
|
||||
}
|
||||
|
||||
function isFunctionNotModelCtor(fn, Model) {
|
||||
return typeof fn === 'function' &&
|
||||
!(fn.prototype instanceof Model);
|
||||
}
|
||||
|
||||
function tryRequire(modulePath) {
|
||||
try {
|
||||
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) {
|
||||
runScripts(app, instructions.files.boot);
|
||||
}
|
||||
|
@ -192,7 +208,7 @@ function enableAnonymousSwagger(app, instructions) {
|
|||
var swagger = app.remotes().exports.swagger;
|
||||
if (!swagger) return;
|
||||
|
||||
var appConfig = instructions.app;
|
||||
var appConfig = instructions.config;
|
||||
var requireTokenForSwagger = appConfig.swagger &&
|
||||
appConfig.swagger.requireToken;
|
||||
swagger.requireToken = requireTokenForSwagger || false;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "loopback-boot",
|
||||
"version": "1.1.0",
|
||||
"version": "2.0.0-beta1",
|
||||
"description": "Convention-based bootstrapper for LoopBack applications",
|
||||
"keywords": [
|
||||
"StrongLoop",
|
||||
|
@ -26,6 +26,7 @@
|
|||
"commondir": "0.0.1",
|
||||
"debug": "^0.8.1",
|
||||
"semver": "^2.3.0",
|
||||
"toposort": "^0.2.10",
|
||||
"underscore": "^1.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -17,6 +17,9 @@ describe('browser support', function() {
|
|||
|
||||
// configured in fixtures/browser-app/boot/configure.js
|
||||
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();
|
||||
});
|
||||
|
@ -53,12 +56,17 @@ function executeBundledApp(bundlePath) {
|
|||
}
|
||||
|
||||
function createBrowserLikeContext() {
|
||||
return vm.createContext({
|
||||
var context = {
|
||||
// required by browserify
|
||||
XMLHttpRequest: function() { throw new Error('not implemented'); },
|
||||
|
||||
// used by loopback to detect browser runtime
|
||||
window: {},
|
||||
localStorage: {
|
||||
// used by `debug` module
|
||||
debug: process.env.DEBUG
|
||||
},
|
||||
|
||||
// used by `debug` module
|
||||
document: { documentElement: { style: {} } },
|
||||
|
||||
// allow the browserified code to log messages
|
||||
// call `printContextLogs(context)` to print the accumulated messages
|
||||
|
@ -78,7 +86,12 @@ function createBrowserLikeContext() {
|
|||
error: []
|
||||
},
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// `window` is used by loopback to detect browser runtime
|
||||
context.window = context;
|
||||
|
||||
return vm.createContext(context);
|
||||
}
|
||||
|
||||
function printContextLogs(context) {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
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');
|
||||
|
@ -16,7 +15,7 @@ describe('compiler', function() {
|
|||
var options, instructions, appConfig;
|
||||
beforeEach(function() {
|
||||
options = {
|
||||
app: {
|
||||
config: {
|
||||
port: 3000,
|
||||
host: '127.0.0.1',
|
||||
restApiRoot: '/rest-api',
|
||||
|
@ -25,9 +24,6 @@ describe('compiler', function() {
|
|||
},
|
||||
models: {
|
||||
'foo-bar-bat-baz': {
|
||||
options: {
|
||||
plural: 'foo-bar-bat-bazzies'
|
||||
},
|
||||
dataSource: 'the-db'
|
||||
}
|
||||
},
|
||||
|
@ -39,7 +35,7 @@ describe('compiler', function() {
|
|||
}
|
||||
};
|
||||
instructions = boot.compile(options);
|
||||
appConfig = instructions.app;
|
||||
appConfig = instructions.config;
|
||||
});
|
||||
|
||||
it('has port setting', function() {
|
||||
|
@ -62,7 +58,15 @@ describe('compiler', 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() {
|
||||
|
@ -73,8 +77,16 @@ describe('compiler', function() {
|
|||
describe('from directory', function() {
|
||||
it('loads config files', function() {
|
||||
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() {
|
||||
|
@ -136,13 +148,14 @@ describe('compiler', function() {
|
|||
it('merges app configs from multiple files', function() {
|
||||
appdir.createConfigFilesSync();
|
||||
|
||||
appdir.writeConfigFileSync('app.local.json', { cfgLocal: 'applied' });
|
||||
appdir.writeConfigFileSync('config.local.json', { cfgLocal: 'applied' });
|
||||
|
||||
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 appConfig = instructions.app;
|
||||
var appConfig = instructions.config;
|
||||
|
||||
expect(appConfig).to.have.property('cfgLocal', 'applied');
|
||||
expect(appConfig).to.have.property('cfgEnv', 'applied');
|
||||
|
@ -157,11 +170,11 @@ describe('compiler', function() {
|
|||
|
||||
it('supports .js for custom app config files', function() {
|
||||
appdir.createConfigFilesSync();
|
||||
appdir.writeFileSync('app.local.js',
|
||||
appdir.writeFileSync('config.local.js',
|
||||
'module.exports = { fromJs: true };');
|
||||
|
||||
var instructions = boot.compile(appdir.PATH);
|
||||
var appConfig = instructions.app;
|
||||
var appConfig = instructions.config;
|
||||
|
||||
expect(appConfig).to.have.property('fromJs', true);
|
||||
});
|
||||
|
@ -189,15 +202,13 @@ describe('compiler', function() {
|
|||
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]);
|
||||
expect(instructions.models).to.have.length(1);
|
||||
expect(instructions.models[0]).to.have.property('name', 'foo');
|
||||
});
|
||||
|
||||
it('includes boot/*.js scripts', function() {
|
||||
|
@ -208,13 +219,192 @@ describe('compiler', function() {
|
|||
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.writeFileSync('models/test/model.test.js',
|
||||
'throw new Error("should not been called");');
|
||||
appdir.writeFileSync('models/my-model.js', '');
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -10,7 +10,6 @@ var SIMPLE_APP = path.join(__dirname, 'fixtures', 'simple-app');
|
|||
|
||||
var app;
|
||||
|
||||
|
||||
describe('executor', function() {
|
||||
beforeEach(sandbox.reset);
|
||||
|
||||
|
@ -21,21 +20,21 @@ describe('executor', function() {
|
|||
});
|
||||
|
||||
var dummyInstructions = someInstructions({
|
||||
app: {
|
||||
config: {
|
||||
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'
|
||||
models: [
|
||||
{
|
||||
name: 'User',
|
||||
config: {
|
||||
dataSource: 'the-db'
|
||||
}
|
||||
}
|
||||
},
|
||||
],
|
||||
dataSources: {
|
||||
'the-db': {
|
||||
connector: 'memory',
|
||||
|
@ -44,19 +43,101 @@ describe('executor', function() {
|
|||
}
|
||||
});
|
||||
|
||||
it('instantiates models', function() {
|
||||
it('configures 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');
|
||||
assert(app.models.User);
|
||||
assert.equal(app.models.User, loopback.User,
|
||||
'Boot should not have extended loopback.User model');
|
||||
assertValidDataSource(app.models.User.dataSource);
|
||||
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() {
|
||||
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() {
|
||||
|
@ -67,6 +148,17 @@ describe('executor', function() {
|
|||
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() {
|
||||
beforeEach(function() {
|
||||
boot.execute(app, simpleAppInstructions());
|
||||
|
@ -76,18 +168,13 @@ describe('executor', 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: {
|
||||
config: {
|
||||
port: undefined,
|
||||
host: undefined
|
||||
}
|
||||
|
@ -155,25 +242,16 @@ describe('executor', 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);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
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; };');
|
||||
|
@ -182,19 +260,6 @@ describe('executor', function() {
|
|||
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();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
@ -217,11 +282,10 @@ assert.isFunc = function (obj, name) {
|
|||
|
||||
function someInstructions(values) {
|
||||
var result = {
|
||||
app: values.app || {},
|
||||
models: values.models || {},
|
||||
dataSources: values.dataSources || {},
|
||||
config: values.config || {},
|
||||
models: values.models || [],
|
||||
dataSources: values.dataSources || { db: { connector: 'memory' } },
|
||||
files: {
|
||||
models: [],
|
||||
boot: []
|
||||
}
|
||||
};
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"db": {
|
||||
"connector": "remote"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"Customer": {
|
||||
"dataSource": "db"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
module.exports = function(Customer, Base) {
|
||||
Customer.settings._customized = 'Customer';
|
||||
Base.settings._customized = 'Base';
|
||||
};
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"name": "Customer",
|
||||
"base": "User"
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"foo": {
|
||||
"User": {
|
||||
"dataSource": "db"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
process.loadedBarJS = true;
|
|
@ -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'}
|
||||
]
|
||||
});
|
|
@ -22,7 +22,7 @@ appdir.init = function(cb) {
|
|||
appdir.createConfigFilesSync = function(appConfig, dataSources, models) {
|
||||
appConfig = extend({
|
||||
}, appConfig);
|
||||
appdir.writeConfigFileSync ('app.json', appConfig);
|
||||
appdir.writeConfigFileSync ('config.json', appConfig);
|
||||
|
||||
dataSources = extend({
|
||||
db: {
|
||||
|
|
Loading…
Reference in New Issue