Merge branch 'release/2.3.0' into production

This commit is contained in:
Raymond Feng 2014-08-08 09:45:32 -07:00
commit 82475b58af
9 changed files with 229 additions and 19 deletions

67
lib/mixins.js Normal file
View File

@ -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;
}
};

View File

@ -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;

View File

@ -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 () {

View File

@ -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
}; };

View File

@ -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) {

View File

@ -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",

91
test/mixins.test.js Normal file
View File

@ -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();
});
});
});

View File

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

View File

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