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 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,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