diff --git a/README.md b/README.md index 0e6c6d4..2b3c406 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ LoopBack Boot is a convention-based bootstrapper for LoopBack applications. **For full documentation, see the official StrongLoop documentation:** - * [Creating a LoopBack application](http://docs.strongloop.com/display/DOC/Creating+a+LoopBack+application) + * [Creating a LoopBack application](http://docs.strongloop.com/display/LB/Creating+a+LoopBack+application) ## Installation @@ -23,7 +23,7 @@ app.use(loopback.rest()); app.listen(); ``` -See [API docs](http://apidocs.strongloop.com/loopback-boot/) for +See [API docs](http://apidocs.strongloop.com/loopback-boot/#api) for complete API reference. ## Versions @@ -34,3 +34,358 @@ up to slc version 2.5. The version range `2.x` supports the new project layout as scaffolded by `yo loopback`. + +This document describes the configuration conventions of the `2.x` versions. +See [Migrating from 1.x to 2.x](http://apidocs.strongloop.com/loopback-boot/#migrating-from-1x-to-2x) +for step-by-step instructions on how to upgrade existing projects. + +## Configurations and conventions + +The bootstrapping process takes care of the following tasks: + + - Configuration of data-sources. + - Definition of custom Models + - Configuration of models, attaching models to data-sources. + - Configuration of app settings like `host`, `port` or `restApiRoot`. + - Running additional boot scripts, so that the custom setup code can be kept + in multiple small files as opposed to keeping everything in the main app file. + +Below is the typical project layout. See the following sections for description +of the project files. + +``` +project/ + app.js + config.json + datasources.json + models.json + models/ + boot/ +``` + +### App settings + +The settings are loaded from the file `config.json` in the project root directory +and can be accessed via `app.get('option-name')` from the code. + +Additionally, the following files can provide values to override `config.json`: + + - `config.local.js` or `config.local.json` + - `config.{env}.js` or `config.{env}.json`, where `{env}` is the value of `NODE_ENV` + (typically `development` or `production`) + +**NOTE:** The additional files can override the top-level keys with +value-types (strings, numbers) only. Nested objects and arrays are +not supported at the moment. + +#### Example settings + +*config.json* + +```json +{ + "host": "localhost", + "port": 3000, + "restApiRoot": "/api" +} +``` + +*config.production.js* + +```js +module.exports = { + host: process.env.CUSTOM_HOST, + port: process.env.CUSTOM_PORT +}; +``` + +### Data sources + +The configuration of data sources is loaded from the file `datasources.json` +in the project root directory, the data sources can be accessed via +`app.datasources['datasource-name']` from the code. + +Additionally, the following files can provide values to override +`datasources.json`: + + - `datasources.local.js` or `datasources.local.json` + - `datasources.{env}.js` or `datasources.{env}.json`, + where `{env}` is the value of `NODE_ENV` + (typically `development` or `production`) + +**NOTE:** The additional files can override the top-level data-source options +with value-types (strings, numbers) only. Nested objects and arrays are +not supported at the moment. + +#### Example data sources + +*datasources.json* + +```js +{ + // the key is the datasource name + // the value is the config object to pass to + // app.dataSource(name, config). + db: { + connector: 'memory' + } +} +``` + +*datasources.production.json* + +```js +{ + db: { + connector: 'mongodb', + database: 'myapp', + user: 'myapp', + password: 'secret' + } +} +``` + +### Models: definition + +Custom models are defined using JSON files in `models/` directory, +one JSON file per model. + +#### Example models + +The following are example JSON files for two `Model` definitions: +`Dealership` and `Location`. + +*models/dealership.json* + +```js +{ + // the model name + "name": "Dealership", + // the options passed to Model.extend(name, properties, options) + "options": { + "relations": { + "cars": { + "type": "hasMany", + "model": "Car", + "foreignKey": "dealerId" + } + } + }, + // the properties passed to Model.extend(name, properties, options) + "properties": { + "id": {"id": true}, + "name": "String", + "zip": "Number", + "address": "String" + } +} +``` + +*models/car.json* +```js +{ + "name": "Car", + // options can be specified at the top level too + "relations": { + "dealer": { + "type": "belongsTo", + "model": "Dealership", + "foreignKey": "dealerId" + }, + } + "properties": { + "id": { + "type": "String", + "required": true, + "id": true + }, + "make": { + "type": "String", + "required": true + }, + "model": { + "type": "String", + "required": true + } + } +} +``` + +#### Adding custom methods to models + +The models created from JSON files come with the set of built-in methods +like `find` and `create`. To implement your custom methods, you should +create a javascript file in `models/` directory with the same base-name +as the JSON file containing model definition (e.g. `models/car.js` for +`models/car.json`) and define the methods there. + +Example: + +*models/car.js* + +```js +// Car is the model constructor +// Base is the parent model (e.g. loopback.PersistedModel) +module.exports = function(Car, Base) { + // Define a static method + Car.customMethod = function(cb) { + // do some work + cb(); + }; + + // Define an instance (prototype) method + Car.prototype.honk = function(duration, cb) { + // make some noise for `duration` seconds + cb(); + }; + + // Provide a custom setup method + Car.setup = function() { + Base.setup.call(this); + + // configure validations, + // configure remoting for methods, etc. + }; +}; +``` + +### Models: configuration + +Before the models can be used in a loopback application, they have to be +configured - attached to a data-source, exposed via the REST API, and so on. + +The configuration is described in the file `models.json`: + +```js +{ + // the key is the model name + "Dealership": { + // a reference, by name, to a dataSource definition + "dataSource": "my-db" + }, + "Car": { + "dataSource": "my-db", + // do not expose Car over the REST API + "public": false + } +} +``` + +The bootstrapper will automatically load definition of every custom model +configured in `models.json`. By default, the definition files are loaded from +`models/` subdirectory. However, it is possible to specify a different location +(or even multiple locations) via `_meta.sources`: + +```js +{ + "_meta": { + "sources": [ + // all paths are relative to models.json + "./models" + "./node_modules/foobar/models" + ] + }, + // use the `FooBar` model from the `foobar` module + "FooBar": { + "dataSource": "db" + } +} +``` + +### Boot scripts + +When the data sources and models are configured, the bootstrapper invokes +all scripts in the `boot/` folder. The scripts are sorted lexicographically +ingoring case. + +#### Example boot script + +*boot/authentication.js* + +```js +module.exports = function(app) { + app.enableAuth(); +}; +``` + +## Running in a browser + +The bootstrap process is implemented in two steps that can be called +independently. + +### Build + +The first step loads all configuration files, merges values from additional +config files like `app.local.js` and produces a set of instructions +that can be used to boot the application. + +These instructions must be included in the browser bundle together +with all configuration scripts from `models/` and `boot/`. + +Don't worry, you don't have to understand these details. +Just call `boot.compileToBrowserify`, it will take care of everything for you. + +*build file (Gruntfile.js, gulpfile.js)* + +```js +var browserify = require('browserify'); +var boot = require('loopback-boot'); + +var b = browserify({ + basedir: appDir, +}); + +// add the main application file +b.require('./browser-app.js', { expose: 'loopback-app' }); + +// add boot instructions +boot.compileToBrowserify(appDir, b); + +// create the bundle +var out = fs.createWriteStream('browser-bundle.js'); +b.bundle().pipe(out); +// handle out.on('error') and out.on('close') +``` + +### Run + +In the browser, the main application file should call loopback-boot +to setup the loopback application by executing the instructions +contained in the browser bundle: + +*browser-app.js* + +```js +var loopback = require('loopback'); +var boot = require('loopback-boot'); + +var app = module.exports = loopback(); +boot(app); +``` + +The app object created above can be accessed via `require('loopback-app')`, +where `loopback-app` is the identifier used for the main app file in +the browserify build shown above. + +Here is a simple example demonstrating the concept: + +*index.html* + +```xml + + +``` diff --git a/docs.json b/docs.json index 05f6402..5607f8f 100644 --- a/docs.json +++ b/docs.json @@ -1,12 +1,12 @@ { "content": [ + "README.md", { - "title": "Bootstrap API", + "title": "API", "depth": 2 }, "index.js", "browser.js", - "docs/configuration.md", - "docs/browserify.md" + "docs/migrating-from-1x-to-2x.md" ] } diff --git a/docs/browserify.md b/docs/browserify.md deleted file mode 100644 index 9873026..0000000 --- a/docs/browserify.md +++ /dev/null @@ -1,74 +0,0 @@ -## Running in a browser - -The bootstrap process is implemented in two steps that can be called -independently. - -### Build - -The first step loads all configuration files, merges values from additional -config files like `app.local.js` and produces a set of instructions -that can be used to boot the application. - -These instructions must be included in the browser bundle together -with all configuration scripts from `models/` and `boot/`. - -Don't worry, you don't have to understand these details. -Just call `boot.compileToBrowserify`, it will take care of everything for you. - -```js -/*-- build file --*/ -var browserify = require('browserify'); -var boot = require('loopback-boot'); - -var b = browserify({ - basedir: appDir, -}); - -// add the main application file -b.require('./app.js', { expose: 'loopback-app' }); - -// add boot instructions -boot.compileToBrowserify(appDir, b); - -// create the bundle -var out = fs.createWriteStream('app.bundle.js'); -b.bundle().pipe(out); -// handle out.on('error') and out.on('close') -``` - -### Run - -In the browser, the main application file should call loopback-boot -to setup the loopback application by executing the instructions -contained in the browser bundle: - -```js -/*-- app.js --*/ -var loopback = require('loopback'); -var boot = require('loopback-boot'); - -var app = module.exports = loopback(); -boot(app); -``` - -The app object created above can be accessed via `require('loopback-app')`, -where `loopback-app` is the identifier used for the main app file in -the browserify build shown above. - -Here is a simple example demonstrating the concept: - -```xml - - -``` diff --git a/docs/configuration.md b/docs/migrating-from-1x-to-2x.md similarity index 56% rename from docs/configuration.md rename to docs/migrating-from-1x-to-2x.md index d209bb7..0f3177e 100644 --- a/docs/configuration.md +++ b/docs/migrating-from-1x-to-2x.md @@ -1,105 +1,4 @@ -## Configuration and conventions - -### Model Definitions - -The following two examples demonstrate how to define models. - -*models/dealership.json* - -```json -{ - "name": "dealership", - "relations": { - "cars": { - "type": "hasMany", - "model": "Car", - "foreignKey": "dealerId" - } - }, - "properties": { - "id": {"id": true}, - "name": "String", - "zip": "Number", - "address": "String" - } -} -``` - -*models/car.json* - -```json -{ - "name": "car", - "properties": { - "id": { - "type": "String", - "required": true, - "id": true - }, - "make": { - "type": "String", - "required": true - }, - "model": { - "type": "String", - "required": true - } - } -} -``` - -To add custom methods to your models, create a `.js` file with the same name -as the `.json` file: - -*models/car.js* - -```js -module.exports = function(Car, Base) { - // Car is the model constructor - // Base is the parent model (e.g. loopback.PersistedModel) - - // Define a static method - Car.customMethod = function(cb) { - // do some work - cb(); - }; - - Car.prototype.honk = function(duration, cb) { - // make some noise for `duration` seconds - cb(); - }; - - Car.setup = function() { - Base.setup.call(this); - - // configure validations, - // configure remoting for methods, etc. - }; -} -``` - -### Model Configuration - -The following is an example JSON configuring the models defined above -for use in an loopback application. - -`dataSource` options is a reference, by name, to a data-source defined -in `datasources.json`. - -*models.json* - -```json -{ - "dealership": { - "dataSource": "my-db", - }, - "car": { - "dataSource": "my-db" - } -} -``` - -### Migrating from 1.x to 2.x +## Migrating from 1.x to 2.x **Starting point: a sample 1.x project** @@ -136,7 +35,22 @@ var app = loopback(); boot(app, __dirname); ``` -#### Model definitions & configurations +### App settings + +The files with applications settings were renamed from `app.*` to `config.*`. +Rename the following files to upgrade a 1.x project for loopback-boot 2.x: + + - `app.json` to `config.json` + - `app.local.json` to `config.local.json` + - `app.local.js` to `config.local.js` + - etc. + +### Data sources + +The configuration of data sources remains the same in both 1.x and 2.x +versions. + +### Models **The 2.x version of loopback-boot no longer creates Models, it's up to the developer to create them before booting the app.** @@ -203,7 +117,7 @@ All code samples are referring to the sample project described above. } ``` -#### Attaching built-in models +### 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 diff --git a/lib/executor.js b/lib/executor.js index f97f930..ba55438 100644 --- a/lib/executor.js +++ b/lib/executor.js @@ -1,6 +1,5 @@ var assert = require('assert'); var _ = require('underscore'); -var loopback = require('loopback'); var semver = require('semver'); var debug = require('debug')('loopback:boot:executor'); @@ -14,6 +13,7 @@ var debug = require('debug')('loopback:boot:executor'); */ module.exports = function execute(app, instructions) { + patchAppLoopback(app); assertLoopBackVersion(app); setHost(app, instructions); @@ -29,18 +29,31 @@ module.exports = function execute(app, instructions) { enableAnonymousSwagger(app, instructions); }; +function patchAppLoopback(app) { + if (app.loopback) return; + // app.loopback was introduced in 1.9.0 + // patch the app object to make loopback-boot work with older versions too + try { + app.loopback = require('loopback'); + } catch(err) { + if (err.code === 'MODULE_NOT_FOUND') { + console.error( + 'When using loopback-boot with loopback <1.9, '+ + 'the loopback module must be available for `require(\'loopback\')`.'); + } + throw err; + } +} + function assertLoopBackVersion(app) { var RANGE = '1.x || 2.x'; - // app.loopback was introduced in 1.9.0 - var loopback = app.loopback || {}; - var version = loopback.version || '1.0.0'; - - if (!semver.satisfies(version, RANGE)) { + var loopback = app.loopback; + if (!semver.satisfies(loopback.version || '1.0.0', RANGE)) { throw new Error( 'The `app` is powered by an incompatible loopback version %s. ' + 'Supported versions: %s', - loopback.version || '<1.9', + loopback.version || '(unknown)', RANGE); } }