diff --git a/lib/datasource.js b/lib/datasource.js index 9602ec4f..fefe10a2 100644 --- a/lib/datasource.js +++ b/lib/datasource.js @@ -14,6 +14,7 @@ var util = require('util'); var assert = require('assert'); var async = require('async'); var traverse = require('traverse'); +var i8n = require('inflection'); if (process.env.DEBUG === 'loopback') { // For back-compatibility @@ -429,19 +430,39 @@ DataSource.prototype.defineRelations = function (modelClass, relations) { for (var rn in relations) { var r = relations[rn]; assert(DataSource.relationTypes.indexOf(r.type) !== -1, "Invalid relation type: " + r.type); - var targetModel = isModelClass(r.model) ? r.model : this.getModel(r.model, true); + var targetModel, polymorphicName; + + if (r.polymorphic && r.type !== 'belongsTo' && !r.model) { + throw new Error('No model specified for polymorphic ' + r.type + ': ' + rn); + } + + if (r.polymorphic) { + polymorphicName = typeof r.model === 'string' ? r.model : rn; + if (typeof r.polymorphic === 'string') { + polymorphicName = r.polymorphic; + } else if (typeof r.polymorphic === 'object' && typeof r.polymorphic.as === 'string') { + polymorphicName = r.polymorphic.as; + } + } + + if (r.model) { + targetModel = isModelClass(r.model) ? r.model : this.getModel(r.model, true); + } + var throughModel = null; if (r.through) { throughModel = isModelClass(r.through) ? r.through : this.getModel(r.through, true); } - if ((!r.polymorphic && !isModelDataSourceAttached(targetModel)) || (throughModel && !isModelDataSourceAttached(throughModel))) { + + if ((targetModel && !isModelDataSourceAttached(targetModel)) + || (throughModel && !isModelDataSourceAttached(throughModel))) { // Create a listener to defer the relation set up createListener(rn, r, targetModel, throughModel); } else { // The target model is resolved var params = traverse(r).clone(); params.as = rn; - params.model = targetModel; + params.model = polymorphicName || targetModel; if (throughModel) { params.through = throughModel; } diff --git a/lib/relation-definition.js b/lib/relation-definition.js index e11b28bc..c1b51ddb 100644 --- a/lib/relation-definition.js +++ b/lib/relation-definition.js @@ -115,12 +115,19 @@ function RelationDefinition(definition) { } RelationDefinition.prototype.toJSON = function () { + var polymorphic = typeof this.polymorphic === 'object'; + + var modelToName = this.modelTo && this.modelTo.modelName; + if (!modelToName && polymorphic && this.type === 'belongsTo') { + modelToName = ''; + } + var json = { name: this.name, type: this.type, modelFrom: this.modelFrom.modelName, keyFrom: this.keyFrom, - modelTo: this.modelTo.modelName, + modelTo: modelToName, keyTo: this.keyTo, multiple: this.multiple }; @@ -128,6 +135,9 @@ RelationDefinition.prototype.toJSON = function () { json.modelThrough = this.modelThrough.modelName; json.keyThrough = this.keyThrough; } + if (polymorphic) { + json.polymorphic = this.polymorphic; + } return json; }; @@ -449,14 +459,20 @@ function lookupModel(models, modelName) { function lookupModelTo(modelFrom, modelTo, params, singularize) { if ('string' === typeof modelTo) { + var modelToName; params.as = params.as || modelTo; modelTo = params.model || modelTo; if (typeof modelTo === 'string') { - var modelToName = (singularize ? i8n.singularize(modelTo) : modelTo).toLowerCase(); + modelToName = (singularize ? i8n.singularize(modelTo) : modelTo).toLowerCase(); + modelTo = lookupModel(modelFrom.dataSource.modelBuilder.models, modelToName) || modelTo; + } + if (typeof modelTo === 'string') { + modelToName = (singularize ? i8n.singularize(params.as) : params.as).toLowerCase(); + console.log(modelToName) modelTo = lookupModel(modelFrom.dataSource.modelBuilder.models, modelToName) || modelTo; } if (typeof modelTo !== 'function') { - throw new Error('Could not find "' + modelTo + '" relation for ' + modelFrom.modelName); + throw new Error('Could not find "' + params.as + '" relation for ' + modelFrom.modelName); } } return modelTo; @@ -510,7 +526,7 @@ RelationDefinition.hasMany = function hasMany(modelFrom, modelTo, params) { if (params.polymorphic) { polymorphic = polymorphicParams(params.polymorphic); - polymorphic.invert = !!params.invert; + if (params.invert) polymorphic.invert = true; discriminator = polymorphic.discriminator; if (!params.invert) { fk = polymorphic.foreignKey; @@ -1172,7 +1188,7 @@ BelongsTo.prototype.related = function (refresh, params) { self.resetCache(params); } else if (typeof params === 'function') { // acts as async getter - if (discriminator && !modelTo) { + if (discriminator) { var modelToName = modelInstance[discriminator]; if (typeof modelToName !== 'string') { throw new Error('Polymorphic model not found: `' + discriminator + '` not set'); diff --git a/test/loopback-dl.test.js b/test/loopback-dl.test.js index 458f91db..9d2d42d2 100644 --- a/test/loopback-dl.test.js +++ b/test/loopback-dl.test.js @@ -923,7 +923,51 @@ describe('Load models with relations', function () { assert(User.relations['posts']); done(); }); - + + it('should set up polymorphic relations', function (done) { + var ds = new DataSource('memory'); + + var Author = ds.define('Author', {name: String}, {relations: { + pictures: {type: 'hasMany', model: 'Picture', polymorphic: 'imageable'} + }}); + var Picture = ds.define('Picture', {name: String}, {relations: { + imageable: {type: 'belongsTo', polymorphic: true} + }}); + + assert(Author.relations['pictures']); + assert.deepEqual(Author.relations['pictures'].toJSON(), { + name: 'pictures', + type: 'hasMany', + modelFrom: 'Author', + keyFrom: 'id', + modelTo: 'Picture', + keyTo: 'imageableId', + multiple: true, + polymorphic: { + as: 'imageable', + foreignKey: 'imageableId', + discriminator: 'imageableType' + } + }); + + assert(Picture.relations['imageable']); + assert.deepEqual(Picture.relations['imageable'].toJSON(), { + name: 'imageable', + type: 'belongsTo', + modelFrom: 'Picture', + keyFrom: 'imageableId', + modelTo: '', + keyTo: 'id', + multiple: false, + polymorphic: { + as: 'imageable', + foreignKey: 'imageableId', + discriminator: 'imageableType' + } + }); + done(); + }); + it('should set up foreign key with the correct type', function (done) { var ds = new DataSource('memory'); diff --git a/test/relations.test.js b/test/relations.test.js index 87610f84..871ecb54 100644 --- a/test/relations.test.js +++ b/test/relations.test.js @@ -686,6 +686,37 @@ describe('relations', function () { discriminator: 'imageableType' } }); Picture.belongsTo('imageable', { polymorphic: true }); + + Author.relations['pictures'].toJSON().should.eql({ + name: 'pictures', + type: 'hasMany', + modelFrom: 'Author', + keyFrom: 'id', + modelTo: 'Picture', + keyTo: 'imageableId', + multiple: true, + polymorphic: { + as: 'imageable', + foreignKey: 'imageableId', + discriminator: 'imageableType' + } + }); + + Picture.relations['imageable'].toJSON().should.eql({ + name: 'imageable', + type: 'belongsTo', + modelFrom: 'Picture', + keyFrom: 'imageableId', + modelTo: '', + keyTo: 'id', + multiple: false, + polymorphic: { + as: 'imageable', + foreignKey: 'imageableId', + discriminator: 'imageableType' + } + }); + db.automigrate(done); });