Merge pull request #270 from fabien/feature/dynamic-scope-modelTo

Enable dynamic modelTo for scopes
This commit is contained in:
Raymond Feng 2014-08-31 08:39:14 -07:00
commit f9a26bcb28
2 changed files with 131 additions and 18 deletions

View File

@ -1,6 +1,8 @@
var i8n = require('inflection');
var utils = require('./utils');
var defineCachedRelations = utils.defineCachedRelations;
var DefaultModelBaseClass = require('./model.js');
/**
* Module exports
*/
@ -13,10 +15,26 @@ function ScopeDefinition(definition) {
this.modelTo = definition.modelTo || definition.modelFrom;
this.name = definition.name;
this.params = definition.params;
this.methods = definition.methods;
this.options = definition.options;
this.methods = definition.methods || {};
this.options = definition.options || {};
}
ScopeDefinition.prototype.targetModel = function(receiver) {
if (typeof this.options.modelTo === 'function') {
var modelTo = this.options.modelTo.call(this, receiver) || this.modelTo;
} else {
var modelTo = this.modelTo;
}
if (!(modelTo.prototype instanceof DefaultModelBaseClass)) {
var msg = 'Invalid target model for scope `';
msg += (this.isStatic ? this.modelFrom : this.modelFrom.constructor).modelName;
msg += this.isStatic ? '.' : '.prototype.';
msg += this.name + '`.';
throw new Error(msg);
}
return modelTo;
};
ScopeDefinition.prototype.related = function(receiver, scopeParams, condOrRefresh, cb) {
var name = this.name;
var self = receiver;
@ -42,7 +60,8 @@ ScopeDefinition.prototype.related = function(receiver, scopeParams, condOrRefres
|| actualRefresh) {
// It either doesn't hit the cache or refresh is required
var params = mergeQuery(actualCond, scopeParams);
return this.modelTo.find(params, function (err, data) {
var targetModel = this.targetModel(receiver);
return targetModel.find(params, function (err, data) {
if (!err && saveOnCache) {
defineCachedRelations(self);
self.__cachedRelations[name] = data;
@ -74,7 +93,6 @@ ScopeDefinition.prototype.defineMethod = function(name, fn) {
* @param methods An object of methods keyed by the method name to be bound to the class
*/
function defineScope(cls, targetClass, name, params, methods, options) {
// collect meta info about scope
if (!cls._scopeMeta) {
cls._scopeMeta = {};
@ -84,7 +102,7 @@ function defineScope(cls, targetClass, name, params, methods, options) {
// are same
if (cls === targetClass) {
cls._scopeMeta[name] = params;
} else {
} else if (targetClass) {
if (!targetClass._scopeMeta) {
targetClass._scopeMeta = {};
}
@ -100,7 +118,7 @@ function defineScope(cls, targetClass, name, params, methods, options) {
name: name,
params: params,
methods: methods,
options: options || {}
options: options
});
if(isStatic) {
@ -127,7 +145,9 @@ function defineScope(cls, targetClass, name, params, methods, options) {
*
*/
get: function () {
var targetModel = definition.targetModel(this);
var self = this;
var f = function(condOrRefresh, cb) {
if(arguments.length === 1) {
definition.related(self, f._scope, condOrRefresh);
@ -135,15 +155,16 @@ function defineScope(cls, targetClass, name, params, methods, options) {
definition.related(self, f._scope, condOrRefresh, cb);
}
};
f._receiver = this;
f._scope = typeof definition.params === 'function' ?
definition.params.call(self) : definition.params;
f._targetClass = definition.modelTo.modelName;
f._targetClass = targetModel.modelName;
if (f._scope.collect) {
f._targetClass = i8n.capitalize(f._scope.collect);
}
f.build = build;
f.create = create;
f.destroyAll = destroyAll;
@ -151,6 +172,8 @@ function defineScope(cls, targetClass, name, params, methods, options) {
for (var i in definition.methods) {
f[i] = definition.methods[i].bind(self);
}
if (!targetClass) return f;
// Define scope-chaining, such as
// Station.scope('active', {where: {isActive: true}});
@ -160,7 +183,7 @@ function defineScope(cls, targetClass, name, params, methods, options) {
Object.defineProperty(f, name, {
enumerable: false,
get: function () {
mergeQuery(f._scope, targetClass._scopeMeta[name]);
mergeQuery(f._scope, targetModel._scopeMeta[name]);
return f;
}
});
@ -207,16 +230,16 @@ function defineScope(cls, targetClass, name, params, methods, options) {
* @param {Object} The data object
* @param {Object} The where clause
*/
function setScopeValuesFromWhere(data, where) {
function setScopeValuesFromWhere(data, where, targetModel) {
for (var i in where) {
if (i === 'and') {
// Find fixed property values from each subclauses
for (var w = 0, n = where[i].length; w < n; w++) {
setScopeValuesFromWhere(data, where[i][w]);
setScopeValuesFromWhere(data, where[i][w], targetModel);
}
continue;
}
var prop = targetClass.definition.properties[i];
var prop = targetModel.definition.properties[i];
if (prop) {
var val = where[i];
if (typeof val !== 'object' || val instanceof prop.type
@ -233,9 +256,10 @@ function defineScope(cls, targetClass, name, params, methods, options) {
function build(data) {
data = data || {};
// Find all fixed property values for the scope
var targetModel = definition.targetModel(this._receiver);
var where = (this._scope && this._scope.where) || {};
setScopeValuesFromWhere(data, where);
return new targetClass(data);
setScopeValuesFromWhere(data, where, targetModel);
return new targetModel(data);
}
function create(data, cb) {
@ -256,14 +280,16 @@ function defineScope(cls, targetClass, name, params, methods, options) {
if (typeof where === 'function') cb = where, where = {};
var scoped = (this._scope && this._scope.where) || {};
var filter = mergeQuery({ where: scoped }, { where: where || {} });
targetClass.destroyAll(filter.where, cb);
var targetModel = definition.targetModel(this._receiver);
targetModel.destroyAll(filter.where, cb);
}
function count(where, cb) {
if (typeof where === 'function') cb = where, where = {};
var scoped = (this._scope && this._scope.where) || {};
var filter = mergeQuery({ where: scoped }, { where: where || {} });
targetClass.count(filter.where, cb);
var targetModel = definition.targetModel(this._receiver);
targetModel.count(filter.where, cb);
}
return definition;

View File

@ -229,3 +229,90 @@ describe('scope - filtered count and destroyAll', function () {
});
});
describe('scope - dynamic target class', function () {
var Collection, Media, Image, Video;
before(function () {
db = getSchema();
Image = db.define('Image', {name: String});
Video = db.define('Video', {name: String});
Collection = db.define('Collection', {name: String, modelName: String});
Collection.scope('items', function() {
return {}; // could return a scope based on `this` (receiver)
}, null, {}, { isStatic: false, modelTo: function(receiver) {
return db.models[receiver.modelName];
} });
});
beforeEach(function (done) {
Collection.destroyAll(function() {
Image.destroyAll(function() {
Video.destroyAll(done);
})
});
});
beforeEach(function (done) {
Collection.create({ name: 'Images', modelName: 'Image' }, done);
});
beforeEach(function (done) {
Collection.create({ name: 'Videos', modelName: 'Video' }, done);
});
beforeEach(function (done) {
Collection.create({ name: 'Things', modelName: 'Unknown' }, done);
});
beforeEach(function (done) {
Image.create({ name: 'Image A' }, done);
});
beforeEach(function (done) {
Video.create({ name: 'Video A' }, done);
});
it('should deduce modelTo at runtime - Image', function(done) {
Collection.findOne({ where: { modelName: 'Image' } }, function(err, coll) {
should.not.exist(err);
coll.name.should.equal('Images');
coll.items(function(err, items) {
should.not.exist(err);
items.length.should.equal(1);
items[0].name.should.equal('Image A');
items[0].should.be.instanceof(Image);
done();
});
});
});
it('should deduce modelTo at runtime - Video', function(done) {
Collection.findOne({ where: { modelName: 'Video' } }, function(err, coll) {
should.not.exist(err);
coll.name.should.equal('Videos');
coll.items(function(err, items) {
should.not.exist(err);
items.length.should.equal(1);
items[0].name.should.equal('Video A');
items[0].should.be.instanceof(Video);
done();
});
});
});
it('should throw if modelTo is invalid', function(done) {
Collection.findOne({ where: { name: 'Things' } }, function(err, coll) {
should.not.exist(err);
coll.modelName.should.equal('Unknown');
(function () {
coll.items(function(err, items) {});
}).should.throw();
done();
});
});
});