Merge branch 'release/2.3.0' into production
This commit is contained in:
commit
82475b58af
|
@ -0,0 +1,67 @@
|
||||||
|
var debug = require('debug')('loopback:mixin');
|
||||||
|
var assert = require('assert');
|
||||||
|
var DefaultModelBaseClass = require('./model.js');
|
||||||
|
|
||||||
|
function isModelClass(cls) {
|
||||||
|
if (!cls) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return cls.prototype instanceof DefaultModelBaseClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = MixinProvider;
|
||||||
|
|
||||||
|
function MixinProvider(modelBuilder) {
|
||||||
|
this.modelBuilder = modelBuilder;
|
||||||
|
this.mixins = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply named mixin to the model class
|
||||||
|
* @param {Model} modelClass
|
||||||
|
* @param {String} name
|
||||||
|
* @param {Object} options
|
||||||
|
*/
|
||||||
|
MixinProvider.prototype.applyMixin = function applyMixin(modelClass, name, options) {
|
||||||
|
var fn = this.mixins[name];
|
||||||
|
if (typeof fn === 'function') {
|
||||||
|
if (modelClass.dataSource) {
|
||||||
|
fn(modelClass, options || {});
|
||||||
|
} else {
|
||||||
|
modelClass.once('dataSourceAttached', function() {
|
||||||
|
fn(modelClass, options || {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Try model name
|
||||||
|
var model = this.modelBuilder.getModel(name);
|
||||||
|
if(model) {
|
||||||
|
debug('Mixin is resolved to a model: %s', name);
|
||||||
|
modelClass.mixin(model, options);
|
||||||
|
} else {
|
||||||
|
debug('Invalid mixin: %s', name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define a mixin with name
|
||||||
|
* @param {String} name Name of the mixin
|
||||||
|
* @param {*) mixin The mixin function or a model
|
||||||
|
*/
|
||||||
|
MixinProvider.prototype.define = function defineMixin(name, mixin) {
|
||||||
|
assert(typeof mixin === 'function', 'The mixin must be a function or model class');
|
||||||
|
if (this.mixins[name]) {
|
||||||
|
debug('Duplicate mixin: %s', name);
|
||||||
|
} else {
|
||||||
|
debug('Defining mixin: %s', name);
|
||||||
|
}
|
||||||
|
if (isModelClass(mixin)) {
|
||||||
|
this.mixins[name] = function (Model, options) {
|
||||||
|
Model.mixin(mixin, options);
|
||||||
|
};
|
||||||
|
} else if (typeof mixin === 'function') {
|
||||||
|
this.mixins[name] = mixin;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
@ -10,6 +10,7 @@ var DefaultModelBaseClass = require('./model.js');
|
||||||
var List = require('./list.js');
|
var List = require('./list.js');
|
||||||
var ModelDefinition = require('./model-definition.js');
|
var ModelDefinition = require('./model-definition.js');
|
||||||
var mergeSettings = require('./utils').mergeSettings;
|
var mergeSettings = require('./utils').mergeSettings;
|
||||||
|
var MixinProvider = require('./mixins');
|
||||||
|
|
||||||
// Set up types
|
// Set up types
|
||||||
require('./types')(ModelBuilder);
|
require('./types')(ModelBuilder);
|
||||||
|
@ -37,6 +38,7 @@ function ModelBuilder() {
|
||||||
// create blank models pool
|
// create blank models pool
|
||||||
this.models = {};
|
this.models = {};
|
||||||
this.definitions = {};
|
this.definitions = {};
|
||||||
|
this.mixins = new MixinProvider(this);
|
||||||
this.defaultModelBaseClass = DefaultModelBaseClass;
|
this.defaultModelBaseClass = DefaultModelBaseClass;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -188,7 +190,7 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett
|
||||||
|
|
||||||
// Add metadata to the ModelClass
|
// Add metadata to the ModelClass
|
||||||
hiddenProperty(ModelClass, 'modelBuilder', modelBuilder);
|
hiddenProperty(ModelClass, 'modelBuilder', modelBuilder);
|
||||||
hiddenProperty(ModelClass, 'dataSource', modelBuilder); // Keep for back-compatibility
|
hiddenProperty(ModelClass, 'dataSource', null); // Keep for back-compatibility
|
||||||
hiddenProperty(ModelClass, 'pluralModelName', pluralName);
|
hiddenProperty(ModelClass, 'pluralModelName', pluralName);
|
||||||
hiddenProperty(ModelClass, 'relations', {});
|
hiddenProperty(ModelClass, 'relations', {});
|
||||||
hiddenProperty(ModelClass, 'http', { path: '/' + pathName });
|
hiddenProperty(ModelClass, 'http', { path: '/' + pathName });
|
||||||
|
@ -428,6 +430,20 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett
|
||||||
ModelClass.registerProperty(propertyName);
|
ModelClass.registerProperty(propertyName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var mixinSettings = settings.mixins || {};
|
||||||
|
keys = Object.keys(mixinSettings);
|
||||||
|
size = keys.length;
|
||||||
|
for (i = 0; i < size; i++) {
|
||||||
|
var name = keys[i];
|
||||||
|
var mixin = mixinSettings[name];
|
||||||
|
if (mixin === true) {
|
||||||
|
mixin = {};
|
||||||
|
}
|
||||||
|
if (typeof mixin === 'object') {
|
||||||
|
modelBuilder.mixins.applyMixin(ModelClass, name, mixin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ModelClass.emit('defined', ModelClass);
|
ModelClass.emit('defined', ModelClass);
|
||||||
|
|
||||||
return ModelClass;
|
return ModelClass;
|
||||||
|
|
21
lib/model.js
21
lib/model.js
|
@ -201,7 +201,11 @@ ModelBaseClass.prototype._initProperties = function (data, options) {
|
||||||
* @param {Object} params Various property configuration
|
* @param {Object} params Various property configuration
|
||||||
*/
|
*/
|
||||||
ModelBaseClass.defineProperty = function (prop, params) {
|
ModelBaseClass.defineProperty = function (prop, params) {
|
||||||
this.dataSource.defineProperty(this.modelName, prop, params);
|
if(this.dataSource) {
|
||||||
|
this.dataSource.defineProperty(this.modelName, prop, params);
|
||||||
|
} else {
|
||||||
|
this.modelBuilder.defineProperty(this.modelName, prop, params);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ModelBaseClass.getPropertyType = function (propName) {
|
ModelBaseClass.getPropertyType = function (propName) {
|
||||||
|
@ -387,7 +391,20 @@ ModelBaseClass.prototype.inspect = function () {
|
||||||
};
|
};
|
||||||
|
|
||||||
ModelBaseClass.mixin = function (anotherClass, options) {
|
ModelBaseClass.mixin = function (anotherClass, options) {
|
||||||
return jutil.mixin(this, anotherClass, options);
|
if (typeof anotherClass === 'string') {
|
||||||
|
this.modelBuilder.mixins.applyMixin(this, anotherClass, options);
|
||||||
|
} else {
|
||||||
|
if (anotherClass.prototype instanceof ModelBaseClass) {
|
||||||
|
var props = anotherClass.definition.properties;
|
||||||
|
for (var i in props) {
|
||||||
|
if (this.definition.properties[i]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
this.defineProperty(i, props[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return jutil.mixin(this, anotherClass, options);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ModelBaseClass.prototype.getDataSource = function () {
|
ModelBaseClass.prototype.getDataSource = function () {
|
||||||
|
|
|
@ -553,7 +553,7 @@ RelationDefinition.hasMany = function hasMany(modelFrom, modelTo, params) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return filter;
|
return filter;
|
||||||
}, scopeMethods);
|
}, scopeMethods, definition.options);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1600,7 +1600,7 @@ RelationDefinition.embedsMany = function embedsMany(modelFrom, modelTo, params)
|
||||||
// Mix the property and scoped methods into the prototype class
|
// Mix the property and scoped methods into the prototype class
|
||||||
var scopeDefinition = defineScope(modelFrom.prototype, modelTo, accessorName, function () {
|
var scopeDefinition = defineScope(modelFrom.prototype, modelTo, accessorName, function () {
|
||||||
return {};
|
return {};
|
||||||
}, scopeMethods);
|
}, scopeMethods, definition.options);
|
||||||
|
|
||||||
scopeDefinition.related = scopeMethods.related;
|
scopeDefinition.related = scopeMethods.related;
|
||||||
};
|
};
|
||||||
|
@ -2014,7 +2014,7 @@ RelationDefinition.referencesMany = function referencesMany(modelFrom, modelTo,
|
||||||
// Mix the property and scoped methods into the prototype class
|
// Mix the property and scoped methods into the prototype class
|
||||||
var scopeDefinition = defineScope(modelFrom.prototype, modelTo, relationName, function () {
|
var scopeDefinition = defineScope(modelFrom.prototype, modelTo, relationName, function () {
|
||||||
return {};
|
return {};
|
||||||
}, scopeMethods);
|
}, scopeMethods, definition.options);
|
||||||
|
|
||||||
scopeDefinition.related = scopeMethods.related; // bound to definition
|
scopeDefinition.related = scopeMethods.related; // bound to definition
|
||||||
};
|
};
|
||||||
|
|
23
lib/scope.js
23
lib/scope.js
|
@ -8,11 +8,12 @@ exports.defineScope = defineScope;
|
||||||
exports.mergeQuery = mergeQuery;
|
exports.mergeQuery = mergeQuery;
|
||||||
|
|
||||||
function ScopeDefinition(definition) {
|
function ScopeDefinition(definition) {
|
||||||
this.sourceModel = definition.sourceModel;
|
this.modelFrom = definition.modelFrom || definition.sourceModel;
|
||||||
this.targetModel = definition.targetModel || definition.sourceModel;
|
this.modelTo = definition.modelTo || definition.targetModel;
|
||||||
this.name = definition.name;
|
this.name = definition.name;
|
||||||
this.params = definition.params;
|
this.params = definition.params;
|
||||||
this.methods = definition.methods;
|
this.methods = definition.methods;
|
||||||
|
this.options = definition.options;
|
||||||
}
|
}
|
||||||
|
|
||||||
ScopeDefinition.prototype.related = function(receiver, scopeParams, condOrRefresh, cb) {
|
ScopeDefinition.prototype.related = function(receiver, scopeParams, condOrRefresh, cb) {
|
||||||
|
@ -40,7 +41,7 @@ ScopeDefinition.prototype.related = function(receiver, scopeParams, condOrRefres
|
||||||
|| actualRefresh) {
|
|| actualRefresh) {
|
||||||
// It either doesn't hit the cache or refresh is required
|
// It either doesn't hit the cache or refresh is required
|
||||||
var params = mergeQuery(actualCond, scopeParams);
|
var params = mergeQuery(actualCond, scopeParams);
|
||||||
return this.targetModel.find(params, function (err, data) {
|
return this.modelTo.find(params, function (err, data) {
|
||||||
if (!err && saveOnCache) {
|
if (!err && saveOnCache) {
|
||||||
defineCachedRelations(self);
|
defineCachedRelations(self);
|
||||||
self.__cachedRelations[name] = data;
|
self.__cachedRelations[name] = data;
|
||||||
|
@ -62,7 +63,7 @@ ScopeDefinition.prototype.related = function(receiver, scopeParams, condOrRefres
|
||||||
* to return the query object
|
* to return the query object
|
||||||
* @param methods An object of methods keyed by the method name to be bound to the class
|
* @param methods An object of methods keyed by the method name to be bound to the class
|
||||||
*/
|
*/
|
||||||
function defineScope(cls, targetClass, name, params, methods) {
|
function defineScope(cls, targetClass, name, params, methods, options) {
|
||||||
|
|
||||||
// collect meta info about scope
|
// collect meta info about scope
|
||||||
if (!cls._scopeMeta) {
|
if (!cls._scopeMeta) {
|
||||||
|
@ -80,13 +81,17 @@ function defineScope(cls, targetClass, name, params, methods) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var definition = new ScopeDefinition({
|
var definition = new ScopeDefinition({
|
||||||
sourceModel: cls,
|
modelFrom: cls,
|
||||||
targetModel: targetClass,
|
modelTo: targetClass,
|
||||||
name: name,
|
name: name,
|
||||||
params: params,
|
params: params,
|
||||||
methods: methods
|
methods: methods,
|
||||||
|
options: options || {}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
cls.scopes = cls.scopes || {};
|
||||||
|
cls.scopes[name] = definition;
|
||||||
|
|
||||||
// Define a property for the scope
|
// Define a property for the scope
|
||||||
Object.defineProperty(cls, name, {
|
Object.defineProperty(cls, name, {
|
||||||
enumerable: false,
|
enumerable: false,
|
||||||
|
@ -115,7 +120,7 @@ function defineScope(cls, targetClass, name, params, methods) {
|
||||||
f._scope = typeof definition.params === 'function' ?
|
f._scope = typeof definition.params === 'function' ?
|
||||||
definition.params.call(self) : definition.params;
|
definition.params.call(self) : definition.params;
|
||||||
|
|
||||||
f._targetClass = definition.targetModel.modelName;
|
f._targetClass = definition.modelTo.modelName;
|
||||||
if (f._scope.collect) {
|
if (f._scope.collect) {
|
||||||
f._targetClass = i8n.capitalize(f._scope.collect);
|
f._targetClass = i8n.capitalize(f._scope.collect);
|
||||||
}
|
}
|
||||||
|
@ -255,7 +260,7 @@ function mergeQuery(base, update) {
|
||||||
} else {
|
} else {
|
||||||
var saved = base.include;
|
var saved = base.include;
|
||||||
base.include = {};
|
base.include = {};
|
||||||
base.include[update.include] = [saved];
|
base.include[update.include] = saved;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (update.collect) {
|
if (update.collect) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "loopback-datasource-juggler",
|
"name": "loopback-datasource-juggler",
|
||||||
"version": "2.2.2",
|
"version": "2.3.0",
|
||||||
"description": "LoopBack DataSoure Juggler",
|
"description": "LoopBack DataSoure Juggler",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"StrongLoop",
|
"StrongLoop",
|
||||||
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
// This test written in mocha+should.js
|
||||||
|
var should = require('./init.js');
|
||||||
|
|
||||||
|
var jdb = require('../');
|
||||||
|
var ModelBuilder = jdb.ModelBuilder;
|
||||||
|
var DataSource = jdb.DataSource;
|
||||||
|
var Memory = require('../lib/connectors/memory');
|
||||||
|
|
||||||
|
var modelBuilder = new ModelBuilder();
|
||||||
|
var mixins = modelBuilder.mixins;
|
||||||
|
|
||||||
|
function timestamps(Model, options) {
|
||||||
|
|
||||||
|
Model.defineProperty('createdAt', { type: Date });
|
||||||
|
Model.defineProperty('updatedAt', { type: Date });
|
||||||
|
|
||||||
|
var originalBeforeSave = Model.beforeSave;
|
||||||
|
Model.beforeSave = function(next, data) {
|
||||||
|
Model.applyTimestamps(data, this.isNewRecord());
|
||||||
|
if (data.createdAt) {
|
||||||
|
this.createdAt = data.createdAt;
|
||||||
|
}
|
||||||
|
if (data.updatedAt) {
|
||||||
|
this.updatedAt = data.updatedAt;
|
||||||
|
}
|
||||||
|
if (originalBeforeSave) {
|
||||||
|
originalBeforeSave.apply(this, arguments);
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Model.applyTimestamps = function(data, creation) {
|
||||||
|
data.updatedAt = new Date();
|
||||||
|
if (creation) {
|
||||||
|
data.createdAt = data.updatedAt;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
mixins.define('TimeStamp', timestamps);
|
||||||
|
|
||||||
|
describe('Model class', function () {
|
||||||
|
|
||||||
|
it('should define a mixin', function() {
|
||||||
|
mixins.define('Example', function(Model, options) {
|
||||||
|
Model.prototype.example = function() {
|
||||||
|
return options;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should apply a mixin class', function() {
|
||||||
|
var Address = modelBuilder.define('Address', {
|
||||||
|
street: { type: 'string', required: true },
|
||||||
|
city: { type: 'string', required: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
var memory = new DataSource('mem', {connector: Memory}, modelBuilder);
|
||||||
|
var Item = memory.createModel('Item', { name: 'string' }, {
|
||||||
|
mixins: { TimeStamp: true, demo: true, Address: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
var properties = Item.definition.properties;
|
||||||
|
|
||||||
|
properties.street.should.eql({ type: String, required: true });
|
||||||
|
properties.city.should.eql({ type: String, required: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should apply mixins', function(done) {
|
||||||
|
var memory = new DataSource('mem', {connector: Memory}, modelBuilder);
|
||||||
|
var Item = memory.createModel('Item', { name: 'string' }, {
|
||||||
|
mixins: { TimeStamp: true, demo: { ok: true } }
|
||||||
|
});
|
||||||
|
|
||||||
|
Item.mixin('Example', { foo: 'bar' });
|
||||||
|
Item.mixin('other');
|
||||||
|
|
||||||
|
var properties = Item.definition.properties;
|
||||||
|
properties.createdAt.should.eql({ type: Date });
|
||||||
|
properties.updatedAt.should.eql({ type: Date });
|
||||||
|
|
||||||
|
Item.create({ name: 'Item 1' }, function(err, inst) {
|
||||||
|
inst.createdAt.should.be.a.date;
|
||||||
|
inst.updatedAt.should.be.a.date;
|
||||||
|
inst.example().should.eql({ foo: 'bar' });
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
|
@ -284,17 +284,17 @@ describe('relations', function () {
|
||||||
Address.create({name: 'z'}, function (err, address) {
|
Address.create({name: 'z'}, function (err, address) {
|
||||||
patient.address(address);
|
patient.address(address);
|
||||||
patient.save(function() {
|
patient.save(function() {
|
||||||
verify(physician);
|
verify(physician, address.id);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
function verify(physician) {
|
function verify(physician, addressId) {
|
||||||
physician.patients({include: 'address'}, function (err, ch) {
|
physician.patients({include: 'address'}, function (err, ch) {
|
||||||
should.not.exist(err);
|
should.not.exist(err);
|
||||||
should.exist(ch);
|
should.exist(ch);
|
||||||
ch.should.have.lengthOf(1);
|
ch.should.have.lengthOf(1);
|
||||||
ch[0].addressId.should.equal(1);
|
ch[0].addressId.should.eql(addressId);
|
||||||
var address = ch[0].address();
|
var address = ch[0].address();
|
||||||
should.exist(address);
|
should.exist(address);
|
||||||
address.should.be.an.instanceof(Address);
|
address.should.be.an.instanceof(Address);
|
||||||
|
|
|
@ -9,6 +9,14 @@ describe('scope', function () {
|
||||||
db = getSchema();
|
db = getSchema();
|
||||||
Railway = db.define('Railway', {
|
Railway = db.define('Railway', {
|
||||||
URID: {type: String, index: true}
|
URID: {type: String, index: true}
|
||||||
|
}, {
|
||||||
|
scopes: {
|
||||||
|
highSpeed: {
|
||||||
|
where: {
|
||||||
|
highSpeed: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
Station = db.define('Station', {
|
Station = db.define('Station', {
|
||||||
USID: {type: String, index: true},
|
USID: {type: String, index: true},
|
||||||
|
@ -24,9 +32,15 @@ describe('scope', function () {
|
||||||
Station.destroyAll(done);
|
Station.destroyAll(done);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should define scope using options.scopes', function () {
|
||||||
|
Railway.scopes.should.have.property('highSpeed');
|
||||||
|
Railway.highSpeed.should.be.function;
|
||||||
|
});
|
||||||
|
|
||||||
it('should define scope with query', function (done) {
|
it('should define scope with query', function (done) {
|
||||||
Station.scope('active', {where: {isActive: true}});
|
Station.scope('active', {where: {isActive: true}});
|
||||||
|
Station.scopes.should.have.property('active');
|
||||||
Station.active.create(function (err, station) {
|
Station.active.create(function (err, station) {
|
||||||
should.not.exist(err);
|
should.not.exist(err);
|
||||||
should.exist(station);
|
should.exist(station);
|
||||||
|
|
Loading…
Reference in New Issue