Merge pull request #9 from strongloop/configure-models

Configure models
This commit is contained in:
Miroslav Bajtoš 2014-06-10 10:27:19 +02:00
commit fccc6e147a
11 changed files with 222 additions and 104 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,124 @@ 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);
```
#### 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

@ -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

@ -20,7 +20,6 @@ module.exports = function execute(app, instructions) {
setupDataSources(app, instructions); setupDataSources(app, instructions);
setupModels(app, instructions); setupModels(app, instructions);
autoAttach();
runBootScripts(app, instructions); runBootScripts(app, instructions);
@ -97,10 +96,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 +116,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);
@ -138,19 +134,6 @@ function tryRequire(modulePath) {
} }
} }
// Deprecated, will be removed soon
function autoAttach() {
try {
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);
} }

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() {
@ -67,6 +68,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,11 +88,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 +172,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 +180,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 +206,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;

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'}
]
});