diff --git a/addon/serializers/drf.js b/addon/serializers/drf.js index 68ed334..a91ddb4 100644 --- a/addon/serializers/drf.js +++ b/addon/serializers/drf.js @@ -10,51 +10,47 @@ import Ember from 'ember'; * @class DRFSerializer * @extends DS.RESTSerializer */ -export default DS.RESTSerializer.extend({ - /** - * Normalizes a part of the JSON payload returned by the server. This - * version simply calls addRelationshipsToLinks() before invoking - * the RESTSerializer's version. - * - * @method normalize - * @param {subclass of DS.Model} typeClass - * @param {Object} hash - * @param {String} prop - * @return {Object} - */ - normalize: function(typeClass, hash, prop) { - this.addRelationshipsToLinks(typeClass, hash); - return this._super(typeClass, hash, prop); - }, +export default DS.JSONSerializer.extend({ + // Remove this in our 2.0 release. + isNewSerializerAPI: true, /** - * Adds relationships to the links hash as expected by the RESTSerializer. + * Returns the resource's relationships formatted as a JSON-API "relationships object". * - * @method addRelationshipsToLinks - * @private - * @param {subclass of DS.Model} typeClass - * @param {Object} hash + * http://jsonapi.org/format/#document-resource-object-relationships + * + * This version adds a 'links'hash with relationship urls before invoking the + * JSONSerializer's version. + * + * @method extractRelationships + * @param {Object} modelClass + * @param {Object} resourceHash + * @return {Object} */ - addRelationshipsToLinks: function(typeClass, hash) { - if (!hash.hasOwnProperty('links')) { - hash['links'] = {}; + extractRelationships: function (modelClass, resourceHash) { + if (!resourceHash.hasOwnProperty('links')) { + resourceHash['links'] = {}; } - typeClass.eachRelationship(function(key, relationship) { + modelClass.eachRelationship(function(key, relationshipMeta) { let payloadRelKey = this.keyForRelationship(key); - if (!hash.hasOwnProperty(payloadRelKey)) { + + if (!resourceHash.hasOwnProperty(payloadRelKey)) { return; } - if (relationship.kind === 'hasMany' || relationship.kind === 'belongsTo') { + + if (relationshipMeta.kind === 'hasMany' || relationshipMeta.kind === 'belongsTo') { // Matches strings starting with: https://, http://, //, / - var payloadRel = hash[payloadRelKey]; + var payloadRel = resourceHash[payloadRelKey]; if (!Ember.isNone(payloadRel) && !Ember.isNone(payloadRel.match) && typeof(payloadRel.match) === 'function' && payloadRel.match(/^((https?:)?\/\/|\/)\w/)) { - hash['links'][key] = hash[payloadRelKey]; - delete hash[payloadRelKey]; + resourceHash['links'][key] = resourceHash[payloadRelKey]; + delete resourceHash[payloadRelKey]; } } }, this); + + return this._super(modelClass, resourceHash); }, /** @@ -77,70 +73,40 @@ export default DS.RESTSerializer.extend({ }, /** - * `extractMeta` is used to deserialize any meta information in the - * adapter payload. By default Ember Data expects meta information to - * be located on the `meta` property of the payload object. + * Normalizes server responses for array or list data using the JSONSerializer's version + * of this function. * - * @method extractMeta - * @param {DS.Store} store - * @param {subclass of DS.Model} type - * @param {Object} payload - */ - extractMeta: function(store, type, payload) { - if (payload && payload.results) { - // Sets the metadata for the type. - store.setMetadataFor(type, { - count: payload.count, - next: this.extractPageNumber(payload.next), - previous: this.extractPageNumber(payload.previous) - }); - - // Keep ember data from trying to parse the metadata as a records - delete payload.count; - delete payload.next; - delete payload.previous; - } - }, - - /** - * `extractSingle` is used to deserialize a single record returned - * from the adapter. + * If the payload has a results property, all properties that aren't in the results + * are added to the 'meta' hash so that Ember Data can use these properties for metadata. + * The next and previous pagination URLs are parsed to make it easier to paginate data + * in applications. * - * @method extractSingle + * @method normalizeArrayResponse * @param {DS.Store} store - * @param {subclass of DS.Model} type + * @param {DS.Model} primaryModelClass * @param {Object} payload - * @param {String or Number} id - * @return {Object} json The deserialized payload + * @param {String|Number} id + * @param {String} requestType + * @return {Object} JSON-API Document */ - extractSingle: function(store, type, payload, id) { - // Convert payload to json format expected by the RESTSerializer. - var convertedPayload = {}; - convertedPayload[type.modelName] = payload; - return this._super(store, type, convertedPayload, id); - }, + normalizeArrayResponse: function(store, primaryModelClass, payload, id, requestType) { + if (!Ember.isNone(payload) && payload.hasOwnProperty('results')) { + // Move DRF metadata to the meta hash. + let modifiedPayload = payload.results; + delete payload.results; + modifiedPayload['meta'] = payload; - /** - * `extractArray` is used to deserialize an array of records - * returned from the adapter. - * - * @method extractArray - * @param {DS.Store} store - * @param {subclass of DS.Model} type - * @param {Object} payload - * @return {Array} array An array of deserialized objects - */ - extractArray: function(store, type, payload) { - // Convert payload to json format expected by the RESTSerializer. - // This function is being overridden instead of normalizePayload() - // because the `results` hash is only in lists of records. - var convertedPayload = {}; - if (payload.results) { - convertedPayload[type.modelName] = payload.results; - } else { - convertedPayload[type.modelName] = payload; + // The next and previous pagination URLs are parsed to make it easier to paginate data in applications. + if (!Ember.isNone(modifiedPayload.meta['next'])) { + modifiedPayload.meta['next'] = this.extractPageNumber(payload.next); + } + if (!Ember.isNone(modifiedPayload.meta['previous'])) { + modifiedPayload.meta['previous'] = this.extractPageNumber(payload.previous); + } + return this._super(store, primaryModelClass, modifiedPayload, id, requestType); } - return this._super(store, type, convertedPayload); + + return this._super(store, primaryModelClass, payload, id, requestType); }, /** diff --git a/docs/hyperlinked-related-fields.md b/docs/hyperlinked-related-fields.md index 4e3ff2b..9c6615a 100644 --- a/docs/hyperlinked-related-fields.md +++ b/docs/hyperlinked-related-fields.md @@ -43,7 +43,7 @@ related comments instead of one URL per related record: } ``` -*Note:* It is also possible to use the [Coalesce Find Requests](coalesce-find-requests.md) +**Note:** It is also possible to use the [Coalesce Find Requests](coalesce-find-requests.md) feature to retrieve related records in a single request, however, this is the preferred solution. diff --git a/docs/non-field-errors.md b/docs/non-field-errors.md index 4465c59..6cee99b 100644 --- a/docs/non-field-errors.md +++ b/docs/non-field-errors.md @@ -32,4 +32,5 @@ In case of several errors, the InvalidError.errors attribute will include { detail: 'error 2', meta { key: 'non_field_errors' } } //or whatever key name you configured ``` - **note** we store the key for non-field errors in a meta object as this is non standard in the error object defined by the jsonapi spec +**Note** We store the key for non-field errors in a meta object as this is non standard in the error +object defined by the JSON API spec. diff --git a/docs/pagination.md b/docs/pagination.md index 180d383..109f105 100644 --- a/docs/pagination.md +++ b/docs/pagination.md @@ -27,7 +27,7 @@ Or you can access the metadata just for this query: var meta = result.get("content.meta"); ``` -**NB** Running a find request against a paginated list view without query params will +**Note** Running a find request against a paginated list view without query params will retrieve the first page with metadata set in only `store.metadataFor`. This is how metadata works in Ember Data. diff --git a/tests/acceptance/crud-failure-test.js b/tests/acceptance/crud-failure-test.js index 4d5ca57..b3840a9 100644 --- a/tests/acceptance/crud-failure-test.js +++ b/tests/acceptance/crud-failure-test.js @@ -15,7 +15,7 @@ module('Acceptance: CRUD Failure', { beforeEach: function() { application = startApp(); - store = application.__container__.lookup('store:main'); + store = application.__container__.lookup('service:store'); server = new Pretender(function() { @@ -174,10 +174,10 @@ test('Update field errors', function(assert) { return store.findRecord('post', 3).then(function(post) { assert.ok(post); - assert.equal(post.get('isDirty'), false); + assert.equal(post.get('hasDirtyAttributes'), false); post.set('postTitle', 'Lorem ipsum dolor sit amet, consectetur adipiscing el'); post.set('body', ''); - assert.equal(post.get('isDirty'), true); + assert.equal(post.get('hasDirtyAttributes'), true); post.save().then({}, function(response) { const postTitleErrors = post.get('errors.postTitle'), diff --git a/tests/acceptance/crud-success-test.js b/tests/acceptance/crud-success-test.js index ff90ed7..8dca2c6 100644 --- a/tests/acceptance/crud-success-test.js +++ b/tests/acceptance/crud-success-test.js @@ -35,7 +35,7 @@ module('Acceptance: CRUD Success', { beforeEach: function() { application = startApp(); - store = application.__container__.lookup('store:main'); + store = application.__container__.lookup('service:store'); server = new Pretender(function() { @@ -152,18 +152,18 @@ test('Update record', function(assert) { return store.findRecord('post', 1).then(function(post) { assert.ok(post); - assert.equal(post.get('isDirty'), false); + assert.equal(post.get('hasDirtyAttributes'), false); return Ember.run(function() { post.set('postTitle', 'new post title'); post.set('body', 'new post body'); - assert.equal(post.get('isDirty'), true); + assert.equal(post.get('hasDirtyAttributes'), true); return post.save().then(function(post) { assert.ok(post); - assert.equal(post.get('isDirty'), false); + assert.equal(post.get('hasDirtyAttributes'), false); assert.equal(post.get('postTitle'), 'new post title'); assert.equal(post.get('body'), 'new post body'); }); diff --git a/tests/acceptance/pagination-test.js b/tests/acceptance/pagination-test.js index 75a46f6..429a861 100644 --- a/tests/acceptance/pagination-test.js +++ b/tests/acceptance/pagination-test.js @@ -53,7 +53,7 @@ module('Acceptance: Pagination', { beforeEach: function() { application = startApp(); - store = application.__container__.lookup('store:main'); + store = application.__container__.lookup('service:store'); // The implementation of the paginated Pretender server is dynamic // so it can be used with all of the pagination tests. Otherwise, @@ -111,9 +111,9 @@ module('Acceptance: Pagination', { }); test('Retrieve list of paginated records', function(assert) { - assert.expect(8); + assert.expect(7); - return store.findAll('post').then(function(response) { + return store.query('post', {page: 1}).then(function(response) { assert.ok(response); assert.equal(response.get('length'), 4); @@ -123,75 +123,53 @@ test('Retrieve list of paginated records', function(assert) { assert.equal(post.get('postTitle'), 'post title 2'); assert.equal(post.get('body'), 'post body 2'); - // Test the type metadata. - var metadata = store.metadataFor('post'); + var metadata = response.get('meta'); assert.equal(metadata.count, 6); assert.equal(metadata.next, 2); assert.equal(metadata.previous, null); - - // No metadata on results when using find without query params. - assert.ok(!response.get('meta')); }); }); test("Type metadata doesn't have previous", function(assert) { - assert.expect(5); + assert.expect(4); - return store.findAll('post').then(function(response) { + return store.query('post', {page: 1}).then(function(response) { assert.ok(response); - // Test the type metadata. - var metadata = store.metadataFor('post'); + var metadata = response.get('meta'); assert.equal(metadata.count, 6); assert.equal(metadata.next, 2); assert.equal(metadata.previous, null); - - // No metadata on results when using findAll. - assert.ok(!response.get('meta')); }); }); test("Type metadata doesn't have next", function(assert) { - assert.expect(8); + assert.expect(5); return store.query('post', {page: 2}).then(function(response) { assert.ok(response); assert.equal(response.get('length'), 2); - // Test the type metadata. - var typeMetadata = store.metadataFor('post'); - assert.equal(typeMetadata.count, 6); - assert.equal(typeMetadata.next, null); - assert.equal(typeMetadata.previous, 1); - - // Test the results metadata. - var resultsMetadata = response.get('meta'); - assert.equal(resultsMetadata.count, 6); - assert.equal(resultsMetadata.next, null); - assert.equal(resultsMetadata.previous, 1); + var metadata = response.get('meta'); + assert.equal(metadata.count, 6); + assert.equal(metadata.next, null); + assert.equal(metadata.previous, 1); }); }); test("Test page_size query param", function(assert) { - assert.expect(8); + assert.expect(5); return store.query('post', {page: 2, page_size: 2}).then(function(response) { assert.ok(response); assert.equal(response.get('length'), 2); - // Test the type metadata. - var typeMetadata = store.metadataFor('post'); - assert.equal(typeMetadata.count, 6); - assert.equal(typeMetadata.previous, 1); - assert.equal(typeMetadata.next, 3); - - // Test the results metadata. - var resultsMetadata = response.get('meta'); - assert.equal(resultsMetadata.count, 6); - assert.equal(resultsMetadata.previous, 1); - assert.equal(resultsMetadata.next, 3); + var metadata = response.get('meta'); + assert.equal(metadata.count, 6); + assert.equal(metadata.previous, 1); + assert.equal(metadata.next, 3); }); }); diff --git a/tests/acceptance/relationship-links-test.js b/tests/acceptance/relationship-links-test.js index 0c56374..871daff 100644 --- a/tests/acceptance/relationship-links-test.js +++ b/tests/acceptance/relationship-links-test.js @@ -48,12 +48,11 @@ var comments = [ } ]; - module('Acceptance: Relationship Links', { beforeEach: function() { application = startApp(); - store = application.__container__.lookup('store:main'); + store = application.__container__.lookup('service:store'); server = new Pretender(function() { this.get('/test-api/posts/:id/', function(request) { @@ -96,7 +95,6 @@ test('belongsTo', function(assert) { }); }); - test('hasMany', function(assert) { assert.expect(9); diff --git a/tests/acceptance/relationships-test.js b/tests/acceptance/relationships-test.js index da037a5..fa9eeda 100644 --- a/tests/acceptance/relationships-test.js +++ b/tests/acceptance/relationships-test.js @@ -52,7 +52,7 @@ module('Acceptance: Relationships', { beforeEach: function() { application = startApp(); - store = application.__container__.lookup('store:main'); + store = application.__container__.lookup('service:store'); server = new Pretender(function() { diff --git a/tests/unit/serializers/drf-test.js b/tests/unit/serializers/drf-test.js index 9d8f2e6..b5ef0e4 100644 --- a/tests/unit/serializers/drf-test.js +++ b/tests/unit/serializers/drf-test.js @@ -5,44 +5,45 @@ import { moduleFor, test } from 'ember-qunit'; // see app/serializers/application.js moduleFor('serializer:application', 'DRFSerializer', {}); -test('extractSingle', function(assert) { - var serializer = this.subject(); - serializer._super = sinon.stub().returns('extracted single'); - var type = {modelName: 'person'}; - - var result = serializer.extractSingle('store', type, 'payload', 'id'); - - assert.ok(serializer._super.calledWith( - 'store', type, {person: 'payload'}, 'id' - ), '_super not called properly'); - assert.equal(result, 'extracted single'); -}); - -test('extractArray - results', function(assert) { - var serializer = this.subject(); - serializer._super = sinon.stub().returns('extracted array'); - var type = {modelName: 'person'}; - var payload = {other: 'stuff', results: ['result']}; - - var result = serializer.extractArray('store', type, payload); +test('normalizeArrayResponse - results', function(assert) { + let serializer = this.subject(); + serializer._super = sinon.spy(); + let primaryModelClass = {modelName: 'person'}; + let payload = { + count: 'count', + next: '/api/posts/?page=3', + previous: '/api/posts/?page=1', + other: 'stuff', + results: ['result'] + }; - assert.ok(serializer._super.calledWith( - 'store', type, {person: ['result']} - ), '_super not called properly'); - assert.equal(result, 'extracted array'); + serializer.normalizeArrayResponse('store', primaryModelClass, payload, 1, 'requestType'); + assert.equal(serializer._super.callCount, 1); + assert.equal(serializer._super.lastCall.args[0],'store'); + assert.propEqual(serializer._super.lastCall.args[1], primaryModelClass); + assert.equal(serializer._super.lastCall.args[3], 1); + assert.equal(serializer._super.lastCall.args[4], 'requestType'); + + let modifiedPayload = serializer._super.lastCall.args[2]; + assert.equal('result', modifiedPayload[0]); + + assert.ok(modifiedPayload.meta); + assert.equal(modifiedPayload.meta['next'], 3); + assert.equal(modifiedPayload.meta['previous'], 1); + // Unknown metadata has been passed along to the meta object. + assert.equal(modifiedPayload.meta['other'], 'stuff'); }); -test('extractArray - no results', function(assert) { - var serializer = this.subject(); +test('normalizeArrayResponse - no results', function(assert) { + let serializer = this.subject(); serializer._super = sinon.stub().returns('extracted array'); - var type = {modelName: 'person'}; - var payload = {other: 'stuff'}; + let primaryModelClass = {modelName: 'person'}; + let payload = {other: 'stuff'}; - var result = serializer.extractArray('store', type, payload); + let result = serializer.normalizeArrayResponse('store', primaryModelClass, payload, 1, 'requestType'); - assert.ok(serializer._super.calledWith( - 'store', type, {person: {other: 'stuff'}} - ), '_super not called properly'); + assert.ok(serializer._super.calledWith('store', primaryModelClass, payload, 1, 'requestType'), + '_super not called properly'); assert.equal(result, 'extracted array'); }); @@ -75,25 +76,6 @@ test('keyForRelationship', function(assert) { assert.equal(result, 'project_managers'); }); -test('extractMeta', function(assert) { - var serializer = this.subject(); - var store = {setMetadataFor: sinon.spy()}; - var payload = { - results: 'mock', - count: 'count', - next: '/api/posts/?page=3', - previous: '/api/posts/?page=1' - }; - - serializer.extractMeta(store, 'type', payload); - - assert.ok(store.setMetadataFor.calledWith('type', {count: 'count', next: 3, previous: 1}), - 'metaForType not called properly'); - assert.ok(!payload.count, 'payload.count not removed'); - assert.ok(!payload.next, 'payload.next not removed'); - assert.ok(!payload.previous, 'payload.previous not removed'); -}); - test('extractPageNumber', function(assert) { var serializer = this.subject(); @@ -116,28 +98,12 @@ test('extractPageNumber', function(assert) { 'extractPageNumber failed on URL with similar query params'); }); -test('normalize', function(assert) { - var serializer = this.subject(); - serializer.addRelationshipsToLinks = sinon.spy(); - var typeClass = { - eachAttribute: sinon.stub(), - eachRelationship: sinon.stub(), - eachTransformedAttribute: sinon.stub() - }; - var payload = {dummy: 'data'}; - - serializer.normalize(typeClass, payload, 'animal'); - - assert.ok(serializer.addRelationshipsToLinks.calledWith(typeClass, payload), - 'addRelationshipsToLinks not called properly'); -}); - -test('addRelationshipsToLinks', function(assert) { - assert.expect(45); +test('extractRelationships', function(assert) { + assert.expect(47); // Generates a payload hash for the specified urls array. This is used to generate // new payloads to test with different the relationships types. - function Payload(urls) { + function ResourceHash(urls) { this.id = 1; var letter = 'a'; var self = this; @@ -149,10 +115,10 @@ test('addRelationshipsToLinks', function(assert) { // Generates a typeClass object for the specified relationship and payload. This is used to generate // new stubbed out typeClasses to test with different the relationships types and payload. - function TypeClass(relationship, payload) { + function TypeClass(relationship, resourceHash) { this.eachRelationship = function(callback, binding) { - for (var key in payload) { - if (payload.hasOwnProperty(key) && key !== 'links' && key !== 'id') { + for (var key in resourceHash) { + if (resourceHash.hasOwnProperty(key) && key !== 'links' && key !== 'id') { callback.call(binding, key, {kind: relationship}); } } @@ -184,38 +150,42 @@ test('addRelationshipsToLinks', function(assert) { var wrongSizeLinksMessage = 'The links hash for the %@ relationship is not the correct size.'; var serializer = this.subject(); + // Add a spy to _super because we only want to test our code. + serializer._super = sinon.spy(); // Test with hasMany and belongsTo relationships. var validRelationships = ['hasMany', 'belongsTo']; validRelationships.forEach(function(relationship) { - var payload = new Payload(testURLs); - serializer.addRelationshipsToLinks(new TypeClass(relationship, payload), payload); + var resourceHash = new ResourceHash(testURLs); + serializer.extractRelationships(new TypeClass(relationship, resourceHash), resourceHash); - assert.equal(Object.keys(payload).length, 9, Ember.String.fmt(wrongSizePayloadMessage, [relationship])); + assert.equal(Object.keys(resourceHash).length, 9, Ember.String.fmt(wrongSizePayloadMessage, [relationship])); // 'j' & 'k' need to be handled separately because they are false values. var expectedPayloadKeys = ['id', 'links', 'e', 'f', 'g', 'h', 'i']; expectedPayloadKeys.forEach(function(key) { - assert.ok(payload[key], Ember.String.fmt(missingKeyMessage, [relationship, key])); + assert.ok(resourceHash[key], Ember.String.fmt(missingKeyMessage, [relationship, key])); }); - assert.equal(payload['j'], '', Ember.String.fmt(missingKeyMessage, [relationship, 'j'])); - assert.equal(payload['k'], null, Ember.String.fmt(missingKeyMessage, [relationship], 'k')); + assert.equal(resourceHash['j'], '', Ember.String.fmt(missingKeyMessage, [relationship, 'j'])); + assert.equal(resourceHash['k'], null, Ember.String.fmt(missingKeyMessage, [relationship], 'k')); - assert.equal(Object.keys(payload.links).length, 4, Ember.String.fmt(wrongSizeLinksMessage, [relationship])); + assert.equal(Object.keys(resourceHash.links).length, 4, Ember.String.fmt(wrongSizeLinksMessage, [relationship])); var i = 0; var expectedLinksKeys = ['a', 'b', 'c', 'd']; expectedLinksKeys.forEach(function(key) { - assert.equal(payload.links[key], testURLs[i], + assert.equal(resourceHash.links[key], testURLs[i], Ember.String.fmt('Links value of property %@ in the %@ relationship is not correct.', [key, relationship])); i++; }); }); + assert.equal(serializer._super.callCount, 2, '_super() was not called once for each relationship.'); + // Test with an unknown relationship. var relationship = 'xxUnknownXX'; - var payload = new Payload(testURLs); - serializer.addRelationshipsToLinks(new TypeClass(relationship, payload), payload); + var payload = new ResourceHash(testURLs); + serializer.extractRelationships(new TypeClass(relationship, payload), payload); assert.equal(Object.keys(payload).length, 13, Ember.String.fmt(wrongSizePayloadMessage, [relationship])); @@ -228,4 +198,6 @@ test('addRelationshipsToLinks', function(assert) { assert.equal(payload['k'], null, Ember.String.fmt(missingKeyMessage, [relationship], 'k')); assert.equal(Object.keys(payload.links).length, 0, Ember.String.fmt(wrongSizeLinksMessage, [relationship])); + + assert.equal(serializer._super.callCount, 3, Ember.String.fmt('_super() was not called for the %@ relationship.', [relationship])); });