diff --git a/packages/ember-data/lib/serializers/json-serializer.js b/packages/ember-data/lib/serializers/json-serializer.js index a550f2ec61b..d3480799b58 100644 --- a/packages/ember-data/lib/serializers/json-serializer.js +++ b/packages/ember-data/lib/serializers/json-serializer.js @@ -585,6 +585,29 @@ export default Serializer.extend({ return { id: coerceId(relationshipHash), type: relationshipModelName }; }, + /** + Returns a polymorphic relationship formatted as a JSON-API "relationship object". + + http://jsonapi.org/format/#document-resource-object-relationships + + `relationshipOptions` is a hash which contains more information about the + polymorphic relationship which should be extracted: + - `resourceHash` complete hash of the resource the relationship should be + extracted from + - `relationshipKey` key under which the value for the relationship is + extracted from the resourceHash + - `relationshipMeta` meta information about the relationship + + @method extractPolymorphicRelationship + @param {Object} relationshipModelName + @param {Object} relationshipHash + @param {Object} relationshipOptions + @return {Object} + */ + extractPolymorphicRelationship: function(relationshipModelName, relationshipHash, relationshipOptions) { + return this.extractRelationship(relationshipModelName, relationshipHash); + }, + /** Returns the resource's relationships formatted as a JSON-API "relationships object". @@ -605,7 +628,15 @@ export default Serializer.extend({ let data = null; let relationshipHash = resourceHash[relationshipKey]; if (relationshipMeta.kind === 'belongsTo') { - data = this.extractRelationship(relationshipMeta.type, relationshipHash); + if (relationshipMeta.options.polymorphic) { + // extracting a polymorphic belongsTo may need more information + // than the type and the hash (which might only be an id) for the + // relationship, hence we pass the key, resource and + // relationshipMeta too + data = this.extractPolymorphicRelationship(relationshipMeta.type, relationshipHash, { key, resourceHash, relationshipMeta }); + } else { + data = this.extractRelationship(relationshipMeta.type, relationshipHash); + } } else if (relationshipMeta.kind === 'hasMany') { data = Ember.isNone(relationshipHash) ? null : relationshipHash.map((item) => this.extractRelationship(relationshipMeta.type, item)); } diff --git a/packages/ember-data/lib/serializers/rest-serializer.js b/packages/ember-data/lib/serializers/rest-serializer.js index 39e5e8295bc..96f7e4bf31e 100644 --- a/packages/ember-data/lib/serializers/rest-serializer.js +++ b/packages/ember-data/lib/serializers/rest-serializer.js @@ -55,6 +55,37 @@ var camelize = Ember.String.camelize; */ var RESTSerializer = JSONSerializer.extend({ + /** + `keyForPolymorphicType` can be used to define a custom key when + serializing and deserializing a polymorphic type. By default, the + returned key is `${key}Type`. + + Example + + ```app/serializers/post.js + import DS from 'ember-data'; + + export default DS.RESTSerializer.extend({ + keyForPolymorphicType: function(key, relationship) { + var relationshipKey = this.keyForRelationship(key); + + return 'type-' + relationshipKey; + } + }); + ``` + + @method keyForPolymorphicType + @param {String} key + @param {String} typeClass + @param {String} method + @return {String} normalized key + */ + keyForPolymorphicType: function(key, typeClass, method) { + var relationshipKey = this.keyForRelationship(key); + + return `${relationshipKey}Type`; + }, + /** Normalizes a part of the JSON payload returned by the server. You should override this method, munge the hash @@ -672,7 +703,7 @@ var RESTSerializer = JSONSerializer.extend({ /** You can use this method to customize how polymorphic objects are serialized. - By default the JSON Serializer creates the key by appending `Type` to + By default the REST Serializer creates the key by appending `Type` to the attribute and value from the model's camelcased model name. @method serializePolymorphicType @@ -683,12 +714,75 @@ var RESTSerializer = JSONSerializer.extend({ serializePolymorphicType: function(snapshot, json, relationship) { var key = relationship.key; var belongsTo = snapshot.belongsTo(key); + var typeKey = this.keyForPolymorphicType(key, relationship.type, 'serialize'); + + // old way of getting the key for the polymorphic type key = this.keyForAttribute ? this.keyForAttribute(key, "serialize") : key; + key = `${key}Type`; + + // The old way of serializing the type of a polymorphic record used + // `keyForAttribute`, which is not correct. The next code checks if the old + // way is used and if it differs from the new way of using + // `keyForPolymorphicType`. If this is the case, a deprecation warning is + // logged and the old way is restored (so nothing breaks). + if (key !== typeKey && this.keyForPolymorphicType === RESTSerializer.prototype.keyForPolymorphicType) { + Ember.deprecate("The key to serialize the type of a polymorphic record is created via keyForAttribute which has been deprecated. Use the keyForPolymorphicType hook instead.", false, { + id: 'ds.rest-serializer.deprecated-key-for-polymorphic-type', + until: '3.0.0' + }); + + typeKey = key; + } + if (Ember.isNone(belongsTo)) { - json[key + "Type"] = null; + json[typeKey] = null; } else { - json[key + "Type"] = Ember.String.camelize(belongsTo.modelName); + json[typeKey] = camelize(belongsTo.modelName); + } + }, + + /** + You can use this method to customize how a polymorphic relationship should + be extracted. + + @method extractPolymorphicRelationship + @param {Object} relationshipType + @param {Object} relationshipHash + @param {Object} relationshipOptions + @return {Object} + */ + extractPolymorphicRelationship: function(relationshipType, relationshipHash, relationshipOptions) { + var { key, resourceHash, relationshipMeta } = relationshipOptions; + + // A polymorphic belongsTo relationship can be present in the payload + // either in the form where the `id` and the `type` are given: + // + // { + // message: { id: 1, type: 'post' } + // } + // + // or by the `id` and a `Type` attribute: + // + // { + // message: 1, + // messageType: 'post' + // } + // + // The next code checks if the latter case is present and returns the + // corresponding JSON-API representation. The former case is handled within + // the base class JSONSerializer. + var isPolymorphic = relationshipMeta.options.polymorphic; + var typeProperty = this.keyForPolymorphicType(key, relationshipType, 'deserialize'); + + if (isPolymorphic && resourceHash.hasOwnProperty(typeProperty) && typeof relationshipHash !== 'object') { + let type = this.modelNameFromPayloadKey(resourceHash[typeProperty]); + return { + id: relationshipHash, + type: type + }; } + + return this._super(...arguments); } }); diff --git a/packages/ember-data/tests/integration/serializers/rest-serializer-test.js b/packages/ember-data/tests/integration/serializers/rest-serializer-test.js index f8df772b567..95ae9a9ca14 100644 --- a/packages/ember-data/tests/integration/serializers/rest-serializer-test.js +++ b/packages/ember-data/tests/integration/serializers/rest-serializer-test.js @@ -455,6 +455,81 @@ test('serializeBelongsTo with async polymorphic', function() { deepEqual(json, expected, 'returned JSON is correct'); }); +test('serializeBelongsTo logs deprecation when old behavior for getting polymorphic type key is used', function() { + var evilMinion, doomsdayDevice; + var json = {}; + var expected = { evilMinion: '1', myCustomKeyType: 'evilMinion' }; + + env.restSerializer.keyForAttribute = function() { + return 'myCustomKey'; + }; + + run(function() { + evilMinion = env.store.createRecord('evil-minion', { id: 1, name: 'Tomster' }); + doomsdayDevice = env.store.createRecord('doomsday-device', { id: 2, name: 'Yehuda', evilMinion: evilMinion }); + }); + + expectDeprecation(function() { + env.restSerializer.serializeBelongsTo(doomsdayDevice._createSnapshot(), json, { key: 'evilMinion', options: { polymorphic: true, async: true } }); + }, "The key to serialize the type of a polymorphic record is created via keyForAttribute which has been deprecated. Use the keyForPolymorphicType hook instead."); + + deepEqual(json, expected, 'returned JSON is correct'); +}); + +test('keyForPolymorphicType can be used to overwrite how the type of a polymorphic record is serialized', function() { + var evilMinion, doomsdayDevice; + var json = {}; + var expected = { evilMinion: '1', typeForEvilMinion: 'evilMinion' }; + + env.restSerializer.keyForPolymorphicType = function() { + return 'typeForEvilMinion'; + }; + + run(function() { + evilMinion = env.store.createRecord('evil-minion', { id: 1, name: 'Tomster' }); + doomsdayDevice = env.store.createRecord('doomsday-device', { id: 2, name: 'Yehuda', evilMinion: evilMinion }); + }); + + env.restSerializer.serializeBelongsTo(doomsdayDevice._createSnapshot(), json, { key: 'evilMinion', options: { polymorphic: true, async: true } }); + + deepEqual(json, expected, 'returned JSON is correct'); +}); + +test('keyForPolymorphicType can be used to overwrite how the type of a polymorphic record is looked up for normalization', function() { + var json = { + doomsdayDevice: { + id: '1', + evilMinion: '2', + typeForEvilMinion: 'evilMinion' + } + }; + + var expected = { + data: { + type: 'doomsday-device', + id: '1', + attributes: {}, + relationships: { + evilMinion: { + data: { + type: 'evil-minion', + id: '2' + } + } + } + }, + included: [] + }; + + env.restSerializer.keyForPolymorphicType = function() { + return 'typeForEvilMinion'; + }; + + var normalized = env.restSerializer.normalizeResponse(env.store, DoomsdayDevice, json, null, 'findRecord'); + + deepEqual(normalized, expected, 'normalized JSON is correct'); +}); + test('serializeIntoHash uses payloadKeyFromModelName to normalize the payload root key', function() { run(function() { league = env.store.createRecord('home-planet', { name: "Umber", id: "123" }); @@ -475,6 +550,42 @@ test('serializeIntoHash uses payloadKeyFromModelName to normalize the payload ro }); }); +test('normalizeResponse with async polymorphic belongsTo, using Type', function() { + env.registry.register('serializer:application', DS.RESTSerializer.extend()); + var store = env.store; + env.adapter.findRecord = (store, type) => { + if (type.modelName === 'doomsday-device') { + return { + doomsdayDevice: { + id: 1, + name: "DeathRay", + evilMinion: 1, + evilMinionType: 'yellowMinion' + } + }; + } + + equal(type.modelName, 'yellow-minion'); + + return { + yellowMinion: { + id: 1, + type: 'yellowMinion', + name: 'Alex', + eyes: 3 + } + }; + }; + + run(function() { + store.findRecord('doomsday-device', 1).then((deathRay) => { + return deathRay.get('evilMinion'); + }).then((evilMinion) => { + equal(evilMinion.get('eyes'), 3); + }); + }); +}); + test('normalizeResponse with async polymorphic belongsTo', function() { env.registry.register('serializer:application', DS.RESTSerializer.extend({ isNewSerializerAPI: true