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..270afa6 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -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" } +} +``` 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..b7332d2 100644 --- a/lib/executor.js +++ b/lib/executor.js @@ -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); } 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..f5c27f8 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() { @@ -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: [] } }; 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; diff --git a/test/global-setup.js b/test/global-setup.js deleted file mode 100644 index 1152607..0000000 --- a/test/global-setup.js +++ /dev/null @@ -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'} - ] -});