Skip to content

Commit

Permalink
Add createModelFromConfig and configureModel()
Browse files Browse the repository at this point in the history
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 with loopback 1.x,
`app.model(name, config)` calls both `createModelFromConfig`
and `configureModel`.
  • Loading branch information
Miroslav Bajtoš committed Jun 5, 2014
1 parent 93a74f2 commit f844459
Show file tree
Hide file tree
Showing 4 changed files with 267 additions and 52 deletions.
118 changes: 67 additions & 51 deletions lib/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/

var DataSource = require('loopback-datasource-juggler').DataSource
, ModelBuilder = require('loopback-datasource-juggler').ModelBuilder
, loopback = require('../')
, compat = require('./compat')
, assert = require('assert')
, fs = require('fs')
Expand Down Expand Up @@ -82,33 +82,40 @@ 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.
*
* ```js
* var Widget = app.model('Widget', {dataSource: 'db'});
* Widget.create({name: 'pencil'});
* app.models.Widget.find(function(err, widgets) {
* console.log(widgets[0]); // => {name: 'pencil'}
* // Attach an existing model
* var User = loopback.User;
* app.model(User);
*
* // Attach an existing model, alter some aspects of the model
* var User = loopback.User;
* app.model(User, { dataSource: 'db' });
*
* // LoopBack 1.x 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.
* @property {String|DataSource} dataSource The `DataSource` to which to attach the model.
* @property {Object} [options] an object containing `Model` options.
* @property {ACL[]} [options.acls] an array of `ACL` definitions.
* @property {String[]} [options.hidden] **experimental** an array of properties to hide when accessed remotely.
* @property {Object} [properties] object defining the `Model` properties in [LoopBack Definition Language](http://docs.strongloop.com/loopback-datasource-juggler/#loopback-definition-language).
* @property {String|DataSource} dataSource The `DataSource` to which to
* attach the model.
* @property {Boolean} [public] whether the model should be exposed via REST API
* @property {Object} [relations] relations to add/update
* @end
* @returns {ModelConstructor} the model class
*/

app.model = function (Model, config) {
if(arguments.length === 1) {
assert(typeof Model === 'function', 'app.model(Model) => Model must be a function / constructor');
assert(Model.modelName, 'Model must have a "modelName" property');
var remotingClassName = compat.getClassNameForRemoting(Model);
assertIsModel(Model);
if(Model.sharedClass) {
this.remotes().addClass(Model.sharedClass);
}
Expand All @@ -117,22 +124,53 @@ app.model = function (Model, config) {
Model.shared = true;
Model.app = this;
Model.emit('attached', this);
return;
return Model;
}
var modelName = Model;

config = config || {};
assert(typeof modelName === 'string', 'app.model(name, config) => "name" name must be a string');

Model =
if (typeof Model === 'string') {
// create & attach the model - loopback 1.x compatibility

// create config for loopback.modelFromConfig
var modelConfig = extend({}, config);
modelConfig.options = extend({}, config.options);
modelConfig.name = Model;

// 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);

var modelName = Model.modelName;
this.models[modelName] =
this.models[classify(modelName)] =
this.models[camelize(modelName)] = modelFromConfig(modelName, config, this);
this.models[camelize(modelName)] = Model;

if(config.public !== false) {
if (config.public !== false) {
this.model(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');
}

/**
Expand Down Expand Up @@ -566,41 +604,23 @@ function dataSourcesFromConfig(config, connectorRegistry) {
return require('./loopback').createDataSource(config);
}

function modelFromConfig(name, config, app) {
var options = buildModelOptionsFromConfig(config);
var properties = config.properties;
function configureModel(ModelCtor, config, app) {
assertIsModel(ModelCtor);

var ModelCtor = require('./loopback').createModel(name, properties, options);
var dataSource = config.dataSource;

if(typeof dataSource === 'string') {
dataSource = app.dataSources[dataSource];
}

assert(isDataSource(dataSource), name + ' is referencing a dataSource that does not exist: "'+ config.dataSource +'"');

ModelCtor.attachTo(dataSource);
return ModelCtor;
}

function buildModelOptionsFromConfig(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;
}
assert(isDataSource(dataSource),
ModelCtor.modelName + ' is referencing a dataSource that does not exist: "' +
config.dataSource +'"');

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;
}
config = extend({}, config);
config.dataSource = dataSource;

options[key] = config[key];
}
return options;
loopback.configureModel(ModelCtor, config);
}

function requireDir(dir, basenames) {
Expand Down Expand Up @@ -672,10 +692,6 @@ function tryReadDir() {
}
}

function isModelCtor(obj) {
return typeof obj === 'function' && obj.modelName && obj.name === 'ModelCtor';
}

function isDataSource(obj) {
return obj instanceof DataSource;
}
Expand Down
89 changes: 88 additions & 1 deletion lib/loopback.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ var express = require('express')
, fs = require('fs')
, ejs = require('ejs')
, path = require('path')
, proto = require('./application')
, DataSource = require('loopback-datasource-juggler').DataSource
, merge = require('util')._extend
, assert = require('assert');
Expand Down Expand Up @@ -66,6 +65,9 @@ loopback.compat = require('./compat');
function createApplication() {
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);

// Create a new instance of models registry per each app instance
Expand Down Expand Up @@ -193,6 +195,91 @@ loopback.createModel = function (name, properties, options) {
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.
* @param {Function} fn
Expand Down
14 changes: 14 additions & 0 deletions test/app.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,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() {
it('is unique per app instance', function() {
app.dataSource('db', { connector: 'memory' });
Expand Down
Loading

0 comments on commit f844459

Please sign in to comment.