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 See [API docs](http://apidocs.strongloop.com/loopback-boot/) 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`.

View File

@ -46,6 +46,7 @@ contained in the browser bundle:
/*-- app.js --*/ /*-- app.js --*/
var loopback = require('loopback'); var loopback = require('loopback');
var boot = require('loopback-boot'); var boot = require('loopback-boot');
require('./models');
var app = module.exports = loopback(); var app = module.exports = loopback();
boot(app); 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 * 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.
@ -27,32 +27,37 @@ 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. 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 * **NOTE:** mixing `app.boot()` and `app.model(name, config)` in multiple
* files may result in models being **undefined** due to race conditions. * files may result in models being **undefined** due to race conditions.
* To avoid this when using `app.boot()` make sure all models are passed * 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. * Throws an error if the config object is not valid or if boot fails.
* *
* @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.
* @end * @end

View File

@ -32,7 +32,7 @@ module.exports = function compile(options) {
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,7 +40,6 @@ 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'));
return { return {
@ -48,7 +47,6 @@ module.exports = function compile(options) {
dataSources: dataSourcesConfig, dataSources: dataSourcesConfig,
models: modelsConfig, models: modelsConfig,
files: { files: {
models: modelsScripts,
boot: bootScripts 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 _) * Find all javascript files (except for those prefixed with _)
* and all directories. * and all directories.

View File

@ -97,10 +97,12 @@ function setupDataSources(app, instructions) {
function setupModels(app, instructions) { function setupModels(app, instructions) {
forEachKeyedObject(instructions.models, function(key, obj) { 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) { function forEachKeyedObject(obj, fn) {
@ -115,16 +117,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)) if (typeof exports === 'function')
exports(app); exports(app);
}); });
} }
function isFunctionNotModelCtor(fn) {
return typeof fn === 'function' &&
!(fn.prototype instanceof loopback.Model);
}
function tryRequire(modulePath) { function tryRequire(modulePath) {
try { try {
return require.apply(this, arguments); return require.apply(this, arguments);

View File

@ -25,9 +25,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'
} }
}, },
@ -73,8 +70,8 @@ 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.User);
assert(instructions.models.foo.dataSource); assert(instructions.models.User.dataSource);
}); });
it('merges datasource configs from multiple files', function() { it('merges datasource configs from multiple files', function() {
@ -189,15 +186,12 @@ 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.property('foo');
expect(instructions.files.models).to.eql([fooJs]);
}); });
it('includes boot/*.js scripts', function() { it('includes boot/*.js scripts', function() {
@ -208,13 +202,32 @@ 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/);
});
}); });
}); });

View File

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

View File

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

View File

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