diff --git a/lib/scope.js b/lib/scope.js index 0dd2b40b..5124251d 100644 --- a/lib/scope.js +++ b/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; diff --git a/test/scope.test.js b/test/scope.test.js index cb464ce4..1a7b4374 100644 --- a/test/scope.test.js +++ b/test/scope.test.js @@ -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(); + }); + }); + +}); \ No newline at end of file