diff --git a/lib/relation-definition.js b/lib/relation-definition.js index 032168e9..cae23603 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.discriminator = definition.discriminator; + if (!this.discriminator) { + 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,18 @@ 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' || this.type === 'hasOne') + && 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); - 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 +132,10 @@ RelationDefinition.prototype.applyProperties = function(modelInstance, target) { target[key] = modelInstance[k]; } } + if ((this.type !== 'belongsTo' || this.type === 'hasOne') + && typeof this.discriminator === 'string') { // polymorphic + target[this.discriminator] = this.modelFrom.modelName; + } }; /** @@ -316,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 * @@ -350,10 +375,23 @@ 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 discriminator; + + if (params.polymorphic) { + var polymorphic = polymorphicParams(params.polymorphic); + discriminator = polymorphic.discriminator; + if (!params.invert) { + fk = polymorphic.foreignKey; + } + if (!params.through) { + modelTo.dataSource.defineProperty(modelTo.modelName, discriminator, { type: 'string', index: true }); + } + } var definition = new RelationDefinition({ name: relationName, @@ -361,6 +399,7 @@ RelationDefinition.hasMany = function hasMany(modelFrom, modelTo, params) { modelFrom: modelFrom, keyFrom: idName, keyTo: fk, + discriminator: discriminator, modelTo: modelTo, multiple: true, properties: params.properties, @@ -368,16 +407,17 @@ 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) { // 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); } @@ -423,10 +463,15 @@ RelationDefinition.hasMany = function hasMany(modelFrom, modelTo, params) { definition.applyScope(this, filter); - if (params.through) { + if (params.through && params.polymorphic && params.invert) { + filter.where[discriminator] = 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; } + return filter; }, scopeMethods); @@ -555,6 +600,21 @@ HasMany.prototype.destroyById = function (fkId, cb) { }); }; +var throughKeys = function(definition) { + var modelThrough = definition.modelThrough; + var pk2 = definition.modelTo.definition.idName(); + + if (definition.discriminator) { // 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 @@ -629,7 +689,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 = {}; @@ -644,9 +704,11 @@ 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); + + var keys = throughKeys(definition); + var fk1 = keys[0]; + var fk2 = keys[1]; + var d = {}; d[fk1] = modelInstance[definition.keyFrom]; d[fk2] = to[pk2]; @@ -668,6 +730,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 @@ -680,14 +744,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; @@ -698,6 +761,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 @@ -722,18 +786,21 @@ HasManyThrough.prototype.exists = 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; - - 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); }); }; @@ -750,13 +817,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; @@ -764,7 +830,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]); @@ -799,8 +865,9 @@ HasManyThrough.prototype.remove = function (acInst, done) { * */ RelationDefinition.belongsTo = function (modelFrom, modelTo, params) { + var discriminator, polymorphic; params = params || {}; - if ('string' === typeof modelTo) { + if ('string' === typeof modelTo && !params.polymorphic) { params.as = modelTo; if (params.model) { modelTo = params.model; @@ -810,9 +877,36 @@ RelationDefinition.belongsTo = function (modelFrom, modelTo, params) { } } - var idName = modelFrom.dataSource.idName(modelTo.modelName) || 'id'; - var relationName = params.as || i8n.camelize(modelTo.modelName, true); - var fk = params.foreignKey || relationName + 'Id'; + var idName, relationName, fk; + if (params.polymorphic) { + if (params.polymorphic === true) { + // modelTo arg will be the name of the polymorphic relation (string) + polymorphic = polymorphicParams(modelTo); + } else { + polymorphic = polymorphicParams(params.polymorphic); + } + + modelTo = null; // will lookup dynamically + + idName = params.idName || 'id'; + relationName = params.as || polymorphic.as; + fk = polymorphic.foreignKey; + discriminator = polymorphic.discriminator; + + 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); + } + + modelFrom.dataSource.defineProperty(modelFrom.modelName, discriminator, { type: 'string', index: true }); + } else { + idName = modelFrom.dataSource.idName(modelTo.modelName) || 'id'; + relationName = params.as || i8n.camelize(modelTo.modelName, true); + fk = params.foreignKey || relationName + 'Id'; + + modelFrom.dataSource.defineForeignKey(modelFrom.modelName, fk, modelTo.modelName); + } var relationDef = modelFrom.relations[relationName] = new RelationDefinition({ name: relationName, @@ -820,14 +914,13 @@ RelationDefinition.belongsTo = function (modelFrom, modelTo, params) { modelFrom: modelFrom, keyFrom: fk, keyTo: idName, + discriminator: discriminator, 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, @@ -837,7 +930,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; } }); @@ -865,7 +960,7 @@ BelongsTo.prototype.create = function(targetModelData, cb) { } this.definition.applyProperties(modelInstance, targetModelData || {}); - + modelTo.create(targetModelData, function(err, targetModel) { if(!err) { modelInstance[fk] = targetModel[pk]; @@ -896,7 +991,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 discriminator = this.definition.discriminator; var pk = this.definition.keyTo; var fk = this.definition.keyFrom; var modelInstance = this.modelInstance; @@ -907,15 +1004,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 (discriminator) modelInstance[discriminator] = params.constructor.modelName; self.resetCache(params); } else if (typeof params === 'function') { // acts as async getter + + if (discriminator && !modelTo) { + var modelToName = modelInstance[discriminator]; + if (typeof modelToName !== 'string') { + throw new Error('Polymorphic model not found: `' + discriminator + '` 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: {}}; @@ -987,24 +1099,40 @@ 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); } } - + if (!params.through) { + 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) || modelFrom.dataSource.define(name1); } - params.through.belongsTo(modelFrom); + + var options = {as: params.as, through: params.through}; + options.properties = params.properties; + options.scope = params.scope; + + 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 + // use the name of the polymorphic rel, not modelTo + params.through.belongsTo(polymorphic.as, { polymorphic: true }); + } + } else { + params.through.belongsTo(modelFrom); + } + params.through.belongsTo(modelTo); - - this.hasMany(modelFrom, modelTo, {as: params.as, through: params.through}); + + this.hasMany(modelFrom, modelTo, options); }; @@ -1039,6 +1167,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 discriminator; + + 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 }); + } + } var relationDef = modelFrom.relations[relationName] = new RelationDefinition({ name: relationName, @@ -1046,6 +1184,7 @@ RelationDefinition.hasOne = function (modelFrom, modelTo, params) { modelFrom: modelFrom, keyFrom: pk, keyTo: fk, + discriminator: discriminator, modelTo: modelTo, properties: params.properties, options: params.options diff --git a/test/relations.test.js b/test/relations.test.js index f091486d..a6d50981 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, PictureLink; describe('relations', function () { @@ -526,6 +527,481 @@ 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({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({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(); + 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: { // alt syntax + as: 'imageable', foreignKey: 'imageableId', + discriminator: 'imageableType' + } }); + 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('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' }); + // 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); + }); + + var author, reader, pictures = []; + it('should create polymorphic relation - author', function (done) { + Author.create({ name: 'Author 1' }, function (err, a) { + should.not.exist(err); + author = a; + author.pictures.create({ name: 'Author Pic 1' }, function (err, p) { + should.not.exist(err); + pictures.push(p); + author.pictures.create({ name: 'Author Pic 2' }, function (err, p) { + should.not.exist(err); + pictures.push(p); + done(); + }); + }); + }); + }); + + it('should create polymorphic relation - reader', function (done) { + Reader.create({ name: 'Reader 1' }, function (err, r) { + should.not.exist(err); + reader = r; + reader.pictures.create({ name: 'Reader Pic 1' }, function (err, p) { + should.not.exist(err); + pictures.push(p); + done(); + }); + }); + }); + + it('should create polymorphic through model', function (done) { + PictureLink.findOne(function(err, link) { + should.not.exist(err); + link.pictureId.should.eql(pictures[0].id); // eql for mongo ObjectId + link.imageableId.should.eql(author.id); + link.imageableType.should.equal('Author'); + link.imageable(function(err, imageable) { + imageable.should.be.instanceof(Author); + imageable.id.should.eql(author.id); + done(); + }); + }); + }); + + it('should get polymorphic relation through model - author', function (done) { + Author.findById(author.id, 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(reader.id, 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(); + }); + }); + }); + + 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.should.have.length(2); + pics[0].name.should.equal('Author Pic 1'); + pics[1].name.should.equal('Author Pic 2'); + done(); + }); + }); + }); + + var anotherPicture; + it('should add to a polymorphic relation - author', function (done) { + Author.findById(author.id, function(err, author) { + Picture.create({name: 'Example' }, function(err, p) { + should.not.exist(err); + pictures.push(p); + anotherPicture = p; + author.pictures.add(p, function(err, link) { + link.should.be.instanceof(PictureLink); + link.pictureId.should.eql(p.id); + link.imageableId.should.eql(author.id); + link.imageableType.should.equal('Author'); + done(); + }); + }); + }); + }); + + it('should create polymorphic through model', function (done) { + PictureLink.findOne({ where: { pictureId: anotherPicture.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(); + }); + }); + + var anotherAuthor, anotherReader; + it('should add to a polymorphic relation - author', function (done) { + Author.create({ name: 'Author 2' }, function (err, author) { + should.not.exist(err); + anotherAuthor = author; + author.pictures.add(anotherPicture.id, function (err, p) { + should.not.exist(err); + done(); + }); + }); + }); + + it('should add to a polymorphic relation - author', function (done) { + Reader.create({name: 'Reader 2' }, function (err, reader) { + should.not.exist(err); + anotherReader = reader; + reader.pictures.add(anotherPicture.id, function (err, p) { + should.not.exist(err); + done(); + }); + }); + }); + + it('should get the inverse polymorphic relation - author', function (done) { + Picture.findById(anotherPicture.id, 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(anotherPicture.id, 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(author.id, 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(author.id, function(err, author) { + author.pictures.exists(anotherPicture.id, function(err, exists) { + exists.should.be.true; + done(); + }); + }); + }); + + it('should remove from a polymorphic relation - author', function (done) { + Author.findById(author.id, function(err, author) { + author.pictures.remove(anotherPicture.id, function(err) { + should.not.exist(err); + done(); + }); + }); + }); + + it('should find polymorphic items - author', function (done) { + Author.findById(author.id, 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(author.id, function(err, author) { + author.pictures.exists(7, function(err, exists) { + exists.should.be.false; + done(); + }); + }); + }); + + }); describe('belongsTo', function () { var List, Item, Fear, Mind; @@ -596,10 +1072,12 @@ describe('relations', function () { }); db.automigrate(done); }); - + + var personCreated; it('should create record on scope', function (done) { var p = new Passport({ name: 'Passport', notes: 'Some notes...' }); - p.person.create({ id: 3, name: 'Fred', age: 36 }, function(err, person) { + p.person.create({name: 'Fred', age: 36 }, function(err, person) { + personCreated = person; p.personId.should.equal(person.id); p.save(function (err, p) { person.name.should.equal('Fred'); @@ -611,7 +1089,7 @@ describe('relations', function () { it('should find record on scope', function (done) { Passport.findOne(function (err, p) { - p.personId.should.equal(3); + p.personId.should.equal(personCreated.id); p.person(function(err, person) { person.name.should.equal('Fred'); person.should.not.have.property('age');