Implemented polymorphic hasMany

This commit is contained in:
Fabien Franzen 2014-07-26 12:47:55 +02:00
parent 27add4ce0b
commit 9b97e1ae77
3 changed files with 227 additions and 18 deletions

View File

@ -64,8 +64,11 @@ function RelationDefinition(definition) {
assert(this.modelFrom, 'Source model is required'); assert(this.modelFrom, 'Source model is required');
this.keyFrom = definition.keyFrom; this.keyFrom = definition.keyFrom;
this.modelTo = definition.modelTo; this.modelTo = definition.modelTo;
assert(this.modelTo, 'Target model is required');
this.keyTo = definition.keyTo; this.keyTo = definition.keyTo;
this.typeTo = definition.typeTo;
if (!this.typeTo) {
assert(this.modelTo, 'Target model is required');
}
this.modelThrough = definition.modelThrough; this.modelThrough = definition.modelThrough;
this.keyThrough = definition.keyThrough; this.keyThrough = definition.keyThrough;
this.multiple = (this.type !== 'belongsTo' && this.type !== 'hasOne'); this.multiple = (this.type !== 'belongsTo' && this.type !== 'hasOne');
@ -97,14 +100,18 @@ RelationDefinition.prototype.toJSON = function () {
* @param {Object} filter (where, order, limit, fields, ...) * @param {Object} filter (where, order, limit, fields, ...)
*/ */
RelationDefinition.prototype.applyScope = function(modelInstance, filter) { 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') { if (typeof this.scope === 'function') {
var scope = this.scope.call(this, modelInstance, filter); var scope = this.scope.call(this, modelInstance, filter);
} else {
var scope = this.scope;
}
if (typeof scope === 'object') { if (typeof scope === 'object') {
mergeQuery(filter, scope); mergeQuery(filter, scope);
} }
} else if (typeof this.scope === 'object') {
mergeQuery(filter, this.scope);
}
}; };
/** /**
@ -124,6 +131,9 @@ RelationDefinition.prototype.applyProperties = function(modelInstance, target) {
target[key] = modelInstance[k]; 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); modelTo = lookupModel(modelFrom.dataSource.modelBuilder.models, modelToName);
} }
} }
var relationName = params.as || i8n.camelize(modelTo.pluralModelName, true); var relationName = params.as || i8n.camelize(modelTo.pluralModelName, true);
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 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({ var definition = new RelationDefinition({
name: relationName, name: relationName,
@ -361,6 +381,7 @@ RelationDefinition.hasMany = function hasMany(modelFrom, modelTo, params) {
modelFrom: modelFrom, modelFrom: modelFrom,
keyFrom: idName, keyFrom: idName,
keyTo: fk, keyTo: fk,
typeTo: typeTo,
modelTo: modelTo, modelTo: modelTo,
multiple: true, multiple: true,
properties: params.properties, properties: params.properties,
@ -378,6 +399,8 @@ RelationDefinition.hasMany = function hasMany(modelFrom, modelTo, params) {
if (!params.through) { if (!params.through) {
// obviously, modelTo should have attribute called `fk` // 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); modelTo.dataSource.defineForeignKey(modelTo.modelName, fk, modelFrom.modelName);
} }
@ -797,8 +820,8 @@ HasManyThrough.prototype.remove = function (acInst, done) {
* *
*/ */
RelationDefinition.belongsTo = function (modelFrom, modelTo, params) { RelationDefinition.belongsTo = function (modelFrom, modelTo, params) {
params = params || {}; var typeTo, params = params || {};
if ('string' === typeof modelTo) { if ('string' === typeof modelTo && !params.polymorphic) {
params.as = modelTo; params.as = modelTo;
if (params.model) { if (params.model) {
modelTo = params.model; modelTo = params.model;
@ -808,24 +831,43 @@ RelationDefinition.belongsTo = function (modelFrom, modelTo, params) {
} }
} }
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 idName = modelFrom.dataSource.idName(modelTo.modelName) || 'id';
var relationName = params.as || i8n.camelize(modelTo.modelName, true); var relationName = params.as || i8n.camelize(modelTo.modelName, true);
var fk = params.foreignKey || relationName + 'Id'; var fk = params.foreignKey || relationName + 'Id';
modelFrom.dataSource.defineForeignKey(modelFrom.modelName, fk, modelTo.modelName);
}
var relationDef = modelFrom.relations[relationName] = new RelationDefinition({ var relationDef = modelFrom.relations[relationName] = new RelationDefinition({
name: relationName, name: relationName,
type: RelationTypes.belongsTo, type: RelationTypes.belongsTo,
modelFrom: modelFrom, modelFrom: modelFrom,
keyFrom: fk, keyFrom: fk,
keyTo: idName, keyTo: idName,
typeTo: typeTo,
modelTo: modelTo, modelTo: modelTo,
properties: params.properties, properties: params.properties,
scope: params.scope, scope: params.scope,
options: params.options 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 // Define a property for the scope so that we have 'this' for the scoped methods
Object.defineProperty(modelFrom.prototype, relationName, { Object.defineProperty(modelFrom.prototype, relationName, {
enumerable: true, enumerable: true,
@ -835,7 +877,9 @@ RelationDefinition.belongsTo = function (modelFrom, modelTo, params) {
var relationMethod = relation.related.bind(relation); var relationMethod = relation.related.bind(relation);
relationMethod.create = relation.create.bind(relation); relationMethod.create = relation.create.bind(relation);
relationMethod.build = relation.build.bind(relation); relationMethod.build = relation.build.bind(relation);
if (relationDef.modelTo) {
relationMethod._targetClass = relationDef.modelTo.modelName; relationMethod._targetClass = relationDef.modelTo.modelName;
}
return relationMethod; return relationMethod;
} }
}); });
@ -894,7 +938,9 @@ BelongsTo.prototype.build = function(targetModelData) {
*/ */
BelongsTo.prototype.related = function (refresh, params) { BelongsTo.prototype.related = function (refresh, params) {
var self = this; var self = this;
var modelFrom = this.definition.modelFrom;
var modelTo = this.definition.modelTo; var modelTo = this.definition.modelTo;
var typeTo = this.definition.typeTo;
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;
@ -911,9 +957,24 @@ BelongsTo.prototype.related = function (refresh, params) {
cachedValue = self.getCache(); cachedValue = self.getCache();
} }
if (params instanceof ModelBaseClass) { // acts as setter if (params instanceof ModelBaseClass) { // acts as setter
modelTo = params.constructor;
modelInstance[fk] = params[pk]; modelInstance[fk] = params[pk];
if (typeTo) modelInstance[typeTo] = params.constructor.modelName;
self.resetCache(params); self.resetCache(params);
} else if (typeof params === 'function') { // acts as async getter } 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; var cb = params;
if (cachedValue === undefined) { if (cachedValue === undefined) {
var query = {where: {}}; var query = {where: {}};

View File

@ -161,3 +161,7 @@ RelationMixin.hasAndBelongsToMany = function hasAndBelongsToMany(modelTo, params
RelationMixin.hasOne = function hasMany(modelTo, params) { RelationMixin.hasOne = function hasMany(modelTo, params) {
RelationDefinition.hasOne(this, modelTo, params); RelationDefinition.hasOne(this, modelTo, params);
}; };
RelationMixin.belongsToPolymorphic = function belongsToPolymorphic(polymorphic, params) {
RelationDefinition.belongsToPolymorphic(this, polymorphic, params);
};

View File

@ -3,6 +3,7 @@ var should = require('./init.js');
var db, Book, Chapter, Author, Reader; var db, Book, Chapter, Author, Reader;
var Category, Product; var Category, Product;
var Picture;
describe('relations', function () { describe('relations', function () {
@ -527,6 +528,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 () { describe('belongsTo', function () {
var List, Item, Fear, Mind; var List, Item, Fear, Mind;