/* packages/mixins/lib/embedded_mixin.js */ (function(Ember, DS) { var get = Ember.get; var forEach = Ember.EnumerableUtils.forEach; /** @module ember-data @submodule mixins **/ /** DS.EmbeddedMixin supports serializing embedded records. To set up embedded records, include the mixin into a serializer then define embedded (model) relationships. Below is an example of a per type serializer (post type). ```js App.PostSerializer = DS.RESTSerializer.extend(DS.EmbeddedMixin, { attrs: { author: {embedded: 'always'}, comments: {embedded: 'always'} } }) ``` Currently only `{embedded: 'always'}` records are supported. @class EmbeddedMixin @namespace DS */ DS.EmbeddedMixin = Ember.Mixin.create({ /** Serialize `belongsTo` relationship when it is configured as an embedded object. This example of an author model belongs to a post model: ```js Post = DS.Model.extend({ title: DS.attr('string'), body: DS.attr('string'), author: DS.belongsTo('author') }); Author = DS.Model.extend({ name: DS.attr('string'), post: DS.belongsTo('post') }); ``` Use a custom (type) serializer for the post model to configure embedded author ```js App.PostSerializer = DS.RESTSerializer.extend(DS.EmbeddedMixin, { attrs: { author: {embedded: 'always'} } }) ``` A payload with an attribute configured for embedded records can serialize the records together under the root attribute's payload: ```js { "post": { "id": "1" "title": "Rails is omakase", "author": { "id": "2" "name": "dhh" } } } ``` @method serializeBelongsTo @param {DS.Model} record @param {Object} json @param relationship */ serializeBelongsTo: function(record, json, relationship) { var attr = relationship.key, config = this.get('attrs'); if (!config || !isEmbedded(config[attr])) { this._super(record, json, relationship); return; } var key = this.keyForAttribute(attr); var embeddedRecord = record.get(attr); if (!embeddedRecord) { json[key] = null; } else { json[key] = embeddedRecord.serialize(); var id = embeddedRecord.get('id'); if (id) { json[key].id = id; } var parentKey = this.keyForAttribute(relationship.parentType.typeKey); if (parentKey) { removeId(parentKey, json[key]); } delete json[key][parentKey]; } }, /** Serialize `hasMany` relationship when it is configured as embedded objects. This example of a post model has many comments: ```js Post = DS.Model.extend({ title: DS.attr('string'), body: DS.attr('string'), comments: DS.hasMany('comment') }); Comment = DS.Model.extend({ body: DS.attr('string'), post: DS.belongsTo('post') }); ``` Use a custom (type) serializer for the post model to configure embedded comments ```js App.PostSerializer = DS.RESTSerializer.extend(DS.EmbeddedMixin, { attrs: { comments: {embedded: 'always'} } }) ``` A payload with an attribute configured for embedded records can serialize the records together under the root attribute's payload: ```js { "post": { "id": "1" "title": "Rails is omakase", "body": "I want this for my ORM, I want that for my template language..." "comments": [{ "id": "1", "body": "Rails is unagi" }, { "id": "2", "body": "Omakase O_o" }] } } ``` To embed the ids for a related object (using a hasMany relationship): ```js App.PostSerializer = DS.RESTSerializer.extend(DS.EmbeddedMixin, { attrs: { comments: {embedded: 'ids'} } }) ``` ```js { "post": { "id": "1" "title": "Rails is omakase", "body": "I want this for my ORM, I want that for my template language..." "comments": ["1", "2"] } } ``` @method serializeHasMany @param {DS.Model} record @param {Object} json @param relationship */ serializeHasMany: function(record, json, relationship) { var attr = relationship.key, config = this.get('attrs'), key; if (!config || (!isEmbedded(config[attr]) && !hasEmbeddedIds(config[attr]))) { this._super(record, json, relationship); return; } if (hasEmbeddedIds(config[attr])) { key = this.keyForRelationship(attr, relationship.kind); json[key] = get(record, attr).mapBy(get(this, 'primaryKey')); } else { key = this.keyForAttribute(attr); json[key] = get(record, attr).map(function(relation) { var data = relation.serialize(), primaryKey = get(this, 'primaryKey'); data[primaryKey] = get(relation, primaryKey); if (data.id === null) { delete data.id; } return data; }, this); } }, /** Extract an embedded object from the payload for a single object and add the object in the compound document (side-loaded) format instead. A payload with an attribute configured for embedded records needs to be extracted: ```js { "post": { "id": 1 "title": "Rails is omakase", "author": { "id": 2 "name": "dhh" } "comments": [] } } ``` Ember Data is expecting a payload with a compound document (side-loaded) like: ```js { "post": { "id": "1" "title": "Rails is omakase", "author": "2" "comments": [] }, "authors": [{ "id": "2" "post": "1" "name": "dhh" }] "comments": [] } ``` The payload's `author` attribute represents an object with a `belongsTo` relationship. The `post` attribute under `author` is the foreign key with the id for the post @method extractSingle @param {DS.Store} store @param {subclass of DS.Model} primaryType @param {Object} payload @param {String} recordId @param {'find'|'createRecord'|'updateRecord'|'deleteRecord'} requestType @return Object the primary response to the original request */ extractSingle: function(store, primaryType, payload, recordId, requestType) { var root = this.keyForAttribute(primaryType.typeKey), partial = payload[root]; updatePayloadWithEmbedded.call(this, store, primaryType, payload, partial); return this._super(store, primaryType, payload, recordId, requestType); }, /** Extract embedded objects in an array when an attr is configured for embedded, and add them as side-loaded objects instead. A payload with an attr configured for embedded records needs to be extracted: ```js { "post": { "id": "1" "title": "Rails is omakase", "comments": [{ "id": "1", "body": "Rails is unagi" }, { "id": "2", "body": "Omakase O_o" }] } } ``` Ember Data is expecting a payload with compound document (side-loaded) like: ```js { "post": { "id": "1" "title": "Rails is omakase", "comments": ["1", "2"] }, "comments": [{ "id": "1", "body": "Rails is unagi" }, { "id": "2", "body": "Omakase O_o" }] } ``` The payload's `comments` attribute represents records in a `hasMany` relationship @method extractArray @param {DS.Store} store @param {subclass of DS.Model} primaryType @param {Object} payload @return {Array} The primary array that was returned in response to the original query. */ extractArray: function(store, primaryType, payload) { var root = this.keyForAttribute(primaryType.typeKey), partials = payload[Ember.String.pluralize(root)]; forEach(partials, function(partial) { updatePayloadWithEmbedded.call(this, store, primaryType, payload, partial); }, this); return this._super(store, primaryType, payload); } }); // checks config for embedded flag function isEmbedded(config) { return config && (config.embedded === 'always' || config.embedded === 'load'); } // checks config for included ids flag function hasEmbeddedIds(config) { return config && (config.embedded === 'ids'); } // used to remove id (foreign key) when embedding function removeId(key, json) { var idKey = key + '_id'; if (json.hasOwnProperty(idKey)) { delete json[idKey]; } } // chooses a relationship kind to branch which function is used to update payload // does not change payload if attr is not embedded function updatePayloadWithEmbedded(store, type, payload, partial) { var attrs = get(this, 'attrs'); if (!attrs) { return; } type.eachRelationship(function(key, relationship) { var config = attrs[key]; if (isEmbedded(config)) { if (relationship.kind === "hasMany") { updatePayloadWithEmbeddedHasMany.call(this, store, key, relationship, payload, partial); } if (relationship.kind === "belongsTo") { updatePayloadWithEmbeddedBelongsTo.call(this, store, key, relationship, payload, partial); } } }, this); } // handles embedding for `hasMany` relationship function updatePayloadWithEmbeddedHasMany(store, primaryType, relationship, payload, partial) { var serializer = store.serializerFor(relationship.type.typeKey); var primaryKey = get(this, 'primaryKey'); var attr = relationship.type.typeKey; // underscore forces the embedded records to be side loaded. // it is needed when main type === relationship.type var embeddedTypeKey = '_' + Ember.String.pluralize(attr); var expandedKey = this.keyForRelationship(primaryType, relationship.kind); var attribute = this.keyForAttribute(primaryType); var ids = []; if (!partial[attribute]) { return; } payload[embeddedTypeKey] = payload[embeddedTypeKey] || []; forEach(partial[attribute], function(data) { var embeddedType = store.modelFor(attr); updatePayloadWithEmbedded.call(serializer, store, embeddedType, payload, data); ids.push(data[primaryKey]); payload[embeddedTypeKey].push(data); }); partial[expandedKey] = ids; delete partial[attribute]; } // handles embedding for `belongsTo` relationship function updatePayloadWithEmbeddedBelongsTo(store, primaryType, relationship, payload, partial) { var attrs = this.get('attrs'); if (!attrs || !(isEmbedded(attrs[Ember.String.camelize(primaryType)]) || isEmbedded(attrs[primaryType]))) { return; } var attr = relationship.type.typeKey; var serializer = store.serializerFor(relationship.type.typeKey); var primaryKey = get(serializer, 'primaryKey'); var embeddedTypeKey = Ember.String.pluralize(attr); var expandedKey = serializer.keyForRelationship(primaryType, relationship.kind); var attribute = serializer.keyForAttribute(primaryType); if (!partial[attribute]) { return; } payload[embeddedTypeKey] = payload[embeddedTypeKey] || []; var embeddedType = store.modelFor(relationship.type.typeKey); for (var key in partial) { if (partial.hasOwnProperty(key) && key.camelize() === attr) { updatePayloadWithEmbedded.call(serializer, store, embeddedType, payload, partial[key]); } } partial[expandedKey] = partial[attribute].id; // Need to move an embedded `belongsTo` object into a pluralized collection payload[embeddedTypeKey].push(partial[attribute]); // Need a reference to the parent so relationship works between both `belongsTo` records partial[attribute][relationship.parentType.typeKey + '_id'] = partial.id; delete partial[attribute]; } }(Ember, DS)); ;