Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for polymorphic associations. #750

Merged
merged 1 commit into from
Apr 2, 2013
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 91 additions & 81 deletions packages/ember-data/lib/serializers/json_serializer.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ DS.JSONSerializer = DS.Serializer.extend({
this.set('transforms', DS.JSONTransforms);
}

this.sideloadMapping = Ember.Map.create();

this.configure({
meta: 'meta',
since: 'since'
Expand All @@ -26,17 +24,13 @@ DS.JSONSerializer = DS.Serializer.extend({
return this._super(type);
}

var sideloadAs = configuration.sideloadAs;
var sideloadAs = configuration.sideloadAs,
sideloadMapping;

if (sideloadAs) {
this.sideloadMapping.set(sideloadAs, type);

// Set a flag indicating that mappings may need to be normalized
// (i.e. converted from strings -> types) before sideloading.
// We can't do this conversion immediately here, because `configure`
// may be called before certain types have been defined.
this.sideloadMapping.normalized = false;

sideloadMapping = this.aliases.sideloadMapping || Ember.Map.create();
sideloadMapping.set(sideloadAs, type);
this.aliases.sideloadMapping = sideloadMapping;
delete configuration.sideloadAs;
}

Expand Down Expand Up @@ -98,24 +92,52 @@ DS.JSONSerializer = DS.Serializer.extend({
return hash[key];
},

extractBelongsToPolymorphic: function(type, hash, key) {
var keyForId = this.keyForPolymorphicId(key),
keyForType,
id = hash[keyForId];

if (id) {
keyForType = this.keyForPolymorphicType(key);
return {id: id, type: hash[keyForType]};
}

return null;
},

addBelongsTo: function(hash, record, key, relationship) {
var type = record.constructor,
name = relationship.key,
value = null,
includeType = (relationship.options && relationship.options.polymorphic),
embeddedChild;

if (this.embeddedType(type, name)) {
if (embeddedChild = get(record, name)) {
value = this.serialize(embeddedChild, { includeId: true });
value = this.serialize(embeddedChild, { includeId: true, includeType: includeType });
}

hash[key] = value;
} else {
var id = get(record, relationship.key+'.id');
if (!Ember.isNone(id)) { hash[key] = id; }
var child = get(record, relationship.key),
id = get(child, 'id');
if (!Ember.isNone(id)) {
if (relationship.options && relationship.options.polymorphic) {
this.addBelongsToPolymorphic(hash, key, id, child.constructor);
} else {
hash[key] = id;
}
}
}
},

addBelongsToPolymorphic: function(hash, key, id, type) {
var keyForId = this.keyForPolymorphicId(key),
keyForType = this.keyForPolymorphicType(key);
hash[keyForId] = id;
hash[keyForType] = this.rootForType(type);
},

/**
Adds a has-many relationship to the JSON hash being built.

Expand All @@ -131,10 +153,12 @@ DS.JSONSerializer = DS.Serializer.extend({
should be saved
@param {Object} relationship metadata about the relationship being serialized
*/

addHasMany: function(hash, record, key, relationship) {
var type = record.constructor,
name = relationship.key,
serializedHasMany = [],
includeType = (relationship.options && relationship.options.polymorphic),
manyArray, embeddedType;

// If the has-many is not embedded, there is nothing to do.
Expand All @@ -146,14 +170,19 @@ DS.JSONSerializer = DS.Serializer.extend({

// Build up the array of serialized records
manyArray.forEach(function (record) {
serializedHasMany.push(this.serialize(record, { includeId: true }));
serializedHasMany.push(this.serialize(record, { includeId: true, includeType: includeType }));
}, this);

// Set the appropriate property of the serialized JSON to the
// array of serialized embedded records
hash[key] = serializedHasMany;
},

addType: function(hash, type) {
var keyForType = this.keyForEmbeddedType();
hash[keyForType] = this.rootForType(type);
},

// EXTRACTION

extract: function(loader, json, type, record) {
Expand Down Expand Up @@ -198,6 +227,19 @@ DS.JSONSerializer = DS.Serializer.extend({
}
},

extractEmbeddedType: function(relationship, data) {
var foundType = relationship.type;
if(relationship.options && relationship.options.polymorphic) {
var key = this.keyFor(relationship),
keyForEmbeddedType = this.keyForEmbeddedType(key);

foundType = this.typeFromAlias(data[keyForEmbeddedType]);
delete data[keyForEmbeddedType];
}

return foundType;
},

/**
@private

Expand All @@ -217,7 +259,6 @@ DS.JSONSerializer = DS.Serializer.extend({
sideload: function(loader, type, json, root) {
var sideloadedType;

this.normalizeSideloadMappings();
this.configureSideloadMappingForType(type);

for (var prop in json) {
Expand All @@ -227,7 +268,7 @@ DS.JSONSerializer = DS.Serializer.extend({
continue;
}

sideloadedType = this.sideloadMapping.get(prop);
sideloadedType = this.typeFromAlias(prop);
Ember.assert("Your server returned a hash with the key " + prop +
" but you have no mapping for it",
!!sideloadedType);
Expand All @@ -236,26 +277,6 @@ DS.JSONSerializer = DS.Serializer.extend({
}
},

/**
@private

Iterates over all the `sideloadAs` mappings and converts any that are
strings to their equivalent types.

This is an optimization used to avoid performing lookups for every
call to `sideload`.
*/
normalizeSideloadMappings: function() {
if (! this.sideloadMapping.normalized) {
this.sideloadMapping.forEach(function(key, value) {
if (typeof value === 'string') {
this.sideloadMapping.set(key, get(Ember.lookup, value));
}
}, this);
this.sideloadMapping.normalized = true;
}
},

/**
@private

Expand All @@ -272,11 +293,9 @@ DS.JSONSerializer = DS.Serializer.extend({

type.eachRelatedType(function(relatedType) {
if (!configured.contains(relatedType)) {
var root = this.sideloadMappingForType(relatedType);
if (!root) {
root = this.defaultSideloadRootForType(relatedType);
this.sideloadMapping.set(root, relatedType);
}
var root = this.defaultSideloadRootForType(relatedType);
this.aliases.set(root, relatedType);

this.configureSideloadMappingForType(relatedType, configured);
}
}, this);
Expand All @@ -292,33 +311,40 @@ DS.JSONSerializer = DS.Serializer.extend({
}
},

// HELPERS
/**
A hook you can use in your serializer subclass to customize
how a polymorphic association's name is converted into a key for the id.

// define a plurals hash in your subclass to define
// special-case pluralization
pluralize: function(name) {
var plurals = this.configurations.get('plurals');
return (plurals && plurals[name]) || name + "s";
},
@param {String} name the association name to convert into a key

// use the same plurals hash to determine
// special-case singularization
singularize: function(name) {
var plurals = this.configurations.get('plurals');
if (plurals) {
for (var i in plurals) {
if (plurals[i] === name) {
return i;
}
}
}
if (name.lastIndexOf('s') === name.length - 1) {
return name.substring(0, name.length - 1);
} else {
return name;
}
@returns {String} the key
*/
keyForPolymorphicId: Ember.K,

/**
A hook you can use in your serializer subclass to customize
how a polymorphic association's name is converted into a key for the type.

@param {String} name the association name to convert into a key

@returns {String} the key
*/
keyForPolymorphicType: Ember.K,

/**
A hook you can use in your serializer subclass to customize
the key used to store the type of a record of an embedded polymorphic association.

By default, this method returns 'type'.

@returns {String} the key
*/
keyForEmbeddedType: function() {
return 'type';
},

// HELPERS

/**
@private

Expand All @@ -342,22 +368,6 @@ DS.JSONSerializer = DS.Serializer.extend({
return name.replace(/([A-Z])/g, '_$1').toLowerCase().slice(1);
},

/**
@private

Determines the root name mapped to a particular sideloaded type.

@param {DS.Model subclass} type
@returns {String} name of the root element, if any is registered
*/
sideloadMappingForType: function(type) {
this.sideloadMapping.forEach(function(key, value) {
if (type === value) {
return key;
}
});
},

/**
@private

Expand Down
8 changes: 8 additions & 0 deletions packages/ember-data/lib/serializers/rest_serializer.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,13 @@ DS.RESTSerializer = DS.JSONSerializer.extend({
}

return this.singularize(key) + "_ids";
},

keyForPolymorphicId: function(key) {
return key;
},

keyForPolymorphicType: function(key) {
return key.replace(/_id$/, '_type');
}
});
48 changes: 42 additions & 6 deletions packages/ember-data/lib/system/model/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,15 @@ DS.Model = Ember.Object.extend(Ember.Evented, LoadPromise, {
var ids = this._data.hasMany[key] || [];

var references = map(ids, function(id) {
// if it was already a reference, return the reference
if (typeof id === 'object') { return id; }
if (typeof id === 'object') {
if( id.clientId ) {
// if it was already a reference, return the reference
return id;
} else {
// <id, type> tuple for a polymorphic association.
return store.referenceForId(id.type, id.id);
}
}
return store.referenceForId(type, id);
});

Expand Down Expand Up @@ -236,12 +243,41 @@ DS.Model = Ember.Object.extend(Ember.Evented, LoadPromise, {
this._data.attributes[name] = value;
},

materializeHasMany: function(name, ids) {
this._data.hasMany[name] = ids;
materializeHasMany: function(name, tuplesOrReferencesOrOpaque) {
var tuplesOrReferencesOrOpaqueType = typeof tuplesOrReferencesOrOpaque;
if (tuplesOrReferencesOrOpaque && tuplesOrReferencesOrOpaqueType !== 'string' && tuplesOrReferencesOrOpaque.length > 1) { Ember.assert('materializeHasMany expects tuples, references or opaque token, not ' + tuplesOrReferencesOrOpaque[0], tuplesOrReferencesOrOpaque[0].hasOwnProperty('id') && tuplesOrReferencesOrOpaque[0].type); }
if( tuplesOrReferencesOrOpaqueType === "string" ) {
this._data.hasMany[name] = tuplesOrReferencesOrOpaque;
} else {
var references = tuplesOrReferencesOrOpaque;

if (tuplesOrReferencesOrOpaque && Ember.isArray(tuplesOrReferencesOrOpaque)) {
references = this._convertTuplesToReferences(tuplesOrReferencesOrOpaque);
}

this._data.hasMany[name] = references;
}
},

materializeBelongsTo: function(name, tupleOrReference) {
if (tupleOrReference) { Ember.assert('materializeBelongsTo expects a tuple or a reference, not a ' + tupleOrReference, !tupleOrReference || (tupleOrReference.hasOwnProperty('id') && tupleOrReference.hasOwnProperty('type'))); }

this._data.belongsTo[name] = tupleOrReference;
},

_convertTuplesToReferences: function(tuplesOrReferences) {
return map(tuplesOrReferences, function(tupleOrReference) {
return this._convertTupleToReference(tupleOrReference);
}, this);
},

materializeBelongsTo: function(name, id) {
this._data.belongsTo[name] = id;
_convertTupleToReference: function(tupleOrReference) {
var store = get(this, 'store');
if(tupleOrReference.clientId) {
return tupleOrReference;
} else {
return store.referenceForId(tupleOrReference.type, tupleOrReference.id);
}
},

rollback: function() {
Expand Down
Loading