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:
parent
170dc661f4
commit
0a9cb72837
62
lib/scope.js
62
lib/scope.js
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
Loading…
Reference in New Issue