Skip to content

Commit

Permalink
Merge pull request #3835 from pangratz/extract-polymorphic-belongs-to
Browse files Browse the repository at this point in the history
[BUGFIX] extract polymorphic belongsTo in RESTSerializer
  • Loading branch information
bmac committed Oct 20, 2015
2 parents 96d211b + 4f7cf7b commit 4e83729
Show file tree
Hide file tree
Showing 3 changed files with 240 additions and 4 deletions.
33 changes: 32 additions & 1 deletion packages/ember-data/lib/serializers/json-serializer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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".
Expand All @@ -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));
}
Expand Down
100 changes: 97 additions & 3 deletions packages/ember-data/lib/serializers/rest-serializer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 `<relationship>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);
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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" });
Expand All @@ -475,6 +550,42 @@ test('serializeIntoHash uses payloadKeyFromModelName to normalize the payload ro
});
});

test('normalizeResponse with async polymorphic belongsTo, using <relationshipName>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
Expand Down

0 comments on commit 4e83729

Please sign in to comment.