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 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;
|
||||||
|
|
|
@ -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