Merge pull request #9 from strongloop/configure-models
Configure models
This commit is contained in:
commit
fccc6e147a
|
@ -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`.
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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" }
|
||||
}
|
||||
```
|
||||
|
|
35
index.js
35
index.js
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -20,7 +20,6 @@ module.exports = function execute(app, instructions) {
|
|||
|
||||
setupDataSources(app, instructions);
|
||||
setupModels(app, instructions);
|
||||
autoAttach();
|
||||
|
||||
runBootScripts(app, instructions);
|
||||
|
||||
|
@ -97,10 +96,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 +116,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);
|
||||
|
@ -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) {
|
||||
runScripts(app, instructions.files.boot);
|
||||
}
|
||||
|
|
|
@ -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/);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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() {
|
||||
|
@ -67,6 +68,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,11 +88,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 +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() {
|
||||
var file = appdir.writeFileSync('boot/init.js',
|
||||
'module.exports = function(app) { app.fnCalled = true; };');
|
||||
|
@ -182,19 +180,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 +206,6 @@ function someInstructions(values) {
|
|||
models: values.models || {},
|
||||
dataSources: values.dataSources || {},
|
||||
files: {
|
||||
models: [],
|
||||
boot: []
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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'}
|
||||
]
|
||||
});
|
Loading…
Reference in New Issue