Merge pull request #10 from strongloop/auto-require-model-definitions
[2.0] Rework model configuration
This commit is contained in:
commit
e22ecd39ce
|
@ -46,7 +46,6 @@ 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);
|
||||||
|
|
|
@ -2,34 +2,34 @@
|
||||||
|
|
||||||
### Model Definitions
|
### Model Definitions
|
||||||
|
|
||||||
The following is example JSON for two `Model` definitions:
|
The following two examples demonstrate how to define models.
|
||||||
"dealership" and "location".
|
|
||||||
|
|
||||||
```js
|
*models/dealership.json*
|
||||||
|
|
||||||
|
```json
|
||||||
{
|
{
|
||||||
"dealership": {
|
"name": "dealership",
|
||||||
// a reference, by name, to a dataSource definition
|
|
||||||
"dataSource": "my-db",
|
|
||||||
// the options passed to Model.extend(name, properties, options)
|
|
||||||
"options": {
|
|
||||||
"relations": {
|
"relations": {
|
||||||
"cars": {
|
"cars": {
|
||||||
"type": "hasMany",
|
"type": "hasMany",
|
||||||
"model": "Car",
|
"model": "Car",
|
||||||
"foreignKey": "dealerId"
|
"foreignKey": "dealerId"
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
},
|
||||||
// the properties passed to Model.extend(name, properties, options)
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {"id": true},
|
"id": {"id": true},
|
||||||
"name": "String",
|
"name": "String",
|
||||||
"zip": "Number",
|
"zip": "Number",
|
||||||
"address": "String"
|
"address": "String"
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"car": {
|
```
|
||||||
"dataSource": "my-db"
|
|
||||||
|
*models/car.json*
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "car",
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"id": {
|
||||||
"type": "String",
|
"type": "String",
|
||||||
|
@ -46,6 +46,56 @@ The following is example JSON for two `Model` definitions:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -72,8 +122,9 @@ The following is example JSON for two `Model` definitions:
|
||||||
var app = require('../app');
|
var app = require('../app');
|
||||||
var Car = app.models.Car;
|
var Car = app.models.Car;
|
||||||
|
|
||||||
Car.prototype.honk = function() {
|
Car.prototype.honk = function(duration, cb) {
|
||||||
// make some noise
|
// make some noise for `duration` seconds
|
||||||
|
cb();
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -92,7 +143,7 @@ developer to create them before booting the app.**
|
||||||
|
|
||||||
The folder `models/` has a different semantincs in 2.x than in 1.x. Instead
|
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`,
|
of extending Models already defined by `app.boot` and `models.json`,
|
||||||
it is an encapsulated component that defines all Models independently of
|
it provides a set of Model definitions that do not depend on
|
||||||
any application that may use them.
|
any application that may use them.
|
||||||
|
|
||||||
Perform the following steps to update a 1.x project for loopback-boot 2.x.
|
Perform the following steps to update a 1.x project for loopback-boot 2.x.
|
||||||
|
@ -122,37 +173,33 @@ All code samples are referring to the sample project described above.
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Change per-model javascript files to build and export the Model class:
|
2. Change per-model javascript files to export a function that adds
|
||||||
|
custom methods to the model class.
|
||||||
|
|
||||||
|
|
||||||
*models/car.js*
|
*models/car.js*
|
||||||
|
|
||||||
```js
|
```js
|
||||||
var loopback = require('loopback');
|
module.exports = function(Car, Base) {
|
||||||
var Car = module.exports = loopback.createModel(require('./car.json'));
|
Car.prototype.honk = function(duration, cb) {
|
||||||
|
// make some noise for `duration` seconds
|
||||||
Car.prototype.honk = function() {
|
cb();
|
||||||
// make some noise
|
};
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Add a new file `models/index.js` to build all models:
|
4. Modify the boot configuration to list the directory containing
|
||||||
|
model definitions.
|
||||||
*models/index.js*
|
|
||||||
|
|
||||||
```js
|
|
||||||
exports.Car = require('./car');
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Modify the main application file to load model definitions before booting
|
|
||||||
the application.
|
|
||||||
|
|
||||||
```js
|
```js
|
||||||
var loopback = require('loopback');
|
var loopback = require('loopback');
|
||||||
var boot = require('loopback-boot');
|
var boot = require('loopback-boot');
|
||||||
require('./models');
|
|
||||||
|
|
||||||
var app = loopback();
|
var app = loopback();
|
||||||
boot(app, __dirname);
|
boot(app, {
|
||||||
|
appRootDir: __dirname,
|
||||||
|
modelSources: ['./models']
|
||||||
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Attaching built-in models
|
#### Attaching built-in models
|
||||||
|
|
8
index.js
8
index.js
|
@ -33,9 +33,9 @@ var addInstructionsToBrowserify = require('./lib/bundler');
|
||||||
* `/boot` subdirectory 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
|
* **NOTE:** The version 2.0 of loopback-boot changed the way how models
|
||||||
* are created. loopback-boot no longer creates the models for you,
|
* are created. The `models.json` file contains only configuration options like
|
||||||
* the `models.json` file contains only configuration options like
|
* dataSource and extra relations. To define a model, create a per-model
|
||||||
* dataSource and extra relations.
|
* JSON file in `models/` directory.
|
||||||
*
|
*
|
||||||
* **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.
|
||||||
|
@ -60,6 +60,8 @@ var addInstructionsToBrowserify = require('./lib/bundler');
|
||||||
* @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.
|
||||||
|
* @property {Array.<String>} [modelSources] List of directories where to look
|
||||||
|
* for files containing model definitions.
|
||||||
* @end
|
* @end
|
||||||
*
|
*
|
||||||
* @header bootLoopBackApp(app, [options])
|
* @header bootLoopBackApp(app, [options])
|
||||||
|
|
|
@ -9,16 +9,38 @@ var commondir = require('commondir');
|
||||||
*/
|
*/
|
||||||
|
|
||||||
module.exports = function addInstructionsToBrowserify(instructions, bundler) {
|
module.exports = function addInstructionsToBrowserify(instructions, bundler) {
|
||||||
bundleScripts(instructions.files, bundler);
|
bundleModelScripts(instructions, bundler);
|
||||||
|
bundleOtherScripts(instructions, bundler);
|
||||||
bundleInstructions(instructions, bundler);
|
bundleInstructions(instructions, bundler);
|
||||||
};
|
};
|
||||||
|
|
||||||
function bundleScripts(files, bundler) {
|
function bundleOtherScripts(instructions, bundler) {
|
||||||
for (var key in files) {
|
for (var key in instructions.files) {
|
||||||
var list = files[key];
|
addScriptsToBundle(key, instructions.files[key], bundler);
|
||||||
if (!list.length) continue;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var root = commondir(files[key].map(path.dirname));
|
function bundleModelScripts(instructions, bundler) {
|
||||||
|
var files = instructions.models
|
||||||
|
.map(function(m) { return m.sourceFile; })
|
||||||
|
.filter(function(f) { return !!f; });
|
||||||
|
|
||||||
|
var modelToFileMapping = instructions.models
|
||||||
|
.map(function(m) { return files.indexOf(m.sourceFile); });
|
||||||
|
|
||||||
|
addScriptsToBundle('models', files, bundler);
|
||||||
|
|
||||||
|
// Update `sourceFile` properties with the new paths
|
||||||
|
modelToFileMapping.forEach(function(fileIx, modelIx) {
|
||||||
|
if (fileIx === -1) return;
|
||||||
|
instructions.models[modelIx].sourceFile = files[fileIx];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addScriptsToBundle(name, list, bundler) {
|
||||||
|
if (!list.length) return;
|
||||||
|
|
||||||
|
var root = commondir(list.map(path.dirname));
|
||||||
|
|
||||||
for (var ix in list) {
|
for (var ix in list) {
|
||||||
var filepath = list[ix];
|
var filepath = list[ix];
|
||||||
|
@ -26,7 +48,7 @@ function bundleScripts(files, bundler) {
|
||||||
// Build a short unique id that does not expose too much
|
// Build a short unique id that does not expose too much
|
||||||
// information about the file system, but still preserves
|
// information about the file system, but still preserves
|
||||||
// useful information about where is the file coming from.
|
// useful information about where is the file coming from.
|
||||||
var fileid = 'loopback-boot#' + key + '#' + path.relative(root, filepath);
|
var fileid = 'loopback-boot#' + name + '#' + path.relative(root, filepath);
|
||||||
|
|
||||||
// Add the file to the bundle.
|
// Add the file to the bundle.
|
||||||
bundler.require(filepath, { expose: fileid });
|
bundler.require(filepath, { expose: fileid });
|
||||||
|
@ -36,7 +58,6 @@ function bundleScripts(files, bundler) {
|
||||||
list[ix] = fileid;
|
list[ix] = fileid;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function bundleInstructions(instructions, bundler) {
|
function bundleInstructions(instructions, bundler) {
|
||||||
var instructionsString = JSON.stringify(instructions, null, 2);
|
var instructionsString = JSON.stringify(instructions, null, 2);
|
||||||
|
|
114
lib/compiler.js
114
lib/compiler.js
|
@ -42,10 +42,14 @@ module.exports = function compile(options) {
|
||||||
// require directories
|
// require directories
|
||||||
var bootScripts = findScripts(path.join(appRootDir, 'boot'));
|
var bootScripts = findScripts(path.join(appRootDir, 'boot'));
|
||||||
|
|
||||||
|
var modelSources = options.modelSources || ['./models'];
|
||||||
|
var modelInstructions = buildAllModelInstructions(
|
||||||
|
appRootDir, modelsConfig, modelSources);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
app: appConfig,
|
app: appConfig,
|
||||||
dataSources: dataSourcesConfig,
|
dataSources: dataSourcesConfig,
|
||||||
models: modelsConfig,
|
models: modelInstructions,
|
||||||
files: {
|
files: {
|
||||||
boot: bootScripts
|
boot: bootScripts
|
||||||
}
|
}
|
||||||
|
@ -138,3 +142,111 @@ function tryReadDir() {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildAllModelInstructions(rootDir, modelsConfig, sources) {
|
||||||
|
var registry = findModelDefinitions(rootDir, sources);
|
||||||
|
|
||||||
|
var modelNamesToBuild = addAllBaseModels(registry, Object.keys(modelsConfig));
|
||||||
|
|
||||||
|
var instructions = modelNamesToBuild
|
||||||
|
.map(function createModelInstructions(name) {
|
||||||
|
var config = modelsConfig[name];
|
||||||
|
var definition = registry[name] || {};
|
||||||
|
|
||||||
|
debug('Using model "%s"\nConfiguration: %j\nDefinition %j',
|
||||||
|
name, config, definition.definition);
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: name,
|
||||||
|
config: config,
|
||||||
|
definition: definition.definition,
|
||||||
|
sourceFile: definition.sourceFile
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return sortByInheritance(instructions);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addAllBaseModels(registry, modelNames) {
|
||||||
|
var result = [];
|
||||||
|
var visited = {};
|
||||||
|
|
||||||
|
while (modelNames.length) {
|
||||||
|
var name = modelNames.shift();
|
||||||
|
result.push(name);
|
||||||
|
|
||||||
|
var definition = registry[name] && registry[name].definition;
|
||||||
|
if (!definition) continue;
|
||||||
|
|
||||||
|
var base = definition.base || definition.options && definition.options.base;
|
||||||
|
if (!base || base in visited) continue;
|
||||||
|
|
||||||
|
visited[base] = true;
|
||||||
|
|
||||||
|
// ignore built-in models like User
|
||||||
|
if (!registry[base]) continue;
|
||||||
|
|
||||||
|
modelNames.push(base);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortByInheritance(instructions) {
|
||||||
|
// TODO implement topological sort
|
||||||
|
return instructions.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
function findModelDefinitions(rootDir, sources) {
|
||||||
|
var registry = {};
|
||||||
|
|
||||||
|
sources.forEach(function(src) {
|
||||||
|
var srcDir = path.resolve(rootDir, src);
|
||||||
|
var files = tryReadDir(srcDir);
|
||||||
|
files
|
||||||
|
.filter(function(f) {
|
||||||
|
return f[0] !== '_' && path.extname(f) === '.json';
|
||||||
|
})
|
||||||
|
.forEach(function(f) {
|
||||||
|
var fullPath = path.resolve(srcDir, f);
|
||||||
|
var entry = loadModelDefinition(rootDir, fullPath);
|
||||||
|
var modelName = entry.definition.name;
|
||||||
|
if (!modelName) {
|
||||||
|
debug('Skipping model definition without Model name: %s',
|
||||||
|
path.relative(srcDir, fullPath));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
registry[modelName] = entry;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return registry;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadModelDefinition(rootDir, jsonFile) {
|
||||||
|
var definition = require(jsonFile);
|
||||||
|
|
||||||
|
var sourceFile = path.join(
|
||||||
|
path.dirname(jsonFile),
|
||||||
|
path.basename(jsonFile, path.extname(jsonFile)));
|
||||||
|
|
||||||
|
try {
|
||||||
|
// resolve the file to `.js` or any other supported extension like `.coffee`
|
||||||
|
sourceFile = require.resolve(sourceFile);
|
||||||
|
} catch (err) {
|
||||||
|
debug('Model source code not found: %s - %s', sourceFile, err.code || err);
|
||||||
|
sourceFile = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sourceFile === jsonFile)
|
||||||
|
sourceFile = undefined;
|
||||||
|
|
||||||
|
debug('Found model "%s" - %s %s', definition.name,
|
||||||
|
path.relative(rootDir, jsonFile),
|
||||||
|
sourceFile ? path.relative(rootDir, sourceFile) : '(no source file)');
|
||||||
|
|
||||||
|
return {
|
||||||
|
definition: definition,
|
||||||
|
sourceFile: sourceFile
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -95,12 +95,37 @@ function setupDataSources(app, instructions) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupModels(app, instructions) {
|
function setupModels(app, instructions) {
|
||||||
forEachKeyedObject(instructions.models, function(key, obj) {
|
instructions.models.forEach(function(data) {
|
||||||
var model = loopback.getModel(key);
|
var name = data.name;
|
||||||
|
var model;
|
||||||
|
|
||||||
|
if (!data.definition) {
|
||||||
|
model = loopback.getModel(name);
|
||||||
if (!model) {
|
if (!model) {
|
||||||
throw new Error('Cannot configure unknown model ' + key);
|
throw new Error('Cannot configure unknown model ' + name);
|
||||||
}
|
}
|
||||||
app.model(model, obj);
|
debug('Configuring existing model %s', name);
|
||||||
|
} else {
|
||||||
|
debug('Creating new model %s %j', name, data.definition);
|
||||||
|
model = loopback.createModel(data.definition);
|
||||||
|
if (data.sourceFile) {
|
||||||
|
debug('Loading customization script %s', data.sourceFile);
|
||||||
|
var code = require(data.sourceFile);
|
||||||
|
if (typeof code === 'function') {
|
||||||
|
debug('Customizing model %s', name);
|
||||||
|
// NOTE model.super_ is set by Node's util.inherits
|
||||||
|
code(model, model.super_);
|
||||||
|
} else {
|
||||||
|
debug('Skipping model file %s - `module.exports` is not a function',
|
||||||
|
data.sourceFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip base models that are not exported to the app
|
||||||
|
if (!data.config) return;
|
||||||
|
|
||||||
|
app.model(model, data.config);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,9 @@ describe('browser support', function() {
|
||||||
|
|
||||||
// configured in fixtures/browser-app/boot/configure.js
|
// configured in fixtures/browser-app/boot/configure.js
|
||||||
expect(app.settings).to.have.property('custom-key', 'custom-value');
|
expect(app.settings).to.have.property('custom-key', 'custom-value');
|
||||||
|
expect(Object.keys(app.models)).to.include('Customer');
|
||||||
|
expect(app.models.Customer.settings)
|
||||||
|
.to.have.property('_customized', 'Customer');
|
||||||
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
@ -53,12 +56,17 @@ function executeBundledApp(bundlePath) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function createBrowserLikeContext() {
|
function createBrowserLikeContext() {
|
||||||
return vm.createContext({
|
var context = {
|
||||||
// required by browserify
|
// required by browserify
|
||||||
XMLHttpRequest: function() { throw new Error('not implemented'); },
|
XMLHttpRequest: function() { throw new Error('not implemented'); },
|
||||||
|
|
||||||
// used by loopback to detect browser runtime
|
localStorage: {
|
||||||
window: {},
|
// used by `debug` module
|
||||||
|
debug: process.env.DEBUG
|
||||||
|
},
|
||||||
|
|
||||||
|
// used by `debug` module
|
||||||
|
document: { documentElement: { style: {} } },
|
||||||
|
|
||||||
// allow the browserified code to log messages
|
// allow the browserified code to log messages
|
||||||
// call `printContextLogs(context)` to print the accumulated messages
|
// call `printContextLogs(context)` to print the accumulated messages
|
||||||
|
@ -78,7 +86,12 @@ function createBrowserLikeContext() {
|
||||||
error: []
|
error: []
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
|
// `window` is used by loopback to detect browser runtime
|
||||||
|
context.window = context;
|
||||||
|
|
||||||
|
return vm.createContext(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
function printContextLogs(context) {
|
function printContextLogs(context) {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
var boot = require('../');
|
var boot = require('../');
|
||||||
var fs = require('fs-extra');
|
var fs = require('fs-extra');
|
||||||
var path = require('path');
|
var path = require('path');
|
||||||
var assert = require('assert');
|
|
||||||
var expect = require('must');
|
var expect = require('must');
|
||||||
var sandbox = require('./helpers/sandbox');
|
var sandbox = require('./helpers/sandbox');
|
||||||
var appdir = require('./helpers/appdir');
|
var appdir = require('./helpers/appdir');
|
||||||
|
@ -59,7 +58,15 @@ describe('compiler', function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has models definition', function() {
|
it('has models definition', function() {
|
||||||
expect(instructions.models).to.eql(options.models);
|
expect(instructions.models).to.have.length(1);
|
||||||
|
expect(instructions.models[0]).to.eql({
|
||||||
|
name: 'foo-bar-bat-baz',
|
||||||
|
config: {
|
||||||
|
dataSource: 'the-db'
|
||||||
|
},
|
||||||
|
definition: undefined,
|
||||||
|
sourceFile: undefined
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has datasources definition', function() {
|
it('has datasources definition', function() {
|
||||||
|
@ -70,8 +77,16 @@ 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.User);
|
|
||||||
assert(instructions.models.User.dataSource);
|
expect(instructions.models).to.have.length(1);
|
||||||
|
expect(instructions.models[0]).to.eql({
|
||||||
|
name: 'User',
|
||||||
|
config: {
|
||||||
|
dataSource: 'db'
|
||||||
|
},
|
||||||
|
definition: undefined,
|
||||||
|
sourceFile: undefined
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('merges datasource configs from multiple files', function() {
|
it('merges datasource configs from multiple files', function() {
|
||||||
|
@ -191,7 +206,8 @@ describe('compiler', function() {
|
||||||
modelsRootDir: path.resolve(appdir.PATH, 'custom')
|
modelsRootDir: path.resolve(appdir.PATH, 'custom')
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(instructions.models).to.have.property('foo');
|
expect(instructions.models).to.have.length(1);
|
||||||
|
expect(instructions.models[0]).to.have.property('name', 'foo');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('includes boot/*.js scripts', function() {
|
it('includes boot/*.js scripts', function() {
|
||||||
|
@ -229,5 +245,123 @@ describe('compiler', function() {
|
||||||
.to.throw(/unsupported 1\.x format/);
|
.to.throw(/unsupported 1\.x format/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('loads models from `./models`', function() {
|
||||||
|
appdir.createConfigFilesSync({}, {}, {
|
||||||
|
Car: { dataSource: 'db' }
|
||||||
|
});
|
||||||
|
appdir.writeConfigFileSync('models/car.json', { name: 'Car' });
|
||||||
|
appdir.writeFileSync('models/car.js', '');
|
||||||
|
|
||||||
|
var instructions = boot.compile(appdir.PATH);
|
||||||
|
|
||||||
|
expect(instructions.models).to.have.length(1);
|
||||||
|
expect(instructions.models[0]).to.eql({
|
||||||
|
name: 'Car',
|
||||||
|
config: {
|
||||||
|
dataSource: 'db'
|
||||||
|
},
|
||||||
|
definition: {
|
||||||
|
name: 'Car'
|
||||||
|
},
|
||||||
|
sourceFile: path.resolve(appdir.PATH, 'models', 'car.js')
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('supports `modelSources` option', function() {
|
||||||
|
appdir.createConfigFilesSync({}, {}, {
|
||||||
|
Car: { dataSource: 'db' }
|
||||||
|
});
|
||||||
|
appdir.writeConfigFileSync('custom-models/car.json', { name: 'Car' });
|
||||||
|
appdir.writeFileSync('custom-models/car.js', '');
|
||||||
|
|
||||||
|
var instructions = boot.compile({
|
||||||
|
appRootDir: appdir.PATH,
|
||||||
|
modelSources: ['./custom-models']
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(instructions.models).to.have.length(1);
|
||||||
|
expect(instructions.models[0]).to.eql({
|
||||||
|
name: 'Car',
|
||||||
|
config: {
|
||||||
|
dataSource: 'db'
|
||||||
|
},
|
||||||
|
definition: {
|
||||||
|
name: 'Car'
|
||||||
|
},
|
||||||
|
sourceFile: path.resolve(appdir.PATH, 'custom-models', 'car.js')
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles model definitions with no code', function() {
|
||||||
|
appdir.createConfigFilesSync({}, {}, {
|
||||||
|
Car: { dataSource: 'db' }
|
||||||
|
});
|
||||||
|
appdir.writeConfigFileSync('models/car.json', { name: 'Car' });
|
||||||
|
|
||||||
|
var instructions = boot.compile(appdir.PATH);
|
||||||
|
|
||||||
|
expect(instructions.models).to.eql([{
|
||||||
|
name: 'Car',
|
||||||
|
config: {
|
||||||
|
dataSource: 'db'
|
||||||
|
},
|
||||||
|
definition: {
|
||||||
|
name: 'Car'
|
||||||
|
},
|
||||||
|
sourceFile: undefined
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('excludes models not listed in `models.json`', function() {
|
||||||
|
appdir.createConfigFilesSync({}, {}, {
|
||||||
|
Car: { dataSource: 'db' }
|
||||||
|
});
|
||||||
|
appdir.writeConfigFileSync('models/car.json', { name: 'Car' });
|
||||||
|
appdir.writeConfigFileSync('models/bar.json', { name: 'Bar' });
|
||||||
|
|
||||||
|
var instructions = boot.compile(appdir.PATH);
|
||||||
|
|
||||||
|
var models = instructions.models.map(getNameProperty);
|
||||||
|
expect(models).to.eql(['Car']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes models used as Base models', function() {
|
||||||
|
appdir.createConfigFilesSync({}, {}, {
|
||||||
|
Car: { dataSource: 'db' }
|
||||||
|
});
|
||||||
|
appdir.writeConfigFileSync('models/car.json', {
|
||||||
|
name: 'Car',
|
||||||
|
base: 'Vehicle'
|
||||||
|
});
|
||||||
|
appdir.writeConfigFileSync('models/vehicle.json', {
|
||||||
|
name: 'Vehicle'
|
||||||
|
});
|
||||||
|
|
||||||
|
var instructions = boot.compile(appdir.PATH);
|
||||||
|
var models = instructions.models;
|
||||||
|
var modelNames = models.map(getNameProperty);
|
||||||
|
|
||||||
|
expect(modelNames).to.eql(['Vehicle', 'Car']);
|
||||||
|
expect(models[0].config).to.equal(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('excludes pre-built base models', function() {
|
||||||
|
appdir.createConfigFilesSync({}, {}, {
|
||||||
|
Car: { dataSource: 'db' }
|
||||||
|
});
|
||||||
|
appdir.writeConfigFileSync('models/car.json', {
|
||||||
|
name: 'Car',
|
||||||
|
base: 'Model'
|
||||||
|
});
|
||||||
|
|
||||||
|
var instructions = boot.compile(appdir.PATH);
|
||||||
|
|
||||||
|
var modelNames = instructions.models.map(getNameProperty);
|
||||||
|
expect(modelNames).to.eql(['Car']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function getNameProperty(obj) {
|
||||||
|
return obj.name;
|
||||||
|
}
|
||||||
|
|
|
@ -8,12 +8,8 @@ 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;
|
||||||
|
|
||||||
|
|
||||||
describe('executor', function() {
|
describe('executor', function() {
|
||||||
beforeEach(sandbox.reset);
|
beforeEach(sandbox.reset);
|
||||||
|
|
||||||
|
@ -31,11 +27,14 @@ describe('executor', function() {
|
||||||
foo: { bar: 'bat' },
|
foo: { bar: 'bat' },
|
||||||
baz: true
|
baz: true
|
||||||
},
|
},
|
||||||
models: {
|
models: [
|
||||||
'User': {
|
{
|
||||||
|
name: 'User',
|
||||||
|
config: {
|
||||||
dataSource: 'the-db'
|
dataSource: 'the-db'
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
],
|
||||||
dataSources: {
|
dataSources: {
|
||||||
'the-db': {
|
'the-db': {
|
||||||
connector: 'memory',
|
connector: 'memory',
|
||||||
|
@ -44,7 +43,7 @@ describe('executor', function() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('instantiates models', function() {
|
it('configures models', function() {
|
||||||
boot.execute(app, dummyInstructions);
|
boot.execute(app, dummyInstructions);
|
||||||
assert(app.models);
|
assert(app.models);
|
||||||
assert(app.models.User);
|
assert(app.models.User);
|
||||||
|
@ -55,6 +54,60 @@ describe('executor', function() {
|
||||||
assert.isFunc(app.models.User, 'create');
|
assert.isFunc(app.models.User, 'create');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('defines and customizes models', function() {
|
||||||
|
appdir.writeFileSync('models/Customer.js', 'module.exports = ' +
|
||||||
|
function(Customer, Base) {
|
||||||
|
Customer.settings._customized = 'Customer';
|
||||||
|
Base.settings._customized = 'Base';
|
||||||
|
}.toString());
|
||||||
|
|
||||||
|
boot.execute(app, someInstructions({
|
||||||
|
dataSources: { db: { connector: 'memory' } },
|
||||||
|
models: [
|
||||||
|
{
|
||||||
|
name: 'Customer',
|
||||||
|
config: { dataSource: 'db' },
|
||||||
|
definition: {
|
||||||
|
name: 'Customer',
|
||||||
|
base: 'User',
|
||||||
|
},
|
||||||
|
sourceFile: path.resolve(appdir.PATH, 'models', 'Customer.js')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(app.models.Customer).to.exist();
|
||||||
|
expect(app.models.Customer.settings._customized).to.be.equal('Customer');
|
||||||
|
expect(loopback.User.settings._customized).to.equal('Base');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defines model without attaching it', function() {
|
||||||
|
boot.execute(app, someInstructions({
|
||||||
|
dataSources: { db: { connector: 'memory' } },
|
||||||
|
models: [
|
||||||
|
{
|
||||||
|
name: 'Vehicle',
|
||||||
|
config: undefined,
|
||||||
|
definition: {
|
||||||
|
name: 'Vehicle'
|
||||||
|
},
|
||||||
|
sourceFile: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Car',
|
||||||
|
config: { dataSource: 'db' },
|
||||||
|
definition: {
|
||||||
|
name: 'Car',
|
||||||
|
base: 'Vehicle',
|
||||||
|
},
|
||||||
|
sourceFile: undefined
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(Object.keys(app.models)).to.eql(['Car']);
|
||||||
|
});
|
||||||
|
|
||||||
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.User.dataSource, app.dataSources.theDb);
|
assert.equal(app.models.User.dataSource, app.dataSources.theDb);
|
||||||
|
@ -203,7 +256,7 @@ assert.isFunc = function (obj, name) {
|
||||||
function someInstructions(values) {
|
function someInstructions(values) {
|
||||||
var result = {
|
var result = {
|
||||||
app: values.app || {},
|
app: values.app || {},
|
||||||
models: values.models || {},
|
models: values.models || [],
|
||||||
dataSources: values.dataSources || {},
|
dataSources: values.dataSources || {},
|
||||||
files: {
|
files: {
|
||||||
boot: []
|
boot: []
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"db": {
|
||||||
|
"connector": "remote"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"Customer": {
|
||||||
|
"dataSource": "db"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
module.exports = function(Customer, Base) {
|
||||||
|
Customer.settings._customized = 'Customer';
|
||||||
|
Base.settings._customized = 'Base';
|
||||||
|
};
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"name": "Customer",
|
||||||
|
"base": "User"
|
||||||
|
}
|
Loading…
Reference in New Issue