From 0a9cb72837a3de1b7d798f5c0b02cf7177666a11 Mon Sep 17 00:00:00 2001 From: Fabien Franzen Date: Sat, 30 Aug 2014 20:58:06 +0200 Subject: [PATCH] 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. --- lib/scope.js | 62 +++++++++++++++++++++++---------- test/scope.test.js | 85 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+), 18 deletions(-) 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