From 9b97e1ae778065a15ad0b2a508a60e9ce3f182dd Mon Sep 17 00:00:00 2001 From: Fabien Franzen Date: Sat, 26 Jul 2014 12:47:55 +0200 Subject: [PATCH 01/10] Implemented polymorphic hasMany --- lib/relation-definition.js | 97 ++++++++++++++++++++----- lib/relations.js | 4 ++ test/relations.test.js | 144 +++++++++++++++++++++++++++++++++++++ 3 files changed, 227 insertions(+), 18 deletions(-) diff --git a/lib/relation-definition.js b/lib/relation-definition.js index 9279fc7e..5411f6a0 100644 --- a/lib/relation-definition.js +++ b/lib/relation-definition.js @@ -64,8 +64,11 @@ function RelationDefinition(definition) { assert(this.modelFrom, 'Source model is required'); this.keyFrom = definition.keyFrom; this.modelTo = definition.modelTo; - assert(this.modelTo, 'Target model is required'); this.keyTo = definition.keyTo; + this.typeTo = definition.typeTo; + if (!this.typeTo) { + assert(this.modelTo, 'Target model is required'); + } this.modelThrough = definition.modelThrough; this.keyThrough = definition.keyThrough; this.multiple = (this.type !== 'belongsTo' && this.type !== 'hasOne'); @@ -97,13 +100,17 @@ RelationDefinition.prototype.toJSON = function () { * @param {Object} filter (where, order, limit, fields, ...) */ RelationDefinition.prototype.applyScope = function(modelInstance, filter) { + filter.where = filter.where || {}; + if (this.type !== 'belongsTo' && typeof this.typeTo === 'string') { + filter.where[this.typeTo] = this.modelFrom.modelName; // polymorphic + } if (typeof this.scope === 'function') { var scope = this.scope.call(this, modelInstance, filter); - if (typeof scope === 'object') { - mergeQuery(filter, scope); - } - } else if (typeof this.scope === 'object') { - mergeQuery(filter, this.scope); + } else { + var scope = this.scope; + } + if (typeof scope === 'object') { + mergeQuery(filter, scope); } }; @@ -124,6 +131,9 @@ RelationDefinition.prototype.applyProperties = function(modelInstance, target) { target[key] = modelInstance[k]; } } + if (this.type !== 'belongsTo' && typeof this.typeTo === 'string') { + target[this.typeTo] = this.modelFrom.modelName; // polymorphic + } }; /** @@ -350,10 +360,20 @@ RelationDefinition.hasMany = function hasMany(modelFrom, modelTo, params) { modelTo = lookupModel(modelFrom.dataSource.modelBuilder.models, modelToName); } } + var relationName = params.as || i8n.camelize(modelTo.pluralModelName, true); var fk = params.foreignKey || i8n.camelize(thisClassName + '_id', true); var idName = modelFrom.dataSource.idName(modelFrom.modelName) || 'id'; + var typeTo; + + if (typeof params.polymorphic === 'string') { + fk = i8n.camelize(params.polymorphic + '_id', true); + var typeTo = i8n.camelize(params.polymorphic + '_type', true); + if (!params.through) { + modelTo.dataSource.defineProperty(modelTo.modelName, typeTo, { type: 'string' }); + } + } var definition = new RelationDefinition({ name: relationName, @@ -361,6 +381,7 @@ RelationDefinition.hasMany = function hasMany(modelFrom, modelTo, params) { modelFrom: modelFrom, keyFrom: idName, keyTo: fk, + typeTo: typeTo, modelTo: modelTo, multiple: true, properties: params.properties, @@ -378,6 +399,8 @@ RelationDefinition.hasMany = function hasMany(modelFrom, modelTo, params) { if (!params.through) { // obviously, modelTo should have attribute called `fk` + // for polymorphic relations, it is assumed to share the same fk type for all + // polymorphic models modelTo.dataSource.defineForeignKey(modelTo.modelName, fk, modelFrom.modelName); } @@ -797,8 +820,8 @@ HasManyThrough.prototype.remove = function (acInst, done) { * */ RelationDefinition.belongsTo = function (modelFrom, modelTo, params) { - params = params || {}; - if ('string' === typeof modelTo) { + var typeTo, params = params || {}; + if ('string' === typeof modelTo && !params.polymorphic) { params.as = modelTo; if (params.model) { modelTo = params.model; @@ -807,10 +830,30 @@ RelationDefinition.belongsTo = function (modelFrom, modelTo, params) { modelTo = lookupModel(modelFrom.dataSource.modelBuilder.models, modelToName); } } - - var idName = modelFrom.dataSource.idName(modelTo.modelName) || 'id'; - var relationName = params.as || i8n.camelize(modelTo.modelName, true); - var fk = params.foreignKey || relationName + 'Id'; + + if (params.polymorphic) { + var polymorphic = modelTo; + modelTo = null; // will lookup dynamically + + var idName = params.idName || 'id'; + var relationName = params.as || i8n.camelize(polymorphic, true); + var fk = i8n.camelize(polymorphic + '_id', true); + var typeTo = i8n.camelize(polymorphic + '_type', true); + + if (typeof params.idType === 'string') { // explicit key type + modelFrom.dataSource.defineProperty(modelFrom.modelName, fk, { type: params.idType }); + } else { // try to use the same foreign key type as modelFrom + modelFrom.dataSource.defineForeignKey(modelFrom.modelName, fk, modelFrom.modelName); + } + + modelFrom.dataSource.defineProperty(modelFrom.modelName, typeTo, { type: 'string' }); + } else { + var idName = modelFrom.dataSource.idName(modelTo.modelName) || 'id'; + var relationName = params.as || i8n.camelize(modelTo.modelName, true); + var fk = params.foreignKey || relationName + 'Id'; + + modelFrom.dataSource.defineForeignKey(modelFrom.modelName, fk, modelTo.modelName); + } var relationDef = modelFrom.relations[relationName] = new RelationDefinition({ name: relationName, @@ -818,14 +861,13 @@ RelationDefinition.belongsTo = function (modelFrom, modelTo, params) { modelFrom: modelFrom, keyFrom: fk, keyTo: idName, + typeTo: typeTo, modelTo: modelTo, properties: params.properties, scope: params.scope, options: params.options }); - - modelFrom.dataSource.defineForeignKey(modelFrom.modelName, fk, modelTo.modelName); - + // Define a property for the scope so that we have 'this' for the scoped methods Object.defineProperty(modelFrom.prototype, relationName, { enumerable: true, @@ -835,7 +877,9 @@ RelationDefinition.belongsTo = function (modelFrom, modelTo, params) { var relationMethod = relation.related.bind(relation); relationMethod.create = relation.create.bind(relation); relationMethod.build = relation.build.bind(relation); - relationMethod._targetClass = relationDef.modelTo.modelName; + if (relationDef.modelTo) { + relationMethod._targetClass = relationDef.modelTo.modelName; + } return relationMethod; } }); @@ -863,7 +907,7 @@ BelongsTo.prototype.create = function(targetModelData, cb) { } this.definition.applyProperties(modelInstance, targetModelData || {}); - + modelTo.create(targetModelData, function(err, targetModel) { if(!err) { modelInstance[fk] = targetModel[pk]; @@ -894,7 +938,9 @@ BelongsTo.prototype.build = function(targetModelData) { */ BelongsTo.prototype.related = function (refresh, params) { var self = this; + var modelFrom = this.definition.modelFrom; var modelTo = this.definition.modelTo; + var typeTo = this.definition.typeTo; var pk = this.definition.keyTo; var fk = this.definition.keyFrom; var modelInstance = this.modelInstance; @@ -905,15 +951,30 @@ BelongsTo.prototype.related = function (refresh, params) { } else if (arguments.length > 2) { throw new Error('Method can\'t be called with more than two arguments'); } - + var cachedValue; if (!refresh) { cachedValue = self.getCache(); } if (params instanceof ModelBaseClass) { // acts as setter + modelTo = params.constructor; modelInstance[fk] = params[pk]; + if (typeTo) modelInstance[typeTo] = params.constructor.modelName; self.resetCache(params); } else if (typeof params === 'function') { // acts as async getter + + if (typeTo && !modelTo) { + var modelToName = modelInstance[typeTo]; + if (typeof modelToName !== 'string') { + throw new Error('Polymorphic model not found: `' + typeTo + '` not set'); + } + modelToName = modelToName.toLowerCase(); + modelTo = lookupModel(modelFrom.dataSource.modelBuilder.models, modelToName); + if (!modelTo) { + throw new Error('Polymorphic model not found: `' + modelToName + '`'); + } + } + var cb = params; if (cachedValue === undefined) { var query = {where: {}}; diff --git a/lib/relations.js b/lib/relations.js index c0985f2f..0e93d649 100644 --- a/lib/relations.js +++ b/lib/relations.js @@ -161,3 +161,7 @@ RelationMixin.hasAndBelongsToMany = function hasAndBelongsToMany(modelTo, params RelationMixin.hasOne = function hasMany(modelTo, params) { RelationDefinition.hasOne(this, modelTo, params); }; + +RelationMixin.belongsToPolymorphic = function belongsToPolymorphic(polymorphic, params) { + RelationDefinition.belongsToPolymorphic(this, polymorphic, params); +}; diff --git a/test/relations.test.js b/test/relations.test.js index f091486d..1d1fcf7c 100644 --- a/test/relations.test.js +++ b/test/relations.test.js @@ -3,6 +3,7 @@ var should = require('./init.js'); var db, Book, Chapter, Author, Reader; var Category, Product; +var Picture; describe('relations', function () { @@ -526,6 +527,149 @@ describe('relations', function () { }); }); + + describe('polymorphic hasMany', function () { + before(function (done) { + db = getSchema(); + Picture = db.define('Picture', {name: String}); + Author = db.define('Author', {name: String}); + Reader = db.define('Reader', {name: String}); + + db.automigrate(function () { + Picture.destroyAll(function () { + Author.destroyAll(function () { + Reader.destroyAll(done); + }); + }); + }); + }); + + it('can be declared', function (done) { + Author.hasMany(Picture, { polymorphic: 'imageable' }); + Reader.hasMany(Picture, { polymorphic: 'imageable' }); + Picture.belongsTo('imageable', { polymorphic: true }); + db.automigrate(done); + }); + + it('should create polymorphic relation - author', function (done) { + Author.create({ name: 'Author 1' }, function (err, author) { + author.pictures.create({ name: 'Author Pic' }, function (err, p) { + should.not.exist(err); + should.exist(p); + p.imageableId.should.equal(author.id); + p.imageableType.should.equal('Author'); + done(); + }); + }); + }); + + it('should create polymorphic relation - reader', function (done) { + Reader.create({ name: 'Reader 1' }, function (err, reader) { + reader.pictures.create({ name: 'Reader Pic' }, function (err, p) { + should.not.exist(err); + should.exist(p); + p.imageableId.should.equal(reader.id); + p.imageableType.should.equal('Reader'); + done(); + }); + }); + }); + + it('should find polymorphic items - author', function (done) { + Author.findOne(function (err, author) { + author.pictures(function (err, pics) { + should.not.exist(err); + pics.should.have.length(1); + pics[0].name.should.equal('Author Pic'); + done(); + }); + }); + }); + + it('should find polymorphic items - reader', function (done) { + Reader.findOne(function (err, reader) { + reader.pictures(function (err, pics) { + should.not.exist(err); + pics.should.have.length(1); + pics[0].name.should.equal('Reader Pic'); + done(); + }); + }); + }); + + it('should find the inverse of polymorphic relation - author', function (done) { + Picture.findOne({ where: { name: 'Author Pic' } }, function (err, p) { + should.not.exist(err); + p.imageableType.should.equal('Author'); + p.imageable(function(err, imageable) { + should.not.exist(err); + imageable.should.be.instanceof(Author); + imageable.name.should.equal('Author 1'); + done(); + }); + }); + }); + + it('should find the inverse of polymorphic relation - reader', function (done) { + Picture.findOne({ where: { name: 'Reader Pic' } }, function (err, p) { + should.not.exist(err); + p.imageableType.should.equal('Reader'); + p.imageable(function(err, imageable) { + should.not.exist(err); + imageable.should.be.instanceof(Reader); + imageable.name.should.equal('Reader 1'); + done(); + }); + }); + }); + + it('should include the inverse of polymorphic relation', function (done) { + Picture.find({ include: 'imageable' }, function (err, pics) { + should.not.exist(err); + pics.should.have.length(2); + pics[0].name.should.equal('Author Pic'); + pics[0].imageable().name.should.equal('Author 1'); + pics[1].name.should.equal('Reader Pic'); + pics[1].imageable().name.should.equal('Reader 1'); + done(); + }); + }); + + it('should assign a polymorphic relation', function(done) { + Author.create({ name: 'Author 2' }, function(err, author) { + var p = new Picture({ name: 'Sample' }); + p.imageable(author); // assign + p.imageableId.should.equal(author.id); + p.imageableType.should.equal('Author'); + p.save(done); + }); + }); + + it('should find polymorphic items - author', function (done) { + Author.findOne({ where: { name: 'Author 2' } }, function (err, author) { + author.pictures(function (err, pics) { + should.not.exist(err); + pics.should.have.length(1); + pics[0].name.should.equal('Sample'); + done(); + }); + }); + }); + + it('should find the inverse of polymorphic relation - author', function (done) { + Picture.findOne({ where: { name: 'Sample' } }, function (err, p) { + should.not.exist(err); + p.imageableType.should.equal('Author'); + p.imageable(function(err, imageable) { + should.not.exist(err); + imageable.should.be.instanceof(Author); + imageable.name.should.equal('Author 2'); + done(); + }); + }); + }); + + }); describe('belongsTo', function () { var List, Item, Fear, Mind; From 295e6fc1f113c70afe4135cfc1a492e1d155bac5 Mon Sep 17 00:00:00 2001 From: Fabien Franzen Date: Sat, 26 Jul 2014 14:54:54 +0200 Subject: [PATCH 02/10] Implemented polymorphic hasAndBelongsToMany --- lib/relation-definition.js | 60 ++++++++++++++++--------- test/relations.test.js | 90 +++++++++++++++++++++++++++++++++++++- 2 files changed, 129 insertions(+), 21 deletions(-) diff --git a/lib/relation-definition.js b/lib/relation-definition.js index 5411f6a0..2db36355 100644 --- a/lib/relation-definition.js +++ b/lib/relation-definition.js @@ -365,13 +365,14 @@ RelationDefinition.hasMany = function hasMany(modelFrom, modelTo, params) { var fk = params.foreignKey || i8n.camelize(thisClassName + '_id', true); var idName = modelFrom.dataSource.idName(modelFrom.modelName) || 'id'; - var typeTo; + var polymorphic, typeTo; if (typeof params.polymorphic === 'string') { - fk = i8n.camelize(params.polymorphic + '_id', true); - var typeTo = i8n.camelize(params.polymorphic + '_type', true); + polymorphic = params.polymorphic; + fk = i8n.camelize(polymorphic + '_id', true); + typeTo = i8n.camelize(polymorphic + '_type', true); if (!params.through) { - modelTo.dataSource.defineProperty(modelTo.modelName, typeTo, { type: 'string' }); + modelTo.dataSource.defineProperty(modelTo.modelName, typeTo, { type: 'string', index: true }); } } @@ -389,12 +390,11 @@ RelationDefinition.hasMany = function hasMany(modelFrom, modelTo, params) { options: params.options }); - if (params.through) { - definition.modelThrough = params.through; - var keyThrough = definition.throughKey || i8n.camelize(modelTo.modelName + '_id', true); - definition.keyThrough = keyThrough; - } - + definition.modelThrough = params.through; + + var keyThrough = definition.throughKey || i8n.camelize(modelTo.modelName + '_id', true); + definition.keyThrough = keyThrough; + modelFrom.relations[relationName] = definition; if (!params.through) { @@ -447,6 +447,7 @@ RelationDefinition.hasMany = function hasMany(modelFrom, modelTo, params) { filter.collect = i8n.camelize(modelTo.modelName, true); filter.include = filter.collect; } + return filter; }, scopeMethods); @@ -645,7 +646,7 @@ HasManyThrough.prototype.create = function create(data, done) { var definition = this.definition; var modelTo = definition.modelTo; var modelThrough = definition.modelThrough; - + if (typeof data === 'function' && !done) { done = data; data = {}; @@ -660,9 +661,16 @@ HasManyThrough.prototype.create = function create(data, done) { } // The primary key for the target model var pk2 = definition.modelTo.definition.idName(); - var fk1 = findBelongsTo(modelThrough, definition.modelFrom, - definition.keyFrom); - var fk2 = findBelongsTo(modelThrough, definition.modelTo, pk2); + + if (definition.typeTo) { // polymorphic + var fk1 = definition.keyTo; + var fk2 = definition.keyThrough; + } else { + var fk1 = findBelongsTo(modelThrough, definition.modelFrom, + definition.keyFrom); + var fk2 = findBelongsTo(modelThrough, definition.modelTo, pk2); + } + var d = {}; d[fk1] = modelInstance[definition.keyFrom]; d[fk2] = to[pk2]; @@ -841,12 +849,12 @@ RelationDefinition.belongsTo = function (modelFrom, modelTo, params) { var typeTo = i8n.camelize(polymorphic + '_type', true); if (typeof params.idType === 'string') { // explicit key type - modelFrom.dataSource.defineProperty(modelFrom.modelName, fk, { type: params.idType }); + modelFrom.dataSource.defineProperty(modelFrom.modelName, fk, { type: params.idType, index: true }); } else { // try to use the same foreign key type as modelFrom modelFrom.dataSource.defineForeignKey(modelFrom.modelName, fk, modelFrom.modelName); } - modelFrom.dataSource.defineProperty(modelFrom.modelName, typeTo, { type: 'string' }); + modelFrom.dataSource.defineProperty(modelFrom.modelName, typeTo, { type: 'string', index: true }); } else { var idName = modelFrom.dataSource.idName(modelTo.modelName) || 'id'; var relationName = params.as || i8n.camelize(modelTo.modelName, true); @@ -1055,10 +1063,22 @@ RelationDefinition.hasAndBelongsToMany = function hasAndBelongsToMany(modelFrom, params.through = lookupModel(models, name1) || lookupModel(models, name2) || modelFrom.dataSource.define(name1); } - params.through.belongsTo(modelFrom); - params.through.belongsTo(modelTo); - - this.hasMany(modelFrom, modelTo, {as: params.as, through: params.through}); + + var options = {as: params.as, through: params.through}; + + if (typeof params.polymorphic === 'string') { + options.polymorphic = params.polymorphic; + var accessor = params.through.prototype[params.polymorphic]; + if (typeof accessor !== 'function') { // declare once + params.through.belongsTo(modelTo); + params.through.belongsTo(params.polymorphic, { polymorphic: true }); + } + } else { + params.through.belongsTo(modelFrom); + params.through.belongsTo(modelTo); + } + + this.hasMany(modelFrom, modelTo, options); }; diff --git a/test/relations.test.js b/test/relations.test.js index 1d1fcf7c..11ffb97a 100644 --- a/test/relations.test.js +++ b/test/relations.test.js @@ -3,7 +3,7 @@ var should = require('./init.js'); var db, Book, Chapter, Author, Reader; var Category, Product; -var Picture; +var Picture, PictureLink; describe('relations', function () { @@ -670,6 +670,94 @@ describe('relations', function () { }); }); + + describe('polymorphic hasAndBelongsToMany through', function () { + before(function (done) { + db = getSchema(); + Picture = db.define('Picture', {name: String}); + Author = db.define('Author', {name: String}); + Reader = db.define('Reader', {name: String}); + PictureLink = db.define('PictureLink', {}); + + db.automigrate(function () { + Picture.destroyAll(function () { + PictureLink.destroyAll(function () { + Author.destroyAll(function () { + Reader.destroyAll(done); + }); + }); + }); + }); + }); + + it('can be declared', function (done) { + Author.hasAndBelongsToMany(Picture, { through: PictureLink, polymorphic: 'imageable' }); + Reader.hasAndBelongsToMany(Picture, { through: PictureLink, polymorphic: 'imageable' }); + db.automigrate(done); + }); + + it('should create polymorphic relation - author', function (done) { + Author.create({ id: 3, name: 'Author 1' }, function (err, author) { + author.pictures.create({ name: 'Author Pic 1' }, function (err, p) { + should.not.exist(err); + p.id.should.equal(1); + author.pictures.create({ name: 'Author Pic 2' }, function (err, p) { + should.not.exist(err); + p.id.should.equal(2); + done(); + }); + }); + }); + }); + + it('should create polymorphic relation - reader', function (done) { + Reader.create({ id: 4, name: 'Reader 1' }, function (err, reader) { + reader.pictures.create({ name: 'Reader Pic 1' }, function (err, p) { + should.not.exist(err); + p.id.should.equal(3); + done(); + }); + }); + }); + + it('should create polymorphic through model', function (done) { + PictureLink.findOne(function(err, link) { + should.not.exist(err); + link.pictureId.should.equal(1); + link.imageableId.should.equal(3); + link.imageableType.should.equal('Author'); + done(); + }); + }); + + it('should get polymorphic relation through model - author', function (done) { + Author.findById(3, function(err, author) { + should.not.exist(err); + author.name.should.equal('Author 1'); + author.pictures(function(err, pics) { + should.not.exist(err); + pics.should.have.length(2); + pics[0].name.should.equal('Author Pic 1'); + pics[1].name.should.equal('Author Pic 2'); + done(); + }); + }); + }); + + it('should get polymorphic relation through model - reader', function (done) { + Reader.findById(4, function(err, reader) { + should.not.exist(err); + reader.name.should.equal('Reader 1'); + reader.pictures(function(err, pics) { + should.not.exist(err); + pics.should.have.length(1); + pics[0].name.should.equal('Reader Pic 1'); + done(); + }); + }); + }); + + }); describe('belongsTo', function () { var List, Item, Fear, Mind; From 00dfe563eb2189ad774e69bab6a6ab774c4418aa Mon Sep 17 00:00:00 2001 From: Fabien Franzen Date: Sat, 26 Jul 2014 15:20:46 +0200 Subject: [PATCH 03/10] Implemented polymorphic hasOne Signed-off-by: Fabien Franzen --- lib/relation-definition.js | 33 +++++++++---- test/relations.test.js | 95 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+), 9 deletions(-) diff --git a/lib/relation-definition.js b/lib/relation-definition.js index 2db36355..92c23ab2 100644 --- a/lib/relation-definition.js +++ b/lib/relation-definition.js @@ -101,8 +101,9 @@ RelationDefinition.prototype.toJSON = function () { */ RelationDefinition.prototype.applyScope = function(modelInstance, filter) { filter.where = filter.where || {}; - if (this.type !== 'belongsTo' && typeof this.typeTo === 'string') { - filter.where[this.typeTo] = this.modelFrom.modelName; // polymorphic + if ((this.type !== 'belongsTo' || this.type === 'hasOne') + && typeof this.typeTo === 'string') { // polymorphic + filter.where[this.typeTo] = this.modelFrom.modelName; } if (typeof this.scope === 'function') { var scope = this.scope.call(this, modelInstance, filter); @@ -131,8 +132,9 @@ RelationDefinition.prototype.applyProperties = function(modelInstance, target) { target[key] = modelInstance[k]; } } - if (this.type !== 'belongsTo' && typeof this.typeTo === 'string') { - target[this.typeTo] = this.modelFrom.modelName; // polymorphic + if ((this.type !== 'belongsTo' || this.type === 'hasOne') + && typeof this.typeTo === 'string') { // polymorphic + target[this.typeTo] = this.modelFrom.modelName; } }; @@ -365,10 +367,10 @@ RelationDefinition.hasMany = function hasMany(modelFrom, modelTo, params) { var fk = params.foreignKey || i8n.camelize(thisClassName + '_id', true); var idName = modelFrom.dataSource.idName(modelFrom.modelName) || 'id'; - var polymorphic, typeTo; + var typeTo; if (typeof params.polymorphic === 'string') { - polymorphic = params.polymorphic; + var polymorphic = params.polymorphic; fk = i8n.camelize(polymorphic + '_id', true); typeTo = i8n.camelize(polymorphic + '_type', true); if (!params.through) { @@ -1049,15 +1051,17 @@ RelationDefinition.hasAndBelongsToMany = function hasAndBelongsToMany(modelFrom, if (params.model) { modelTo = params.model; } else { - modelTo = lookupModel(models, i8n.singularize(modelTo)) || - modelTo; + modelTo = lookupModel(models, i8n.singularize(modelTo)) || modelTo; } if (typeof modelTo === 'string') { throw new Error('Could not find "' + modelTo + '" relation for ' + modelFrom.modelName); } } + var isPolymorphic = (typeof params.polymorphic === 'string'); + if (!params.through) { + if (isPolymorphic) throw new Error('Polymorphic relations need a through model'); var name1 = modelFrom.modelName + modelTo.modelName; var name2 = modelTo.modelName + modelFrom.modelName; params.through = lookupModel(models, name1) || lookupModel(models, name2) || @@ -1066,7 +1070,7 @@ RelationDefinition.hasAndBelongsToMany = function hasAndBelongsToMany(modelFrom, var options = {as: params.as, through: params.through}; - if (typeof params.polymorphic === 'string') { + if (isPolymorphic) { options.polymorphic = params.polymorphic; var accessor = params.through.prototype[params.polymorphic]; if (typeof accessor !== 'function') { // declare once @@ -1113,6 +1117,16 @@ RelationDefinition.hasOne = function (modelFrom, modelTo, params) { var relationName = params.as || i8n.camelize(modelTo.modelName, true); var fk = params.foreignKey || i8n.camelize(modelFrom.modelName + '_id', true); + var typeTo; + + if (typeof params.polymorphic === 'string') { + var polymorphic = params.polymorphic; + fk = i8n.camelize(polymorphic + '_id', true); + typeTo = i8n.camelize(polymorphic + '_type', true); + if (!params.through) { + modelTo.dataSource.defineProperty(modelTo.modelName, typeTo, { type: 'string', index: true }); + } + } var relationDef = modelFrom.relations[relationName] = new RelationDefinition({ name: relationName, @@ -1120,6 +1134,7 @@ RelationDefinition.hasOne = function (modelFrom, modelTo, params) { modelFrom: modelFrom, keyFrom: pk, keyTo: fk, + typeTo: typeTo, modelTo: modelTo, properties: params.properties, options: params.options diff --git a/test/relations.test.js b/test/relations.test.js index 11ffb97a..824d710f 100644 --- a/test/relations.test.js +++ b/test/relations.test.js @@ -528,6 +528,101 @@ describe('relations', function () { }); + describe('polymorphic hasOne', function () { + before(function (done) { + db = getSchema(); + Picture = db.define('Picture', {name: String}); + Author = db.define('Author', {name: String}); + Reader = db.define('Reader', {name: String}); + + db.automigrate(function () { + Picture.destroyAll(function () { + Author.destroyAll(function () { + Reader.destroyAll(done); + }); + }); + }); + }); + + it('can be declared', function (done) { + Author.hasOne(Picture, { as: 'avatar', polymorphic: 'imageable' }); + Reader.hasOne(Picture, { as: 'mugshot', polymorphic: 'imageable' }); + Picture.belongsTo('imageable', { polymorphic: true }); + db.automigrate(done); + }); + + it('should create polymorphic relation - author', function (done) { + Author.create({ id: 8, name: 'Author 1' }, function (err, author) { + author.avatar.create({ name: 'Avatar' }, function (err, p) { + should.not.exist(err); + should.exist(p); + p.imageableId.should.equal(author.id); + p.imageableType.should.equal('Author'); + done(); + }); + }); + }); + + it('should create polymorphic relation - reader', function (done) { + Reader.create({ id: 6, name: 'Reader 1' }, function (err, reader) { + reader.mugshot.create({ name: 'Mugshot' }, function (err, p) { + should.not.exist(err); + should.exist(p); + p.imageableId.should.equal(reader.id); + p.imageableType.should.equal('Reader'); + done(); + }); + }); + }); + + it('should find polymorphic relation - author', function (done) { + Author.findOne(function (err, author) { + author.avatar(function (err, p) { + should.not.exist(err); + p.name.should.equal('Avatar'); + p.imageableId.should.equal(author.id); + p.imageableType.should.equal('Author'); + done(); + }); + }); + }); + + it('should find polymorphic relation - reader', function (done) { + Reader.findOne(function (err, reader) { + reader.mugshot(function (err, p) { + should.not.exist(err); + p.name.should.equal('Mugshot'); + p.imageableId.should.equal(reader.id); + p.imageableType.should.equal('Reader'); + done(); + }); + }); + }); + + it('should find inverse polymorphic relation - author', function (done) { + Picture.findOne({ where: { name: 'Avatar' } }, function (err, p) { + p.imageable(function (err, imageable) { + should.not.exist(err); + imageable.should.be.instanceof(Author); + imageable.name.should.equal('Author 1'); + done(); + }); + }); + }); + + it('should find inverse polymorphic relation - reader', function (done) { + Picture.findOne({ where: { name: 'Mugshot' } }, function (err, p) { + p.imageable(function (err, imageable) { + should.not.exist(err); + imageable.should.be.instanceof(Reader); + imageable.name.should.equal('Reader 1'); + done(); + }); + }); + }); + + }); + describe('polymorphic hasMany', function () { before(function (done) { db = getSchema(); From 9f94ec9bde589319af6c63e39aab5975755c636b Mon Sep 17 00:00:00 2001 From: Fabien Franzen Date: Sat, 26 Jul 2014 15:23:40 +0200 Subject: [PATCH 04/10] Minor cleanup --- lib/relation-definition.js | 4 ++-- lib/relations.js | 4 ---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/relation-definition.js b/lib/relation-definition.js index 92c23ab2..3f7d4477 100644 --- a/lib/relation-definition.js +++ b/lib/relation-definition.js @@ -1074,14 +1074,14 @@ RelationDefinition.hasAndBelongsToMany = function hasAndBelongsToMany(modelFrom, options.polymorphic = params.polymorphic; var accessor = params.through.prototype[params.polymorphic]; if (typeof accessor !== 'function') { // declare once - params.through.belongsTo(modelTo); params.through.belongsTo(params.polymorphic, { polymorphic: true }); } } else { params.through.belongsTo(modelFrom); - params.through.belongsTo(modelTo); } + params.through.belongsTo(modelTo); + this.hasMany(modelFrom, modelTo, options); }; diff --git a/lib/relations.js b/lib/relations.js index 0e93d649..c0985f2f 100644 --- a/lib/relations.js +++ b/lib/relations.js @@ -161,7 +161,3 @@ RelationMixin.hasAndBelongsToMany = function hasAndBelongsToMany(modelTo, params RelationMixin.hasOne = function hasMany(modelTo, params) { RelationDefinition.hasOne(this, modelTo, params); }; - -RelationMixin.belongsToPolymorphic = function belongsToPolymorphic(polymorphic, params) { - RelationDefinition.belongsToPolymorphic(this, polymorphic, params); -}; From d1896553fdd213e473b3a5a309fa336751fac230 Mon Sep 17 00:00:00 2001 From: Fabien Franzen Date: Sat, 26 Jul 2014 17:20:25 +0200 Subject: [PATCH 05/10] More hasAndBelongsToMany tests --- lib/relation-definition.js | 60 ++++++++++++++++++--------------- test/relations.test.js | 68 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 99 insertions(+), 29 deletions(-) diff --git a/lib/relation-definition.js b/lib/relation-definition.js index 3f7d4477..5ab62a55 100644 --- a/lib/relation-definition.js +++ b/lib/relation-definition.js @@ -574,6 +574,21 @@ HasMany.prototype.destroyById = function (fkId, cb) { }); }; +var throughKeys = function(definition) { + var modelThrough = definition.modelThrough; + var pk2 = definition.modelTo.definition.idName(); + + if (definition.typeTo) { // polymorphic + var fk1 = definition.keyTo; + var fk2 = definition.keyThrough; + } else { + var fk1 = findBelongsTo(modelThrough, definition.modelFrom, + definition.keyFrom); + var fk2 = findBelongsTo(modelThrough, definition.modelTo, pk2); + } + return [fk1, fk2]; +} + /** * Find a related item by foreign key * @param {*} fkId The foreign key value @@ -664,14 +679,9 @@ HasManyThrough.prototype.create = function create(data, done) { // The primary key for the target model var pk2 = definition.modelTo.definition.idName(); - if (definition.typeTo) { // polymorphic - var fk1 = definition.keyTo; - var fk2 = definition.keyThrough; - } else { - var fk1 = findBelongsTo(modelThrough, definition.modelFrom, - definition.keyFrom); - var fk2 = findBelongsTo(modelThrough, definition.modelTo, pk2); - } + var keys = throughKeys(definition); + var fk1 = keys[0]; + var fk2 = keys[1]; var d = {}; d[fk1] = modelInstance[definition.keyFrom]; @@ -694,6 +704,8 @@ HasManyThrough.prototype.create = function create(data, done) { }); }; + + /** * Add the target model instance to the 'hasMany' relation * @param {Object|ID} acInst The actual instance or id value @@ -706,14 +718,13 @@ HasManyThrough.prototype.add = function (acInst, done) { var data = {}; var query = {}; - - var fk1 = findBelongsTo(modelThrough, definition.modelFrom, - definition.keyFrom); - + // The primary key for the target model var pk2 = definition.modelTo.definition.idName(); - var fk2 = findBelongsTo(modelThrough, definition.modelTo, pk2); + var keys = throughKeys(definition); + var fk1 = keys[0]; + var fk2 = keys[1]; query[fk1] = this.modelInstance[pk1]; query[fk2] = (acInst instanceof definition.modelTo) ? acInst[pk2] : acInst; @@ -724,6 +735,7 @@ HasManyThrough.prototype.add = function (acInst, done) { data[fk1] = this.modelInstance[pk1]; data[fk2] = (acInst instanceof definition.modelTo) ? acInst[pk2] : acInst; + definition.applyProperties(this.modelInstance, data); // Create an instance of the through model @@ -749,15 +761,12 @@ HasManyThrough.prototype.exists = function (acInst, done) { var data = {}; var query = {}; - var fk1 = findBelongsTo(modelThrough, definition.modelFrom, - definition.keyFrom); - // The primary key for the target model var pk2 = definition.modelTo.definition.idName(); - - var fk2 = findBelongsTo(modelThrough, definition.modelTo, pk2); - - query[fk1] = this.modelInstance[pk1]; + + var keys = throughKeys(definition); + var fk1 = keys[0]; + var fk2 = keys[1]; query[fk2] = (acInst instanceof definition.modelTo) ? acInst[pk2] : acInst; @@ -781,13 +790,12 @@ HasManyThrough.prototype.remove = function (acInst, done) { var query = {}; - var fk1 = findBelongsTo(modelThrough, definition.modelFrom, - definition.keyFrom); - // The primary key for the target model var pk2 = definition.modelTo.definition.idName(); - - var fk2 = findBelongsTo(modelThrough, definition.modelTo, pk2); + + var keys = throughKeys(definition); + var fk1 = keys[0]; + var fk2 = keys[1]; query[fk1] = this.modelInstance[pk1]; query[fk2] = (acInst instanceof definition.modelTo) ? acInst[pk2] : acInst; @@ -795,7 +803,7 @@ HasManyThrough.prototype.remove = function (acInst, done) { var filter = { where: query }; definition.applyScope(this.modelInstance, filter); - + modelThrough.deleteAll(filter.where, function (err) { if (!err) { self.removeFromCache(query[fk2]); diff --git a/test/relations.test.js b/test/relations.test.js index 824d710f..fe2751d6 100644 --- a/test/relations.test.js +++ b/test/relations.test.js @@ -792,7 +792,7 @@ describe('relations', function () { }); it('should create polymorphic relation - author', function (done) { - Author.create({ id: 3, name: 'Author 1' }, function (err, author) { + Author.create({ id: 4, name: 'Author 1' }, function (err, author) { author.pictures.create({ name: 'Author Pic 1' }, function (err, p) { should.not.exist(err); p.id.should.equal(1); @@ -819,14 +819,14 @@ describe('relations', function () { PictureLink.findOne(function(err, link) { should.not.exist(err); link.pictureId.should.equal(1); - link.imageableId.should.equal(3); + link.imageableId.should.equal(4); link.imageableType.should.equal('Author'); done(); }); }); it('should get polymorphic relation through model - author', function (done) { - Author.findById(3, function(err, author) { + Author.findById(4, function(err, author) { should.not.exist(err); author.name.should.equal('Author 1'); author.pictures(function(err, pics) { @@ -851,6 +851,68 @@ describe('relations', function () { }); }); }); + + it('should include polymorphic items', function (done) { + Author.find({ include: 'pictures' }, function(err, authors) { + authors.should.have.length(1); + authors[0].pictures(function(err, pics) { + pics[0].name.should.equal('Author Pic 1'); + pics[1].name.should.equal('Author Pic 2'); + done(); + }); + }); + }); + + it('should add to a polymorphic relation - author', function (done) { + Author.findById(4, function(err, author) { + Picture.create({ id: 7, name: 'Example' }, function(err, p) { + author.pictures.add(p, function(err, link) { + link.should.be.instanceof(PictureLink); + link.pictureId.should.equal(7); + link.imageableId.should.equal(4); + link.imageableType.should.equal('Author'); + done(); + }); + }); + }); + }); + + it('should create polymorphic through model', function (done) { + PictureLink.findOne({ where: { pictureId: 7 } }, function(err, link) { + should.not.exist(err); + link.pictureId.should.equal(7); + link.imageableId.should.equal(4); + link.imageableType.should.equal('Author'); + done(); + }); + }); + + it('should check if polymorphic relation exists - author', function (done) { + Author.findById(4, function(err, author) { + author.pictures.exists(7, function(err, exists) { + exists.should.be.true; + done(); + }); + }); + }); + + it('should remove from a polymorphic relation - author', function (done) { + Author.findById(4, function(err, author) { + author.pictures.remove(7, function(err) { + should.not.exist(err); + done(); + }); + }); + }); + + it('should check if polymorphic relation exists - author', function (done) { + Author.findById(4, function(err, author) { + author.pictures.exists(7, function(err, exists) { + exists.should.be.false; + done(); + }); + }); + }); }); From 5e30ec863797e5b3e0ce09e60283e81bced47891 Mon Sep 17 00:00:00 2001 From: Fabien Franzen Date: Sat, 26 Jul 2014 21:11:25 +0200 Subject: [PATCH 06/10] Implemented polymorphic hasMany through inverse Added option invert: true to enable inverse polymorphic hasMany relations. Fixed missing fk1 in query of HasManyThrough.prototype.exists. --- lib/relation-definition.js | 23 +++++++----- test/relations.test.js | 73 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 86 insertions(+), 10 deletions(-) diff --git a/lib/relation-definition.js b/lib/relation-definition.js index 5ab62a55..d85f04a0 100644 --- a/lib/relation-definition.js +++ b/lib/relation-definition.js @@ -371,8 +371,10 @@ RelationDefinition.hasMany = function hasMany(modelFrom, modelTo, params) { if (typeof params.polymorphic === 'string') { var polymorphic = params.polymorphic; - fk = i8n.camelize(polymorphic + '_id', true); typeTo = i8n.camelize(polymorphic + '_type', true); + if (!params.invert) { + fk = i8n.camelize(polymorphic + '_id', true); + } if (!params.through) { modelTo.dataSource.defineProperty(modelTo.modelName, typeTo, { type: 'string', index: true }); } @@ -445,7 +447,11 @@ RelationDefinition.hasMany = function hasMany(modelFrom, modelTo, params) { definition.applyScope(this, filter); - if (params.through) { + if (params.through && params.polymorphic && params.invert) { + filter.where[typeTo] = modelTo.modelName; // overwrite + filter.collect = params.polymorphic; + filter.include = filter.collect; + } else if (params.through) { filter.collect = i8n.camelize(modelTo.modelName, true); filter.include = filter.collect; } @@ -758,7 +764,6 @@ HasManyThrough.prototype.exists = function (acInst, done) { var modelThrough = definition.modelThrough; var pk1 = definition.keyFrom; - var data = {}; var query = {}; // The primary key for the target model @@ -768,12 +773,14 @@ HasManyThrough.prototype.exists = function (acInst, done) { var fk1 = keys[0]; var fk2 = keys[1]; + query[fk1] = this.modelInstance[pk1]; query[fk2] = (acInst instanceof definition.modelTo) ? acInst[pk2] : acInst; - - data[fk1] = this.modelInstance[pk1]; - data[fk2] = (acInst instanceof definition.modelTo) ? acInst[pk2] : acInst; - - modelThrough.count(query, function(err, ac) { + + var filter = { where: query }; + + definition.applyScope(this.modelInstance, filter); + + modelThrough.count(filter.where, function(err, ac) { done(err, ac > 0); }); }; diff --git a/test/relations.test.js b/test/relations.test.js index fe2751d6..8e66dfee 100644 --- a/test/relations.test.js +++ b/test/relations.test.js @@ -788,6 +788,8 @@ describe('relations', function () { it('can be declared', function (done) { Author.hasAndBelongsToMany(Picture, { through: PictureLink, polymorphic: 'imageable' }); Reader.hasAndBelongsToMany(Picture, { through: PictureLink, polymorphic: 'imageable' }); + Picture.hasMany(Author, { through: PictureLink, polymorphic: 'imageable', invert: true }); + Picture.hasMany(Reader, { through: PictureLink, polymorphic: 'imageable', invert: true }); db.automigrate(done); }); @@ -821,7 +823,11 @@ describe('relations', function () { link.pictureId.should.equal(1); link.imageableId.should.equal(4); link.imageableType.should.equal('Author'); - done(); + link.imageable(function(err, imageable) { + imageable.should.be.instanceof(Author); + imageable.id.should.equal(4); + done(); + }); }); }); @@ -856,6 +862,7 @@ describe('relations', function () { Author.find({ include: 'pictures' }, function(err, authors) { authors.should.have.length(1); authors[0].pictures(function(err, pics) { + pics.should.have.length(2); pics[0].name.should.equal('Author Pic 1'); pics[1].name.should.equal('Author Pic 2'); done(); @@ -878,7 +885,7 @@ describe('relations', function () { }); it('should create polymorphic through model', function (done) { - PictureLink.findOne({ where: { pictureId: 7 } }, function(err, link) { + PictureLink.findOne({ where: { pictureId: 7, imageableType: 'Author' } }, function(err, link) { should.not.exist(err); link.pictureId.should.equal(7); link.imageableId.should.equal(4); @@ -887,6 +894,57 @@ describe('relations', function () { }); }); + it('should add to a polymorphic relation - author', function (done) { + Author.create({ id: 5, name: 'Author 2' }, function (err, author) { + author.pictures.add(7, function (err, p) { + should.not.exist(err); + done(); + }); + }); + }); + + it('should add to a polymorphic relation - author', function (done) { + Reader.create({ id: 5, name: 'Reader 2' }, function (err, reader) { + reader.pictures.add(7, function (err, p) { + should.not.exist(err); + done(); + }); + }); + }); + + it('should get the inverse polymorphic relation - author', function (done) { + Picture.findById(7, function(err, p) { + p.authors(function(err, authors) { + authors.should.have.length(2); + authors[0].name.should.equal('Author 1'); + authors[1].name.should.equal('Author 2'); + done(); + }); + }); + }); + + it('should get the inverse polymorphic relation - reader', function (done) { + Picture.findById(7, function(err, p) { + p.readers(function(err, readers) { + readers.should.have.length(1); + readers[0].name.should.equal('Reader 2'); + done(); + }); + }); + }); + + it('should find polymorphic items - author', function (done) { + Author.findById(4, function(err, author) { + author.pictures(function(err, pics) { + pics.should.have.length(3); + pics[0].name.should.equal('Author Pic 1'); + pics[1].name.should.equal('Author Pic 2'); + pics[2].name.should.equal('Example'); + done(); + }); + }); + }); + it('should check if polymorphic relation exists - author', function (done) { Author.findById(4, function(err, author) { author.pictures.exists(7, function(err, exists) { @@ -905,6 +963,17 @@ describe('relations', function () { }); }); + it('should find polymorphic items - author', function (done) { + Author.findById(4, function(err, author) { + author.pictures(function(err, pics) { + pics.should.have.length(2); + pics[0].name.should.equal('Author Pic 1'); + pics[1].name.should.equal('Author Pic 2'); + done(); + }); + }); + }); + it('should check if polymorphic relation exists - author', function (done) { Author.findById(4, function(err, author) { author.pictures.exists(7, function(err, exists) { From 5a5aa3f14d19d376bff8f8dc9f0e3e98ad6e6776 Mon Sep 17 00:00:00 2001 From: Fabien Franzen Date: Sat, 26 Jul 2014 21:32:24 +0200 Subject: [PATCH 07/10] Minor tweaks; pass-through properties/scope for hasAndBelongsToMany --- lib/relation-definition.js | 2 ++ test/relations.test.js | 1 + 2 files changed, 3 insertions(+) diff --git a/lib/relation-definition.js b/lib/relation-definition.js index d85f04a0..856e252d 100644 --- a/lib/relation-definition.js +++ b/lib/relation-definition.js @@ -1084,6 +1084,8 @@ RelationDefinition.hasAndBelongsToMany = function hasAndBelongsToMany(modelFrom, } var options = {as: params.as, through: params.through}; + options.properties = params.properties; + options.scope = params.scope; if (isPolymorphic) { options.polymorphic = params.polymorphic; diff --git a/test/relations.test.js b/test/relations.test.js index 8e66dfee..a6580649 100644 --- a/test/relations.test.js +++ b/test/relations.test.js @@ -788,6 +788,7 @@ describe('relations', function () { it('can be declared', function (done) { Author.hasAndBelongsToMany(Picture, { through: PictureLink, polymorphic: 'imageable' }); Reader.hasAndBelongsToMany(Picture, { through: PictureLink, polymorphic: 'imageable' }); + // Optionally, define inverse relations: Picture.hasMany(Author, { through: PictureLink, polymorphic: 'imageable', invert: true }); Picture.hasMany(Reader, { through: PictureLink, polymorphic: 'imageable', invert: true }); db.automigrate(done); From 4e76c2a77f487702534325106a51a391b170526c Mon Sep 17 00:00:00 2001 From: Fabien Franzen Date: Mon, 28 Jul 2014 10:18:42 +0200 Subject: [PATCH 08/10] typeTo => discriminator --- lib/relation-definition.js | 50 +++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/lib/relation-definition.js b/lib/relation-definition.js index 856e252d..c46b1c3a 100644 --- a/lib/relation-definition.js +++ b/lib/relation-definition.js @@ -65,8 +65,8 @@ function RelationDefinition(definition) { this.keyFrom = definition.keyFrom; this.modelTo = definition.modelTo; this.keyTo = definition.keyTo; - this.typeTo = definition.typeTo; - if (!this.typeTo) { + this.discriminator = definition.discriminator; + if (!this.discriminator) { assert(this.modelTo, 'Target model is required'); } this.modelThrough = definition.modelThrough; @@ -102,8 +102,8 @@ RelationDefinition.prototype.toJSON = function () { RelationDefinition.prototype.applyScope = function(modelInstance, filter) { filter.where = filter.where || {}; if ((this.type !== 'belongsTo' || this.type === 'hasOne') - && typeof this.typeTo === 'string') { // polymorphic - filter.where[this.typeTo] = this.modelFrom.modelName; + && typeof this.discriminator === 'string') { // polymorphic + filter.where[this.discriminator] = this.modelFrom.modelName; } if (typeof this.scope === 'function') { var scope = this.scope.call(this, modelInstance, filter); @@ -133,8 +133,8 @@ RelationDefinition.prototype.applyProperties = function(modelInstance, target) { } } if ((this.type !== 'belongsTo' || this.type === 'hasOne') - && typeof this.typeTo === 'string') { // polymorphic - target[this.typeTo] = this.modelFrom.modelName; + && typeof this.discriminator === 'string') { // polymorphic + target[this.discriminator] = this.modelFrom.modelName; } }; @@ -367,16 +367,16 @@ RelationDefinition.hasMany = function hasMany(modelFrom, modelTo, params) { var fk = params.foreignKey || i8n.camelize(thisClassName + '_id', true); var idName = modelFrom.dataSource.idName(modelFrom.modelName) || 'id'; - var typeTo; + var discriminator; if (typeof params.polymorphic === 'string') { var polymorphic = params.polymorphic; - typeTo = i8n.camelize(polymorphic + '_type', true); + discriminator = i8n.camelize(polymorphic + '_type', true); if (!params.invert) { fk = i8n.camelize(polymorphic + '_id', true); } if (!params.through) { - modelTo.dataSource.defineProperty(modelTo.modelName, typeTo, { type: 'string', index: true }); + modelTo.dataSource.defineProperty(modelTo.modelName, discriminator, { type: 'string', index: true }); } } @@ -386,7 +386,7 @@ RelationDefinition.hasMany = function hasMany(modelFrom, modelTo, params) { modelFrom: modelFrom, keyFrom: idName, keyTo: fk, - typeTo: typeTo, + discriminator: discriminator, modelTo: modelTo, multiple: true, properties: params.properties, @@ -448,7 +448,7 @@ RelationDefinition.hasMany = function hasMany(modelFrom, modelTo, params) { definition.applyScope(this, filter); if (params.through && params.polymorphic && params.invert) { - filter.where[typeTo] = modelTo.modelName; // overwrite + filter.where[discriminator] = modelTo.modelName; // overwrite filter.collect = params.polymorphic; filter.include = filter.collect; } else if (params.through) { @@ -584,7 +584,7 @@ var throughKeys = function(definition) { var modelThrough = definition.modelThrough; var pk2 = definition.modelTo.definition.idName(); - if (definition.typeTo) { // polymorphic + if (definition.discriminator) { // polymorphic var fk1 = definition.keyTo; var fk2 = definition.keyThrough; } else { @@ -845,7 +845,7 @@ HasManyThrough.prototype.remove = function (acInst, done) { * */ RelationDefinition.belongsTo = function (modelFrom, modelTo, params) { - var typeTo, params = params || {}; + var discriminator, params = params || {}; if ('string' === typeof modelTo && !params.polymorphic) { params.as = modelTo; if (params.model) { @@ -863,7 +863,7 @@ RelationDefinition.belongsTo = function (modelFrom, modelTo, params) { var idName = params.idName || 'id'; var relationName = params.as || i8n.camelize(polymorphic, true); var fk = i8n.camelize(polymorphic + '_id', true); - var typeTo = i8n.camelize(polymorphic + '_type', true); + var discriminator = i8n.camelize(polymorphic + '_type', true); if (typeof params.idType === 'string') { // explicit key type modelFrom.dataSource.defineProperty(modelFrom.modelName, fk, { type: params.idType, index: true }); @@ -871,7 +871,7 @@ RelationDefinition.belongsTo = function (modelFrom, modelTo, params) { modelFrom.dataSource.defineForeignKey(modelFrom.modelName, fk, modelFrom.modelName); } - modelFrom.dataSource.defineProperty(modelFrom.modelName, typeTo, { type: 'string', index: true }); + modelFrom.dataSource.defineProperty(modelFrom.modelName, discriminator, { type: 'string', index: true }); } else { var idName = modelFrom.dataSource.idName(modelTo.modelName) || 'id'; var relationName = params.as || i8n.camelize(modelTo.modelName, true); @@ -886,7 +886,7 @@ RelationDefinition.belongsTo = function (modelFrom, modelTo, params) { modelFrom: modelFrom, keyFrom: fk, keyTo: idName, - typeTo: typeTo, + discriminator: discriminator, modelTo: modelTo, properties: params.properties, scope: params.scope, @@ -965,7 +965,7 @@ BelongsTo.prototype.related = function (refresh, params) { var self = this; var modelFrom = this.definition.modelFrom; var modelTo = this.definition.modelTo; - var typeTo = this.definition.typeTo; + var discriminator = this.definition.discriminator; var pk = this.definition.keyTo; var fk = this.definition.keyFrom; var modelInstance = this.modelInstance; @@ -984,14 +984,14 @@ BelongsTo.prototype.related = function (refresh, params) { if (params instanceof ModelBaseClass) { // acts as setter modelTo = params.constructor; modelInstance[fk] = params[pk]; - if (typeTo) modelInstance[typeTo] = params.constructor.modelName; + if (discriminator) modelInstance[discriminator] = params.constructor.modelName; self.resetCache(params); } else if (typeof params === 'function') { // acts as async getter - if (typeTo && !modelTo) { - var modelToName = modelInstance[typeTo]; + if (discriminator && !modelTo) { + var modelToName = modelInstance[discriminator]; if (typeof modelToName !== 'string') { - throw new Error('Polymorphic model not found: `' + typeTo + '` not set'); + throw new Error('Polymorphic model not found: `' + discriminator + '` not set'); } modelToName = modelToName.toLowerCase(); modelTo = lookupModel(modelFrom.dataSource.modelBuilder.models, modelToName); @@ -1134,14 +1134,14 @@ RelationDefinition.hasOne = function (modelFrom, modelTo, params) { var relationName = params.as || i8n.camelize(modelTo.modelName, true); var fk = params.foreignKey || i8n.camelize(modelFrom.modelName + '_id', true); - var typeTo; + var discriminator; if (typeof params.polymorphic === 'string') { var polymorphic = params.polymorphic; fk = i8n.camelize(polymorphic + '_id', true); - typeTo = i8n.camelize(polymorphic + '_type', true); + discriminator = i8n.camelize(polymorphic + '_type', true); if (!params.through) { - modelTo.dataSource.defineProperty(modelTo.modelName, typeTo, { type: 'string', index: true }); + modelTo.dataSource.defineProperty(modelTo.modelName, discriminator, { type: 'string', index: true }); } } @@ -1151,7 +1151,7 @@ RelationDefinition.hasOne = function (modelFrom, modelTo, params) { modelFrom: modelFrom, keyFrom: pk, keyTo: fk, - typeTo: typeTo, + discriminator: discriminator, modelTo: modelTo, properties: params.properties, options: params.options From 7ddfbb64095cbd90eaf7e994951980bc74cdbf32 Mon Sep 17 00:00:00 2001 From: Fabien Franzen Date: Mon, 28 Jul 2014 10:44:26 +0200 Subject: [PATCH 09/10] polymorphics can now be declared using object --- lib/relation-definition.js | 39 ++++++++++++++++++++++++++++---------- test/relations.test.js | 5 ++++- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/lib/relation-definition.js b/lib/relation-definition.js index c46b1c3a..bf7d3826 100644 --- a/lib/relation-definition.js +++ b/lib/relation-definition.js @@ -328,6 +328,19 @@ function lookupModel(models, modelName) { } } +/*! + * Normalize polymorphic parameters + * @param {Object|String} params Name of the polymorphic relation or params + * @returns {Object} The normalized parameters + */ +function polymorphicParams(params) { + if (typeof params === 'string') params = { as: params }; + if (typeof params.as !== 'string') params.as = 'reference'; // default + params.foreignKey = params.foreignKey || i8n.camelize(params.as + '_id', true); + params.discriminator = params.discriminator || i8n.camelize(params.as + '_type', true); + return params; +}; + /** * Define a "one to many" relationship by specifying the model name * @@ -369,11 +382,11 @@ RelationDefinition.hasMany = function hasMany(modelFrom, modelTo, params) { var idName = modelFrom.dataSource.idName(modelFrom.modelName) || 'id'; var discriminator; - if (typeof params.polymorphic === 'string') { - var polymorphic = params.polymorphic; - discriminator = i8n.camelize(polymorphic + '_type', true); + if (params.polymorphic) { + var polymorphic = polymorphicParams(params.polymorphic); + discriminator = polymorphic.discriminator; if (!params.invert) { - fk = i8n.camelize(polymorphic + '_id', true); + fk = polymorphic.foreignKey; } if (!params.through) { modelTo.dataSource.defineProperty(modelTo.modelName, discriminator, { type: 'string', index: true }); @@ -857,16 +870,22 @@ RelationDefinition.belongsTo = function (modelFrom, modelTo, params) { } if (params.polymorphic) { - var polymorphic = modelTo; + if (params.polymorphic === true) { + // modelTo will be the name of the polymorphic relation (string) + var polymorphic = polymorphicParams(modelTo); + } else { + var polymorphic = polymorphicParams(params.polymorphic); + } + modelTo = null; // will lookup dynamically var idName = params.idName || 'id'; - var relationName = params.as || i8n.camelize(polymorphic, true); - var fk = i8n.camelize(polymorphic + '_id', true); - var discriminator = i8n.camelize(polymorphic + '_type', true); + var relationName = params.as || polymorphic.as; + var fk = polymorphic.foreignKey; + var discriminator = polymorphic.discriminator; - if (typeof params.idType === 'string') { // explicit key type - modelFrom.dataSource.defineProperty(modelFrom.modelName, fk, { type: params.idType, index: true }); + if (typeof polymorphic.idType === 'string') { // explicit key type + modelFrom.dataSource.defineProperty(modelFrom.modelName, fk, { type: polymorphic.idType, index: true }); } else { // try to use the same foreign key type as modelFrom modelFrom.dataSource.defineForeignKey(modelFrom.modelName, fk, modelFrom.modelName); } diff --git a/test/relations.test.js b/test/relations.test.js index a6580649..6809e43b 100644 --- a/test/relations.test.js +++ b/test/relations.test.js @@ -641,7 +641,10 @@ describe('relations', function () { it('can be declared', function (done) { Author.hasMany(Picture, { polymorphic: 'imageable' }); - Reader.hasMany(Picture, { polymorphic: 'imageable' }); + Reader.hasMany(Picture, { polymorphic: { // alt syntax + as: 'imageable', foreignKey: 'imageableId', + discriminator: 'imageableType' + } }); Picture.belongsTo('imageable', { polymorphic: true }); db.automigrate(done); }); From 309105c4adcc594897cc69e39306fee3364992d2 Mon Sep 17 00:00:00 2001 From: Fabien Franzen Date: Mon, 28 Jul 2014 11:12:20 +0200 Subject: [PATCH 10/10] Updated remaining relations to use polymorphicParams --- lib/relation-definition.js | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/relation-definition.js b/lib/relation-definition.js index bf7d3826..eddcd91c 100644 --- a/lib/relation-definition.js +++ b/lib/relation-definition.js @@ -871,7 +871,7 @@ RelationDefinition.belongsTo = function (modelFrom, modelTo, params) { if (params.polymorphic) { if (params.polymorphic === true) { - // modelTo will be the name of the polymorphic relation (string) + // modelTo arg will be the name of the polymorphic relation (string) var polymorphic = polymorphicParams(modelTo); } else { var polymorphic = polymorphicParams(params.polymorphic); @@ -1091,11 +1091,9 @@ RelationDefinition.hasAndBelongsToMany = function hasAndBelongsToMany(modelFrom, throw new Error('Could not find "' + modelTo + '" relation for ' + modelFrom.modelName); } } - - var isPolymorphic = (typeof params.polymorphic === 'string'); - + if (!params.through) { - if (isPolymorphic) throw new Error('Polymorphic relations need a through model'); + if (params.polymorphic) throw new Error('Polymorphic relations need a through model'); var name1 = modelFrom.modelName + modelTo.modelName; var name2 = modelTo.modelName + modelFrom.modelName; params.through = lookupModel(models, name1) || lookupModel(models, name2) || @@ -1106,11 +1104,13 @@ RelationDefinition.hasAndBelongsToMany = function hasAndBelongsToMany(modelFrom, options.properties = params.properties; options.scope = params.scope; - if (isPolymorphic) { - options.polymorphic = params.polymorphic; - var accessor = params.through.prototype[params.polymorphic]; + if (params.polymorphic) { + var polymorphic = polymorphicParams(params.polymorphic); + options.polymorphic = polymorphic; // pass through + var accessor = params.through.prototype[polymorphic.as]; if (typeof accessor !== 'function') { // declare once - params.through.belongsTo(params.polymorphic, { polymorphic: true }); + // use the name of the polymorphic rel, not modelTo + params.through.belongsTo(polymorphic.as, { polymorphic: true }); } } else { params.through.belongsTo(modelFrom); @@ -1155,10 +1155,10 @@ RelationDefinition.hasOne = function (modelFrom, modelTo, params) { var fk = params.foreignKey || i8n.camelize(modelFrom.modelName + '_id', true); var discriminator; - if (typeof params.polymorphic === 'string') { - var polymorphic = params.polymorphic; - fk = i8n.camelize(polymorphic + '_id', true); - discriminator = i8n.camelize(polymorphic + '_type', true); + if (params.polymorphic) { + var polymorphic = polymorphicParams(params.polymorphic); + fk = polymorphic.foreignKey; + discriminator = polymorphic.discriminator; if (!params.through) { modelTo.dataSource.defineProperty(modelTo.modelName, discriminator, { type: 'string', index: true }); }