Add createModelFromConfig and configureModel()
Add new API allowing developers to split the model definition and configuration into two steps: 1. Build models from JSON config, export them for re-use: ```js var Customer = loopback.createModelFromConfig({ name: 'Customer', base: 'User', properties: { address: 'string' } }); ``` 2. Attach existing models to a dataSource and a loopback app, modify certain model aspects like relations: ```js loopback.configureModel(Customer, { dataSource: db, relations: { /* ... */ } }); ``` Rework `app.model` to use `loopback.configureModel` under the hood. Here is the new usage: ```js var Customer = require('./models').Customer; app.model(Customer, { dataSource: 'db', relations: { /* ... */ } }); ``` In order to preserve backwards compatibility, `app.model(name, config)` calls both `createModelFromConfig` and `configureModel`.
This commit is contained in:
parent
b5f0057ea4
commit
fc0fad4a9f
|
@ -3,7 +3,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var DataSource = require('loopback-datasource-juggler').DataSource
|
var DataSource = require('loopback-datasource-juggler').DataSource
|
||||||
, ModelBuilder = require('loopback-datasource-juggler').ModelBuilder
|
, loopback = require('../')
|
||||||
, compat = require('./compat')
|
, compat = require('./compat')
|
||||||
, assert = require('assert')
|
, assert = require('assert')
|
||||||
, fs = require('fs')
|
, fs = require('fs')
|
||||||
|
@ -82,43 +82,69 @@ app.disuse = function (route) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define and attach a model to the app. The `Model` will be available on the
|
* Attach a model to the app. The `Model` will be available on the
|
||||||
* `app.models` object.
|
* `app.models` object.
|
||||||
*
|
*
|
||||||
* ```js
|
* ```js
|
||||||
* var Widget = app.model('Widget', {dataSource: 'db'});
|
* // Attach an existing model
|
||||||
* Widget.create({name: 'pencil'});
|
* var User = loopback.User;
|
||||||
* app.models.Widget.find(function(err, widgets) {
|
* app.model(User);
|
||||||
* console.log(widgets[0]); // => {name: 'pencil'}
|
*
|
||||||
|
* // Attach an existing model, alter some aspects of the model
|
||||||
|
* var User = loopback.User;
|
||||||
|
* app.model(User, { dataSource: 'db' });
|
||||||
|
*
|
||||||
|
* // The old way: create and attach a new model (deprecated)
|
||||||
|
* var Widget = app.model('Widget', {
|
||||||
|
* dataSource: 'db',
|
||||||
|
* properties: {
|
||||||
|
* name: 'string'
|
||||||
|
* }
|
||||||
* });
|
* });
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* @param {String} modelName The name of the model to define.
|
* @param {Object|String} Model The model to attach.
|
||||||
* @options {Object} config The model's configuration.
|
* @options {Object} config The model's configuration.
|
||||||
* @property {String|DataSource} dataSource The `DataSource` to which to attach the model.
|
* @property {String|DataSource} dataSource The `DataSource` to which to
|
||||||
* @property {Object} [options] an object containing `Model` options.
|
* attach the model.
|
||||||
* @property {ACL[]} [options.acls] an array of `ACL` definitions.
|
* @property {Boolean} [public] whether the model should be exposed via REST API
|
||||||
* @property {String[]} [options.hidden] **experimental** an array of properties to hide when accessed remotely.
|
* @property {Object} [relations] relations to add/update
|
||||||
* @property {Object} [properties] object defining the `Model` properties in [LoopBack Definition Language](http://docs.strongloop.com/loopback-datasource-juggler/#loopback-definition-language).
|
|
||||||
* @end
|
* @end
|
||||||
* @returns {ModelConstructor} the model class
|
* @returns {ModelConstructor} the model class
|
||||||
*/
|
*/
|
||||||
|
|
||||||
app.model = function (Model, config) {
|
app.model = function (Model, config) {
|
||||||
var modelName = null;
|
|
||||||
var isPublic = true;
|
var isPublic = true;
|
||||||
if (arguments.length > 1) {
|
if (arguments.length > 1) {
|
||||||
config = config || {};
|
config = config || {};
|
||||||
modelName = Model;
|
if (typeof Model === 'string') {
|
||||||
assert(typeof modelName === 'string', 'app.model(name, config) => "name" name must be a string');
|
// create & attach the model - backwards compatibility
|
||||||
Model = modelFromConfig(modelName, config, this);
|
|
||||||
isPublic = config.public !== false;
|
// create config for loopback.modelFromConfig
|
||||||
} else {
|
var modelConfig = extend({}, config);
|
||||||
assert(typeof Model === 'function', 'app.model(Model) => Model must be a function / constructor');
|
modelConfig.options = extend({}, config.options);
|
||||||
modelName = Model.modelName;
|
modelConfig.name = Model;
|
||||||
assert(modelName, 'Model must have a "modelName" property');
|
|
||||||
|
// modeller does not understand `dataSource` option
|
||||||
|
delete modelConfig.dataSource;
|
||||||
|
|
||||||
|
Model = loopback.createModelFromConfig(modelConfig);
|
||||||
|
|
||||||
|
// delete config options already applied
|
||||||
|
['relations', 'base', 'acls', 'hidden'].forEach(function(prop) {
|
||||||
|
delete config[prop];
|
||||||
|
if (config.options) delete config.options[prop];
|
||||||
|
});
|
||||||
|
delete config.properties;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
configureModel(Model, config, this);
|
||||||
|
isPublic = config.public !== false;
|
||||||
|
} else {
|
||||||
|
assertIsModel(Model);
|
||||||
|
}
|
||||||
|
|
||||||
|
var modelName = Model.modelName;
|
||||||
this.models[modelName] =
|
this.models[modelName] =
|
||||||
this.models[classify(modelName)] =
|
this.models[classify(modelName)] =
|
||||||
this.models[camelize(modelName)] = Model;
|
this.models[camelize(modelName)] = Model;
|
||||||
|
@ -137,6 +163,14 @@ app.model = function (Model, config) {
|
||||||
return Model;
|
return Model;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function assertIsModel(Model) {
|
||||||
|
assert(typeof Model === 'function',
|
||||||
|
'Model must be a function / constructor');
|
||||||
|
assert(Model.modelName, 'Model must have a "modelName" property');
|
||||||
|
assert(Model.prototype instanceof loopback.Model,
|
||||||
|
'Model must be a descendant of loopback.Model');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the models exported by the app. Returns only models defined using `app.model()`
|
* Get the models exported by the app. Returns only models defined using `app.model()`
|
||||||
*
|
*
|
||||||
|
@ -570,41 +604,23 @@ function dataSourcesFromConfig(config, connectorRegistry) {
|
||||||
return require('./loopback').createDataSource(config);
|
return require('./loopback').createDataSource(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
function modelFromConfig(name, config, app) {
|
function configureModel(ModelCtor, config, app) {
|
||||||
var options = buildModelOptionsFromConfig(config);
|
assertIsModel(ModelCtor);
|
||||||
var properties = config.properties;
|
|
||||||
|
|
||||||
var ModelCtor = require('./loopback').createModel(name, properties, options);
|
|
||||||
var dataSource = config.dataSource;
|
var dataSource = config.dataSource;
|
||||||
|
|
||||||
if(typeof dataSource === 'string') {
|
if(typeof dataSource === 'string') {
|
||||||
dataSource = app.dataSources[dataSource];
|
dataSource = app.dataSources[dataSource];
|
||||||
}
|
}
|
||||||
|
|
||||||
assert(isDataSource(dataSource), name + ' is referencing a dataSource that does not exist: "'+ config.dataSource +'"');
|
assert(isDataSource(dataSource),
|
||||||
|
ModelCtor.modelName + ' is referencing a dataSource that does not exist: "' +
|
||||||
|
config.dataSource +'"');
|
||||||
|
|
||||||
ModelCtor.attachTo(dataSource);
|
config = extend({}, config);
|
||||||
return ModelCtor;
|
config.dataSource = dataSource;
|
||||||
}
|
|
||||||
|
|
||||||
function buildModelOptionsFromConfig(config) {
|
loopback.configureModel(ModelCtor, config);
|
||||||
var options = extend({}, config.options);
|
|
||||||
for (var key in config) {
|
|
||||||
if (['properties', 'options', 'dataSource'].indexOf(key) !== -1) {
|
|
||||||
// Skip items which have special meaning
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options[key] !== undefined) {
|
|
||||||
// When both `config.key` and `config.options.key` are set,
|
|
||||||
// use the latter one to preserve backwards compatibility
|
|
||||||
// with loopback 1.x
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
options[key] = config[key];
|
|
||||||
}
|
|
||||||
return options;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function requireDir(dir, basenames) {
|
function requireDir(dir, basenames) {
|
||||||
|
@ -676,10 +692,6 @@ function tryReadDir() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isModelCtor(obj) {
|
|
||||||
return typeof obj === 'function' && obj.modelName && obj.name === 'ModelCtor';
|
|
||||||
}
|
|
||||||
|
|
||||||
function isDataSource(obj) {
|
function isDataSource(obj) {
|
||||||
return obj instanceof DataSource;
|
return obj instanceof DataSource;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@ var express = require('express')
|
||||||
, ejs = require('ejs')
|
, ejs = require('ejs')
|
||||||
, EventEmitter = require('events').EventEmitter
|
, EventEmitter = require('events').EventEmitter
|
||||||
, path = require('path')
|
, path = require('path')
|
||||||
, proto = require('./application')
|
|
||||||
, DataSource = require('loopback-datasource-juggler').DataSource
|
, DataSource = require('loopback-datasource-juggler').DataSource
|
||||||
, ModelBuilder = require('loopback-datasource-juggler').ModelBuilder
|
, ModelBuilder = require('loopback-datasource-juggler').ModelBuilder
|
||||||
, i8n = require('inflection')
|
, i8n = require('inflection')
|
||||||
|
@ -69,6 +68,9 @@ loopback.compat = require('./compat');
|
||||||
function createApplication() {
|
function createApplication() {
|
||||||
var app = express();
|
var app = express();
|
||||||
|
|
||||||
|
// Defer loading of `./application` until all `loopback` static methods
|
||||||
|
// are defined, because `./application` depends on loopback.
|
||||||
|
var proto = require('./application');
|
||||||
merge(app, proto);
|
merge(app, proto);
|
||||||
|
|
||||||
// Create a new instance of models registry per each app instance
|
// Create a new instance of models registry per each app instance
|
||||||
|
@ -179,6 +181,91 @@ loopback.createModel = function (name, properties, options) {
|
||||||
return model;
|
return model;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a model as described by the configuration object.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* loopback.createModelFromConfig({
|
||||||
|
* name: 'Author',
|
||||||
|
* properties: {
|
||||||
|
* firstName: 'string',
|
||||||
|
* lastName: 'string
|
||||||
|
* },
|
||||||
|
* relations: {
|
||||||
|
* books: {
|
||||||
|
* model: 'Book',
|
||||||
|
* type: 'hasAndBelongsToMany'
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @options {Object} model configuration
|
||||||
|
* @property {String} name Unique name.
|
||||||
|
* @property {Object=} properties Model properties
|
||||||
|
* @property {Object=} options Model options. Options can be specified on the
|
||||||
|
* top level config object too. E.g. `{ base: 'User' }` is the same as
|
||||||
|
* `{ options: { base: 'User' } }`.
|
||||||
|
*/
|
||||||
|
loopback.createModelFromConfig = function(config) {
|
||||||
|
var name = config.name;
|
||||||
|
var properties = config.properties;
|
||||||
|
var options = buildModelOptionsFromConfig(config);
|
||||||
|
|
||||||
|
assert(typeof name === 'string',
|
||||||
|
'The model-config property `name` must be a string');
|
||||||
|
|
||||||
|
return loopback.createModel(name, properties, options);
|
||||||
|
};
|
||||||
|
|
||||||
|
function buildModelOptionsFromConfig(config) {
|
||||||
|
var options = merge({}, config.options);
|
||||||
|
for (var key in config) {
|
||||||
|
if (['name', 'properties', 'options'].indexOf(key) !== -1) {
|
||||||
|
// Skip items which have special meaning
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options[key] !== undefined) {
|
||||||
|
// When both `config.key` and `config.options.key` are set,
|
||||||
|
// use the latter one
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
options[key] = config[key];
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alter an existing Model class.
|
||||||
|
* @param {Model} ModelCtor The model constructor to alter.
|
||||||
|
* @options {Object} Additional configuration to apply
|
||||||
|
* @property {DataSource} dataSource Attach the model to a dataSource.
|
||||||
|
* @property {Object} relations Model relations to add/update.
|
||||||
|
*/
|
||||||
|
loopback.configureModel = function(ModelCtor, config) {
|
||||||
|
var settings = ModelCtor.settings;
|
||||||
|
|
||||||
|
if (config.relations) {
|
||||||
|
var relations = settings.relations = settings.relations || {};
|
||||||
|
Object.keys(config.relations).forEach(function(key) {
|
||||||
|
relations[key] = merge(relations[key] || {}, config.relations[key]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// It's important to attach the datasource after we have updated
|
||||||
|
// configuration, so that the datasource picks up updated relations
|
||||||
|
if (config.dataSource) {
|
||||||
|
assert(config.dataSource instanceof DataSource,
|
||||||
|
'Cannot configure ' + ModelCtor.modelName +
|
||||||
|
': config.dataSource must be an instance of loopback.DataSource');
|
||||||
|
ModelCtor.attachTo(config.dataSource);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a remote method to a model.
|
* Add a remote method to a model.
|
||||||
* @param {Function} fn
|
* @param {Function} fn
|
||||||
|
|
|
@ -136,6 +136,20 @@ describe('app', function() {
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('app.model(ModelCtor, config)', function() {
|
||||||
|
it('attaches the model to a datasource', function() {
|
||||||
|
app.dataSource('db', { connector: 'memory' });
|
||||||
|
var TestModel = loopback.Model.extend('TestModel');
|
||||||
|
// TestModel was most likely already defined in a different test,
|
||||||
|
// thus TestModel.dataSource may be already set
|
||||||
|
delete TestModel.dataSource;
|
||||||
|
|
||||||
|
app.model(TestModel, { dataSource: 'db' });
|
||||||
|
|
||||||
|
expect(app.models.TestModel.dataSource).to.equal(app.dataSources.db);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('app.models', function() {
|
describe('app.models', function() {
|
||||||
it('is unique per app instance', function() {
|
it('is unique per app instance', function() {
|
||||||
app.dataSource('db', { connector: 'memory' });
|
app.dataSource('db', { connector: 'memory' });
|
||||||
|
|
|
@ -1,4 +1,11 @@
|
||||||
describe('loopback', function() {
|
describe('loopback', function() {
|
||||||
|
var nameCounter = 0;
|
||||||
|
var uniqueModelName;
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
uniqueModelName = 'TestModel-' + (++nameCounter);
|
||||||
|
});
|
||||||
|
|
||||||
describe('exports', function() {
|
describe('exports', function() {
|
||||||
it('ValidationError', function() {
|
it('ValidationError', function() {
|
||||||
expect(loopback.ValidationError).to.be.a('function')
|
expect(loopback.ValidationError).to.be.a('function')
|
||||||
|
@ -119,4 +126,95 @@ describe('loopback', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('loopback.createModelFromConfig(config)', function() {
|
||||||
|
it('creates the model', function() {
|
||||||
|
var model = loopback.createModelFromConfig({
|
||||||
|
name: uniqueModelName
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(model.prototype).to.be.instanceof(loopback.Model);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('interprets extra first-level keys as options', function() {
|
||||||
|
var model = loopback.createModelFromConfig({
|
||||||
|
name: uniqueModelName,
|
||||||
|
base: 'User'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(model.prototype).to.be.instanceof(loopback.User);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prefers config.options.key over config.key', function() {
|
||||||
|
var model = loopback.createModelFromConfig({
|
||||||
|
name: uniqueModelName,
|
||||||
|
base: 'User',
|
||||||
|
options: {
|
||||||
|
base: 'Application'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(model.prototype).to.be.instanceof(loopback.Application);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('loopback.configureModel(ModelCtor, config)', function() {
|
||||||
|
it('adds new relations', function() {
|
||||||
|
var model = loopback.Model.extend(uniqueModelName);
|
||||||
|
|
||||||
|
loopback.configureModel(model, {
|
||||||
|
relations: {
|
||||||
|
owner: {
|
||||||
|
type: 'belongsTo',
|
||||||
|
model: 'User'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(model.settings.relations).to.have.property('owner');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates existing relations', function() {
|
||||||
|
var model = loopback.Model.extend(uniqueModelName, {}, {
|
||||||
|
relations: {
|
||||||
|
owner: {
|
||||||
|
type: 'belongsTo',
|
||||||
|
model: 'User'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
loopback.configureModel(model, {
|
||||||
|
relations: {
|
||||||
|
owner: {
|
||||||
|
model: 'Application'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(model.settings.relations.owner).to.eql({
|
||||||
|
type: 'belongsTo',
|
||||||
|
model: 'Application'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates relations before attaching to a dataSource', function() {
|
||||||
|
var db = loopback.createDataSource({ connector: loopback.Memory });
|
||||||
|
var model = loopback.Model.extend(uniqueModelName);
|
||||||
|
|
||||||
|
loopback.configureModel(model, {
|
||||||
|
dataSource: db,
|
||||||
|
relations: {
|
||||||
|
owner: {
|
||||||
|
type: 'belongsTo',
|
||||||
|
model: 'User'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var owner = model.prototype.owner;
|
||||||
|
expect(owner, 'model.prototype.owner').to.be.a('function');
|
||||||
|
expect(owner._targetClass).to.equal('User');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue