From 8fc8a51f992d692558bbd0d1114f16c4f058185e Mon Sep 17 00:00:00 2001 From: Christoffer Persson Date: Fri, 5 Jun 2015 17:09:58 +0300 Subject: [PATCH 1/6] Revert breaking out commonalities to normalizeArray --- .../lib/serializers/rest-serializer.js | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/packages/ember-data/lib/serializers/rest-serializer.js b/packages/ember-data/lib/serializers/rest-serializer.js index 8f084246e37..71ad7926941 100644 --- a/packages/ember-data/lib/serializers/rest-serializer.js +++ b/packages/ember-data/lib/serializers/rest-serializer.js @@ -290,10 +290,14 @@ var RESTSerializer = JSONSerializer.extend({ continue; } - var normalizedArray = this.normalizeArray(store, modelName, value, prop); - /*jshint loopfunc:true*/ - forEach.call(normalizedArray, function(hash) { + forEach.call(value, function(hash) { + var typeName = this.modelNameFromPayloadKey(prop); + var type = store.modelFor(typeName); + var typeSerializer = store.serializerFor(type); + + hash = typeSerializer.normalize(type, hash, prop); + var isFirstCreatedRecord = isPrimary && !recordId && !primaryRecord; var isUpdatedRecord = isPrimary && coerceId(hash.id) === recordId; @@ -434,10 +438,15 @@ var RESTSerializer = JSONSerializer.extend({ Ember.warn(this.warnMessageNoModelForKey(prop, typeName), false); continue; } - - var normalizedArray = this.normalizeArray(store, typeName, payload[prop], prop); + var type = store.modelFor(typeName); + var typeSerializer = store.serializerFor(type); var isPrimary = (!forcedSecondary && this.isPrimaryType(store, typeName, primaryTypeClass)); + /*jshint loopfunc:true*/ + var normalizedArray = map.call(payload[prop], function(hash) { + return typeSerializer.normalize(type, hash, prop); + }, this); + if (isPrimary) { primaryArray = normalizedArray; } else { @@ -448,16 +457,6 @@ var RESTSerializer = JSONSerializer.extend({ return primaryArray; }, - normalizeArray: function(store, typeName, arrayHash, prop) { - var typeClass = store.modelFor(typeName); - var typeSerializer = store.serializerFor(typeName); - - /*jshint loopfunc:true*/ - return map.call(arrayHash, function(hash) { - return typeSerializer.normalize(typeClass, hash, prop); - }, this); - }, - isPrimaryType: function(store, typeName, primaryTypeClass) { var typeClass = store.modelFor(typeName); return typeClass.modelName === primaryTypeClass.modelName; From 4adcc1813ab0b7b1992d4c29148a459af7789750 Mon Sep 17 00:00:00 2001 From: Christoffer Persson Date: Fri, 8 May 2015 18:27:35 +0200 Subject: [PATCH 2/6] Refactor the Serializer API --- FEATURES.md | 3 + config/features.json | 1 + .../adapter-populated-record-array.js | 13 + packages/ember-data/lib/system/serializer.js | 31 +++ packages/ember-data/lib/system/store.js | 26 +- .../system/store/container-instance-cache.js | 3 + .../ember-data/lib/system/store/finders.js | 49 ++-- .../lib/system/store/serializer-response.js | 240 ++++++++++++++++++ 8 files changed, 333 insertions(+), 33 deletions(-) create mode 100644 packages/ember-data/lib/system/store/serializer-response.js diff --git a/FEATURES.md b/FEATURES.md index d866f00ae24..7c720870a27 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -11,3 +11,6 @@ entry in `config/features.json`. ## Feature Flags +* `ds-new-serializer-api` + + Activates the new Serializer API for default serializers. diff --git a/config/features.json b/config/features.json index 2c63c085104..9e71e4691c1 100644 --- a/config/features.json +++ b/config/features.json @@ -1,2 +1,3 @@ { + "ds-new-serializer-api": null } diff --git a/packages/ember-data/lib/system/record-arrays/adapter-populated-record-array.js b/packages/ember-data/lib/system/record-arrays/adapter-populated-record-array.js index 65670d58b90..d0a93c80207 100644 --- a/packages/ember-data/lib/system/record-arrays/adapter-populated-record-array.js +++ b/packages/ember-data/lib/system/record-arrays/adapter-populated-record-array.js @@ -41,6 +41,19 @@ export default RecordArray.extend({ var type = get(this, 'type'); var modelName = type.modelName; var records = store.pushMany(modelName, data); + + this.loadRecords(records); + }, + + /** + @method loadRecords + @param {Array} records + @private + */ + loadRecords: function(records) { + var store = get(this, 'store'); + var type = get(this, 'type'); + var modelName = type.modelName; var meta = store.metadataFor(modelName); //TODO Optimize diff --git a/packages/ember-data/lib/system/serializer.js b/packages/ember-data/lib/system/serializer.js index cd78055fc52..06cc95e2942 100644 --- a/packages/ember-data/lib/system/serializer.js +++ b/packages/ember-data/lib/system/serializer.js @@ -23,6 +23,37 @@ */ var Serializer = Ember.Object.extend({ + + /* + This is only to be used temporarily during the transition from the old + serializer API to the new one. + + To activate the new Serializer API you need to enable the feature flag + `ds-new-serializer-api`. + + http://guides.emberjs.com/v1.12.0/configuring-ember/feature-flags/ + + This makes the store and the built-in serializers use the new Serializer API. + + + ## Custom Serializers + + If you have custom serializers you need to do the following: + + 1. Opt-in to the new Serializer API by setting `isNewSerializerAPI` to `true` + when extending one of the built-in serializers. This indicates that the + store should call `normalizeResponse` instead of `extract` and to expect + a JSON-API Document back. + 2. If you have a custom `extract` hooks you need to refactor it to the new + `normalizeResponse` hooks and make sure it returns a JSON-API Document. + 3. If you have a custom `normalize` method you need to make sure it also + returns a JSON-API Document with the record in question as the primary + data. + + @property isNewSerializerAPI + */ + isNewSerializerAPI: false, + /** The `store` property is the application's `store` that contains all records. It's injected as a service. diff --git a/packages/ember-data/lib/system/store.js b/packages/ember-data/lib/system/store.js index 17cdc59208f..2159b60e1e7 100644 --- a/packages/ember-data/lib/system/store.js +++ b/packages/ember-data/lib/system/store.js @@ -24,6 +24,12 @@ import { _objectIsAlive } from "ember-data/system/store/common"; +import { + convertResourceObject, + normalizeResponseHelper, + pushPayload +} from "ember-data/system/store/serializer-response"; + import { serializerForAdapter } from "ember-data/system/store/serializers"; @@ -1590,17 +1596,25 @@ Store = Service.extend({ @method push @param {String} modelName @param {Object} data - @return {DS.Model} the record that was created or + @return {DS.Model|Array} the record(s) that was created or updated. */ push: function(modelName, data) { Ember.assert('Passing classes to store methods has been removed. Please pass a dasherized string instead of '+ Ember.inspect(modelName), typeof modelName === 'string'); var internalModel = this._pushInternalModel(modelName, data); + if (Ember.isArray(internalModel)) { + return map(internalModel, (item) => { + return item.getRecord(); + }); + } return internalModel.getRecord(); }, _pushInternalModel: function(modelName, data) { - Ember.assert("Expected an object as `data` in a call to `push` for " + modelName + " , but was " + data, Ember.typeOf(data) === 'object'); + if (Ember.typeOf(modelName) === 'object' && Ember.typeOf(data) === 'undefined') { + return pushPayload(this, modelName); + } + Ember.assert("Expected an object as `data` in a call to `push` for " + modelName + " , but was " + Ember.typeOf(data), Ember.typeOf(data) === 'object'); Ember.assert("You must include an `id` for " + modelName + " in an object passed to `push`", data.id != null && data.id !== ''); var type = this.modelFor(modelName); @@ -2089,13 +2103,13 @@ function _commit(adapter, store, operation, snapshot) { promise = _guard(promise, _bind(_objectIsAlive, record)); return promise.then(function(adapterPayload) { - var payload; - store._adapterRun(function() { + var payload, data; if (adapterPayload) { - payload = serializer.extract(store, type, adapterPayload, snapshot.id, operation); + payload = normalizeResponseHelper(serializer, store, type, adapterPayload, snapshot.id, operation); + data = convertResourceObject(payload.data); } - store.didSaveRecord(record, payload); + store.didSaveRecord(record, data); }); return record; diff --git a/packages/ember-data/lib/system/store/container-instance-cache.js b/packages/ember-data/lib/system/store/container-instance-cache.js index cc86980874d..e9f81501885 100644 --- a/packages/ember-data/lib/system/store/container-instance-cache.js +++ b/packages/ember-data/lib/system/store/container-instance-cache.js @@ -46,6 +46,9 @@ Ember.merge(ContainerInstanceCache.prototype, { let instance = this.instanceFor(lookupKey); if (instance) { + if (fallback === '-default') { + instance.set('isNewSerializerAPI', true); + } return instance; } } diff --git a/packages/ember-data/lib/system/store/finders.js b/packages/ember-data/lib/system/store/finders.js index dbb6be5fbb6..5f96c371810 100644 --- a/packages/ember-data/lib/system/store/finders.js +++ b/packages/ember-data/lib/system/store/finders.js @@ -4,11 +4,15 @@ import { _objectIsAlive } from "ember-data/system/store/common"; +import { + normalizeResponseHelper, + pushPayload +} from "ember-data/system/store/serializer-response"; + import { serializerForAdapter } from "ember-data/system/store/serializers"; - var Promise = Ember.RSVP.Promise; var map = Ember.EnumerableUtils.map; @@ -24,10 +28,9 @@ export function _find(adapter, store, typeClass, id, internalModel) { return promise.then(function(adapterPayload) { Ember.assert("You made a request for a " + typeClass.typeClassKey + " with id " + id + ", but the adapter's response did not have any data", adapterPayload); return store._adapterRun(function() { - var payload = serializer.extract(store, typeClass, adapterPayload, id, 'find'); - + var payload = normalizeResponseHelper(serializer, store, typeClass, adapterPayload, id, 'find'); //TODO Optimize - var record = store.push(typeClass.modelName, payload); + var record = pushPayload(store, payload); return record._internalModel; }); }, function(error) { @@ -56,12 +59,9 @@ export function _findMany(adapter, store, typeClass, ids, internalModels) { return promise.then(function(adapterPayload) { return store._adapterRun(function() { - var payload = serializer.extract(store, typeClass, adapterPayload, null, 'findMany'); - - Ember.assert("The response from a findMany must be an Array, not " + Ember.inspect(payload), Ember.typeOf(payload) === 'array'); - + var payload = normalizeResponseHelper(serializer, store, typeClass, adapterPayload, null, 'findMany'); //TODO Optimize, no need to materialize here - var records = store.pushMany(typeClass.modelName, payload); + var records = pushPayload(store, payload); return map(records, function(record) { return record._internalModel; }); }); }, null, "DS: Extract payload of " + typeClass); @@ -80,12 +80,9 @@ export function _findHasMany(adapter, store, internalModel, link, relationship) return promise.then(function(adapterPayload) { return store._adapterRun(function() { - var payload = serializer.extract(store, typeClass, adapterPayload, null, 'findHasMany'); - - Ember.assert("The response from a findHasMany must be an Array, not " + Ember.inspect(payload), Ember.typeOf(payload) === 'array'); - + var payload = normalizeResponseHelper(serializer, store, typeClass, adapterPayload, null, 'findHasMany'); //TODO Use a non record creating push - var records = store.pushMany(relationship.type, payload); + var records = pushPayload(store, payload); return map(records, function(record) { return record._internalModel; }); }); }, null, "DS: Extract payload of " + internalModel + " : hasMany " + relationship.type); @@ -104,14 +101,14 @@ export function _findBelongsTo(adapter, store, internalModel, link, relationship return promise.then(function(adapterPayload) { return store._adapterRun(function() { - var payload = serializer.extract(store, typeClass, adapterPayload, null, 'findBelongsTo'); + var payload = normalizeResponseHelper(serializer, store, typeClass, adapterPayload, null, 'findBelongsTo'); - if (!payload) { + if (!payload.data) { return null; } - var record = store.push(relationship.type, payload); //TODO Optimize + var record = pushPayload(store, payload); return record._internalModel; }); }, null, "DS: Extract payload of " + internalModel + " : " + relationship.type); @@ -128,11 +125,9 @@ export function _findAll(adapter, store, typeClass, sinceToken) { return promise.then(function(adapterPayload) { store._adapterRun(function() { - var payload = serializer.extract(store, typeClass, adapterPayload, null, 'findAll'); - - Ember.assert("The response from a findAll must be an Array, not " + Ember.inspect(payload), Ember.typeOf(payload) === 'array'); - - store.pushMany(modelName, payload); + var payload = normalizeResponseHelper(serializer, store, typeClass, adapterPayload, null, 'findAll'); + //TODO Optimize + pushPayload(store, payload); }); store.didUpdateAll(typeClass); @@ -150,14 +145,14 @@ export function _findQuery(adapter, store, typeClass, query, recordArray) { promise = _guard(promise, _bind(_objectIsAlive, store)); return promise.then(function(adapterPayload) { - var payload; + var records; store._adapterRun(function() { - payload = serializer.extract(store, typeClass, adapterPayload, null, 'findQuery'); - - Ember.assert("The response from a findQuery must be an Array, not " + Ember.inspect(payload), Ember.typeOf(payload) === 'array'); + var payload = normalizeResponseHelper(serializer, store, typeClass, adapterPayload, null, 'findQuery'); + //TODO Optimize + records = pushPayload(store, payload); }); - recordArray.load(payload); + recordArray.loadRecords(records); return recordArray; }, null, "DS: Extract payload of findQuery " + typeClass); diff --git a/packages/ember-data/lib/system/store/serializer-response.js b/packages/ember-data/lib/system/store/serializer-response.js new file mode 100644 index 00000000000..36de338dcbd --- /dev/null +++ b/packages/ember-data/lib/system/store/serializer-response.js @@ -0,0 +1,240 @@ +var forEach = Ember.EnumerableUtils.forEach; +var map = Ember.EnumerableUtils.map; + +/** + This is a helper method that always returns a JSON-API Document. + + If the feature flag `ds-new-serializer-api` is enabled and the current serializer + has `isNewSerializerAPI` set to `true` this helper calls `normalizeResponse` + instead of `extract`. + + All the built-in serializers get `isNewSerializerAPI` set to `true` automatically + if the feature flag is enabled. + + @method normalizeResponseHelper + @param {DS.Serializer} serializer + @param {DS.Store} store + @param {subclass of DS.Model} modelClass + @param {Object} payload + @param {String|Number} id + @param {String} requestType + @return {Object} JSON-API Document +*/ +export function normalizeResponseHelper(serializer, store, modelClass, payload, id, requestType) { + if (Ember.FEATURES.isEnabled('ds-new-serializer-api') && serializer.get('isNewSerializerAPI')) { + return serializer.normalizeResponse(store, modelClass, payload, id, requestType); + } else { + if (Ember.FEATURES.isEnabled('ds-new-serializer-api')) { + Ember.deprecate('Your custom serializer uses the old version of the Serializer API, with `extract` hooks. Please upgrade your serializers to the new Serializer API using `normalizeResponse` hooks instead.'); + } + let serializerPayload = serializer.extract(store, modelClass, payload, id, requestType); + return _normalizeSerializerPayload(modelClass, serializerPayload); + } +} + +/** + Convert the payload from `serializer.extract` to a JSON-API Document. + + @method _normalizeSerializerPayload + @private + @param {subclass of DS.Model} modelClass + @param {Object} payload + @return {Object} JSON-API Document +*/ +export function _normalizeSerializerPayload(modelClass, payload) { + let data = null; + + if (payload) { + if (Ember.isArray(payload)) { + data = map(payload, (payload) => { + return _normalizeSerializerPayloadItem(modelClass, payload); + }); + } else { + data = _normalizeSerializerPayloadItem(modelClass, payload); + } + } + + return { data }; +} + +/** + Convert the payload representing a single record from `serializer.extract` to + a JSON-API Resource Object. + + @method _normalizeSerializerPayloadItem + @private + @param {subclass of DS.Model} modelClass + @param {Object} payload + @return {Object} JSON-API Resource Object +*/ +export function _normalizeSerializerPayloadItem(modelClass, itemPayload) { + var item = {}; + + item.id = '' + itemPayload.id; + item.type = modelClass.modelName; + item.attributes = {}; + item.relationships = {}; + + modelClass.eachAttribute(function(name) { + if (itemPayload.hasOwnProperty(name)) { + item.attributes[name] = itemPayload[name]; + } + }); + + modelClass.eachRelationship(function(key, relationshipMeta) { + var relationship, value; + + if (itemPayload.hasOwnProperty(key)) { + relationship = {}; + value = itemPayload[key]; + + let normalizeRelationshipData = function(value, relationshipMeta) { + if (Ember.isNone(value)) { + return null; + } + if (Ember.typeOf(value) === 'object') { + if (value.id) { + value.id = `${value.id}`; + } + return value; + } + return { id: `${value}`, type: relationshipMeta.type }; + }; + + if (relationshipMeta.kind === 'belongsTo') { + relationship.data = normalizeRelationshipData(value, relationshipMeta); + } else if (relationshipMeta.kind === 'hasMany') { + relationship.data = map(Ember.A(value), function(item) { + return normalizeRelationshipData(item, relationshipMeta); + }); + } + } + + if (itemPayload.links && itemPayload.links.hasOwnProperty(key)) { + relationship = relationship || {}; + value = itemPayload.links[key]; + + relationship.links = { + related: value + }; + } + + if (relationship) { + item.relationships[key] = relationship; + } + }); + + return item; +} + +/** + Push a JSON-API Document to the store. + + This will push both primary data located in `data` and secondary data located + in `included` (if present). + + @method pushPayload + @param {DS.Store} store + @param {Object} payload + @return {DS.Model|Array} one or multiple records from `data` +*/ +export function pushPayload(store, payload) { + var result = pushPayloadData(store, payload); + pushPayloadIncluded(store, payload); + return result; +} + +/** + Push the primary data of a JSON-API Document to the store. + + This method only pushes the primary data located in `data`. + + @method pushPayloadData + @param {DS.Store} store + @param {Object} payload + @return {DS.Model|Array} one or multiple records from `data` +*/ +export function pushPayloadData(store, payload) { + var result; + if (payload && payload.data) { + if (Ember.isArray(payload.data)) { + result = map(payload.data, (item) => { + return _pushResourceObject(store, item); + }); + } else { + result = _pushResourceObject(store, payload.data); + } + } + return result; +} + +/** + Push the secondary data of a JSON-API Document to the store. + + This method only pushes the secondary data located in `included`. + + @method pushPayloadIncluded + @param {DS.Store} store + @param {Object} payload + @return {Array} an array containing zero or more records from `included` +*/ +export function pushPayloadIncluded(store, payload) { + var result; + if (payload && payload.included && Ember.isArray(payload.included)) { + result = map(payload.included, (item) => { + return _pushResourceObject(store, item); + }); + } + return result; +} + +/** + Push a single JSON-API Resource Object to the store. + + @method _pushResourceObject + @private + @param {Object} resourceObject + @return {DS.Model} a record +*/ +export function _pushResourceObject(store, resourceObject) { + return store.push(resourceObject.type, convertResourceObject(resourceObject)); +} + +/** + This method converts a JSON-API Resource Object to a format that DS.Store + understands. + + TODO: This method works as an interim until DS.Store understands JSON-API. + + @method convertResourceObject + @param {Object} payload + @return {Object} an object formatted the way DS.Store understands +*/ +export function convertResourceObject(payload) { + if (!payload) { + return payload; + } + + var data = { + id: payload.id, + type: payload.type, + links: {} + }; + + var attributeKeys = Ember.keys(payload.attributes); + forEach(attributeKeys, function(key) { + var attribute = payload.attributes[key]; + data[key] = attribute; + }); + + var relationshipKeys = Ember.keys(payload.relationships); + forEach(relationshipKeys, function(key) { + var relationship = payload.relationships[key]; + if (relationship.hasOwnProperty('data')) { + data[key] = relationship.data; + } else if (relationship.links && relationship.links.related) { + data.links[key] = relationship.links.related; + } + }); + return data; +} From 994c705895a36e5a5cca0074e98156539befd212 Mon Sep 17 00:00:00 2001 From: Christoffer Persson Date: Wed, 3 Jun 2015 00:35:32 +0300 Subject: [PATCH 3/6] Make JSONSerializer work with the new Serializer API --- .../lib/serializers/json-serializer.js | 460 +++++++++++++++++- .../serializers/json-serializer-new-test.js | 194 ++++++++ 2 files changed, 653 insertions(+), 1 deletion(-) create mode 100644 packages/ember-data/tests/integration/serializers/json-serializer-new-test.js diff --git a/packages/ember-data/lib/serializers/json-serializer.js b/packages/ember-data/lib/serializers/json-serializer.js index ee50f533d8c..655fb65af2d 100644 --- a/packages/ember-data/lib/serializers/json-serializer.js +++ b/packages/ember-data/lib/serializers/json-serializer.js @@ -1,10 +1,70 @@ import Serializer from "ember-data/system/serializer"; +import coerceId from "ember-data/system/coerce-id"; var get = Ember.get; var isNone = Ember.isNone; var map = Ember.ArrayPolyfills.map; var merge = Ember.merge; +/* + Ember Data 2.0 Serializer: + + In Ember Data a Serializer is used to serialize and deserialize + records when they are transferred in and out of an external source. + This process involves normalizing property names, transforming + attribute values and serializing relationships. + + By default Ember Data recommends using the JSONApiSerializer. + + `JSONSerializer` is useful for simpler or legacy backends that may + not support the http://jsonapi.org/ spec. + + `JSONSerializer` normalizes a JSON payload that looks like: + + ```js + App.User = DS.Model.extend({ + name: DS.attr(), + friends: DS.hasMany('user'), + house: DS.belongsTo('location'), + }); + ``` + ```js + { id: 1, + name: 'Sebastian', + friends: [3, 4], + links: { + house: '/houses/lefkada' + } + } + ``` + to JSONApi format that the Ember Data store expects. + + You can customize how JSONSerializer processes it's payload by passing options in + the attrs hash or by subclassing the JSONSerializer and overriding hooks: + + -To customize how a single record is normalized, use the `normalize` hook + -To customize how JSONSerializer normalizes the whole server response, use the + normalizeResponse hook + -To customize how JSONSerializer normalizes a specific response from the server, + use one of the many specific normalizeResponse hooks + -To customize how JSONSerializer normalizes your attributes or relationships, + use the extractAttributes and extractRelationships hooks. + + JSONSerializer normalization process follows these steps: + - `normalizeResponse` - entry method to the Serializer + - `normalizeCreateRecordResponse` - a normalizeResponse for a specific operation is called + - `normalizeSingleResponse`|`normalizeArrayResponse` - for methods like `createRecord` we expect + a single record back, while for methods like `findAll` we expect multiple methods back + - `normalize` - normalizeArray iterates and calls normalize for each of it's records while normalizeSingle + calls it once. This is the method you most likely want to subclass + - `extractId` | `extractAttributes` | `extractRelationships` - normalize delegates to these methods to + turn the record payload into the JSONApi format + + @class JSONSerializer + @namespace DS + @extends DS.Serializer +*/ + /** In Ember Data a Serializer is used to serialize and deserialize records when they are transferred in and out of an external source. @@ -22,6 +82,7 @@ var merge = Ember.merge; @extends DS.Serializer */ export default Serializer.extend({ + /** The primaryKey is used when serializing and deserializing data. Ember Data always uses the `id` property to store the id of @@ -134,6 +195,252 @@ export default Serializer.extend({ return data; }, + /* + The `normalizeResponse` method is used to normalize a payload from the + server to a JSON-API Document. + + http://jsonapi.org/format/#document-structure + + This method delegates to a more specific normalize method based on + the `requestType`. + + To override this method with a custom one, make sure to call + `return this._super(store, primaryModelClass, payload, id, requestType)` with your + pre-processed data. + + Here's an example of using `normalizeResponse` manually: + + ```javascript + socket.on('message', function(message) { + var data = message.data; + var modelClass = store.modelFor(data.modelName); + var serializer = store.serializerFor(data.modelName); + var json = serializer.normalizeSingleResponse(store, modelClass, data, data.id); + + store.push(normalized); + }); + ``` + + @method normalizeResponse + @param {DS.Store} store + @param {DS.Model} primaryModelClass + @param {Object} payload + @param {String|Number} id + @param {String} requestType + @return {Object} JSON-API Document + */ + normalizeResponse: function(store, primaryModelClass, payload, id, requestType) { + switch (requestType) { + case 'find': + return this.normalizeFindResponse(...arguments); + case 'findAll': + return this.normalizeFindAllResponse(...arguments); + case 'findBelongsTo': + return this.normalizeFindBelongsToResponse(...arguments); + case 'findHasMany': + return this.normalizeFindHasManyResponse(...arguments); + case 'findMany': + return this.normalizeFindManyResponse(...arguments); + case 'findQuery': + return this.normalizeFindQueryResponse(...arguments); + case 'createRecord': + return this.normalizeCreateRecordResponse(...arguments); + case 'deleteRecord': + return this.normalizeDeleteRecordResponse(...arguments); + case 'updateRecord': + return this.normalizeUpdateRecordResponse(...arguments); + } + }, + + /* + @method normalizeFindResponse + @param {DS.Store} store + @param {DS.Model} primaryModelClass + @param {Object} payload + @param {String|Number} id + @param {String} requestType + @return {Object} JSON-API Document + */ + normalizeFindResponse: function(store, primaryModelClass, payload, id, requestType) { + return this.normalizeSingleResponse(...arguments); + }, + + /* + @method normalizeFindAllResponse + @param {DS.Store} store + @param {DS.Model} primaryModelClass + @param {Object} payload + @param {String|Number} id + @param {String} requestType + @return {Object} JSON-API Document + */ + normalizeFindAllResponse: function(store, primaryModelClass, payload, id, requestType) { + return this.normalizeArrayResponse(...arguments); + }, + + /* + @method normalizeFindBelongsToResponse + @param {DS.Store} store + @param {DS.Model} primaryModelClass + @param {Object} payload + @param {String|Number} id + @param {String} requestType + @return {Object} JSON-API Document + */ + normalizeFindBelongsToResponse: function(store, primaryModelClass, payload, id, requestType) { + return this.normalizeSingleResponse(...arguments); + }, + + /* + @method normalizeFindHasManyResponse + @param {DS.Store} store + @param {DS.Model} primaryModelClass + @param {Object} payload + @param {String|Number} id + @param {String} requestType + @return {Object} JSON-API Document + */ + normalizeFindHasManyResponse: function(store, primaryModelClass, payload, id, requestType) { + return this.normalizeArrayResponse(...arguments); + }, + + /* + @method normalizeFindManyResponse + @param {DS.Store} store + @param {DS.Model} primaryModelClass + @param {Object} payload + @param {String|Number} id + @param {String} requestType + @return {Object} JSON-API Document + */ + normalizeFindManyResponse: function(store, primaryModelClass, payload, id, requestType) { + return this.normalizeArrayResponse(...arguments); + }, + + /* + @method normalizeFindQueryResponse + @param {DS.Store} store + @param {DS.Model} primaryModelClass + @param {Object} payload + @param {String|Number} id + @param {String} requestType + @return {Object} JSON-API Document + */ + normalizeFindQueryResponse: function(store, primaryModelClass, payload, id, requestType) { + return this.normalizeArrayResponse(...arguments); + }, + + /* + @method normalizeCreateRecordResponse + @param {DS.Store} store + @param {DS.Model} primaryModelClass + @param {Object} payload + @param {String|Number} id + @param {String} requestType + @return {Object} JSON-API Document + */ + normalizeCreateRecordResponse: function(store, primaryModelClass, payload, id, requestType) { + return this.normalizeSaveResponse(...arguments); + }, + + /* + @method normalizeDeleteRecordResponse + @param {DS.Store} store + @param {DS.Model} primaryModelClass + @param {Object} payload + @param {String|Number} id + @param {String} requestType + @return {Object} JSON-API Document + */ + normalizeDeleteRecordResponse: function(store, primaryModelClass, payload, id, requestType) { + return this.normalizeSaveResponse(...arguments); + }, + + /* + @method normalizeUpdateRecordResponse + @param {DS.Store} store + @param {DS.Model} primaryModelClass + @param {Object} payload + @param {String|Number} id + @param {String} requestType + @return {Object} JSON-API Document + */ + normalizeUpdateRecordResponse: function(store, primaryModelClass, payload, id, requestType) { + return this.normalizeSaveResponse(...arguments); + }, + + /* + @method normalizeSaveResponse + @param {DS.Store} store + @param {DS.Model} primaryModelClass + @param {Object} payload + @param {String|Number} id + @param {String} requestType + @return {Object} JSON-API Document + */ + normalizeSaveResponse: function(store, primaryModelClass, payload, id, requestType) { + return this.normalizeSingleResponse(...arguments); + }, + + /* + @method normalizeSingleResponse + @param {DS.Store} store + @param {DS.Model} primaryModelClass + @param {Object} payload + @param {String|Number} id + @param {String} requestType + @return {Object} JSON-API Document + */ + normalizeSingleResponse: function(store, primaryModelClass, payload, id, requestType) { + return this._normalizeResponse(store, primaryModelClass, payload, id, requestType, true); + }, + + /* + @method normalizeArrayResponse + @param {DS.Store} store + @param {DS.Model} primaryModelClass + @param {Object} payload + @param {String|Number} id + @param {String} requestType + @return {Object} JSON-API Document + */ + normalizeArrayResponse: function(store, primaryModelClass, payload, id, requestType) { + return this._normalizeResponse(store, primaryModelClass, payload, id, requestType, false); + }, + + /* + @method _normalizeResponse + @param {DS.Store} store + @param {DS.Model} primaryModelClass + @param {Object} payload + @param {String|Number} id + @param {String} requestType + @param {Boolean} isSingle + @return {Object} JSON-API Document + @private + */ + _normalizeResponse: function(store, primaryModelClass, payload, id, requestType, isSingle) { + let documentHash = { + data: null, + included: [] + }; + + payload = this.normalizePayload(payload); + + if (isSingle) { + let { data } = this.normalize(primaryModelClass, payload); + documentHash.data = data; + } else { + documentHash.data = payload.map((item) => { + let { data } = this.normalize(primaryModelClass, item); + return data; + }); + } + + return documentHash; + }, + + /** Normalizes a part of the JSON payload returned by the server. You should override this method, munge the hash @@ -172,6 +479,10 @@ export default Serializer.extend({ @return {Object} */ normalize: function(typeClass, hash) { + if (Ember.FEATURES.isEnabled('ds-new-serializer-api') && this.get('isNewSerializerAPI')) { + return _newNormalize.apply(this, arguments); + } + if (!hash) { return hash; } this.normalizeId(hash); @@ -183,6 +494,115 @@ export default Serializer.extend({ return hash; }, + /* + Returns the resource's ID. + + @method extractId + @param {Object} resourceHash + @return {String} + */ + extractId: function(resourceHash) { + var primaryKey = get(this, 'primaryKey'); + var id = resourceHash[primaryKey]; + return coerceId(id); + }, + + /* + Returns the resource's attributes formatted as a JSON-API "attributes object". + + http://jsonapi.org/format/#document-resource-object-attributes + + @method extractId + @param {Object} resourceHash + @return {Object} + */ + extractAttributes: function(modelClass, resourceHash) { + var attributeKey; + var attributes = {}; + + modelClass.eachAttribute(function(key) { + attributeKey = this.keyForAttribute(key, 'deserialize'); + if (resourceHash.hasOwnProperty(attributeKey)) { + attributes[key] = resourceHash[attributeKey]; + } + }, this); + + return attributes; + }, + + /* + Returns a relationship formatted as a JSON-API "relationship object". + + http://jsonapi.org/format/#document-resource-object-relationships + + @method extractRelationship + @param {Object} relationshipModelName + @param {Object} relationshipHash + @return {Object} + */ + extractRelationship: function(relationshipModelName, relationshipHash) { + if (Ember.isNone(relationshipHash)) { return null; } + /* + When `relationshipHash` is an object it usually means that the relationship + is polymorphic. It could however also be embedded resources that the + EmbeddedRecordsMixin has be able to process. + */ + if (Ember.typeOf(relationshipHash) === 'object') { + if (relationshipHash.id) { + relationshipHash.id = coerceId(relationshipHash.id); + } + if (relationshipHash.type) { + relationshipHash.type = this.modelNameFromPayloadKey(relationshipHash.type); + } + return relationshipHash; + } + return { id: coerceId(relationshipHash), type: relationshipModelName }; + }, + + /* + Returns the resource's relationships formatted as a JSON-API "relationships object". + + http://jsonapi.org/format/#document-resource-object-relationships + + @method extractRelationships + @param {Object} modelClass + @param {Object} resourceHash + @return {Object} + */ + extractRelationships: function(modelClass, resourceHash) { + let relationships = {}; + + modelClass.eachRelationship(function(key, relationshipMeta) { + let relationship = null; + let relationshipKey = this.keyForRelationship(key, relationshipMeta.kind, 'deserialize'); + if (resourceHash.hasOwnProperty(relationshipKey)) { + let data = null; + let relationshipHash = resourceHash[relationshipKey]; + if (relationshipMeta.kind === 'belongsTo') { + data = this.extractRelationship(relationshipMeta.type, relationshipHash); + } else if (relationshipMeta.kind === 'hasMany') { + data = Ember.A(relationshipHash).map(function(item) { + return this.extractRelationship(relationshipMeta.type, item); + }, this); + } + relationship = { data }; + } + + let linkKey = this.keyForLink(key, relationshipMeta.kind); + if (resourceHash.links && resourceHash.links.hasOwnProperty(linkKey)) { + let related = resourceHash.links[linkKey]; + relationship = relationship || {}; + relationship.links = { related }; + } + + if (relationship) { + relationships[key] = relationship; + } + }, this); + + return relationships; + }, + /** You can use this method to normalize all payloads, regardless of whether they represent single records or an array. @@ -1114,11 +1534,23 @@ export default Serializer.extend({ @param {String} method @return {String} normalized key */ - keyForRelationship: function(key, typeClass, method) { return key; }, + /** + `keyForLink` can be used to define a custom key when deserializing link + properties. + + @method keyForLink + @param {String} key + @param {String} kind `belongsTo` or `hasMany` + @return {String} normalized key + */ + keyForLink: function(key, kind) { + return key; + }, + // HELPERS /** @@ -1134,3 +1566,29 @@ export default Serializer.extend({ return transform; } }); + +/* + @method _newNormalize + @param {DS.Model} modelClass + @param {Object} resourceHash + @return {Object} + @private +*/ +function _newNormalize(modelClass, resourceHash) { + let data = null; + + if (resourceHash) { + this.normalizeUsingDeclaredMapping(modelClass, resourceHash); + + data = { + id: this.extractId(resourceHash), + type: modelClass.modelName, + attributes: this.extractAttributes(modelClass, resourceHash), + relationships: this.extractRelationships(modelClass, resourceHash) + }; + + this.applyTransforms(modelClass, data.attributes); + } + + return { data }; +} diff --git a/packages/ember-data/tests/integration/serializers/json-serializer-new-test.js b/packages/ember-data/tests/integration/serializers/json-serializer-new-test.js new file mode 100644 index 00000000000..0e0b2caa58f --- /dev/null +++ b/packages/ember-data/tests/integration/serializers/json-serializer-new-test.js @@ -0,0 +1,194 @@ +var Post, post, Comment, Favorite, TestSerializer, env; +var run = Ember.run; + +module("integration/serializer/json - JSONSerializer (new API)", { + setup: function() { + Post = DS.Model.extend({ + title: DS.attr('string'), + comments: DS.hasMany('comment', { inverse: null }) + }); + Comment = DS.Model.extend({ + body: DS.attr('string'), + post: DS.belongsTo('post') + }); + Favorite = DS.Model.extend({ + post: DS.belongsTo('post', { async: true, polymorphic: true }) + }); + TestSerializer = DS.JSONSerializer.extend({ + isNewSerializerAPI: true + }); + env = setupStore({ + post: Post, + comment: Comment, + favorite: Favorite + }); + env.store.modelFor('post'); + env.store.modelFor('comment'); + env.store.modelFor('favorite'); + + env.registry.register('serializer:application', TestSerializer); + }, + + teardown: function() { + run(env.store, 'destroy'); + } +}); + +if (Ember.FEATURES.isEnabled('ds-new-serializer-api')) { + + test("normalizeArrayResponse normalizes each record in the array", function() { + var postNormalizeCount = 0; + var posts = [ + { id: "1", title: "Rails is omakase" }, + { id: "2", title: "Another Post" } + ]; + + env.registry.register('serializer:post', TestSerializer.extend({ + normalize: function () { + postNormalizeCount++; + return this._super.apply(this, arguments); + } + })); + + run(function() { + env.container.lookup("serializer:post").normalizeArrayResponse(env.store, Post, posts, null, 'findAll'); + }); + equal(postNormalizeCount, 2, "two posts are normalized"); + }); + + test('Serializer should respect the attrs hash when extracting records', function() { + env.registry.register("serializer:post", TestSerializer.extend({ + attrs: { + title: "title_payload_key", + comments: { key: 'my_comments' } + } + })); + + var jsonHash = { + id: "1", + title_payload_key: "Rails is omakase", + my_comments: [1, 2] + }; + + var post = env.container.lookup("serializer:post").normalizeSingleResponse(env.store, Post, jsonHash, '1', 'find'); + + equal(post.data.attributes.title, "Rails is omakase"); + deepEqual(post.data.relationships.comments.data, [{ id: "1", type: "comment" }, { id: "2", type: "comment" }]); + }); + + test("Serializer should respect the primaryKey attribute when extracting records", function() { + env.registry.register('serializer:post', TestSerializer.extend({ + primaryKey: '_ID_' + })); + + var jsonHash = { "_ID_": 1, title: "Rails is omakase" }; + + run(function() { + post = env.container.lookup("serializer:post").normalizeSingleResponse(env.store, Post, jsonHash, '1', 'find'); + }); + + equal(post.data.id, "1"); + equal(post.data.attributes.title, "Rails is omakase"); + }); + + test("Serializer should respect keyForAttribute when extracting records", function() { + env.registry.register('serializer:post', TestSerializer.extend({ + keyForAttribute: function(key) { + return key.toUpperCase(); + } + })); + + var jsonHash = { id: 1, TITLE: 'Rails is omakase' }; + + post = env.container.lookup("serializer:post").normalize(Post, jsonHash); + + equal(post.data.id, "1"); + equal(post.data.attributes.title, "Rails is omakase"); + }); + + test("Serializer should respect keyForRelationship when extracting records", function() { + env.registry.register('serializer:post', TestSerializer.extend({ + keyForRelationship: function(key, type) { + return key.toUpperCase(); + } + })); + + var jsonHash = { id: 1, title: 'Rails is omakase', COMMENTS: ['1'] }; + + post = env.container.lookup("serializer:post").normalize(Post, jsonHash); + + deepEqual(post.data.relationships.comments.data, [{ id: "1", type: "comment" }]); + }); + + test("normalizePayload is called during normalizeSingleResponse", function() { + var counter = 0; + + env.registry.register('serializer:post', TestSerializer.extend({ + normalizePayload: function(payload) { + counter++; + return payload.response; + } + })); + + var jsonHash = { + response: { + id: 1, + title: "Rails is omakase" + } + }; + + run(function() { + post = env.container.lookup("serializer:post").normalizeSingleResponse(env.store, Post, jsonHash, '1', 'find'); + }); + + equal(counter, 1); + equal(post.data.id, "1"); + equal(post.data.attributes.title, "Rails is omakase"); + }); + + test("Calling normalize should normalize the payload (only the passed keys)", function () { + expect(1); + var Person = DS.Model.extend({ + posts: DS.hasMany('post') + }); + env.registry.register('serializer:post', TestSerializer.extend({ + attrs: { + notInHash: 'aCustomAttrNotInHash', + inHash: 'aCustomAttrInHash' + } + })); + + env.registry.register('model:person', Person); + + Post.reopen({ + content: DS.attr('string'), + author: DS.belongsTo('person'), + notInHash: DS.attr('string'), + inHash: DS.attr('string') + }); + + var normalizedPayload = env.container.lookup("serializer:post").normalize(Post, { + id: '1', + title: 'Ember rocks', + author: 1, + aCustomAttrInHash: 'blah' + }); + + deepEqual(normalizedPayload, { + "data": { + "id": "1", + "type": "post", + "attributes": { + "inHash": "blah", + "title": "Ember rocks" + }, + "relationships": { + "author": { + "data": { "id": "1", "type": "person" } + } + } + } + }); + }); + +} From 2ba2d588d2a4b0b7ab7630851dd8b072dc41acf2 Mon Sep 17 00:00:00 2001 From: Christoffer Persson Date: Sun, 31 May 2015 12:27:10 +0200 Subject: [PATCH 4/6] Make RESTSerializer work with the new Serializer API --- .../lib/serializers/rest-serializer.js | 202 +++++++++ .../serializers/rest-serializer-new-test.js | 395 ++++++++++++++++++ .../serializers/rest-serializer-test.js | 338 +++++++-------- tests/ember-configuration.js | 4 + 4 files changed, 775 insertions(+), 164 deletions(-) create mode 100644 packages/ember-data/tests/integration/serializers/rest-serializer-new-test.js diff --git a/packages/ember-data/lib/serializers/rest-serializer.js b/packages/ember-data/lib/serializers/rest-serializer.js index 71ad7926941..da97a9d4350 100644 --- a/packages/ember-data/lib/serializers/rest-serializer.js +++ b/packages/ember-data/lib/serializers/rest-serializer.js @@ -6,6 +6,7 @@ import JSONSerializer from "ember-data/serializers/json-serializer"; import normalizeModelName from "ember-data/system/normalize-model-name"; import {singularize} from "ember-inflector/lib/system/string"; import coerceId from "ember-data/system/coerce-id"; +import { pushPayload } from "ember-data/system/store/serializer-response"; var forEach = Ember.ArrayPolyfills.forEach; var map = Ember.ArrayPolyfills.map; @@ -55,6 +56,7 @@ var camelize = Ember.String.camelize; @extends DS.JSONSerializer */ var RESTSerializer = JSONSerializer.extend({ + /** If you want to do normalizations specific to some part of the payload, you can specify those under `normalizeHash`. @@ -173,6 +175,11 @@ var RESTSerializer = JSONSerializer.extend({ @return {Object} */ normalize: function(typeClass, hash, prop) { + if (Ember.FEATURES.isEnabled('ds-new-serializer-api') && this.get('isNewSerializerAPI')) { + _newNormalize.apply(this, arguments); + return this._super(...arguments); + } + this.normalizeId(hash); this.normalizeAttributes(typeClass, hash); this.normalizeRelationships(typeClass, hash); @@ -187,6 +194,150 @@ var RESTSerializer = JSONSerializer.extend({ return hash; }, + /* + Normalizes an array of resource payloads and returns a JSON-API Document + with primary data and, if any, included data as `{ data, included }`. + + @method normalizeArray + @param {DS.Store} store + @param {String} modelName + @param {Object} arrayHash + @param {String} prop + @return {Object} + */ + normalizeArray: function(store, modelName, arrayHash, prop) { + let documentHash = { + data: [], + included: [] + }; + + let modelClass = store.modelFor(modelName); + let serializer = store.serializerFor(modelName); + + /*jshint loopfunc:true*/ + forEach.call(arrayHash, (hash) => { + let { data, included } = serializer.normalize(modelClass, hash, prop); + documentHash.data.push(data); + documentHash.included.push(...included); + }, this); + + return documentHash; + }, + + /* + @method _normalizeResponse + @param {DS.Store} store + @param {DS.Model} primaryModelClass + @param {Object} payload + @param {String|Number} id + @param {String} requestType + @param {Boolean} isSingle + @return {Object} JSON-API Document + @private + */ + _normalizeResponse: function(store, primaryModelClass, payload, id, requestType, isSingle) { + var document = { + data: null, + included: [] + }; + + Ember.keys(payload).forEach((prop) => { + var modelName = prop; + var forcedSecondary = false; + + /* + If you want to provide sideloaded records of the same type that the + primary data you can do that by prefixing the key with `_`. + + Example + + ``` + { + users: [ + { id: 1, title: 'Tom', manager: 3 }, + { id: 2, title: 'Yehuda', manager: 3 } + ], + _users: [ + { id: 3, title: 'Tomster' } + ] + } + ``` + + This forces `_users` to be added to `included` instead of `data`. + */ + if (prop.charAt(0) === '_') { + forcedSecondary = true; + modelName = prop.substr(1); + } + + var typeName = this.modelNameFromPayloadKey(modelName); + if (!store.modelFactoryFor(typeName)) { + Ember.warn(this.warnMessageNoModelForKey(modelName, typeName), false); + return; + } + + var isPrimary = (!forcedSecondary && this.isPrimaryType(store, typeName, primaryModelClass)); + var value = payload[prop]; + + if (value === null) { + return; + } + + /* + Support primary data as an object instead of an array. + + Example + + ``` + { + user: { id: 1, title: 'Tom', manager: 3 } + } + ``` + */ + if (isPrimary && Ember.typeOf(value) !== 'array') { + let { data, included } = this.normalize(primaryModelClass, value, prop); + document.data = data; + document.included.push(...included); + return; + } + + let { data, included } = this.normalizeArray(store, typeName, value, prop); + + document.included.push(...included); + + if (isSingle) { + /*jshint loopfunc:true*/ + forEach.call(data, function(resource) { + + /* + Figures out if this is the primary record or not. + + It's either: + + 1. The record with the same ID as the original request + 2. If it's a newly created record without an ID, the first record + in the array + */ + var isUpdatedRecord = isPrimary && coerceId(resource.id) === id; + var isFirstCreatedRecord = isPrimary && !id && !document.data; + + if (isFirstCreatedRecord || isUpdatedRecord) { + document.data = resource; + } else { + document.included.push(resource); + } + }); + } else { + if (isPrimary) { + document.data = data; + } else { + document.included.push(...data); + } + } + }); + + return document; + }, /** Called when the server has returned a payload representing @@ -494,6 +645,11 @@ var RESTSerializer = JSONSerializer.extend({ @param {Object} rawPayload */ pushPayload: function(store, rawPayload) { + if (Ember.FEATURES.isEnabled('ds-new-serializer-api') && this.get('isNewSerializerAPI')) { + _newPushPayload.apply(this, arguments); + return; + } + var payload = this.normalizePayload(rawPayload); for (var prop in payload) { @@ -855,3 +1011,49 @@ Ember.runInDebug(function() { }); export default RESTSerializer; + +/* + @method _newNormalize + @param {DS.Model} modelClass + @param {Object} resourceHash + @param {String} prop + @return {Object} + @private +*/ +function _newNormalize(modelClass, resourceHash, prop) { + if (this.normalizeHash && this.normalizeHash[prop]) { + this.normalizeHash[prop](resourceHash); + } +} + +/* + @method _newPushPayload + @param {DS.Store} store + @param {Object} rawPayload +*/ +function _newPushPayload(store, rawPayload) { + let documentHash = { + data: [], + included: [] + }; + let payload = this.normalizePayload(rawPayload); + + for (var prop in payload) { + var modelName = this.modelNameFromPayloadKey(prop); + if (!store.modelFactoryFor(modelName)) { + Ember.warn(this.warnMessageNoModelForKey(prop, modelName), false); + continue; + } + var type = store.modelFor(modelName); + var typeSerializer = store.serializerFor(type); + + /*jshint loopfunc:true*/ + forEach.call(Ember.makeArray(payload[prop]), (hash) => { + let { data, included } = typeSerializer.normalize(type, hash, prop); + documentHash.data.push(data); + documentHash.included.push(...included); + }, this); + } + + pushPayload(store, documentHash); +} diff --git a/packages/ember-data/tests/integration/serializers/rest-serializer-new-test.js b/packages/ember-data/tests/integration/serializers/rest-serializer-new-test.js new file mode 100644 index 00000000000..ca85ab4c304 --- /dev/null +++ b/packages/ember-data/tests/integration/serializers/rest-serializer-new-test.js @@ -0,0 +1,395 @@ +var HomePlanet, SuperVillain, EvilMinion, YellowMinion, DoomsdayDevice, Comment, TestSerializer, env; +var run = Ember.run; + +module("integration/serializer/rest - RESTSerializer (new API)", { + setup: function() { + HomePlanet = DS.Model.extend({ + name: DS.attr('string'), + superVillains: DS.hasMany('superVillain') + }); + SuperVillain = DS.Model.extend({ + firstName: DS.attr('string'), + lastName: DS.attr('string'), + homePlanet: DS.belongsTo("homePlanet"), + evilMinions: DS.hasMany("evilMinion") + }); + EvilMinion = DS.Model.extend({ + superVillain: DS.belongsTo('superVillain'), + name: DS.attr('string') + }); + YellowMinion = EvilMinion.extend(); + DoomsdayDevice = DS.Model.extend({ + name: DS.attr('string'), + evilMinion: DS.belongsTo('evilMinion', { polymorphic: true }) + }); + Comment = DS.Model.extend({ + body: DS.attr('string'), + root: DS.attr('boolean'), + children: DS.hasMany('comment', { inverse: null }) + }); + TestSerializer = DS.RESTSerializer.extend({ + isNewSerializerAPI: true + }); + env = setupStore({ + superVillain: SuperVillain, + homePlanet: HomePlanet, + evilMinion: EvilMinion, + yellowMinion: YellowMinion, + doomsdayDevice: DoomsdayDevice, + comment: Comment + }); + + //env.registry.register('serializer:application', TestSerializer.extend()); + + env.store.modelFor('superVillain'); + env.store.modelFor('homePlanet'); + env.store.modelFor('evilMinion'); + env.store.modelFor('yellowMinion'); + env.store.modelFor('doomsdayDevice'); + env.store.modelFor('comment'); + + env.registry.register('serializer:application', TestSerializer); + }, + + teardown: function() { + run(env.store, 'destroy'); + } +}); + +if (Ember.FEATURES.isEnabled('ds-new-serializer-api')) { + + test("normalizeSingleResponse with custom modelNameFromPayloadKey", function() { + expect(1); + + env.restNewSerializer.modelNameFromPayloadKey = function(root) { + var camelized = Ember.String.camelize(root); + return Ember.String.singularize(camelized); + }; + + var jsonHash = { + home_planets: [{ id: "1", name: "Umber", superVillains: [1] }], + super_villains: [{ id: "1", firstName: "Tom", lastName: "Dale", homePlanet: "1" }] + }; + var array; + + run(function() { + array = env.container.lookup("serializer:application").normalizeSingleResponse(env.store, HomePlanet, jsonHash, '1', 'find'); + }); + + deepEqual(array, { + data: { + id: '1', + type: 'home-planet', + attributes: { + name: 'Umber' + }, + relationships: { + superVillains: { + data: [{ id: '1', type: 'super-villain' }] + } + } + }, + included: [{ + id: '1', + type: 'super-villain', + attributes: { + firstName: 'Tom', + lastName: 'Dale' + }, + relationships: { + homePlanet: { + data: { id: '1', type: 'home-planet' } + } + } + }] + }); + }); + + test("normalizeArrayResponse warning with custom modelNameFromPayloadKey", function() { + var homePlanets; + env.restNewSerializer.modelNameFromPayloadKey = function(root) { + //return some garbage that won"t resolve in the container + return "garbage"; + }; + + var jsonHash = { + home_planets: [{ id: "1", name: "Umber", superVillains: [1] }] + }; + + warns(function() { + env.restNewSerializer.normalizeArrayResponse(env.store, HomePlanet, jsonHash, null, 'findAll'); + }, /Encountered "home_planets" in payload, but no model was found for model name "garbage"/); + + // should not warn if a model is found. + env.restNewSerializer.modelNameFromPayloadKey = function(root) { + return Ember.String.camelize(Ember.String.singularize(root)); + }; + + jsonHash = { + home_planets: [{ id: "1", name: "Umber", superVillains: [1] }] + }; + + noWarns(function() { + run(function() { + homePlanets = env.restNewSerializer.normalizeArrayResponse(env.store, HomePlanet, jsonHash, null, 'findAll'); + }); + }); + + equal(homePlanets.data.length, 1); + equal(homePlanets.data[0].attributes.name, "Umber"); + deepEqual(homePlanets.data[0].relationships.superVillains.data, [{ id: '1', type: 'super-villain' }]); + }); + + test("normalizeSingleResponse warning with custom modelNameFromPayloadKey", function() { + var homePlanet; + var oldModelNameFromPayloadKey = env.restNewSerializer.modelNameFromPayloadKey; + env.restNewSerializer.modelNameFromPayloadKey = function(root) { + //return some garbage that won"t resolve in the container + return "garbage"; + }; + + var jsonHash = { + home_planet: { id: "1", name: "Umber", superVillains: [1] } + }; + + warns(Ember.run.bind(null, function() { + run(function() { + env.restNewSerializer.normalizeSingleResponse(env.store, HomePlanet, jsonHash, '1', 'find'); + }); + }), /Encountered "home_planet" in payload, but no model was found for model name "garbage"/); + + // should not warn if a model is found. + env.restNewSerializer.modelNameFromPayloadKey = oldModelNameFromPayloadKey; + jsonHash = { + home_planet: { id: "1", name: "Umber", superVillains: [1] } + }; + + noWarns(function() { + run(function() { + homePlanet = env.restNewSerializer.normalizeSingleResponse(env.store, HomePlanet, jsonHash, 1, 'find'); + }); + }); + + equal(homePlanet.data.attributes.name, "Umber"); + deepEqual(homePlanet.data.relationships.superVillains.data, [{ id: '1', type: 'super-villain' }]); + }); + + test("normalizeResponse can load secondary records of the same type without affecting the query count", function() { + var jsonHash = { + comments: [{ id: "1", body: "Parent Comment", root: true, children: [2, 3] }], + _comments: [ + { id: "2", body: "Child Comment 1", root: false }, + { id: "3", body: "Child Comment 2", root: false } + ] + }; + var array; + + run(function() { + array = env.restNewSerializer.normalizeResponse(env.store, Comment, jsonHash, '1', 'find'); + }); + + deepEqual(array, { + "data": { + "id": "1", + "type": "comment", + "attributes": { + "body": "Parent Comment", + "root": true + }, + "relationships": { + "children": { + "data": [ + { "id": "2", "type": "comment" }, + { "id": "3", "type": "comment" } + ] + } + } + }, + "included": [{ + "id": "2", + "type": "comment", + "attributes": { + "body": "Child Comment 1", + "root": false + }, + "relationships": {} + }, { + "id": "3", + "type": "comment", + "attributes": { + "body": "Child Comment 2", + "root": false + }, + "relationships": {} + }] + }); + + // normalizeResponse does not push records to the store + //equal(env.store.recordForId("comment", "2").get("body"), "Child Comment 1", "Secondary records are in the store"); + //equal(env.store.recordForId("comment", "3").get("body"), "Child Comment 2", "Secondary records are in the store"); + }); + + test("normalizeSingleResponse loads secondary records with correct serializer", function() { + var superVillainNormalizeCount = 0; + + env.registry.register('serializer:super-villain', TestSerializer.extend({ + normalize: function() { + superVillainNormalizeCount++; + return this._super.apply(this, arguments); + } + })); + + var jsonHash = { + evilMinion: { id: "1", name: "Tom Dale", superVillain: 1 }, + superVillains: [{ id: "1", firstName: "Yehuda", lastName: "Katz", homePlanet: "1" }] + }; + + run(function() { + env.restNewSerializer.normalizeSingleResponse(env.store, EvilMinion, jsonHash, '1', 'find'); + }); + + equal(superVillainNormalizeCount, 1, "superVillain is normalized once"); + }); + + test("normalizeSingleResponse returns null if payload contains null", function() { + expect(1); + + var jsonHash = { + evilMinion: null + }; + var value; + + run(function() { + value = env.restNewSerializer.normalizeSingleResponse(env.store, EvilMinion, jsonHash, null, 'find'); + }); + + deepEqual(value, { data: null, included: [] }, "returned value is null"); + }); + + test("normalizeArrayResponse loads secondary records with correct serializer", function() { + var superVillainNormalizeCount = 0; + + env.registry.register('serializer:super-villain', TestSerializer.extend({ + normalize: function() { + superVillainNormalizeCount++; + return this._super.apply(this, arguments); + } + })); + + var jsonHash = { + evilMinions: [{ id: "1", name: "Tom Dale", superVillain: 1 }], + superVillains: [{ id: "1", firstName: "Yehuda", lastName: "Katz", homePlanet: "1" }] + }; + + run(function() { + env.restNewSerializer.normalizeArrayResponse(env.store, EvilMinion, jsonHash, null, 'findAll'); + }); + + equal(superVillainNormalizeCount, 1, "superVillain is normalized once"); + }); + + test('normalizeHash normalizes specific parts of the payload', function() { + env.registry.register('serializer:application', TestSerializer.extend({ + normalizeHash: { + homePlanets: function(hash) { + hash.id = hash._id; + delete hash._id; + return hash; + } + } + })); + + var jsonHash = { + homePlanets: [{ _id: "1", name: "Umber", superVillains: [1] }] + }; + var array; + + run(function() { + array = env.restNewSerializer.normalizeArrayResponse(env.store, HomePlanet, jsonHash, null, 'findAll'); + }); + + deepEqual(array, { + "data": [{ + "id": "1", + "type": "home-planet", + "attributes": { + "name": "Umber" + }, + "relationships": { + "superVillains": { + "data": [ + { "id": "1", "type": "super-villain" } + ] + } + } + }], + "included": [] + }); + + }); + + test('normalizeHash works with transforms', function() { + env.registry.register('serializer:application', TestSerializer.extend({ + normalizeHash: { + evilMinions: function(hash) { + hash.condition = hash._condition; + delete hash._condition; + return hash; + } + } + })); + + env.registry.register('transform:condition', DS.Transform.extend({ + deserialize: function(serialized) { + if (serialized === 1) { + return "healing"; + } else { + return "unknown"; + } + }, + serialize: function(deserialized) { + if (deserialized === "healing") { + return 1; + } else { + return 2; + } + } + })); + + EvilMinion.reopen({ condition: DS.attr('condition') }); + + var jsonHash = { + evilMinions: [{ id: "1", name: "Tom Dale", superVillain: 1, _condition: 1 }] + }; + var array; + + run(function() { + array = env.restNewSerializer.normalizeArrayResponse(env.store, EvilMinion, jsonHash, null, 'findAll'); + }); + + equal(array.data[0].attributes.condition, "healing"); + }); + + test('normalize should allow for different levels of normalization', function() { + env.registry.register('serializer:application', TestSerializer.extend({ + attrs: { + superVillain: 'is_super_villain' + }, + keyForAttribute: function(attr) { + return Ember.String.decamelize(attr); + } + })); + + var jsonHash = { + evilMinions: [{ id: "1", name: "Tom Dale", is_super_villain: 1 }] + }; + var array; + + run(function() { + array = env.restNewSerializer.normalizeArrayResponse(env.store, EvilMinion, jsonHash, null, 'findAll'); + }); + + equal(array.data[0].relationships.superVillain.data.id, 1); + }); + +} 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 c2e1f746a79..9dc79a01c6f 100644 --- a/packages/ember-data/tests/integration/serializers/rest-serializer-test.js +++ b/packages/ember-data/tests/integration/serializers/rest-serializer-test.js @@ -57,69 +57,73 @@ test("modelNameFromPayloadKey returns always same modelName even for uncountable equal(env.restSerializer.modelNameFromPayloadKey('multi-words'), expectedModelName); }); -test("extractArray with custom modelNameFromPayloadKey", function() { - env.restSerializer.modelNameFromPayloadKey = function(root) { - var camelized = Ember.String.camelize(root); - return Ember.String.singularize(camelized); - }; - - var jsonHash = { - home_planets: [{ id: "1", name: "Umber", superVillains: [1] }], - super_villains: [{ id: "1", firstName: "Tom", lastName: "Dale", homePlanet: "1" }] - }; - var array; +if (!Ember.FEATURES.isEnabled('ds-new-serializer-api')) { + test("extractArray with custom modelNameFromPayloadKey", function() { + env.restSerializer.modelNameFromPayloadKey = function(root) { + var camelized = Ember.String.camelize(root); + return Ember.String.singularize(camelized); + }; + + var jsonHash = { + home_planets: [{ id: "1", name: "Umber", superVillains: [1] }], + super_villains: [{ id: "1", firstName: "Tom", lastName: "Dale", homePlanet: "1" }] + }; + var array; - run(function() { - array = env.restSerializer.extractArray(env.store, HomePlanet, jsonHash); - }); + run(function() { + array = env.restSerializer.extractArray(env.store, HomePlanet, jsonHash); + }); - deepEqual(array, [{ - id: "1", - name: "Umber", - superVillains: [1] - }]); + deepEqual(array, [{ + id: "1", + name: "Umber", + superVillains: [1] + }]); - run(function() { - env.store.find('super-villain', 1).then(function(minion) { - equal(minion.get('firstName'), "Tom"); + run(function() { + env.store.find('super-villain', 1).then(function(minion) { + equal(minion.get('firstName'), "Tom"); + }); }); }); -}); +} -test("extractArray warning with custom modelNameFromPayloadKey", function() { - var homePlanets; - env.restSerializer.modelNameFromPayloadKey = function(root) { - //return some garbage that won"t resolve in the container - return "garbage"; - }; +if (!Ember.FEATURES.isEnabled('ds-new-serializer-api')) { + test("extractArray warning with custom modelNameFromPayloadKey", function() { + var homePlanets; + env.restSerializer.modelNameFromPayloadKey = function(root) { + //return some garbage that won"t resolve in the container + return "garbage"; + }; - var jsonHash = { - home_planets: [{ id: "1", name: "Umber", superVillains: [1] }] - }; + var jsonHash = { + home_planets: [{ id: "1", name: "Umber", superVillains: [1] }] + }; - warns(function() { - env.restSerializer.extractArray(env.store, HomePlanet, jsonHash); - }, /Encountered "home_planets" in payload, but no model was found for model name "garbage"/); + warns(function() { + env.restSerializer.extractArray(env.store, HomePlanet, jsonHash); + }, /Encountered "home_planets" in payload, but no model was found for model name "garbage"/); - // should not warn if a model is found. - env.restSerializer.modelNameFromPayloadKey = function(root) { - return Ember.String.camelize(Ember.String.singularize(root)); - }; + // should not warn if a model is found. + env.restSerializer.modelNameFromPayloadKey = function(root) { + return Ember.String.camelize(Ember.String.singularize(root)); + }; - jsonHash = { - home_planets: [{ id: "1", name: "Umber", superVillains: [1] }] - }; + jsonHash = { + home_planets: [{ id: "1", name: "Umber", superVillains: [1] }] + }; - noWarns(function() { - run(function() { - homePlanets = Ember.A(env.restSerializer.extractArray(env.store, HomePlanet, jsonHash)); + noWarns(function() { + run(function() { + homePlanets = Ember.A(env.restSerializer.extractArray(env.store, HomePlanet, jsonHash)); + }); }); - }); - equal(get(homePlanets, "length"), 1); - equal(get(homePlanets, "firstObject.name"), "Umber"); - deepEqual(get(homePlanets, "firstObject.superVillains"), [1]); -}); + equal(get(homePlanets, "length"), 1); + equal(get(homePlanets, "firstObject.name"), "Umber"); + deepEqual(get(homePlanets, "firstObject.superVillains"), [1]); + }); +} test("extractSingle warning with custom modelNameFromPayloadKey", function() { var homePlanet; @@ -157,116 +161,120 @@ test("extractSingle warning with custom modelNameFromPayloadKey", function() { deepEqual(get(homePlanet, "superVillains"), [1]); }); -test("pushPayload - single record payload - warning with custom modelNameFromPayloadKey", function() { - var homePlanet; - var HomePlanetRestSerializer = DS.RESTSerializer.extend({ - modelNameFromPayloadKey: function(root) { - //return some garbage that won"t resolve in the container - if (root === "home_planet") { - return "garbage"; - } else { - return Ember.String.singularize(Ember.String.camelize(root)); +if (!Ember.FEATURES.isEnabled('ds-new-serializer-api')) { + test("pushPayload - single record payload - warning with custom modelNameFromPayloadKey", function() { + var homePlanet; + var HomePlanetRestSerializer = DS.RESTSerializer.extend({ + modelNameFromPayloadKey: function(root) { + //return some garbage that won"t resolve in the container + if (root === "home_planet") { + return "garbage"; + } else { + return Ember.String.singularize(Ember.String.camelize(root)); + } } - } - }); - - env.registry.register("serializer:home-planet", HomePlanetRestSerializer); - - var jsonHash = { - home_planet: { id: "1", name: "Umber", superVillains: [1] }, - super_villains: [{ id: "1", firstName: "Stanley" }] - }; - - warns(function() { - run(function() { - env.store.pushPayload('home-planet', jsonHash); }); - }, /Encountered "home_planet" in payload, but no model was found for model name "garbage"/); - - // assert non-warned records get pushed into store correctly - var superVillain = env.store.getById('super-villain', "1"); - equal(get(superVillain, "firstName"), "Stanley"); + env.registry.register("serializer:home-planet", HomePlanetRestSerializer); - // Serializers are singletons, so that"s why we use the store which - // looks at the container to look it up - env.store.serializerFor('home-planet').reopen({ - modelNameFromPayloadKey: function(root) { - // should not warn if a model is found. - return Ember.String.camelize(Ember.String.singularize(root)); - } - }); + var jsonHash = { + home_planet: { id: "1", name: "Umber", superVillains: [1] }, + super_villains: [{ id: "1", firstName: "Stanley" }] + }; - jsonHash = { - home_planet: { id: "1", name: "Umber", superVillains: [1] }, - super_villains: [{ id: "1", firstName: "Stanley" }] - }; + warns(function() { + run(function() { + env.store.pushPayload('home-planet', jsonHash); + }); + }, /Encountered "home_planet" in payload, but no model was found for model name "garbage"/); - noWarns(function() { - run(function() { - env.store.pushPayload('home-planet', jsonHash); - homePlanet = env.store.getById('home-planet', "1"); - }); - }); - equal(get(homePlanet, "name"), "Umber"); - deepEqual(get(homePlanet, "superVillains.firstObject.firstName"), "Stanley"); -}); + // assert non-warned records get pushed into store correctly + var superVillain = env.store.getById('super-villain', "1"); + equal(get(superVillain, "firstName"), "Stanley"); -test("pushPayload - multiple record payload (extractArray) - warning with custom modelNameFromPayloadKey", function() { - var homePlanet; - var HomePlanetRestSerializer = DS.RESTSerializer.extend({ - modelNameFromPayloadKey: function(root) { - //return some garbage that won"t resolve in the container - if (root === "home_planets") { - return "garbage"; - } else { - return Ember.String.singularize(Ember.String.camelize(root)); + // Serializers are singletons, so that"s why we use the store which + // looks at the container to look it up + env.store.serializerFor('home-planet').reopen({ + modelNameFromPayloadKey: function(root) { + // should not warn if a model is found. + return Ember.String.camelize(Ember.String.singularize(root)); } - } - }); - - env.registry.register("serializer:home-planet", HomePlanetRestSerializer); + }); - var jsonHash = { - home_planets: [{ id: "1", name: "Umber", superVillains: [1] }], - super_villains: [{ id: "1", firstName: "Stanley" }] - }; + jsonHash = { + home_planet: { id: "1", name: "Umber", superVillains: [1] }, + super_villains: [{ id: "1", firstName: "Stanley" }] + }; - warns(function() { - run(function() { - env.store.pushPayload('home-planet', jsonHash); + noWarns(function() { + run(function() { + env.store.pushPayload('home-planet', jsonHash); + homePlanet = env.store.getById('home-planet', "1"); + }); }); - }, /Encountered "home_planets" in payload, but no model was found for model name "garbage"/); - - // assert non-warned records get pushed into store correctly - var superVillain = env.store.getById('super-villain', "1"); - equal(get(superVillain, "firstName"), "Stanley"); - // Serializers are singletons, so that"s why we use the store which - // looks at the container to look it up - env.store.serializerFor('home-planet').reopen({ - modelNameFromPayloadKey: function(root) { - // should not warn if a model is found. - return Ember.String.camelize(Ember.String.singularize(root)); - } + equal(get(homePlanet, "name"), "Umber"); + deepEqual(get(homePlanet, "superVillains.firstObject.firstName"), "Stanley"); }); +} + +if (!Ember.FEATURES.isEnabled('ds-new-serializer-api')) { + test("pushPayload - multiple record payload (extractArray) - warning with custom modelNameFromPayloadKey", function() { + var homePlanet; + var HomePlanetRestSerializer = DS.RESTSerializer.extend({ + modelNameFromPayloadKey: function(root) { + //return some garbage that won"t resolve in the container + if (root === "home_planets") { + return "garbage"; + } else { + return Ember.String.singularize(Ember.String.camelize(root)); + } + } + }); - jsonHash = { - home_planets: [{ id: "1", name: "Umber", superVillains: [1] }], - super_villains: [{ id: "1", firstName: "Stanley" }] - }; + env.registry.register("serializer:home-planet", HomePlanetRestSerializer); + + var jsonHash = { + home_planets: [{ id: "1", name: "Umber", superVillains: [1] }], + super_villains: [{ id: "1", firstName: "Stanley" }] + }; + + warns(function() { + run(function() { + env.store.pushPayload('home-planet', jsonHash); + }); + }, /Encountered "home_planets" in payload, but no model was found for model name "garbage"/); + + // assert non-warned records get pushed into store correctly + var superVillain = env.store.getById('super-villain', "1"); + equal(get(superVillain, "firstName"), "Stanley"); + + // Serializers are singletons, so that"s why we use the store which + // looks at the container to look it up + env.store.serializerFor('home-planet').reopen({ + modelNameFromPayloadKey: function(root) { + // should not warn if a model is found. + return Ember.String.camelize(Ember.String.singularize(root)); + } + }); - noWarns(function() { - run(function() { - env.store.pushPayload('home-planet', jsonHash); - homePlanet = env.store.getById('home-planet', "1"); + jsonHash = { + home_planets: [{ id: "1", name: "Umber", superVillains: [1] }], + super_villains: [{ id: "1", firstName: "Stanley" }] + }; + + noWarns(function() { + run(function() { + env.store.pushPayload('home-planet', jsonHash); + homePlanet = env.store.getById('home-planet', "1"); + }); }); - }); - equal(get(homePlanet, "name"), "Umber"); - deepEqual(get(homePlanet, "superVillains.firstObject.firstName"), "Stanley"); -}); + equal(get(homePlanet, "name"), "Umber"); + deepEqual(get(homePlanet, "superVillains.firstObject.firstName"), "Stanley"); + }); +} test("serialize polymorphicType", function() { var tom, ray; @@ -332,32 +340,34 @@ test("serialize polymorphic when associated object is null", function() { deepEqual(json["evilMinionType"], null); }); -test("extractArray can load secondary records of the same type without affecting the query count", function() { - var jsonHash = { - comments: [{ id: "1", body: "Parent Comment", root: true, children: [2, 3] }], - _comments: [ - { id: "2", body: "Child Comment 1", root: false }, - { id: "3", body: "Child Comment 2", root: false } - ] - }; - var array; +if (!Ember.FEATURES.isEnabled('ds-new-serializer-api')) { + test("extractArray can load secondary records of the same type without affecting the query count", function() { + var jsonHash = { + comments: [{ id: "1", body: "Parent Comment", root: true, children: [2, 3] }], + _comments: [ + { id: "2", body: "Child Comment 1", root: false }, + { id: "3", body: "Child Comment 2", root: false } + ] + }; + var array; - run(function() { - array = env.restSerializer.extractArray(env.store, Comment, jsonHash); - }); + run(function() { + array = env.restSerializer.extractArray(env.store, Comment, jsonHash); + }); - deepEqual(array, [{ - "id": "1", - "body": "Parent Comment", - "root": true, - "children": [2, 3] - }]); + deepEqual(array, [{ + "id": "1", + "body": "Parent Comment", + "root": true, + "children": [2, 3] + }]); - equal(array.length, 1, "The query count is unaffected"); + equal(array.length, 1, "The query count is unaffected"); - equal(env.store.recordForId('comment', "2").get("body"), "Child Comment 1", "Secondary records are in the store"); - equal(env.store.recordForId('comment', "3").get("body"), "Child Comment 2", "Secondary records are in the store"); -}); + equal(env.store.recordForId('comment', "2").get("body"), "Child Comment 1", "Secondary records are in the store"); + equal(env.store.recordForId('comment', "3").get("body"), "Child Comment 2", "Secondary records are in the store"); + }); +} test("extractSingle loads secondary records with correct serializer", function() { var superVillainNormalizeCount = 0; diff --git a/tests/ember-configuration.js b/tests/ember-configuration.js index 12108ded4f7..3d0c7a4f754 100644 --- a/tests/ember-configuration.js +++ b/tests/ember-configuration.js @@ -92,14 +92,18 @@ registry.register('serializer:-default', DS.JSONSerializer); registry.register('serializer:-rest', DS.RESTSerializer); + registry.register('serializer:-rest-new', DS.RESTSerializer.extend({ isNewSerializerAPI: true })); + registry.register('adapter:-active-model', DS.ActiveModelAdapter); registry.register('serializer:-active-model', DS.ActiveModelSerializer); + registry.register('adapter:-rest', DS.RESTAdapter); registry.injection('serializer', 'store', 'store:main'); env.serializer = container.lookup('serializer:-default'); env.restSerializer = container.lookup('serializer:-rest'); + env.restNewSerializer = container.lookup('serializer:-rest-new'); env.store = container.lookup('store:main'); env.adapter = env.store.get('defaultAdapter'); From 7931842f0d65ad70104d86c65bb25430c3a3d1ae Mon Sep 17 00:00:00 2001 From: Christoffer Persson Date: Tue, 2 Jun 2015 15:50:19 +0300 Subject: [PATCH 5/6] Make ActiveModelSerializer work with the new Serializer API --- .../lib/system/active-model-serializer.js | 15 +- ...rializer-namespaced-model-name-new-test.js | 126 ++++++ ...-serializer-namespaced-model-name-test.js} | 0 .../active-model-serializer-new-test.js | 361 ++++++++++++++++++ 4 files changed, 500 insertions(+), 2 deletions(-) create mode 100644 packages/activemodel-adapter/tests/integration/active-model-serializer-namespaced-model-name-new-test.js rename packages/activemodel-adapter/tests/integration/{active-model-serializer-namespaced-modelname-test.js => active-model-serializer-namespaced-model-name-test.js} (100%) create mode 100644 packages/activemodel-adapter/tests/integration/active-model-serializer-new-test.js diff --git a/packages/activemodel-adapter/lib/system/active-model-serializer.js b/packages/activemodel-adapter/lib/system/active-model-serializer.js index 366d28882db..21dc587cf19 100644 --- a/packages/activemodel-adapter/lib/system/active-model-serializer.js +++ b/packages/activemodel-adapter/lib/system/active-model-serializer.js @@ -132,6 +132,19 @@ var ActiveModelSerializer = RESTSerializer.extend({ } }, + /** + `keyForLink` can be used to define a custom key when deserializing link + properties. The `ActiveModelSerializer` camelizes link keys by default. + + @method keyForLink + @param {String} key + @param {String} kind `belongsTo` or `hasMany` + @return {String} normalized key + */ + keyForLink: function(key, relationshipKind) { + return camelize(key); + }, + /* Does not serialize hasMany relationships by default. */ @@ -205,10 +218,8 @@ var ActiveModelSerializer = RESTSerializer.extend({ @param {String} prop @return Object */ - normalize: function(typeClass, hash, prop) { this.normalizeLinks(hash); - return this._super(typeClass, hash, prop); }, diff --git a/packages/activemodel-adapter/tests/integration/active-model-serializer-namespaced-model-name-new-test.js b/packages/activemodel-adapter/tests/integration/active-model-serializer-namespaced-model-name-new-test.js new file mode 100644 index 00000000000..f4464c1162c --- /dev/null +++ b/packages/activemodel-adapter/tests/integration/active-model-serializer-namespaced-model-name-new-test.js @@ -0,0 +1,126 @@ +var SuperVillain, EvilMinion, YellowMinion, DoomsdayDevice, MediocreVillain, TestSerializer, env; +var run = Ember.run; + +module("integration/active_model - AMS-namespaced-model-names (new API)", { + setup: function() { + SuperVillain = DS.Model.extend({ + firstName: DS.attr('string'), + lastName: DS.attr('string'), + evilMinions: DS.hasMany("evilMinion") + }); + + EvilMinion = DS.Model.extend({ + superVillain: DS.belongsTo('superVillain'), + name: DS.attr('string') + }); + YellowMinion = EvilMinion.extend(); + DoomsdayDevice = DS.Model.extend({ + name: DS.attr('string'), + evilMinion: DS.belongsTo('evilMinion', { polymorphic: true }) + }); + MediocreVillain = DS.Model.extend({ + name: DS.attr('string'), + evilMinions: DS.hasMany('evilMinion', { polymorphic: true }) + }); + TestSerializer = DS.ActiveModelSerializer.extend({ + isNewSerializerAPI: true + }); + env = setupStore({ + superVillain: SuperVillain, + evilMinion: EvilMinion, + 'evilMinions/yellowMinion': YellowMinion, + doomsdayDevice: DoomsdayDevice, + mediocreVillain: MediocreVillain + }); + env.store.modelFor('superVillain'); + env.store.modelFor('evilMinion'); + env.store.modelFor('evilMinions/yellowMinion'); + env.store.modelFor('doomsdayDevice'); + env.store.modelFor('mediocreVillain'); + env.registry.register('serializer:application', TestSerializer); + env.registry.register('serializer:-active-model', TestSerializer); + env.registry.register('adapter:-active-model', TestSerializer); + env.amsSerializer = env.container.lookup("serializer:-active-model"); + env.amsAdapter = env.container.lookup("adapter:-active-model"); + }, + + teardown: function() { + run(env.store, 'destroy'); + } +}); + +if (Ember.FEATURES.isEnabled('ds-new-serializer-api')) { + + test("extractPolymorphic hasMany", function() { + var json_hash = { + mediocre_villain: { id: 1, name: "Dr Horrible", evil_minion_ids: [{ type: "EvilMinions::YellowMinion", id: 12 }] }, + "evil-minions/yellow-minion": [{ id: 12, name: "Alex", doomsday_device_ids: [1] }] + }; + var json; + + run(function() { + json = env.amsSerializer.normalizeResponse(env.store, MediocreVillain, json_hash, '1', 'find'); + }); + + deepEqual(json, { + "data": { + "id": "1", + "type": "mediocre-villain", + "attributes": { + "name": "Dr Horrible" + }, + "relationships": { + "evilMinions": { + "data": [ + { "id": "12", "type": "evil-minions/yellow-minion" } + ] + } + } + }, + "included": [{ + "id": "12", + "type": "evil-minions/yellow-minion", + "attributes": { + "name": "Alex" + }, + "relationships": {} + }] + }); + }); + + test("extractPolymorphic belongsTo", function() { + var json_hash = { + doomsday_device: { id: 1, name: "DeathRay", evil_minion_id: { type: "EvilMinions::YellowMinion", id: 12 } }, + "evil-minions/yellow-minion": [{ id: 12, name: "Alex", doomsday_device_ids: [1] }] + }; + var json; + + run(function() { + json = env.amsSerializer.normalizeResponse(env.store, DoomsdayDevice, json_hash, '1', 'find'); + }); + + deepEqual(json, { + "data": { + "id": "1", + "type": "doomsday-device", + "attributes": { + "name": "DeathRay" + }, + "relationships": { + "evilMinion": { + "data": { "id": "12", "type": "evil-minions/yellow-minion" } + } + } + }, + "included": [{ + "id": "12", + "type": "evil-minions/yellow-minion", + "attributes": { + "name": "Alex" + }, + "relationships": {} + }] + }); + }); + +} diff --git a/packages/activemodel-adapter/tests/integration/active-model-serializer-namespaced-modelname-test.js b/packages/activemodel-adapter/tests/integration/active-model-serializer-namespaced-model-name-test.js similarity index 100% rename from packages/activemodel-adapter/tests/integration/active-model-serializer-namespaced-modelname-test.js rename to packages/activemodel-adapter/tests/integration/active-model-serializer-namespaced-model-name-test.js diff --git a/packages/activemodel-adapter/tests/integration/active-model-serializer-new-test.js b/packages/activemodel-adapter/tests/integration/active-model-serializer-new-test.js new file mode 100644 index 00000000000..fcfba817517 --- /dev/null +++ b/packages/activemodel-adapter/tests/integration/active-model-serializer-new-test.js @@ -0,0 +1,361 @@ +var HomePlanet, SuperVillain, EvilMinion, YellowMinion, DoomsdayDevice, MediocreVillain, TestSerializer, env; +var run = Ember.run; + +module("integration/active_model - ActiveModelSerializer (new API)", { + setup: function() { + SuperVillain = DS.Model.extend({ + firstName: DS.attr('string'), + lastName: DS.attr('string'), + homePlanet: DS.belongsTo("homePlanet"), + evilMinions: DS.hasMany("evilMinion") + }); + HomePlanet = DS.Model.extend({ + name: DS.attr('string'), + superVillains: DS.hasMany('superVillain', { async: true }) + }); + EvilMinion = DS.Model.extend({ + superVillain: DS.belongsTo('superVillain'), + name: DS.attr('string') + }); + YellowMinion = EvilMinion.extend(); + DoomsdayDevice = DS.Model.extend({ + name: DS.attr('string'), + evilMinion: DS.belongsTo('evilMinion', { polymorphic: true }) + }); + MediocreVillain = DS.Model.extend({ + name: DS.attr('string'), + evilMinions: DS.hasMany('evilMinion', { polymorphic: true }) + }); + TestSerializer = DS.ActiveModelSerializer.extend({ + isNewSerializerAPI: true + }); + env = setupStore({ + superVillain: SuperVillain, + homePlanet: HomePlanet, + evilMinion: EvilMinion, + yellowMinion: YellowMinion, + doomsdayDevice: DoomsdayDevice, + mediocreVillain: MediocreVillain + }); + env.store.modelFor('superVillain'); + env.store.modelFor('homePlanet'); + env.store.modelFor('evilMinion'); + env.store.modelFor('yellowMinion'); + env.store.modelFor('doomsdayDevice'); + env.store.modelFor('mediocreVillain'); + env.registry.register('serializer:application', TestSerializer); + env.registry.register('serializer:-active-model', TestSerializer); + env.registry.register('adapter:-active-model', DS.ActiveModelAdapter); + env.amsSerializer = env.container.lookup("serializer:-active-model"); + env.amsAdapter = env.container.lookup("adapter:-active-model"); + }, + + teardown: function() { + run(env.store, 'destroy'); + } +}); + +if (Ember.FEATURES.isEnabled('ds-new-serializer-api')) { + + test("normalize", function() { + SuperVillain.reopen({ + yellowMinion: DS.belongsTo('yellowMinion') + }); + + var superVillain_hash = { + id: "1", + first_name: "Tom", + last_name: "Dale", + home_planet_id: "123", + evil_minion_ids: [1, 2] + }; + + var json = env.amsSerializer.normalize(SuperVillain, superVillain_hash, "superVillain"); + + deepEqual(json, { + "data": { + "id": "1", + "type": "super-villain", + "attributes": { + "firstName": "Tom", + "lastName": "Dale" + }, + "relationships": { + "evilMinions": { + "data": [ + { "id": "1", "type": "evil-minion" }, + { "id": "2", "type": "evil-minion" } + ] + }, + "homePlanet": { + "data": { "id": "123", "type": "home-planet" } + } + } + } + }); + }); + + test("normalize links", function() { + var home_planet = { + id: "1", + name: "Umber", + links: { super_villains: "/api/super_villians/1" } + }; + + var json = env.amsSerializer.normalize(HomePlanet, home_planet, "homePlanet"); + + equal(json.data.relationships.superVillains.links.related, "/api/super_villians/1", "normalize links"); + }); + + test("normalizeSingleResponse", function() { + env.registry.register('adapter:superVillain', DS.ActiveModelAdapter); + + var json_hash = { + home_planet: { id: "1", name: "Umber", super_villain_ids: [1] }, + super_villains: [{ + id: "1", + first_name: "Tom", + last_name: "Dale", + home_planet_id: "1" + }] + }; + + var json; + run(function() { + json = env.amsSerializer.normalizeSingleResponse(env.store, HomePlanet, json_hash, '1', 'find'); + }); + + deepEqual(json, { + "data": { + "id": "1", + "type": "home-planet", + "attributes": { + "name": "Umber" + }, + "relationships": { + "superVillains": { + "data": [ + { "id": "1", "type": "super-villain" } + ] + } + } + }, + "included": [{ + "id": "1", + "type": "super-villain", + "attributes": { + "firstName": "Tom", + "lastName": "Dale" + }, + "relationships": { + "homePlanet": { + "data": { "id": "1", "type": "home-planet" } + } + } + }] + }); + }); + + test("normalizeArrayResponse", function() { + env.registry.register('adapter:superVillain', DS.ActiveModelAdapter); + var array; + + var json_hash = { + home_planets: [{ id: "1", name: "Umber", super_villain_ids: [1] }], + super_villains: [{ id: "1", first_name: "Tom", last_name: "Dale", home_planet_id: "1" }] + }; + + run(function() { + array = env.amsSerializer.normalizeArrayResponse(env.store, HomePlanet, json_hash, null, 'findAll'); + }); + + deepEqual(array, { + "data": [{ + "id": "1", + "type": "home-planet", + "attributes": { + "name": "Umber" + }, + "relationships": { + "superVillains": { + "data": [ + { "id": "1", "type": "super-villain" } + ] + } + } + }], + "included": [{ + "id": "1", + "type": "super-villain", + "attributes": { + "firstName": "Tom", + "lastName": "Dale" + }, + "relationships": { + "homePlanet": { + "data": { "id": "1", "type": "home-planet" } + } + } + }] + }); + }); + + test("extractPolymorphic hasMany", function() { + env.registry.register('adapter:yellowMinion', DS.ActiveModelAdapter); + MediocreVillain.toString = function() { return "MediocreVillain"; }; + YellowMinion.toString = function() { return "YellowMinion"; }; + + var json_hash = { + mediocre_villain: { id: 1, name: "Dr Horrible", evil_minion_ids: [{ type: "yellow_minion", id: 12 }] }, + yellow_minions: [{ id: 12, name: "Alex" }] + }; + var json; + + run(function() { + json = env.amsSerializer.normalizeResponse(env.store, MediocreVillain, json_hash, '1', 'find'); + }); + + deepEqual(json, { + "data": { + "id": "1", + "type": "mediocre-villain", + "attributes": { + "name": "Dr Horrible" + }, + "relationships": { + "evilMinions": { + "data": [ + { "id": "12", "type": "yellow-minion" } + ] + } + } + }, + "included": [{ + "id": "12", + "type": "yellow-minion", + "attributes": { + "name": "Alex" + }, + "relationships": {} + }] + }); + }); + + test("extractPolymorphic belongsTo", function() { + env.registry.register('adapter:yellowMinion', DS.ActiveModelAdapter); + EvilMinion.toString = function() { return "EvilMinion"; }; + YellowMinion.toString = function() { return "YellowMinion"; }; + + var json_hash = { + doomsday_device: { id: 1, name: "DeathRay", evil_minion_id: { type: "yellow_minion", id: 12 } }, + yellow_minions: [{ id: 12, name: "Alex", doomsday_device_ids: [1] }] + }; + var json; + + run(function() { + json = env.amsSerializer.normalizeResponse(env.store, DoomsdayDevice, json_hash, '1', 'find'); + }); + + deepEqual(json, { + "data": { + "id": "1", + "type": "doomsday-device", + "attributes": { + "name": "DeathRay" + }, + "relationships": { + "evilMinion": { + "data": { "id": "12", "type": "yellow-minion" } + } + } + }, + "included": [{ + "id": "12", + "type": "yellow-minion", + "attributes": { + "name": "Alex" + }, + "relationships": {} + }] + }); + }); + + test("extractPolymorphic when the related data is not specified", function() { + var json = { + doomsday_device: { id: 1, name: "DeathRay" }, + evil_minions: [{ id: 12, name: "Alex", doomsday_device_ids: [1] }] + }; + + run(function() { + json = env.amsSerializer.normalizeResponse(env.store, DoomsdayDevice, json, '1', 'find'); + }); + + deepEqual(json, { + "data": { + "id": "1", + "type": "doomsday-device", + "attributes": { + "name": "DeathRay" + }, + "relationships": {} + }, + "included": [{ + "id": "12", + "type": "evil-minion", + "attributes": { + "name": "Alex" + }, + "relationships": {} + }] + }); + }); + + test("extractPolymorphic hasMany when the related data is not specified", function() { + var json = { + mediocre_villain: { id: 1, name: "Dr Horrible" } + }; + + run(function() { + json = env.amsSerializer.normalizeResponse(env.store, MediocreVillain, json, '1', 'find'); + }); + + deepEqual(json, { + "data": { + "id": "1", + "type": "mediocre-villain", + "attributes": { + "name": "Dr Horrible" + }, + "relationships": {} + }, + "included": [] + }); + }); + + test("extractPolymorphic does not break hasMany relationships", function() { + var json = { + mediocre_villain: { id: 1, name: "Dr. Evil", evil_minion_ids: [] } + }; + + run(function () { + json = env.amsSerializer.normalizeResponse(env.store, MediocreVillain, json, '1', 'find'); + }); + + deepEqual(json, { + "data": { + "id": "1", + "type": "mediocre-villain", + "attributes": { + "name": "Dr. Evil" + }, + "relationships": { + "evilMinions": { + "data": [] + } + } + }, + "included": [] + }); + }); + +} From 3313864ebbeedd5955a45af5e15ba3abe494feba Mon Sep 17 00:00:00 2001 From: Christoffer Persson Date: Tue, 2 Jun 2015 03:06:47 +0300 Subject: [PATCH 6/6] Make EmbeddedRecordsMixin work with the new Serializer API --- .../lib/serializers/embedded-records-mixin.js | 246 +++- .../embedded-records-mixin-new-test.js | 1200 +++++++++++++++++ 2 files changed, 1377 insertions(+), 69 deletions(-) create mode 100644 packages/ember-data/tests/integration/serializers/embedded-records-mixin-new-test.js diff --git a/packages/ember-data/lib/serializers/embedded-records-mixin.js b/packages/ember-data/lib/serializers/embedded-records-mixin.js index d61102408ae..f600c5fdc5c 100644 --- a/packages/ember-data/lib/serializers/embedded-records-mixin.js +++ b/packages/ember-data/lib/serializers/embedded-records-mixin.js @@ -1,3 +1,5 @@ +var get = Ember.get; +var set = Ember.set; var forEach = Ember.EnumerableUtils.forEach; var camelize = Ember.String.camelize; @@ -122,7 +124,7 @@ var EmbeddedRecordsMixin = Ember.Mixin.create({ **/ normalize: function(typeClass, hash, prop) { var normalizedHash = this._super(typeClass, hash, prop); - return extractEmbeddedRecords(this, this.store, typeClass, normalizedHash); + return this._extractEmbeddedRecords(this, this.store, typeClass, normalizedHash); }, keyForRelationship: function(key, typeClass, method) { @@ -393,63 +395,123 @@ var EmbeddedRecordsMixin = Ember.Mixin.create({ attrsOption: function(attr) { var attrs = this.get('attrs'); return attrs && (attrs[camelize(attr)] || attrs[attr]); - } -}); + }, -// chooses a relationship kind to branch which function is used to update payload -// does not change payload if attr is not embedded -function extractEmbeddedRecords(serializer, store, typeClass, partial) { + /** + @method _extractEmbeddedRecords + @private + */ + _extractEmbeddedRecords: function(serializer, store, typeClass, partial) { + if (Ember.FEATURES.isEnabled('ds-new-serializer-api') && this.get('isNewSerializerAPI')) { + return _newExtractEmbeddedRecords.apply(this, arguments); + } - typeClass.eachRelationship(function(key, relationship) { - if (serializer.hasDeserializeRecordsOption(key)) { - var embeddedTypeClass = store.modelFor(relationship.type); - if (relationship.kind === "hasMany") { - if (relationship.options.polymorphic) { - extractEmbeddedHasManyPolymorphic(store, key, partial); - } else { - extractEmbeddedHasMany(store, key, embeddedTypeClass, partial); + typeClass.eachRelationship(function(key, relationship) { + if (serializer.hasDeserializeRecordsOption(key)) { + var embeddedTypeClass = store.modelFor(relationship.type); + if (relationship.kind === "hasMany") { + if (relationship.options.polymorphic) { + this._extractEmbeddedHasManyPolymorphic(store, key, partial); + } else { + this._extractEmbeddedHasMany(store, key, embeddedTypeClass, partial); + } } - } - if (relationship.kind === "belongsTo") { - if (relationship.options.polymorphic) { - extractEmbeddedBelongsToPolymorphic(store, key, partial); - } else { - extractEmbeddedBelongsTo(store, key, embeddedTypeClass, partial); + if (relationship.kind === "belongsTo") { + if (relationship.options.polymorphic) { + this._extractEmbeddedBelongsToPolymorphic(store, key, partial); + } else { + this._extractEmbeddedBelongsTo(store, key, embeddedTypeClass, partial); + } } } + }, this); + + return partial; + }, + + /** + @method _extractEmbeddedHasMany + @private + */ + _extractEmbeddedHasMany: function(store, key, embeddedTypeClass, hash) { + if (Ember.FEATURES.isEnabled('ds-new-serializer-api') && this.get('isNewSerializerAPI')) { + return _newExtractEmbeddedHasMany.apply(this, arguments); } - }); - return partial; -} + if (!hash[key]) { + return hash; + } + + var ids = []; -// handles embedding for `hasMany` relationship -function extractEmbeddedHasMany(store, key, embeddedTypeClass, hash) { - if (!hash[key]) { + var embeddedSerializer = store.serializerFor(embeddedTypeClass.modelName); + forEach(hash[key], function(data) { + var embeddedRecord = embeddedSerializer.normalize(embeddedTypeClass, data, null); + store.push(embeddedTypeClass.modelName, embeddedRecord); + ids.push(embeddedRecord.id); + }); + + hash[key] = ids; return hash; - } + }, - var ids = []; + /** + @method _extractEmbeddedHasManyPolymorphic + @private + */ + _extractEmbeddedHasManyPolymorphic: function(store, key, hash) { + if (!hash[key]) { + return hash; + } - var embeddedSerializer = store.serializerFor(embeddedTypeClass.modelName); - forEach(hash[key], function(data) { - var embeddedRecord = embeddedSerializer.normalize(embeddedTypeClass, data, null); - store.push(embeddedTypeClass.modelName, embeddedRecord); - ids.push(embeddedRecord.id); - }); + var ids = []; - hash[key] = ids; - return hash; -} + forEach(hash[key], function(data) { + var modelName = data.type; + var embeddedSerializer = store.serializerFor(modelName); + var embeddedTypeClass = store.modelFor(modelName); + // var primaryKey = embeddedSerializer.get('primaryKey'); + + var embeddedRecord = embeddedSerializer.normalize(embeddedTypeClass, data, null); + store.push(embeddedTypeClass.modelName, embeddedRecord); + ids.push({ id: embeddedRecord.id, type: modelName }); + }); -function extractEmbeddedHasManyPolymorphic(store, key, hash) { - if (!hash[key]) { + hash[key] = ids; return hash; - } + }, - var ids = []; + /** + @method _extractEmbeddedBelongsTo + @private + */ + _extractEmbeddedBelongsTo: function(store, key, embeddedTypeClass, hash) { + if (Ember.FEATURES.isEnabled('ds-new-serializer-api') && this.get('isNewSerializerAPI')) { + return _newExtractEmbeddedBelongsTo.apply(this, arguments); + } + + if (!hash[key]) { + return hash; + } - forEach(hash[key], function(data) { + var embeddedSerializer = store.serializerFor(embeddedTypeClass.modelName); + var embeddedRecord = embeddedSerializer.normalize(embeddedTypeClass, hash[key], null); + store.push(embeddedTypeClass.modelName, embeddedRecord); + + hash[key] = embeddedRecord.id; + return hash; + }, + + /** + @method _extractEmbeddedBelongsToPolymorphic + @private + */ + _extractEmbeddedBelongsToPolymorphic: function(store, key, hash) { + if (!hash[key]) { + return hash; + } + + var data = hash[key]; var modelName = data.type; var embeddedSerializer = store.serializerFor(modelName); var embeddedTypeClass = store.modelFor(modelName); @@ -457,43 +519,89 @@ function extractEmbeddedHasManyPolymorphic(store, key, hash) { var embeddedRecord = embeddedSerializer.normalize(embeddedTypeClass, data, null); store.push(embeddedTypeClass.modelName, embeddedRecord); - ids.push({ id: embeddedRecord.id, type: modelName }); - }); - hash[key] = ids; - return hash; -} - -function extractEmbeddedBelongsTo(store, key, embeddedTypeClass, hash) { - if (!hash[key]) { + hash[key] = embeddedRecord.id; + hash[key + 'Type'] = modelName; return hash; + }, + + /** + @method _normalizeEmbeddedRelationship + @private + */ + _normalizeEmbeddedRelationship: function(store, relationshipMeta, relationshipHash) { + let modelName = relationshipMeta.type; + if (relationshipMeta.options.polymorphic) { + modelName = relationshipHash.type; + } + let modelClass = store.modelFor(modelName); + let serializer = store.serializerFor(modelName); + + return serializer.normalize(modelClass, relationshipHash, null); } - var embeddedSerializer = store.serializerFor(embeddedTypeClass.modelName); - var embeddedRecord = embeddedSerializer.normalize(embeddedTypeClass, hash[key], null); - store.push(embeddedTypeClass.modelName, embeddedRecord); +}); - hash[key] = embeddedRecord.id; - //TODO Need to add a reference to the parent later so relationship works between both `belongsTo` records - return hash; +export default EmbeddedRecordsMixin; + +/* + @method _newExtractEmbeddedRecords + @private +*/ +function _newExtractEmbeddedRecords(serializer, store, typeClass, partial) { + typeClass.eachRelationship((key, relationship) => { + if (serializer.hasDeserializeRecordsOption(key)) { + if (relationship.kind === "hasMany") { + this._extractEmbeddedHasMany(store, key, partial, relationship); + } + if (relationship.kind === "belongsTo") { + this._extractEmbeddedBelongsTo(store, key, partial, relationship); + } + } + }, this); + return partial; } -function extractEmbeddedBelongsToPolymorphic(store, key, hash) { - if (!hash[key]) { - return hash; +/* + @method _newExtractEmbeddedHasMany + @private +*/ +function _newExtractEmbeddedHasMany(store, key, hash, relationshipMeta) { + let relationshipHash = get(hash, `data.relationships.${key}.data`); + if (!relationshipHash) { + return; } - var data = hash[key]; - var modelName = data.type; - var embeddedSerializer = store.serializerFor(modelName); - var embeddedTypeClass = store.modelFor(modelName); + let hasMany = relationshipHash.map(item => { + let { data, included } = this._normalizeEmbeddedRelationship(store, relationshipMeta, item); + hash.included = hash.included || []; + hash.included.push(data); + hash.included.push(...included); - var embeddedRecord = embeddedSerializer.normalize(embeddedTypeClass, data, null); - store.push(embeddedTypeClass.modelName, embeddedRecord); + return { id: data.id, type: data.type }; + }); - hash[key] = embeddedRecord.id; - hash[key + 'Type'] = modelName; - return hash; + let relationship = { data: hasMany }; + set(hash, `data.relationships.${key}`, relationship); } -export default EmbeddedRecordsMixin; +/* + @method _newExtractEmbeddedBelongsTo + @private +*/ +function _newExtractEmbeddedBelongsTo(store, key, hash, relationshipMeta) { + let relationshipHash = get(hash, `data.relationships.${key}.data`); + if (!relationshipHash) { + return; + } + + let { data, included } = this._normalizeEmbeddedRelationship(store, relationshipMeta, relationshipHash); + hash.included = hash.included || []; + hash.included.push(data); + hash.included.push(...included); + + let belongsTo = { id: data.id, type: data.type }; + let relationship = { data: belongsTo }; + + set(hash, `data.relationships.${key}`, relationship); +} diff --git a/packages/ember-data/tests/integration/serializers/embedded-records-mixin-new-test.js b/packages/ember-data/tests/integration/serializers/embedded-records-mixin-new-test.js new file mode 100644 index 00000000000..b1224e40dd3 --- /dev/null +++ b/packages/ember-data/tests/integration/serializers/embedded-records-mixin-new-test.js @@ -0,0 +1,1200 @@ +var HomePlanet, SuperVillain, EvilMinion, SecretLab, SecretWeapon, BatCave, Comment, env; +var run = Ember.run; +var LightSaber; +var TestSerializer; + +module("integration/embedded_records_mixin - EmbeddedRecordsMixin (new API)", { + setup: function() { + SuperVillain = DS.Model.extend({ + firstName: DS.attr('string'), + lastName: DS.attr('string'), + homePlanet: DS.belongsTo("homePlanet", { inverse: 'villains' }), + secretLab: DS.belongsTo("secretLab"), + secretWeapons: DS.hasMany("secretWeapon"), + evilMinions: DS.hasMany("evilMinion") + }); + HomePlanet = DS.Model.extend({ + name: DS.attr('string'), + villains: DS.hasMany('superVillain', { inverse: 'homePlanet' }) + }); + SecretLab = DS.Model.extend({ + minionCapacity: DS.attr('number'), + vicinity: DS.attr('string'), + superVillain: DS.belongsTo('superVillain') + }); + BatCave = SecretLab.extend({ + infiltrated: DS.attr('boolean') + }); + SecretWeapon = DS.Model.extend({ + name: DS.attr('string'), + superVillain: DS.belongsTo('superVillain') + }); + LightSaber = SecretWeapon.extend({ + color: DS.attr('string') + }); + EvilMinion = DS.Model.extend({ + superVillain: DS.belongsTo('superVillain'), + name: DS.attr('string') + }); + Comment = DS.Model.extend({ + body: DS.attr('string'), + root: DS.attr('boolean'), + children: DS.hasMany('comment', { inverse: null }) + }); + TestSerializer = DS.RESTSerializer.extend({ + isNewSerializerAPI: true + }); + env = setupStore({ + superVillain: SuperVillain, + homePlanet: HomePlanet, + secretLab: SecretLab, + batCave: BatCave, + secretWeapon: SecretWeapon, + lightSaber: LightSaber, + evilMinion: EvilMinion, + comment: Comment + }); + env.store.modelFor('superVillain'); + env.store.modelFor('homePlanet'); + env.store.modelFor('secretLab'); + env.store.modelFor('batCave'); + env.store.modelFor('secretWeapon'); + env.store.modelFor('lightSaber'); + env.store.modelFor('evilMinion'); + env.store.modelFor('comment'); + + env.registry.register('serializer:application', TestSerializer.extend(DS.EmbeddedRecordsMixin)); + }, + + teardown: function() { + run(env.store, 'destroy'); + } +}); + +if (Ember.FEATURES.isEnabled('ds-new-serializer-api')) { + + test("normalizeSingleResponse with embedded objects", function() { + env.registry.register('adapter:super-villain', DS.RESTAdapter); + env.registry.register('serializer:super-villain', TestSerializer.extend()); + env.registry.register('serializer:home-planet', TestSerializer.extend(DS.EmbeddedRecordsMixin, { + attrs: { + villains: { embedded: 'always' } + } + })); + + var serializer = env.container.lookup("serializer:home-planet"); + var json_hash = { + homePlanet: { + id: "1", + name: "Umber", + villains: [{ + id: "2", + firstName: "Tom", + lastName: "Dale" + }] + } + }; + var json; + + run(function() { + json = serializer.normalizeSingleResponse(env.store, HomePlanet, json_hash, '1', 'find'); + }); + + deepEqual(json, { + "data": { + "id": "1", + "type": "home-planet", + "attributes": { + "name": "Umber" + }, + "relationships": { + "villains": { + "data": [ + { "id": "2", "type": "super-villain" } + ] + } + } + }, + "included": [ + { + "id": "2", + "type": "super-villain", + "attributes": { + "firstName": "Tom", + "lastName": "Dale" + }, + "relationships": {} + } + ] + }); + }); + + test("normalizeSingleResponse with embedded objects inside embedded objects", function() { + env.registry.register('adapter:super-villain', DS.RESTAdapter); + env.registry.register('serializer:home-planet', TestSerializer.extend(DS.EmbeddedRecordsMixin, { + attrs: { + villains: { embedded: 'always' } + } + })); + env.registry.register('serializer:super-villain', TestSerializer.extend(DS.EmbeddedRecordsMixin, { + attrs: { + evilMinions: { embedded: 'always' } + } + })); + env.registry.register('serializer:evil-minion', TestSerializer); + + var serializer = env.container.lookup("serializer:home-planet"); + var json_hash = { + homePlanet: { + id: "1", + name: "Umber", + villains: [{ + id: "2", + firstName: "Tom", + lastName: "Dale", + evilMinions: [{ + id: "3", + name: "Alex" + }] + }] + } + }; + var json; + + run(function() { + json = serializer.normalizeSingleResponse(env.store, HomePlanet, json_hash, '1', 'find'); + }); + + deepEqual(json, { + "data": { + "id": "1", + "type": "home-planet", + "attributes": { + "name": "Umber" + }, + "relationships": { + "villains": { + "data": [ + { "id": "2", "type": "super-villain" } + ] + } + } + }, + "included": [{ + "id": "2", + "type": "super-villain", + "attributes": { + "firstName": "Tom", + "lastName": "Dale" + }, + "relationships": { + "evilMinions": { + "data": [ + { "id": "3", "type": "evil-minion" } + ] + } + } + }, { + "id": "3", + "type": "evil-minion", + "attributes": { + "name": "Alex" + }, + "relationships": {} + }] + }); + }); + + test("normalizeSingleResponse with embedded objects of same type", function() { + env.registry.register('adapter:comment', DS.RESTAdapter); + env.registry.register('serializer:comment', TestSerializer.extend(DS.EmbeddedRecordsMixin, { + attrs: { + children: { embedded: 'always' } + } + })); + + var serializer = env.container.lookup("serializer:comment"); + var json_hash = { + comment: { + id: "1", + body: "Hello", + root: true, + children: [{ + id: "2", + body: "World", + root: false + }, + { + id: "3", + body: "Foo", + root: false + }] + } + }; + var json; + run(function() { + json = serializer.normalizeSingleResponse(env.store, Comment, json_hash, '1', 'find'); + }); + + deepEqual(json, { + "data": { + "id": "1", + "type": "comment", + "attributes": { + "body": "Hello", + "root": true + }, + "relationships": { + "children": { + "data": [ + { "id": "2", "type": "comment" }, + { "id": "3", "type": "comment" } + ] + } + } + }, + "included": [{ + "id": "2", + "type": "comment", + "attributes": { + "body": "World", + "root": false + }, + "relationships": {} + }, { + "id": "3", + "type": "comment", + "attributes": { + "body": "Foo", + "root": false + }, + "relationships": {} + }] + }, "Primary record was correct"); + }); + + test("normalizeSingleResponse with embedded objects inside embedded objects of same type", function() { + env.registry.register('adapter:comment', DS.RESTAdapter); + env.registry.register('serializer:comment', TestSerializer.extend(DS.EmbeddedRecordsMixin, { + attrs: { + children: { embedded: 'always' } + } + })); + + var serializer = env.container.lookup("serializer:comment"); + var json_hash = { + comment: { + id: "1", + body: "Hello", + root: true, + children: [{ + id: "2", + body: "World", + root: false, + children: [{ + id: "4", + body: "Another", + root: false + }] + }, + { + id: "3", + body: "Foo", + root: false + }] + } + }; + var json; + run(function() { + json = serializer.normalizeSingleResponse(env.store, Comment, json_hash, '1', 'find'); + }); + + deepEqual(json, { + "data": { + "id": "1", + "type": "comment", + "attributes": { + "body": "Hello", + "root": true + }, + "relationships": { + "children": { + "data": [ + { "id": "2", "type": "comment" }, + { "id": "3", "type": "comment" } + ] + } + } + }, + "included": [{ + "id": "2", + "type": "comment", + "attributes": { + "body": "World", + "root": false + }, + "relationships": { + "children": { + "data": [ + { "id": "4", "type": "comment" } + ] + } + } + }, { + "id": "4", + "type": "comment", + "attributes": { + "body": "Another", + "root": false + }, + "relationships": {} + }, { + "id": "3", + "type": "comment", + "attributes": { + "body": "Foo", + "root": false + }, + "relationships": {} + }] + }, "Primary record was correct"); + }); + + test("normalizeSingleResponse with embedded objects of same type, but from separate attributes", function() { + HomePlanet.reopen({ + reformedVillains: DS.hasMany('superVillain', { inverse: null }) + }); + + env.registry.register('adapter:home-planet', DS.RESTAdapter); + env.registry.register('serializer:home-planet', TestSerializer.extend(DS.EmbeddedRecordsMixin, { + attrs: { + villains: { embedded: 'always' }, + reformedVillains: { embedded: 'always' } + } + })); + env.registry.register('serializer:super-villain', TestSerializer); + + var serializer = env.container.lookup("serializer:home-planet"); + var json_hash = { + homePlanet: { + id: "1", + name: "Earth", + villains: [{ + id: "1", + firstName: "Tom" + }, { + id: "3", + firstName: "Yehuda" + }], + reformedVillains: [{ + id: "2", + firstName: "Alex" + },{ + id: "4", + firstName: "Erik" + }] + } + }; + var json; + run(function() { + json = serializer.normalizeSingleResponse(env.store, HomePlanet, json_hash, '1', 'find'); + }); + + deepEqual(json, { + "data": { + "id": "1", + "type": "home-planet", + "attributes": { + "name": "Earth" + }, + "relationships": { + "villains": { + "data": [ + { "id": "1", "type": "super-villain" }, + { "id": "3", "type": "super-villain" } + ] + }, + "reformedVillains": { + "data": [ + { "id": "2", "type": "super-villain" }, + { "id": "4", "type": "super-villain" } + ] + } + } + }, + "included": [{ + "id": "1", + "type": "super-villain", + "attributes": { + "firstName": "Tom" + }, + "relationships": {} + }, { + "id": "3", + "type": "super-villain", + "attributes": { + "firstName": "Yehuda" + }, + "relationships": {} + }, { + "id": "2", + "type": "super-villain", + "attributes": { + "firstName": "Alex" + }, + "relationships": {} + }, { + "id": "4", + "type": "super-villain", + "attributes": { + "firstName": "Erik" + }, + "relationships": {} + }] + }, "Primary hash was correct"); + }); + + test("normalizeArrayResponse with embedded objects", function() { + env.registry.register('adapter:super-villain', DS.RESTAdapter); + env.registry.register('serializer:home-planet', TestSerializer.extend(DS.EmbeddedRecordsMixin, { + attrs: { + villains: { embedded: 'always' } + } + })); + env.registry.register('serializer:super-villain', TestSerializer); + + var serializer = env.container.lookup("serializer:home-planet"); + + var json_hash = { + homePlanets: [{ + id: "1", + name: "Umber", + villains: [{ + id: "1", + firstName: "Tom", + lastName: "Dale" + }] + }] + }; + var array; + + run(function() { + array = serializer.normalizeArrayResponse(env.store, HomePlanet, json_hash, null, 'findAll'); + }); + + deepEqual(array, { + "data": [{ + "id": "1", + "type": "home-planet", + "attributes": { + "name": "Umber" + }, + "relationships": { + "villains": { + "data": [ + { "id": "1", "type": "super-villain" } + ] + } + } + }], + "included": [{ + "id": "1", + "type": "super-villain", + "attributes": { + "firstName": "Tom", + "lastName": "Dale" + }, + "relationships": {} + }] + }); + }); + + test("normalizeArrayResponse with embedded objects with custom primary key", function() { + expect(1); + env.registry.register('adapter:super-villain', DS.RESTAdapter); + env.registry.register('serializer:super-villain', TestSerializer.extend({ + primaryKey: 'villain_id' + })); + env.registry.register('serializer:home-planet', TestSerializer.extend(DS.EmbeddedRecordsMixin, { + attrs: { + villains: { embedded: 'always' } + } + })); + + var serializer = env.container.lookup("serializer:home-planet"); + + var json_hash = { + homePlanets: [{ + id: "1", + name: "Umber", + villains: [{ + villain_id: "2", + firstName: "Alex", + lastName: "Baizeau" + }] + }] + }; + var array; + + run(function() { + array = serializer.normalizeArrayResponse(env.store, HomePlanet, json_hash, null, 'findAll'); + }); + + deepEqual(array, { + "data": [{ + "id": "1", + "type": "home-planet", + "attributes": { + "name": "Umber" + }, + "relationships": { + "villains": { + "data": [ + { "id": "2", "type": "super-villain" } + ] + } + } + }], + "included": [{ + "id": "2", + "type": "super-villain", + "attributes": { + "firstName": "Alex", + "lastName": "Baizeau" + }, + "relationships": {} + }] + }); + }); + + test("normalizeArrayResponse with embedded objects with identical relationship and attribute key ", function() { + expect(1); + env.registry.register('adapter:super-villain', DS.RESTAdapter); + env.registry.register('serializer:home-planet', TestSerializer.extend(DS.EmbeddedRecordsMixin, { + attrs: { + villains: { embedded: 'always' } + }, + //Makes the keyForRelationship and keyForAttribute collide. + keyForRelationship: function(key, type) { + return this.keyForAttribute(key, type); + } + })); + env.registry.register('serializer:super-villain', TestSerializer); + + var serializer = env.container.lookup("serializer:home-planet"); + + var json_hash = { + homePlanets: [{ + id: "1", + name: "Umber", + villains: [{ + id: "1", + firstName: "Alex", + lastName: "Baizeau" + }] + }] + }; + var array; + + run(function() { + array = serializer.normalizeArrayResponse(env.store, HomePlanet, json_hash, null, 'findAll'); + }); + + deepEqual(array, { + "data": [{ + "id": "1", + "type": "home-planet", + "attributes": { + "name": "Umber" + }, + "relationships": { + "villains": { + "data": [ + { "id": "1", "type": "super-villain" } + ] + } + } + }], + "included": [{ + "id": "1", + "type": "super-villain", + "attributes": { + "firstName": "Alex", + "lastName": "Baizeau" + }, + "relationships": {} + }] + }); + }); + test("normalizeArrayResponse with embedded objects of same type as primary type", function() { + env.registry.register('adapter:comment', DS.RESTAdapter); + env.registry.register('serializer:comment', TestSerializer.extend(DS.EmbeddedRecordsMixin, { + attrs: { + children: { embedded: 'always' } + } + })); + + var serializer = env.container.lookup("serializer:comment"); + + var json_hash = { + comments: [{ + id: "1", + body: "Hello", + root: true, + children: [{ + id: "2", + body: "World", + root: false + }, + { + id: "3", + body: "Foo", + root: false + }] + }] + }; + var array; + + run(function() { + array = serializer.normalizeArrayResponse(env.store, Comment, json_hash, null, 'findAll'); + }); + + deepEqual(array, { + "data": [{ + "id": "1", + "type": "comment", + "attributes": { + "body": "Hello", + "root": true + }, + "relationships": { + "children": { + "data": [ + { "id": "2", "type": "comment" }, + { "id": "3", "type": "comment" } + ] + } + } + }], + "included": [{ + "id": "2", + "type": "comment", + "attributes": { + "body": "World", + "root": false + }, + "relationships": {} + }, { + "id": "3", + "type": "comment", + "attributes": { + "body": "Foo", + "root": false + }, + "relationships": {} + }] + }, "Primary array is correct"); + }); + + test("normalizeArrayResponse with embedded objects of same type, but from separate attributes", function() { + HomePlanet.reopen({ + reformedVillains: DS.hasMany('superVillain') + }); + + env.registry.register('adapter:home-planet', DS.RESTAdapter); + env.registry.register('serializer:home-planet', TestSerializer.extend(DS.EmbeddedRecordsMixin, { + attrs: { + villains: { embedded: 'always' }, + reformedVillains: { embedded: 'always' } + } + })); + env.registry.register('serializer:super-villain', TestSerializer); + + var serializer = env.container.lookup("serializer:home-planet"); + var json_hash = { + homePlanets: [{ + id: "1", + name: "Earth", + villains: [{ + id: "1", + firstName: "Tom" + },{ + id: "3", + firstName: "Yehuda" + }], + reformedVillains: [{ + id: "2", + firstName: "Alex" + },{ + id: "4", + firstName: "Erik" + }] + },{ + id: "2", + name: "Mars", + villains: [{ + id: "1", + firstName: "Tom" + },{ + id: "3", + firstName: "Yehuda" + }], + reformedVillains: [{ + id: "5", + firstName: "Peter" + },{ + id: "6", + firstName: "Trek" + }] + }] + }; + + var json; + run(function() { + json = serializer.normalizeArrayResponse(env.store, HomePlanet, json_hash, null, 'findAll'); + }); + + deepEqual(json, { + "data": [{ + "id": "1", + "type": "home-planet", + "attributes": { + "name": "Earth" + }, + "relationships": { + "reformedVillains": { + "data": [ + { "id": "2", "type": "super-villain" }, + { "id": "4", "type": "super-villain" } + ] + }, + "villains": { + "data": [ + { "id": "1", "type": "super-villain" }, + { "id": "3", "type": "super-villain" } + ] + } + } + }, { + "id": "2", + "type": "home-planet", + "attributes": { + "name": "Mars" + }, + "relationships": { + "reformedVillains": { + "data": [ + { "id": "5", "type": "super-villain" }, + { "id": "6", "type": "super-villain" } + ] + }, + "villains": { + "data": [ + { "id": "1", "type": "super-villain" }, + { "id": "3", "type": "super-villain" } + ] + } + } + }], + "included": [{ + "id": "1", + "type": "super-villain", + "attributes": { + "firstName": "Tom" + }, + "relationships": {} + }, { + "id": "3", + "type": "super-villain", + "attributes": { + "firstName": "Yehuda" + }, + "relationships": {} + }, { + "id": "2", + "type": "super-villain", + "attributes": { + "firstName": "Alex" + }, + "relationships": {} + }, { + "id": "4", + "type": "super-villain", + "attributes": { + "firstName": "Erik" + }, + "relationships": {} + }, { + "id": "1", + "type": "super-villain", + "attributes": { + "firstName": "Tom" + }, + "relationships": {} + }, { + "id": "3", + "type": "super-villain", + "attributes": { + "firstName": "Yehuda" + }, + "relationships": {} + }, { + "id": "5", + "type": "super-villain", + "attributes": { + "firstName": "Peter" + }, + "relationships": {} + }, { + "id": "6", + "type": "super-villain", + "attributes": { + "firstName": "Trek" + }, + "relationships": {} + }] + }, "Primary array was correct"); + }); + + test("normalizeSingleResponse with embedded object (belongsTo relationship)", function() { + env.registry.register('adapter:super-villain', DS.RESTAdapter); + env.registry.register('serializer:super-villain', TestSerializer.extend(DS.EmbeddedRecordsMixin, { + attrs: { + secretLab: { embedded: 'always' } + } + })); + //env.registry.register('serializer:secret-lab', TestSerializer); + + var serializer = env.container.lookup("serializer:super-villain"); + + var json_hash = { + superVillain: { + id: "1", + firstName: "Tom", + lastName: "Dale", + homePlanet: "123", + evilMinions: ["1", "2", "3"], + secretLab: { + minionCapacity: 5000, + vicinity: "California, USA", + id: "101" + }, + secretWeapons: [] + } + }; + var json; + + run(function() { + json = serializer.normalizeSingleResponse(env.store, SuperVillain, json_hash, '1', 'find'); + }); + + deepEqual(json, { + "data": { + "id": "1", + "type": "super-villain", + "attributes": { + "firstName": "Tom", + "lastName": "Dale" + }, + "relationships": { + "evilMinions": { + "data": [ + { "id": "1", "type": "evil-minion" }, + { "id": "2", "type": "evil-minion" }, + { "id": "3", "type": "evil-minion" } + ] + }, + "homePlanet": { + "data": { "id": "123", "type": "home-planet" } + }, + "secretLab": { + "data": { "id": "101", "type": "secret-lab" } + }, + "secretWeapons": { + "data": [] + } + } + }, + "included": [{ + "id": "101", + "type": "secret-lab", + "attributes": { + "minionCapacity": 5000, + "vicinity": "California, USA" + }, + "relationships": {} + }] + }); + }); + + test("normalizeSingleResponse with multiply-nested belongsTo", function() { + env.registry.register('adapter:evil-minion', DS.RESTAdapter); + env.registry.register('serializer:evil-minion', TestSerializer.extend(DS.EmbeddedRecordsMixin, { + attrs: { + superVillain: { embedded: 'always' } + } + })); + env.registry.register('serializer:super-villain', TestSerializer.extend(DS.EmbeddedRecordsMixin, { + attrs: { + homePlanet: { embedded: 'always' } + } + })); + + var serializer = env.container.lookup("serializer:evil-minion"); + var json_hash = { + evilMinion: { + id: "1", + name: "Alex", + superVillain: { + id: "1", + firstName: "Tom", + lastName: "Dale", + evilMinions: ["1"], + homePlanet: { + id: "1", + name: "Umber", + villains: ["1"] + } + } + } + }; + var json; + + run(function() { + json = serializer.normalizeSingleResponse(env.store, EvilMinion, json_hash, '1', 'find'); + }); + + deepEqual(json, { + "data": { + "id": "1", + "type": "evil-minion", + "attributes": { + "name": "Alex" + }, + "relationships": { + "superVillain": { + "data": { "id": "1", "type": "super-villain" } + } + } + }, + "included": [{ + "id": "1", + "type": "super-villain", + "attributes": { + "firstName": "Tom", + "lastName": "Dale" + }, + "relationships": { + "evilMinions": { + "data": [ + { "id": "1", "type": "evil-minion" } + ] + }, + "homePlanet": { + "data": { "id": "1", "type": "home-planet" } + } + } + }, { + "id": "1", + "type": "home-planet", + "attributes": { + "name": "Umber" + }, + "relationships": { + "villains": { + "data": [ + { "id": "1", "type": "super-villain" } + ] + } + } + }] + }, "Primary hash was correct"); + }); + + test("normalizeSingleResponse with polymorphic hasMany", function() { + SuperVillain.reopen({ + secretWeapons: DS.hasMany("secretWeapon", { polymorphic: true }) + }); + + env.registry.register('adapter:super-villain', DS.RESTAdapter); + env.registry.register('serializer:super-villain', TestSerializer.extend(DS.EmbeddedRecordsMixin, { + attrs: { + secretWeapons: { embedded: 'always' } + } + })); + var serializer = env.container.lookup("serializer:super-villain"); + + var json_hash = { + superVillain: { + id: "1", + firstName: "Tom", + lastName: "Dale", + secretWeapons: [ + { + id: "1", + type: "LightSaber", + name: "Tom's LightSaber", + color: "Red" + }, + { + id: "1", + type: "SecretWeapon", + name: "The Death Star" + } + ] + } + }; + var json; + + run(function() { + json = serializer.normalizeSingleResponse(env.store, SuperVillain, json_hash, '1', 'findAll'); + }); + + deepEqual(json, { + "data": { + "id": "1", + "type": "super-villain", + "attributes": { + "firstName": "Tom", + "lastName": "Dale" + }, + "relationships": { + "secretWeapons": { + "data": [ + { "id": "1", "type": "light-saber" }, + { "id": "1", "type": "secret-weapon" } + ] + } + } + }, + "included": [{ + "id": "1", + "type": "light-saber", + "attributes": { + "color": "Red", + "name": "Tom's LightSaber" + }, + "relationships": {} + }, { + "id": "1", + "type": "secret-weapon", + "attributes": { + "name": "The Death Star" + }, + "relationships": {} + }] + }, "Primary hash was correct"); + }); + + test("normalizeSingleResponse with polymorphic belongsTo", function() { + SuperVillain.reopen({ + secretLab: DS.belongsTo("secretLab", { polymorphic: true }) + }); + + env.registry.register('adapter:super-villain', DS.RESTAdapter); + env.registry.register('serializer:super-villain', TestSerializer.extend(DS.EmbeddedRecordsMixin, { + attrs: { + secretLab: { embedded: 'always' } + } + })); + var serializer = env.container.lookup("serializer:super-villain"); + + var json_hash = { + superVillain: { + id: "1", + firstName: "Tom", + lastName: "Dale", + secretLab: { + id: "1", + type: "bat-cave", + infiltrated: true + } + } + }; + + var json; + + run(function() { + json = serializer.normalizeSingleResponse(env.store, SuperVillain, json_hash, '1', 'find'); + }); + + deepEqual(json, { + "data": { + "id": "1", + "type": "super-villain", + "attributes": { + "firstName": "Tom", + "lastName": "Dale" + }, + "relationships": { + "secretLab": { + "data": { "id": "1", "type": "bat-cave" } + } + } + }, + "included": [{ + "id": "1", + "type": "bat-cave", + "attributes": { + "infiltrated": true + }, + "relationships": {} + }] + }, "Primary has was correct"); + }); + + test("normalize with custom belongsTo primary key", function() { + env.registry.register('adapter:evil-minion', DS.RESTAdapter); + env.registry.register('serializer:evil-minion', TestSerializer.extend(DS.EmbeddedRecordsMixin, { + attrs: { + superVillain: { embedded: 'always' } + } + })); + env.registry.register('serializer:super-villain', TestSerializer.extend({ + primaryKey: 'custom' + })); + + var serializer = env.container.lookup("serializer:evil-minion"); + var json_hash = { + evil_minion: { + id: "1", + name: "Alex", + superVillain: { + custom: "1", + firstName: "Tom", + lastName: "Dale" + } + } + }; + var json; + + run(function() { + json = serializer.normalizeResponse(env.store, EvilMinion, json_hash, '1', 'find'); + }); + + deepEqual(json, { + "data": { + "id": "1", + "type": "evil-minion", + "attributes": { + "name": "Alex" + }, + "relationships": { + "superVillain": { + "data": { "id": "1", "type": "super-villain" } + } + } + }, + "included": [{ + "id": "1", + "type": "super-villain", + "attributes": { + "firstName": "Tom", + "lastName": "Dale" + }, + "relationships": {} + }] + }, "Primary hash was correct"); + }); + +}