Merge pull request #270 from fabien/feature/dynamic-scope-modelTo
Enable dynamic modelTo for scopes
This commit is contained in:
commit
f9a26bcb28
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,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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue