From 47b5bb5f5ca7dc9a17a458b11e2310524c548b08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 9 Jun 2014 14:43:44 +0200 Subject: [PATCH] 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. --- README.md | 9 +++ docs/browserify.md | 1 + docs/configuration.md | 106 +++++++++++++++++++++++++ index.js | 35 ++++---- lib/compiler.js | 20 ++++- lib/executor.js | 15 ++-- test/compiler.test.js | 37 ++++++--- test/executor.test.js | 49 +++--------- test/fixtures/simple-app/models.json | 2 +- test/fixtures/simple-app/models/bar.js | 1 - 10 files changed, 196 insertions(+), 79 deletions(-) delete mode 100644 test/fixtures/simple-app/models/bar.js diff --git a/README.md b/README.md index 55b0e4d..0e6c6d4 100644 --- a/README.md +++ b/README.md @@ -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`. diff --git a/docs/browserify.md b/docs/browserify.md index 9873026..4f63b1f 100644 --- a/docs/browserify.md +++ b/docs/browserify.md @@ -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); diff --git a/docs/configuration.md b/docs/configuration.md index 082e959..18dc878 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -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); + ``` diff --git a/index.js b/index.js index fc0e2ef..c6806e6 100644 --- a/index.js +++ b/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 diff --git a/lib/compiler.js b/lib/compiler.js index ac35dab..0f3989d 100644 --- a/lib/compiler.js +++ b/lib/compiler.js @@ -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. diff --git a/lib/executor.js b/lib/executor.js index 538970e..81f5a40 100644 --- a/lib/executor.js +++ b/lib/executor.js @@ -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); diff --git a/test/compiler.test.js b/test/compiler.test.js index b1076b0..b93f294 100644 --- a/test/compiler.test.js +++ b/test/compiler.test.js @@ -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/); + }); + }); }); diff --git a/test/executor.test.js b/test/executor.test.js index 1c64e95..bf86562 100644 --- a/test/executor.test.js +++ b/test/executor.test.js @@ -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: [] } }; diff --git a/test/fixtures/simple-app/models.json b/test/fixtures/simple-app/models.json index 3a22f13..0468a66 100644 --- a/test/fixtures/simple-app/models.json +++ b/test/fixtures/simple-app/models.json @@ -1,5 +1,5 @@ { - "foo": { + "User": { "dataSource": "db" } } diff --git a/test/fixtures/simple-app/models/bar.js b/test/fixtures/simple-app/models/bar.js deleted file mode 100644 index 0eef5d9..0000000 --- a/test/fixtures/simple-app/models/bar.js +++ /dev/null @@ -1 +0,0 @@ -process.loadedBarJS = true;