Refactor polymorphic relations, fix inverse #215

See #215 - when creating a related item through a the inverse of a
polymorphic HABTM relation,
the through-model was not created correctly. By refactoring the
specifics into the `polymorphic`
property of a RelationDefinition, it's now possible to handle this
correctly.
This commit is contained in:
Fabien Franzen 2014-08-15 12:55:10 +02:00
parent 35850f6632
commit c3c2c85ce4
2 changed files with 76 additions and 20 deletions

View File

@ -98,8 +98,8 @@ function RelationDefinition(definition) {
this.keyFrom = definition.keyFrom; this.keyFrom = definition.keyFrom;
this.modelTo = definition.modelTo; this.modelTo = definition.modelTo;
this.keyTo = definition.keyTo; this.keyTo = definition.keyTo;
this.discriminator = definition.discriminator; this.polymorphic = definition.polymorphic;
if (!this.discriminator) { if (typeof this.polymorphic !== 'object') {
assert(this.modelTo, 'Target model is required'); assert(this.modelTo, 'Target model is required');
} }
this.modelThrough = definition.modelThrough; this.modelThrough = definition.modelThrough;
@ -137,8 +137,13 @@ RelationDefinition.prototype.applyScope = function(modelInstance, filter) {
filter = filter || {}; filter = filter || {};
filter.where = filter.where || {}; filter.where = filter.where || {};
if ((this.type !== 'belongsTo' || this.type === 'hasOne') if ((this.type !== 'belongsTo' || this.type === 'hasOne')
&& typeof this.discriminator === 'string') { // polymorphic && typeof this.polymorphic === 'object') { // polymorphic
filter.where[this.discriminator] = this.modelFrom.modelName; var discriminator = this.polymorphic.discriminator;
if (this.polymorphic.invert) {
filter.where[discriminator] = this.modelTo.modelName;
} else {
filter.where[discriminator] = this.modelFrom.modelName;
}
} }
if (typeof this.scope === 'function') { if (typeof this.scope === 'function') {
var scope = this.scope.call(this, modelInstance, filter); var scope = this.scope.call(this, modelInstance, filter);
@ -172,8 +177,13 @@ RelationDefinition.prototype.applyProperties = function(modelInstance, obj) {
} }
} }
if ((this.type !== 'belongsTo' || this.type === 'hasOne') if ((this.type !== 'belongsTo' || this.type === 'hasOne')
&& typeof this.discriminator === 'string') { // polymorphic && typeof this.polymorphic === 'object') { // polymorphic
target[this.discriminator] = this.modelFrom.modelName; var discriminator = this.polymorphic.discriminator;
if (this.polymorphic.invert) {
target[discriminator] = this.modelTo.modelName;
} else {
target[discriminator] = this.modelFrom.modelName;
}
} }
}; };
@ -461,10 +471,11 @@ RelationDefinition.hasMany = function hasMany(modelFrom, modelTo, params) {
var fk = params.foreignKey || i8n.camelize(thisClassName + '_id', true); var fk = params.foreignKey || i8n.camelize(thisClassName + '_id', true);
var idName = modelFrom.dataSource.idName(modelFrom.modelName) || 'id'; var idName = modelFrom.dataSource.idName(modelFrom.modelName) || 'id';
var discriminator; var discriminator, polymorphic;
if (params.polymorphic) { if (params.polymorphic) {
var polymorphic = polymorphicParams(params.polymorphic); polymorphic = polymorphicParams(params.polymorphic);
polymorphic.invert = !!params.invert;
discriminator = polymorphic.discriminator; discriminator = polymorphic.discriminator;
if (!params.invert) { if (!params.invert) {
fk = polymorphic.foreignKey; fk = polymorphic.foreignKey;
@ -480,12 +491,12 @@ RelationDefinition.hasMany = function hasMany(modelFrom, modelTo, params) {
modelFrom: modelFrom, modelFrom: modelFrom,
keyFrom: idName, keyFrom: idName,
keyTo: fk, keyTo: fk,
discriminator: discriminator,
modelTo: modelTo, modelTo: modelTo,
multiple: true, multiple: true,
properties: params.properties, properties: params.properties,
scope: params.scope, scope: params.scope,
options: params.options options: params.options,
polymorphic: polymorphic
}); });
definition.modelThrough = params.through; definition.modelThrough = params.through;
@ -700,9 +711,13 @@ var throughKeys = function(definition) {
var modelThrough = definition.modelThrough; var modelThrough = definition.modelThrough;
var pk2 = definition.modelTo.definition.idName(); var pk2 = definition.modelTo.definition.idName();
if (definition.discriminator) { // polymorphic if (typeof definition.polymorphic === 'object') { // polymorphic
var fk1 = definition.keyTo; var fk1 = definition.keyTo;
if (definition.polymorphic.invert) {
var fk2 = definition.polymorphic.foreignKey;
} else {
var fk2 = definition.keyThrough; var fk2 = definition.keyThrough;
}
} else { } else {
var fk1 = findBelongsTo(modelThrough, definition.modelFrom, var fk1 = findBelongsTo(modelThrough, definition.modelFrom,
definition.keyFrom); definition.keyFrom);
@ -1004,11 +1019,11 @@ RelationDefinition.belongsTo = function (modelFrom, modelTo, params) {
modelFrom: modelFrom, modelFrom: modelFrom,
keyFrom: fk, keyFrom: fk,
keyTo: idName, keyTo: idName,
discriminator: discriminator,
modelTo: modelTo, modelTo: modelTo,
properties: params.properties, properties: params.properties,
scope: params.scope, scope: params.scope,
options: params.options options: params.options,
polymorphic: polymorphic
}); });
// Define a property for the scope so that we have 'this' for the scoped methods // Define a property for the scope so that we have 'this' for the scoped methods
@ -1086,10 +1101,10 @@ BelongsTo.prototype.related = function (refresh, params) {
var self = this; var self = this;
var modelFrom = this.definition.modelFrom; var modelFrom = this.definition.modelFrom;
var modelTo = this.definition.modelTo; var modelTo = this.definition.modelTo;
var discriminator = this.definition.discriminator;
var pk = this.definition.keyTo; var pk = this.definition.keyTo;
var fk = this.definition.keyFrom; var fk = this.definition.keyFrom;
var modelInstance = this.modelInstance; var modelInstance = this.modelInstance;
var discriminator;
if (arguments.length === 1) { if (arguments.length === 1) {
params = refresh; params = refresh;
@ -1098,6 +1113,10 @@ BelongsTo.prototype.related = function (refresh, params) {
throw new Error('Method can\'t be called with more than two arguments'); throw new Error('Method can\'t be called with more than two arguments');
} }
if (typeof this.definition.polymorphic === 'object') {
discriminator = this.definition.polymorphic.discriminator;
}
var cachedValue; var cachedValue;
if (!refresh) { if (!refresh) {
cachedValue = self.getCache(); cachedValue = self.getCache();
@ -1105,7 +1124,10 @@ BelongsTo.prototype.related = function (refresh, params) {
if (params instanceof ModelBaseClass) { // acts as setter if (params instanceof ModelBaseClass) { // acts as setter
modelTo = params.constructor; modelTo = params.constructor;
modelInstance[fk] = params[pk]; modelInstance[fk] = params[pk];
if (discriminator) modelInstance[discriminator] = params.constructor.modelName;
if (discriminator) {
modelInstance[discriminator] = params.constructor.modelName;
}
this.definition.applyProperties(modelInstance, params); this.definition.applyProperties(modelInstance, params);
@ -1245,10 +1267,10 @@ RelationDefinition.hasOne = function (modelFrom, modelTo, params) {
var relationName = params.as || i8n.camelize(modelTo.modelName, true); var relationName = params.as || i8n.camelize(modelTo.modelName, true);
var fk = params.foreignKey || i8n.camelize(modelFrom.modelName + '_id', true); var fk = params.foreignKey || i8n.camelize(modelFrom.modelName + '_id', true);
var discriminator; var discriminator, polymorphic;
if (params.polymorphic) { if (params.polymorphic) {
var polymorphic = polymorphicParams(params.polymorphic); polymorphic = polymorphicParams(params.polymorphic);
fk = polymorphic.foreignKey; fk = polymorphic.foreignKey;
discriminator = polymorphic.discriminator; discriminator = polymorphic.discriminator;
if (!params.through) { if (!params.through) {
@ -1262,10 +1284,10 @@ RelationDefinition.hasOne = function (modelFrom, modelTo, params) {
modelFrom: modelFrom, modelFrom: modelFrom,
keyFrom: pk, keyFrom: pk,
keyTo: fk, keyTo: fk,
discriminator: discriminator,
modelTo: modelTo, modelTo: modelTo,
properties: params.properties, properties: params.properties,
options: params.options options: params.options,
polymorphic: polymorphic
}); });
modelFrom.dataSource.defineForeignKey(modelTo.modelName, fk, modelFrom.modelName); modelFrom.dataSource.defineForeignKey(modelTo.modelName, fk, modelFrom.modelName);

View File

@ -1031,6 +1031,40 @@ describe('relations', function () {
}); });
}); });
it('should create polymorphic item through relation scope', function (done) {
Picture.findById(anotherPicture.id, function(err, p) {
p.authors.create({ name: 'Author 3' }, function(err, a) {
should.not.exist(err);
author = a;
author.name.should.equal('Author 3');
done();
});
});
});
it('should create polymorphic through model - new author', function (done) {
PictureLink.findOne({ where: {
pictureId: anotherPicture.id, imageableId: author.id, imageableType: 'Author'
} }, function(err, link) {
should.not.exist(err);
link.pictureId.should.eql(anotherPicture.id);
link.imageableId.should.eql(author.id);
link.imageableType.should.equal('Author');
done();
});
});
it('should find polymorphic items - new author', function (done) {
Author.findById(author.id, function(err, author) {
author.pictures(function(err, pics) {
pics.should.have.length(1);
pics[0].id.should.eql(anotherPicture.id);
pics[0].name.should.equal('Example');
done();
});
});
});
}); });
describe('belongsTo', function () { describe('belongsTo', function () {