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 i8n = require('inflection');
var utils = require('./utils'); var utils = require('./utils');
var defineCachedRelations = utils.defineCachedRelations; var defineCachedRelations = utils.defineCachedRelations;
var DefaultModelBaseClass = require('./model.js');
/** /**
* Module exports * Module exports
*/ */
@ -13,10 +15,26 @@ function ScopeDefinition(definition) {
this.modelTo = definition.modelTo || definition.modelFrom; this.modelTo = definition.modelTo || definition.modelFrom;
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; 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) { ScopeDefinition.prototype.related = function(receiver, scopeParams, condOrRefresh, cb) {
var name = this.name; var name = this.name;
var self = receiver; var self = receiver;
@ -42,7 +60,8 @@ 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.modelTo.find(params, function (err, data) { var targetModel = this.targetModel(receiver);
return targetModel.find(params, function (err, data) {
if (!err && saveOnCache) { if (!err && saveOnCache) {
defineCachedRelations(self); defineCachedRelations(self);
self.__cachedRelations[name] = data; 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 * @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) { function defineScope(cls, targetClass, name, params, methods, options) {
// collect meta info about scope // collect meta info about scope
if (!cls._scopeMeta) { if (!cls._scopeMeta) {
cls._scopeMeta = {}; cls._scopeMeta = {};
@ -84,7 +102,7 @@ function defineScope(cls, targetClass, name, params, methods, options) {
// are same // are same
if (cls === targetClass) { if (cls === targetClass) {
cls._scopeMeta[name] = params; cls._scopeMeta[name] = params;
} else { } else if (targetClass) {
if (!targetClass._scopeMeta) { if (!targetClass._scopeMeta) {
targetClass._scopeMeta = {}; targetClass._scopeMeta = {};
} }
@ -100,7 +118,7 @@ function defineScope(cls, targetClass, name, params, methods, options) {
name: name, name: name,
params: params, params: params,
methods: methods, methods: methods,
options: options || {} options: options
}); });
if(isStatic) { if(isStatic) {
@ -127,7 +145,9 @@ function defineScope(cls, targetClass, name, params, methods, options) {
* *
*/ */
get: function () { get: function () {
var targetModel = definition.targetModel(this);
var self = this; var self = this;
var f = function(condOrRefresh, cb) { var f = function(condOrRefresh, cb) {
if(arguments.length === 1) { if(arguments.length === 1) {
definition.related(self, f._scope, condOrRefresh); 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); definition.related(self, f._scope, condOrRefresh, cb);
} }
}; };
f._receiver = this;
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.modelTo.modelName; f._targetClass = targetModel.modelName;
if (f._scope.collect) { if (f._scope.collect) {
f._targetClass = i8n.capitalize(f._scope.collect); f._targetClass = i8n.capitalize(f._scope.collect);
} }
f.build = build; f.build = build;
f.create = create; f.create = create;
f.destroyAll = destroyAll; f.destroyAll = destroyAll;
@ -151,6 +172,8 @@ function defineScope(cls, targetClass, name, params, methods, options) {
for (var i in definition.methods) { for (var i in definition.methods) {
f[i] = definition.methods[i].bind(self); f[i] = definition.methods[i].bind(self);
} }
if (!targetClass) return f;
// Define scope-chaining, such as // Define scope-chaining, such as
// Station.scope('active', {where: {isActive: true}}); // Station.scope('active', {where: {isActive: true}});
@ -160,7 +183,7 @@ function defineScope(cls, targetClass, name, params, methods, options) {
Object.defineProperty(f, name, { Object.defineProperty(f, name, {
enumerable: false, enumerable: false,
get: function () { get: function () {
mergeQuery(f._scope, targetClass._scopeMeta[name]); mergeQuery(f._scope, targetModel._scopeMeta[name]);
return f; return f;
} }
}); });
@ -207,16 +230,16 @@ function defineScope(cls, targetClass, name, params, methods, options) {
* @param {Object} The data object * @param {Object} The data object
* @param {Object} The where clause * @param {Object} The where clause
*/ */
function setScopeValuesFromWhere(data, where) { function setScopeValuesFromWhere(data, where, targetModel) {
for (var i in where) { for (var i in where) {
if (i === 'and') { if (i === 'and') {
// Find fixed property values from each subclauses // Find fixed property values from each subclauses
for (var w = 0, n = where[i].length; w < n; w++) { for (var w = 0, n = where[i].length; w < n; w++) {
setScopeValuesFromWhere(data, where[i][w]); setScopeValuesFromWhere(data, where[i][w], targetModel);
} }
continue; continue;
} }
var prop = targetClass.definition.properties[i]; var prop = targetModel.definition.properties[i];
if (prop) { if (prop) {
var val = where[i]; var val = where[i];
if (typeof val !== 'object' || val instanceof prop.type if (typeof val !== 'object' || val instanceof prop.type
@ -233,9 +256,10 @@ function defineScope(cls, targetClass, name, params, methods, options) {
function build(data) { function build(data) {
data = data || {}; data = data || {};
// Find all fixed property values for the scope // Find all fixed property values for the scope
var targetModel = definition.targetModel(this._receiver);
var where = (this._scope && this._scope.where) || {}; var where = (this._scope && this._scope.where) || {};
setScopeValuesFromWhere(data, where); setScopeValuesFromWhere(data, where, targetModel);
return new targetClass(data); return new targetModel(data);
} }
function create(data, cb) { function create(data, cb) {
@ -256,14 +280,16 @@ function defineScope(cls, targetClass, name, params, methods, options) {
if (typeof where === 'function') cb = where, where = {}; if (typeof where === 'function') cb = where, where = {};
var scoped = (this._scope && this._scope.where) || {}; var scoped = (this._scope && this._scope.where) || {};
var filter = mergeQuery({ where: scoped }, { where: 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) { function count(where, cb) {
if (typeof where === 'function') cb = where, where = {}; if (typeof where === 'function') cb = where, where = {};
var scoped = (this._scope && this._scope.where) || {}; var scoped = (this._scope && this._scope.where) || {};
var filter = mergeQuery({ where: scoped }, { where: 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; 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();
});
});
});