Change models.json to configure existing models

Breaking change.

In the new 2.x project layout, definition of loopback Models is out of
scope of the boot process. The bootstrapper only configures existing
models - attaches them to a dataSource and the app object.
This commit is contained in:
Miroslav Bajtoš 2014-06-09 14:43:44 +02:00
parent 7c255f089e
commit 47b5bb5f5c
10 changed files with 196 additions and 79 deletions

View File

@ -25,3 +25,12 @@ app.listen();
See [API docs](http://apidocs.strongloop.com/loopback-boot/) 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`.

View File

@ -46,6 +46,7 @@ contained in the browser bundle:
/*-- app.js --*/
var loopback = require('loopback');
var boot = require('loopback-boot');
require('./models');
var app = module.exports = loopback();
boot(app);

View File

@ -48,3 +48,109 @@ The following is example JSON for two `Model` definitions:
}
}
```
### 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() {
// make some noise
};
```
*app.js*
```js
var loopback = require('loopback');
var boot = require('loopback-boot');
var app = loopback();
boot(app, __dirname);
```
#### Model definitions & configurations
**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 is an encapsulated component that defines all Models independently of
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*
```js
{
"car": {
"dataSource": "db"
}
}
```
2. Change per-model javascript files to build and export the Model class:
*models/car.js*
```js
var loopback = require('loopback');
var Car = module.exports = loopback.createModel(require('./car.json'));
Car.prototype.honk = function() {
// make some noise
};
```
3. Add a new file `models/index.js` to build all models:
*models/index.js*
```js
exports.Car = require('./car');
```
4. Modify the main application file to load model definitions before booting
the application.
```js
var loopback = require('loopback');
var boot = require('loopback-boot');
require('./models');
var app = loopback();
boot(app, __dirname);
```

View File

@ -16,10 +16,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.
@ -27,32 +27,37 @@ 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. loopback-boot no longer creates the models for you,
* the `models.json` file contains only configuration options like
* dataSource and extra relations.
*
* **NOTE:** mixing `app.boot()` and `app.model(name, config)` in multiple
* files may result in models being **undefined** due to race conditions.
* To avoid this when using `app.boot()` make sure all models are passed
* as part of the `models` definition.
* as part of the `models` configuration.
*
*
* Throws an error if the config object is not valid or if boot fails.
*
* @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.
* @end

View File

@ -32,7 +32,7 @@ module.exports = function compile(options) {
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,7 +40,6 @@ 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'));
return {
@ -48,7 +47,6 @@ module.exports = function compile(options) {
dataSources: dataSourcesConfig,
models: modelsConfig,
files: {
models: modelsScripts,
boot: bootScripts
}
};
@ -61,6 +59,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.

View File

@ -97,10 +97,12 @@ function setupDataSources(app, instructions) {
function setupModels(app, instructions) {
forEachKeyedObject(instructions.models, function(key, obj) {
app.model(key, obj);
var model = loopback.getModel(key);
if (!model) {
throw new Error('Cannot configure unknown model ' + key);
}
app.model(model, obj);
});
runScripts(app, instructions.files.models);
}
function forEachKeyedObject(obj, fn) {
@ -115,16 +117,11 @@ function runScripts(app, list) {
if (!list || !list.length) return;
list.forEach(function(filepath) {
var exports = tryRequire(filepath);
if (isFunctionNotModelCtor(exports))
if (typeof exports === 'function')
exports(app);
});
}
function isFunctionNotModelCtor(fn) {
return typeof fn === 'function' &&
!(fn.prototype instanceof loopback.Model);
}
function tryRequire(modulePath) {
try {
return require.apply(this, arguments);

View File

@ -25,9 +25,6 @@ describe('compiler', function() {
},
models: {
'foo-bar-bat-baz': {
options: {
plural: 'foo-bar-bat-bazzies'
},
dataSource: 'the-db'
}
},
@ -73,8 +70,8 @@ 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);
assert(instructions.models.User);
assert(instructions.models.User.dataSource);
});
it('merges datasource configs from multiple files', function() {
@ -189,15 +186,12 @@ 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]);
});
it('includes boot/*.js scripts', function() {
@ -208,13 +202,32 @@ 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/);
});
});
});

View File

@ -8,6 +8,9 @@ var appdir = require('./helpers/appdir');
var SIMPLE_APP = path.join(__dirname, 'fixtures', 'simple-app');
// ensure simple-app's models are known by loopback
require(path.join(SIMPLE_APP, '/models'));
var app;
@ -29,10 +32,7 @@ describe('executor', function() {
baz: true
},
models: {
'foo-bar-bat-baz': {
options: {
plural: 'foo-bar-bat-bazzies'
},
'User': {
dataSource: 'the-db'
}
},
@ -47,16 +47,17 @@ describe('executor', function() {
it('instantiates models', function() {
boot.execute(app, dummyInstructions);
assert(app.models);
assert(app.models.FooBarBatBaz);
assert(app.models.fooBarBatBaz);
assertValidDataSource(app.models.FooBarBatBaz.dataSource);
assert.isFunc(app.models.FooBarBatBaz, 'find');
assert.isFunc(app.models.FooBarBatBaz, 'create');
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('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('instantiates data sources', function() {
@ -76,11 +77,6 @@ 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() {
@ -165,15 +161,6 @@ describe('executor', function() {
});
});
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 +169,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();
});
});
@ -221,7 +195,6 @@ function someInstructions(values) {
models: values.models || {},
dataSources: values.dataSources || {},
files: {
models: [],
boot: []
}
};

View File

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

View File

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