diff --git a/packages/ember-data/lib/adapters.js b/packages/ember-data/lib/adapters.js index 3fa577e3e66..4ad9bb43a6a 100644 --- a/packages/ember-data/lib/adapters.js +++ b/packages/ember-data/lib/adapters.js @@ -3,9 +3,11 @@ */ import FixtureAdapter from "ember-data/adapters/fixture-adapter"; +import JSONAPIAdapter from "ember-data/adapters/json-api-adapter"; import RESTAdapter from "ember-data/adapters/rest-adapter"; export { - RESTAdapter, - FixtureAdapter + FixtureAdapter, + JSONAPIAdapter, + RESTAdapter }; diff --git a/packages/ember-data/lib/adapters/json-api-adapter.js b/packages/ember-data/lib/adapters/json-api-adapter.js new file mode 100644 index 00000000000..2a3af835455 --- /dev/null +++ b/packages/ember-data/lib/adapters/json-api-adapter.js @@ -0,0 +1,85 @@ +/** + @module ember-data +*/ + +import RESTAdapter from "ember-data/adapters/rest-adapter"; + +/** + @class JSONAPIAdapter + @constructor + @namespace DS + @extends DS.RESTAdapter +*/ +export default RESTAdapter.extend({ + defaultSerializer: '-json-api', + + /** + @method ajaxOptions + @private + @param {String} url + @param {String} type The request type GET, POST, PUT, DELETE etc. + @param {Object} options + @return {Object} + */ + ajaxOptions: function(url, type, options) { + let hash = this._super(...arguments); + + if (hash.contentType) { + hash.contentType = 'application/vnd.api+json'; + } + + let beforeSend = hash.beforeSend; + hash.beforeSend = function(xhr) { + xhr.setRequestHeader('Accept', 'application/vnd.api+json'); + if (beforeSend) { + beforeSend(xhr); + } + }; + + return hash; + }, + + /** + @method findMany + @param {DS.Store} store + @param {DS.Model} type + @param {Array} ids + @param {Array} snapshots + @return {Promise} promise + */ + findMany: function(store, type, ids, snapshots) { + var url = this.buildURL(type.modelName, ids, snapshots, 'findMany'); + return this.ajax(url, 'GET', { data: { filter: { id: ids.join(',') } } }); + }, + + /** + @method pathForType + @param {String} modelName + @return {String} path + **/ + pathForType: function(modelName) { + var dasherized = Ember.String.dasherize(modelName); + return Ember.String.pluralize(dasherized); + }, + + /** + TODO: Remove this once we have a better way to override HTTP verbs. + + @method updateRecord + @param {DS.Store} store + @param {DS.Model} type + @param {DS.Snapshot} snapshot + @return {Promise} promise + */ + updateRecord: function(store, type, snapshot) { + var data = {}; + var serializer = store.serializerFor(type.modelName); + + serializer.serializeIntoHash(data, type, snapshot, { includeId: true }); + + var id = snapshot.id; + var url = this.buildURL(type.modelName, id, snapshot, 'updateRecord'); + + return this.ajax(url, 'PATCH', { data: data }); + } +}); diff --git a/packages/ember-data/lib/initializers/store.js b/packages/ember-data/lib/initializers/store.js index 654af83bb34..e4190513c8e 100644 --- a/packages/ember-data/lib/initializers/store.js +++ b/packages/ember-data/lib/initializers/store.js @@ -1,5 +1,5 @@ -import {JSONSerializer, RESTSerializer} from "ember-data/serializers"; -import {RESTAdapter} from "ember-data/adapters"; +import { JSONAPISerializer, JSONSerializer, RESTSerializer } from "ember-data/serializers"; +import { JSONAPIAdapter, RESTAdapter } from "ember-data/adapters"; import ContainerProxy from "ember-data/system/container-proxy"; /** @@ -37,4 +37,9 @@ export default function initializeStore(registry, application) { registry.register('serializer:-default', JSONSerializer); registry.register('serializer:-rest', RESTSerializer); registry.register('adapter:-rest', RESTAdapter); + + if (Ember.FEATURES.isEnabled('ds-new-serializer-api')) { + registry.register('adapter:-json-api', JSONAPIAdapter); + registry.register('serializer:-json-api', JSONAPISerializer); + } } diff --git a/packages/ember-data/lib/main.js b/packages/ember-data/lib/main.js index 09a6a1ae872..05198d343af 100644 --- a/packages/ember-data/lib/main.js +++ b/packages/ember-data/lib/main.js @@ -51,12 +51,16 @@ import { import ManyArray from "ember-data/system/many-array"; import RecordArrayManager from "ember-data/system/record-array-manager"; import { - RESTAdapter, - FixtureAdapter + FixtureAdapter, + JSONAPIAdapter, + RESTAdapter } from "ember-data/adapters"; import BuildURLMixin from "ember-data/adapters/build-url-mixin"; -import JSONSerializer from "ember-data/serializers/json-serializer"; -import RESTSerializer from "ember-data/serializers/rest-serializer"; +import { + JSONAPISerializer, + JSONSerializer, + RESTSerializer +} from "ember-data/serializers"; import "ember-inflector"; import EmbeddedRecordsMixin from "ember-data/serializers/embedded-records-mixin"; import { @@ -114,6 +118,11 @@ DS.BuildURLMixin = BuildURLMixin; DS.RESTSerializer = RESTSerializer; DS.JSONSerializer = JSONSerializer; +if (Ember.FEATURES.isEnabled('ds-new-serializer-api')) { + DS.JSONAPIAdapter = JSONAPIAdapter; + DS.JSONAPISerializer = JSONAPISerializer; +} + DS.Transform = Transform; DS.DateTransform = DateTransform; DS.StringTransform = StringTransform; diff --git a/packages/ember-data/lib/serializers.js b/packages/ember-data/lib/serializers.js index 5d163be2984..f423d5300e8 100644 --- a/packages/ember-data/lib/serializers.js +++ b/packages/ember-data/lib/serializers.js @@ -1,7 +1,13 @@ +/** + @module ember-data +*/ + +import JSONAPISerializer from "ember-data/serializers/json-api-serializer"; import JSONSerializer from "ember-data/serializers/json-serializer"; import RESTSerializer from "ember-data/serializers/rest-serializer"; export { + JSONAPISerializer, JSONSerializer, RESTSerializer }; diff --git a/packages/ember-data/lib/serializers/json-api-serializer.js b/packages/ember-data/lib/serializers/json-api-serializer.js new file mode 100644 index 00000000000..53b587cb184 --- /dev/null +++ b/packages/ember-data/lib/serializers/json-api-serializer.js @@ -0,0 +1,288 @@ +/** + @module ember-data +*/ + +import JSONSerializer from 'ember-data/serializers/json-serializer'; +import normalizeModelName from 'ember-data/system/normalize-model-name'; +import { pluralize, singularize } from 'ember-inflector/lib/system/string'; + +var dasherize = Ember.String.dasherize; + +/** + @class JSONAPISerializer + @namespace DS + @extends DS.JSONSerializer +*/ +export default JSONSerializer.extend({ + + /** + @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 normalizeHelper = (resourceHash) => { + let modelName = this.modelNameFromPayloadKey(resourceHash.type); + let modelClass = this.store.modelFor(modelName); + let serializer = this.store.serializerFor(modelName); + let { data } = serializer.normalize(modelClass, resourceHash); + return data; + }; + + if (Ember.typeOf(payload.data) === 'object') { + payload.data = normalizeHelper(payload.data); + } else { + payload.data = payload.data.map(normalizeHelper); + } + + if (Ember.typeOf(payload.included) === 'array') { + payload.included = payload.included.map(normalizeHelper); + } + + return payload; + }, + + /* + @method extractAttributes + @param {DS.Model} modelClass + @param {Object} resourceHash + @return {Object} + */ + extractAttributes: function(modelClass, resourceHash) { + var attributes = {}; + + if (resourceHash.attributes) { + modelClass.eachAttribute((key) => { + let attributeKey = this.keyForAttribute(key, 'deserialize'); + if (resourceHash.attributes.hasOwnProperty(attributeKey)) { + attributes[key] = resourceHash.attributes[attributeKey]; + } + }); + } + + return attributes; + }, + + /* + @method extractRelationship + @param {Object} relationshipHash + @return {Object} + */ + extractRelationship: function(relationshipHash) { + + let normalizeRelationshipDataHelper = (dataHash) => { + let type = this.modelNameFromPayloadKey(dataHash.type); + dataHash.type = type; + return dataHash; + }; + + if (Ember.typeOf(relationshipHash.data) === 'object') { + relationshipHash.data = normalizeRelationshipDataHelper(relationshipHash.data); + } + + if (Ember.typeOf(relationshipHash.data) === 'array') { + relationshipHash.data = relationshipHash.data.map(normalizeRelationshipDataHelper); + } + + return relationshipHash; + }, + + /* + @method extractRelationships + @param {Object} modelClass + @param {Object} resourceHash + @return {Object} + */ + extractRelationships: function(modelClass, resourceHash) { + let relationships = {}; + + if (resourceHash.relationships) { + modelClass.eachRelationship((key, relationshipMeta) => { + let relationshipKey = this.keyForRelationship(key, relationshipMeta.kind, 'deserialize'); + if (resourceHash.relationships.hasOwnProperty(relationshipKey)) { + + let relationshipHash = resourceHash.relationships[relationshipKey]; + relationships[key] = this.extractRelationship(relationshipHash); + + } + }); + } + + return relationships; + }, + + /* + Returns the resource's ID. + + @method extractType + @param {DS.Model} modelClass + @param {Object} resourceHash + @return {String} + */ + extractType: function(modelClass, resourceHash) { + return singularize(resourceHash.type); + }, + + /** + @method modelNameFromPayloadKey + @param {String} key + @return {String} the model's modelName + */ + modelNameFromPayloadKey: function(key) { + return singularize(normalizeModelName(key)); + }, + + /** + @method payloadKeyFromModelName + @param {String} modelName + @return {String} + */ + payloadKeyFromModelName: function(modelName) { + return pluralize(modelName); + }, + + /* + @method normalize + @param {DS.Model} modelClass + @param {Object} resourceHash + @return {String} + */ + normalize: function(modelClass, resourceHash) { + let data = null; + + if (resourceHash) { + data = { + id: this.extractId(resourceHash), + type: this.extractType(modelClass, resourceHash), + attributes: this.extractAttributes(modelClass, resourceHash), + relationships: this.extractRelationships(modelClass, resourceHash) + }; + + this.applyTransforms(modelClass, data.attributes); + } + + return { data }; + }, + + /** + @method keyForAttribute + @param {String} key + @param {String} method + @return {String} normalized key + */ + keyForAttribute: function(key, method) { + return dasherize(key); + }, + + /** + @method serialize + @param {DS.Snapshot} snapshot + @param {Object} options + @return {Object} json + */ + serialize: function(snapshot, options) { + let data = this._super(...arguments); + data.type = pluralize(snapshot.modelName); + return { data }; + }, + + /** + @method serializeAttribute + @param {DS.Snapshot} snapshot + @param {Object} json + @param {String} key + @param {Object} attribute + */ + serializeAttribute: function(snapshot, json, key, attribute) { + var type = attribute.type; + + if (this._canSerialize(key)) { + json.attributes = json.attributes || {}; + + var value = snapshot.attr(key); + if (type) { + var transform = this.transformFor(type); + value = transform.serialize(value); + } + + var payloadKey = this._getMappedKey(key); + if (payloadKey === key) { + payloadKey = this.keyForAttribute(key, 'serialize'); + } + + json.attributes[payloadKey] = value; + } + }, + + /** + @method serializeBelongsTo + @param {DS.Snapshot} snapshot + @param {Object} json + @param {Object} relationship + */ + serializeBelongsTo: function(snapshot, json, relationship) { + var key = relationship.key; + + if (this._canSerialize(key)) { + var belongsTo = snapshot.belongsTo(key); + if (belongsTo !== undefined) { + + json.relationships = json.relationships || {}; + + var payloadKey = this._getMappedKey(key); + if (payloadKey === key) { + payloadKey = this.keyForRelationship(key, 'belongsTo', 'serialize'); + } + + let data = null; + if (belongsTo) { + data = { + type: this.payloadKeyFromModelName(belongsTo.modelName), + id: belongsTo.id + }; + } + + json.relationships[payloadKey] = { data }; + } + } + }, + + /** + @method serializeHasMany + @param {DS.Snapshot} snapshot + @param {Object} json + @param {Object} relationship + */ + serializeHasMany: function(snapshot, json, relationship) { + var key = relationship.key; + + if (this._shouldSerializeHasMany(snapshot, key, relationship)) { + var hasMany = snapshot.hasMany(key); + if (hasMany !== undefined) { + + json.relationships = json.relationships || {}; + + var payloadKey = this._getMappedKey(key); + if (payloadKey === key && this.keyForRelationship) { + payloadKey = this.keyForRelationship(key, 'hasMany', 'serialize'); + } + + let data = hasMany.map((item) => { + return { + type: item.modelName, + id: item.id + }; + }); + + json.relationships[payloadKey] = { data }; + } + } + } +}); diff --git a/packages/ember-data/tests/integration/adapter/json-api-adapter-test.js b/packages/ember-data/tests/integration/adapter/json-api-adapter-test.js new file mode 100644 index 00000000000..12554d73af9 --- /dev/null +++ b/packages/ember-data/tests/integration/adapter/json-api-adapter-test.js @@ -0,0 +1,804 @@ +var env, store, adapter; +var passedUrl, passedVerb, passedHash; + +var run = Ember.run; + +var User, Post, Comment, Handle, GithubHandle, TwitterHandle, Company, DevelopmentShop, DesignStudio; + +module('integration/adapter/json-api-adapter - JSONAPIAdapter', { + setup: function() { + User = DS.Model.extend({ + firstName: DS.attr('string'), + lastName: DS.attr('string'), + posts: DS.hasMany('post', { async: true }), + handles: DS.hasMany('handle', { async: true, polymorphic: true }), + company: DS.belongsTo('company', { async: true, polymorphic: true }) + }); + + Post = DS.Model.extend({ + title: DS.attr('string'), + author: DS.belongsTo('user', { async: true }), + comments: DS.hasMany('comment', { async: true }) + }); + + Comment = DS.Model.extend({ + text: DS.attr('string'), + post: DS.belongsTo('post', { async: true }) + }); + + Handle = DS.Model.extend({ + user: DS.belongsTo('user', { async: true }) + }); + + GithubHandle = Handle.extend({ + username: DS.attr('string') + }); + + TwitterHandle = Handle.extend({ + nickname: DS.attr('string') + }); + + Company = DS.Model.extend({ + name: DS.attr('string'), + employees: DS.hasMany('user', { async: true }) + }); + + DevelopmentShop = Company.extend({ + coffee: DS.attr('boolean') + }); + + DesignStudio = Company.extend({ + hipsters: DS.attr('number') + }); + + env = setupStore({ + adapter: DS.JSONAPIAdapter, + + 'user': User, + 'post': Post, + 'comment': Comment, + 'handle': Handle, + 'github-handle': GithubHandle, + 'twitter-handle': TwitterHandle, + 'company': Company, + 'development-shop': DevelopmentShop, + 'design-studio': DesignStudio + }); + + store = env.store; + adapter = env.adapter; + }, + + teardown: function() { + run(env.store, 'destroy'); + } +}); + +function ajaxResponse(responses) { + var counter = 0; + var index; + + passedUrl = []; + passedVerb = []; + passedHash = []; + + adapter.ajax = function(url, verb, hash) { + index = counter++; + + passedUrl[index] = url; + passedVerb[index] = verb; + passedHash[index] = hash; + + return run(Ember.RSVP, 'resolve', responses[index]); + }; +} + +if (Ember.FEATURES.isEnabled('ds-new-serializer-api')) { + + test('find a single record', function() { + expect(3); + + ajaxResponse([{ + data: { + type: 'post', + id: '1', + attributes: { + title: 'Ember.js rocks' + } + } + }]); + + run(function() { + store.find('post', 1).then(function(post) { + equal(passedUrl[0], '/posts/1'); + + equal(post.get('id'), '1'); + equal(post.get('title'), 'Ember.js rocks'); + }); + }); + }); + + test('find all records with sideloaded relationships', function() { + expect(9); + + ajaxResponse([{ + data: [{ + type: 'posts', + id: '1', + attributes: { + title: 'Ember.js rocks' + }, + relationships: { + author: { + data: { type: 'users', id: '3' } + } + } + }, { + type: 'posts', + id: '2', + attributes: { + title: 'Tomster rules' + }, + relationships: { + author: { + data: { type: 'users', id: '3' } + }, + comments: { + data: [ + { type: 'comments', id: '4' }, + { type: 'comments', id: '5' } + ] + } + } + }], + included: [{ + type: 'users', + id: '3', + attributes: { + 'first-name': 'Yehuda', + 'last-name': 'Katz' + } + }, { + type: 'comments', + id: '4', + attributes: { + text: 'This is the first comment' + } + }, { + type: 'comments', + id: '5', + attributes: { + text: 'This is the second comment' + } + }] + }]); + + run(function() { + store.findAll('post').then(function(posts) { + equal(passedUrl[0], '/posts'); + + equal(posts.get('length'), '2'); + equal(posts.get('firstObject.title'), 'Ember.js rocks'); + equal(posts.get('lastObject.title'), 'Tomster rules'); + + equal(posts.get('firstObject.author.firstName'), 'Yehuda'); + equal(posts.get('lastObject.author.lastName'), 'Katz'); + + equal(posts.get('firstObject.comments.length'), 0); + + equal(posts.get('lastObject.comments.firstObject.text'), 'This is the first comment'); + equal(posts.get('lastObject.comments.lastObject.text'), 'This is the second comment'); + }); + }); + }); + + test('find many records', function() { + expect(4); + + ajaxResponse([{ + data: [{ + type: 'posts', + id: '1', + attributes: { + title: 'Ember.js rocks' + } + }] + }]); + + run(function() { + store.find('post', { filter: { id: 1 } }).then(function(posts) { + equal(passedUrl[0], '/posts'); + deepEqual(passedHash[0], { data: { filter: { id: 1 } } }); + + equal(posts.get('length'), '1'); + equal(posts.get('firstObject.title'), 'Ember.js rocks'); + }); + }); + }); + + test('find a single record with belongsTo link as object { related }', function() { + expect(7); + + ajaxResponse([{ + data: { + type: 'posts', + id: '1', + attributes: { + title: 'Ember.js rocks' + }, + relationships: { + author: { + links: { + related: 'http://example.com/user/2' + } + } + } + } + }, { + data: { + type: 'users', + id: '2', + attributes: { + 'first-name': 'Yehuda', + 'last-name': 'Katz' + } + } + }]); + + run(function() { + store.find('post', 1).then(function(post) { + equal(passedUrl[0], '/posts/1'); + + equal(post.get('id'), '1'); + equal(post.get('title'), 'Ember.js rocks'); + + post.get('author').then(function(author) { + equal(passedUrl[1], 'http://example.com/user/2'); + + equal(author.get('id'), '2'); + equal(author.get('firstName'), 'Yehuda'); + equal(author.get('lastName'), 'Katz'); + }); + }); + }); + }); + + test('find a single record with belongsTo link as object { data }', function() { + expect(7); + + ajaxResponse([{ + data: { + type: 'posts', + id: '1', + attributes: { + title: 'Ember.js rocks' + }, + relationships: { + author: { + data: { type: 'users', id: '2' } + } + } + } + }, { + data: { + type: 'users', + id: '2', + attributes: { + 'first-name': 'Yehuda', + 'last-name': 'Katz' + } + } + }]); + + run(function() { + store.find('post', 1).then(function(post) { + equal(passedUrl[0], '/posts/1'); + + equal(post.get('id'), '1'); + equal(post.get('title'), 'Ember.js rocks'); + + post.get('author').then(function(author) { + equal(passedUrl[1], '/users/2'); + + equal(author.get('id'), '2'); + equal(author.get('firstName'), 'Yehuda'); + equal(author.get('lastName'), 'Katz'); + }); + }); + }); + }); + + test('find a single record with belongsTo link as object { data } (polymorphic)', function() { + expect(8); + + ajaxResponse([{ + data: { + type: 'users', + id: '1', + attributes: { + 'first-name': 'Yehuda', + 'last-name': 'Katz' + }, + relationships: { + company: { + data: { type: 'development-shops', id: '2' } + } + } + } + }, { + data: { + type: 'development-shop', + id: '2', + attributes: { + name: 'Tilde', + coffee: true + } + } + }]); + + run(function() { + store.find('user', 1).then(function(user) { + equal(passedUrl[0], '/users/1'); + + equal(user.get('id'), '1'); + equal(user.get('firstName'), 'Yehuda'); + equal(user.get('lastName'), 'Katz'); + + user.get('company').then(function(company) { + equal(passedUrl[1], '/development-shops/2'); + + equal(company.get('id'), '2'); + equal(company.get('name'), 'Tilde'); + equal(company.get('coffee'), true); + }); + }); + }); + }); + + test('find a single record with sideloaded belongsTo link as object { data }', function() { + expect(7); + + ajaxResponse([{ + data: { + type: 'post', + id: '1', + attributes: { + title: 'Ember.js rocks' + }, + relationships: { + author: { + data: { type: 'user', id: '2' } + } + } + }, + included: [{ + type: 'user', + id: '2', + attributes: { + 'first-name': 'Yehuda', + 'last-name': 'Katz' + } + }] + }]); + + run(function() { + + store.find('post', 1).then(function(post) { + equal(passedUrl[0], '/posts/1'); + + equal(post.get('id'), '1'); + equal(post.get('title'), 'Ember.js rocks'); + + post.get('author').then(function(author) { + equal(passedUrl.length, 1); + + equal(author.get('id'), '2'); + equal(author.get('firstName'), 'Yehuda'); + equal(author.get('lastName'), 'Katz'); + }); + }); + }); + }); + + test('find a single record with hasMany link as object { related }', function() { + expect(7); + + ajaxResponse([{ + data: { + type: 'post', + id: '1', + attributes: { + title: 'Ember.js rocks' + }, + relationships: { + comments: { + links: { + related: 'http://example.com/post/1/comments' + } + } + } + } + }, { + data: [{ + type: 'comment', + id: '2', + attributes: { + text: 'This is the first comment' + } + }, { + type: 'comment', + id: '3', + attributes: { + text: 'This is the second comment' + } + }] + }]); + + run(function() { + store.find('post', 1).then(function(post) { + equal(passedUrl[0], '/posts/1'); + + equal(post.get('id'), '1'); + equal(post.get('title'), 'Ember.js rocks'); + + post.get('comments').then(function(comments) { + equal(passedUrl[1], 'http://example.com/post/1/comments'); + + equal(comments.get('length'), 2); + equal(comments.get('firstObject.text'), 'This is the first comment'); + equal(comments.get('lastObject.text'), 'This is the second comment'); + }); + }); + }); + }); + + test('find a single record with hasMany link as object { data }', function() { + expect(8); + + ajaxResponse([{ + data: { + type: 'post', + id: '1', + attributes: { + title: 'Ember.js rocks' + }, + relationships: { + comments: { + data: [ + { type: 'comment', id: '2' }, + { type: 'comment', id: '3' } + ] + } + } + } + }, { + data: { + type: 'comment', + id: '2', + attributes: { + text: 'This is the first comment' + } + } + }, { + data: { + type: 'comment', + id: '3', + attributes: { + text: 'This is the second comment' + } + } + }]); + + run(function() { + store.find('post', 1).then(function(post) { + equal(passedUrl[0], '/posts/1'); + + equal(post.get('id'), '1'); + equal(post.get('title'), 'Ember.js rocks'); + + post.get('comments').then(function(comments) { + equal(passedUrl[1], '/comments/2'); + equal(passedUrl[2], '/comments/3'); + + equal(comments.get('length'), 2); + equal(comments.get('firstObject.text'), 'This is the first comment'); + equal(comments.get('lastObject.text'), 'This is the second comment'); + }); + }); + }); + }); + + test('find a single record with hasMany link as object { data } (polymorphic)', function() { + expect(9); + + ajaxResponse([{ + data: { + type: 'user', + id: '1', + attributes: { + 'first-name': 'Yehuda', + 'last-name': 'Katz' + }, + relationships: { + handles: { + data: [ + { type: 'github-handle', id: '2' }, + { type: 'twitter-handle', id: '3' } + ] + } + } + } + }, { + data: { + type: 'github-handle', + id: '2', + attributes: { + username: 'wycats' + } + } + }, { + data: { + type: 'twitter-handle', + id: '3', + attributes: { + nickname: '@wycats' + } + } + }]); + + run(function() { + store.find('user', 1).then(function(user) { + equal(passedUrl[0], '/users/1'); + + equal(user.get('id'), '1'); + equal(user.get('firstName'), 'Yehuda'); + equal(user.get('lastName'), 'Katz'); + + user.get('handles').then(function(handles) { + equal(passedUrl[1], '/github-handles/2'); + equal(passedUrl[2], '/twitter-handles/3'); + + equal(handles.get('length'), 2); + equal(handles.get('firstObject.username'), 'wycats'); + equal(handles.get('lastObject.nickname'), '@wycats'); + }); + }); + }); + }); + + test('find a single record with sideloaded hasMany link as object { data }', function() { + expect(7); + + ajaxResponse([{ + data: { + type: 'post', + id: '1', + attributes: { + title: 'Ember.js rocks' + }, + relationships: { + comments: { + data: [ + { type: 'comment', id: '2' }, + { type: 'comment', id: '3' } + ] + } + } + }, + included: [{ + type: 'comment', + id: '2', + attributes: { + text: 'This is the first comment' + } + }, { + type: 'comment', + id: '3', + attributes: { + text: 'This is the second comment' + } + }] + }]); + + run(function() { + store.find('post', 1).then(function(post) { + equal(passedUrl[0], '/posts/1'); + + equal(post.get('id'), '1'); + equal(post.get('title'), 'Ember.js rocks'); + + post.get('comments').then(function(comments) { + equal(passedUrl.length, 1); + + equal(comments.get('length'), 2); + equal(comments.get('firstObject.text'), 'This is the first comment'); + equal(comments.get('lastObject.text'), 'This is the second comment'); + }); + }); + }); + }); + + test('find a single record with sideloaded hasMany link as object { data } (polymorphic)', function() { + expect(8); + + ajaxResponse([{ + data: { + type: 'user', + id: '1', + attributes: { + 'first-name': 'Yehuda', + 'last-name': 'Katz' + }, + relationships: { + handles: { + data: [ + { type: 'github-handle', id: '2' }, + { type: 'twitter-handle', id: '3' } + ] + } + } + }, + included: [{ + type: 'github-handle', + id: '2', + attributes: { + username: 'wycats' + } + }, { + type: 'twitter-handle', + id: '3', + attributes: { + nickname: '@wycats' + } + }] + }]); + + run(function() { + store.find('user', 1).then(function(user) { + equal(passedUrl[0], '/users/1'); + + equal(user.get('id'), '1'); + equal(user.get('firstName'), 'Yehuda'); + equal(user.get('lastName'), 'Katz'); + + user.get('handles').then(function(handles) { + equal(passedUrl.length, 1); + + equal(handles.get('length'), 2); + equal(handles.get('firstObject.username'), 'wycats'); + equal(handles.get('lastObject.nickname'), '@wycats'); + }); + }); + }); + }); + + test('create record', function() { + expect(3); + + ajaxResponse([{ + data: { + type: 'users', + id: '3' + } + }]); + + run(function() { + + var company = store.push({ data: { + type: 'company', + id: '1', + attributes: { + name: 'Tilde Inc.' + } + } }); + + var githubHandle = store.push({ data: { + type: 'github-handle', + id: '2', + attributes: { + username: 'wycats' + } + } }); + + var user = store.createRecord('user', { + firstName: 'Yehuda', + lastName: 'Katz', + company: company + }); + + user.get('handles').then(function(handles) { + handles.addObject(githubHandle); + + user.save().then(function() { + equal(passedUrl[0], '/users'); + equal(passedVerb[0], 'POST'); + deepEqual(passedHash[0], { + data: { + data : { + type: 'users', + attributes: { + 'first-name': 'Yehuda', + 'last-name': 'Katz' + }, + relationships: { + company: { + data: { type: 'companies', id: '1' } + } + } + } + } + }); + }); + }); + }); + }); + + test('update record', function() { + expect(3); + + ajaxResponse([{ + data: { + type: 'users', + id: '1' + } + }]); + + run(function() { + var user = store.push({ data: { + type: 'user', + id: '1', + attributes: { + firstName: 'Yehuda', + lastName: 'Katz' + } + } }); + + var company = store.push({ data: { + type: 'company', + id: '2', + attributes: { + name: 'Tilde Inc.' + } + } }); + + var githubHandle = store.push({ data: { + type: 'github-handle', + id: '3', + attributes: { + username: 'wycats' + } + } }); + + user.set('firstName', 'Yehuda!'); + user.set('company', company); + + user.get('handles').then(function(handles) { + handles.addObject(githubHandle); + + user.save().then(function() { + equal(passedUrl[0], '/users/1'); + equal(passedVerb[0], 'PATCH'); + deepEqual(passedHash[0], { + data: { + data : { + type: 'users', + id: '1', + attributes: { + 'first-name': 'Yehuda!', + 'last-name': 'Katz' + }, + relationships: { + company: { + data: { type: 'companies', id: '2' } + } + } + } + } + }); + }); + + }); + }); + }); + +} diff --git a/packages/ember-data/tests/integration/serializers/json-api-serializer-test.js b/packages/ember-data/tests/integration/serializers/json-api-serializer-test.js new file mode 100644 index 00000000000..d31ce1e8d64 --- /dev/null +++ b/packages/ember-data/tests/integration/serializers/json-api-serializer-test.js @@ -0,0 +1,17 @@ +var env; +var run = Ember.run; + +module('integration/serializers/json-api-serializer - JSONAPISerializer', { + setup: function() { + env = setupStore({ + }); + }, + + teardown: function() { + run(env.store, 'destroy'); + } +}); + +/*test('...', function() { + +});*/ diff --git a/packages/ember-data/tests/unit/adapters/json-api-adapter/ajax-test.js b/packages/ember-data/tests/unit/adapters/json-api-adapter/ajax-test.js new file mode 100644 index 00000000000..cb2a30149c5 --- /dev/null +++ b/packages/ember-data/tests/unit/adapters/json-api-adapter/ajax-test.js @@ -0,0 +1,71 @@ +var Person, Place, store, adapter, env; +var run = Ember.run; + +module("unit/adapters/json-api-adapter/ajax - building requests", { + setup: function() { + Person = { modelName: 'person' }; + Place = { modelName: 'place' }; + env = setupStore({ adapter: DS.JSONAPIAdapter, person: Person, place: Place }); + store = env.store; + adapter = env.adapter; + }, + + teardown: function() { + run(function() { + store.destroy(); + env.container.destroy(); + }); + } +}); + +if (Ember.FEATURES.isEnabled('ds-new-serializer-api')) { + + test("ajaxOptions() adds Accept when no other headers exist", function() { + var url = 'example.com'; + var type = 'GET'; + var ajaxOptions = adapter.ajaxOptions(url, type, {}); + var receivedHeaders = []; + var fakeXHR = { + setRequestHeader: function(key, value) { + receivedHeaders.push([key, value]); + } + }; + ajaxOptions.beforeSend(fakeXHR); + deepEqual(receivedHeaders, [['Accept', 'application/vnd.api+json']], 'headers assigned'); + }); + + + test("ajaxOptions() adds Accept header to existing headers", function() { + adapter.headers = { 'Other-key': 'Other Value' }; + var url = 'example.com'; + var type = 'GET'; + var ajaxOptions = adapter.ajaxOptions(url, type, {}); + var receivedHeaders = []; + var fakeXHR = { + setRequestHeader: function(key, value) { + receivedHeaders.push([key, value]); + } + }; + ajaxOptions.beforeSend(fakeXHR); + deepEqual(receivedHeaders, [['Accept', 'application/vnd.api+json'], ['Other-key', 'Other Value']], 'headers assigned'); + }); + + + test("ajaxOptions() adds Accept header to existing computed properties headers", function() { + adapter.headers = Ember.computed(function() { + return { 'Other-key': 'Other Value' }; + }); + var url = 'example.com'; + var type = 'GET'; + var ajaxOptions = adapter.ajaxOptions(url, type, {}); + var receivedHeaders = []; + var fakeXHR = { + setRequestHeader: function(key, value) { + receivedHeaders.push([key, value]); + } + }; + ajaxOptions.beforeSend(fakeXHR); + deepEqual(receivedHeaders, [['Accept', 'application/vnd.api+json'], ['Other-key', 'Other Value']], 'headers assigned'); + }); + +} diff --git a/tests/ember-configuration.js b/tests/ember-configuration.js index 3d0c7a4f754..96de7230dc9 100644 --- a/tests/ember-configuration.js +++ b/tests/ember-configuration.js @@ -99,6 +99,11 @@ registry.register('adapter:-rest', DS.RESTAdapter); + if (Ember.FEATURES.isEnabled('ds-new-serializer-api')) { + registry.register('adapter:-json-api', DS.JSONAPIAdapter); + registry.register('serializer:-json-api', DS.JSONAPISerializer.extend({ isNewSerializerAPI: true })); + } + registry.injection('serializer', 'store', 'store:main'); env.serializer = container.lookup('serializer:-default');