Skip to content

Commit 4e83729

Browse files
committed
Merge pull request #3835 from pangratz/extract-polymorphic-belongs-to
[BUGFIX] extract polymorphic belongsTo in RESTSerializer
2 parents 96d211b + 4f7cf7b commit 4e83729

File tree

3 files changed

+240
-4
lines changed

3 files changed

+240
-4
lines changed

packages/ember-data/lib/serializers/json-serializer.js

+32-1
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,29 @@ export default Serializer.extend({
585585
return { id: coerceId(relationshipHash), type: relationshipModelName };
586586
},
587587

588+
/**
589+
Returns a polymorphic relationship formatted as a JSON-API "relationship object".
590+
591+
http://jsonapi.org/format/#document-resource-object-relationships
592+
593+
`relationshipOptions` is a hash which contains more information about the
594+
polymorphic relationship which should be extracted:
595+
- `resourceHash` complete hash of the resource the relationship should be
596+
extracted from
597+
- `relationshipKey` key under which the value for the relationship is
598+
extracted from the resourceHash
599+
- `relationshipMeta` meta information about the relationship
600+
601+
@method extractPolymorphicRelationship
602+
@param {Object} relationshipModelName
603+
@param {Object} relationshipHash
604+
@param {Object} relationshipOptions
605+
@return {Object}
606+
*/
607+
extractPolymorphicRelationship: function(relationshipModelName, relationshipHash, relationshipOptions) {
608+
return this.extractRelationship(relationshipModelName, relationshipHash);
609+
},
610+
588611
/**
589612
Returns the resource's relationships formatted as a JSON-API "relationships object".
590613
@@ -605,7 +628,15 @@ export default Serializer.extend({
605628
let data = null;
606629
let relationshipHash = resourceHash[relationshipKey];
607630
if (relationshipMeta.kind === 'belongsTo') {
608-
data = this.extractRelationship(relationshipMeta.type, relationshipHash);
631+
if (relationshipMeta.options.polymorphic) {
632+
// extracting a polymorphic belongsTo may need more information
633+
// than the type and the hash (which might only be an id) for the
634+
// relationship, hence we pass the key, resource and
635+
// relationshipMeta too
636+
data = this.extractPolymorphicRelationship(relationshipMeta.type, relationshipHash, { key, resourceHash, relationshipMeta });
637+
} else {
638+
data = this.extractRelationship(relationshipMeta.type, relationshipHash);
639+
}
609640
} else if (relationshipMeta.kind === 'hasMany') {
610641
data = Ember.isNone(relationshipHash) ? null : relationshipHash.map((item) => this.extractRelationship(relationshipMeta.type, item));
611642
}

packages/ember-data/lib/serializers/rest-serializer.js

+97-3
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,37 @@ var camelize = Ember.String.camelize;
5555
*/
5656
var RESTSerializer = JSONSerializer.extend({
5757

58+
/**
59+
`keyForPolymorphicType` can be used to define a custom key when
60+
serializing and deserializing a polymorphic type. By default, the
61+
returned key is `${key}Type`.
62+
63+
Example
64+
65+
```app/serializers/post.js
66+
import DS from 'ember-data';
67+
68+
export default DS.RESTSerializer.extend({
69+
keyForPolymorphicType: function(key, relationship) {
70+
var relationshipKey = this.keyForRelationship(key);
71+
72+
return 'type-' + relationshipKey;
73+
}
74+
});
75+
```
76+
77+
@method keyForPolymorphicType
78+
@param {String} key
79+
@param {String} typeClass
80+
@param {String} method
81+
@return {String} normalized key
82+
*/
83+
keyForPolymorphicType: function(key, typeClass, method) {
84+
var relationshipKey = this.keyForRelationship(key);
85+
86+
return `${relationshipKey}Type`;
87+
},
88+
5889
/**
5990
Normalizes a part of the JSON payload returned by
6091
the server. You should override this method, munge the hash
@@ -672,7 +703,7 @@ var RESTSerializer = JSONSerializer.extend({
672703

673704
/**
674705
You can use this method to customize how polymorphic objects are serialized.
675-
By default the JSON Serializer creates the key by appending `Type` to
706+
By default the REST Serializer creates the key by appending `Type` to
676707
the attribute and value from the model's camelcased model name.
677708
678709
@method serializePolymorphicType
@@ -683,12 +714,75 @@ var RESTSerializer = JSONSerializer.extend({
683714
serializePolymorphicType: function(snapshot, json, relationship) {
684715
var key = relationship.key;
685716
var belongsTo = snapshot.belongsTo(key);
717+
var typeKey = this.keyForPolymorphicType(key, relationship.type, 'serialize');
718+
719+
// old way of getting the key for the polymorphic type
686720
key = this.keyForAttribute ? this.keyForAttribute(key, "serialize") : key;
721+
key = `${key}Type`;
722+
723+
// The old way of serializing the type of a polymorphic record used
724+
// `keyForAttribute`, which is not correct. The next code checks if the old
725+
// way is used and if it differs from the new way of using
726+
// `keyForPolymorphicType`. If this is the case, a deprecation warning is
727+
// logged and the old way is restored (so nothing breaks).
728+
if (key !== typeKey && this.keyForPolymorphicType === RESTSerializer.prototype.keyForPolymorphicType) {
729+
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, {
730+
id: 'ds.rest-serializer.deprecated-key-for-polymorphic-type',
731+
until: '3.0.0'
732+
});
733+
734+
typeKey = key;
735+
}
736+
687737
if (Ember.isNone(belongsTo)) {
688-
json[key + "Type"] = null;
738+
json[typeKey] = null;
689739
} else {
690-
json[key + "Type"] = Ember.String.camelize(belongsTo.modelName);
740+
json[typeKey] = camelize(belongsTo.modelName);
741+
}
742+
},
743+
744+
/**
745+
You can use this method to customize how a polymorphic relationship should
746+
be extracted.
747+
748+
@method extractPolymorphicRelationship
749+
@param {Object} relationshipType
750+
@param {Object} relationshipHash
751+
@param {Object} relationshipOptions
752+
@return {Object}
753+
*/
754+
extractPolymorphicRelationship: function(relationshipType, relationshipHash, relationshipOptions) {
755+
var { key, resourceHash, relationshipMeta } = relationshipOptions;
756+
757+
// A polymorphic belongsTo relationship can be present in the payload
758+
// either in the form where the `id` and the `type` are given:
759+
//
760+
// {
761+
// message: { id: 1, type: 'post' }
762+
// }
763+
//
764+
// or by the `id` and a `<relationship>Type` attribute:
765+
//
766+
// {
767+
// message: 1,
768+
// messageType: 'post'
769+
// }
770+
//
771+
// The next code checks if the latter case is present and returns the
772+
// corresponding JSON-API representation. The former case is handled within
773+
// the base class JSONSerializer.
774+
var isPolymorphic = relationshipMeta.options.polymorphic;
775+
var typeProperty = this.keyForPolymorphicType(key, relationshipType, 'deserialize');
776+
777+
if (isPolymorphic && resourceHash.hasOwnProperty(typeProperty) && typeof relationshipHash !== 'object') {
778+
let type = this.modelNameFromPayloadKey(resourceHash[typeProperty]);
779+
return {
780+
id: relationshipHash,
781+
type: type
782+
};
691783
}
784+
785+
return this._super(...arguments);
692786
}
693787
});
694788

packages/ember-data/tests/integration/serializers/rest-serializer-test.js

+111
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,81 @@ test('serializeBelongsTo with async polymorphic', function() {
455455
deepEqual(json, expected, 'returned JSON is correct');
456456
});
457457

458+
test('serializeBelongsTo logs deprecation when old behavior for getting polymorphic type key is used', function() {
459+
var evilMinion, doomsdayDevice;
460+
var json = {};
461+
var expected = { evilMinion: '1', myCustomKeyType: 'evilMinion' };
462+
463+
env.restSerializer.keyForAttribute = function() {
464+
return 'myCustomKey';
465+
};
466+
467+
run(function() {
468+
evilMinion = env.store.createRecord('evil-minion', { id: 1, name: 'Tomster' });
469+
doomsdayDevice = env.store.createRecord('doomsday-device', { id: 2, name: 'Yehuda', evilMinion: evilMinion });
470+
});
471+
472+
expectDeprecation(function() {
473+
env.restSerializer.serializeBelongsTo(doomsdayDevice._createSnapshot(), json, { key: 'evilMinion', options: { polymorphic: true, async: true } });
474+
}, "The key to serialize the type of a polymorphic record is created via keyForAttribute which has been deprecated. Use the keyForPolymorphicType hook instead.");
475+
476+
deepEqual(json, expected, 'returned JSON is correct');
477+
});
478+
479+
test('keyForPolymorphicType can be used to overwrite how the type of a polymorphic record is serialized', function() {
480+
var evilMinion, doomsdayDevice;
481+
var json = {};
482+
var expected = { evilMinion: '1', typeForEvilMinion: 'evilMinion' };
483+
484+
env.restSerializer.keyForPolymorphicType = function() {
485+
return 'typeForEvilMinion';
486+
};
487+
488+
run(function() {
489+
evilMinion = env.store.createRecord('evil-minion', { id: 1, name: 'Tomster' });
490+
doomsdayDevice = env.store.createRecord('doomsday-device', { id: 2, name: 'Yehuda', evilMinion: evilMinion });
491+
});
492+
493+
env.restSerializer.serializeBelongsTo(doomsdayDevice._createSnapshot(), json, { key: 'evilMinion', options: { polymorphic: true, async: true } });
494+
495+
deepEqual(json, expected, 'returned JSON is correct');
496+
});
497+
498+
test('keyForPolymorphicType can be used to overwrite how the type of a polymorphic record is looked up for normalization', function() {
499+
var json = {
500+
doomsdayDevice: {
501+
id: '1',
502+
evilMinion: '2',
503+
typeForEvilMinion: 'evilMinion'
504+
}
505+
};
506+
507+
var expected = {
508+
data: {
509+
type: 'doomsday-device',
510+
id: '1',
511+
attributes: {},
512+
relationships: {
513+
evilMinion: {
514+
data: {
515+
type: 'evil-minion',
516+
id: '2'
517+
}
518+
}
519+
}
520+
},
521+
included: []
522+
};
523+
524+
env.restSerializer.keyForPolymorphicType = function() {
525+
return 'typeForEvilMinion';
526+
};
527+
528+
var normalized = env.restSerializer.normalizeResponse(env.store, DoomsdayDevice, json, null, 'findRecord');
529+
530+
deepEqual(normalized, expected, 'normalized JSON is correct');
531+
});
532+
458533
test('serializeIntoHash uses payloadKeyFromModelName to normalize the payload root key', function() {
459534
run(function() {
460535
league = env.store.createRecord('home-planet', { name: "Umber", id: "123" });
@@ -475,6 +550,42 @@ test('serializeIntoHash uses payloadKeyFromModelName to normalize the payload ro
475550
});
476551
});
477552

553+
test('normalizeResponse with async polymorphic belongsTo, using <relationshipName>Type', function() {
554+
env.registry.register('serializer:application', DS.RESTSerializer.extend());
555+
var store = env.store;
556+
env.adapter.findRecord = (store, type) => {
557+
if (type.modelName === 'doomsday-device') {
558+
return {
559+
doomsdayDevice: {
560+
id: 1,
561+
name: "DeathRay",
562+
evilMinion: 1,
563+
evilMinionType: 'yellowMinion'
564+
}
565+
};
566+
}
567+
568+
equal(type.modelName, 'yellow-minion');
569+
570+
return {
571+
yellowMinion: {
572+
id: 1,
573+
type: 'yellowMinion',
574+
name: 'Alex',
575+
eyes: 3
576+
}
577+
};
578+
};
579+
580+
run(function() {
581+
store.findRecord('doomsday-device', 1).then((deathRay) => {
582+
return deathRay.get('evilMinion');
583+
}).then((evilMinion) => {
584+
equal(evilMinion.get('eyes'), 3);
585+
});
586+
});
587+
});
588+
478589
test('normalizeResponse with async polymorphic belongsTo', function() {
479590
env.registry.register('serializer:application', DS.RESTSerializer.extend({
480591
isNewSerializerAPI: true

0 commit comments

Comments
 (0)