Enable dynamic modelTo for scopes

This is especially useful for relations/prototype scopes, as it allows
you to dynamically deduce the target model from the current receiver
(model instance). This is an advanced option that should otherwise have
no effect on the previous/default functionality. Basically, the
targetClass is taken out of the closure and handled by a method called
targetModel now.
This commit is contained in:
Fabien Franzen 2014-08-30 20:58:06 +02:00
parent 170dc661f4
commit 0a9cb72837
2 changed files with 129 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,88 @@ 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', {}, 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();
});
});
});