├── vendor └── no-loader.js ├── .bowerrc ├── testem.json ├── .gitignore ├── .travis.yml ├── ember-addon-main.js ├── tests ├── helpers │ ├── qunit-setup.js │ ├── setup-polymorphic-models.js │ ├── setup-store.js │ ├── setup-models.js │ ├── begin.js │ └── pretender.js ├── unit │ ├── adapter │ │ ├── build-url-test.js │ │ └── ajax-error-test.js │ └── serializer │ │ └── extract-links-test.js ├── integration │ ├── specs │ │ ├── creating-an-individual-resource-test.js │ │ ├── individual-resource-representations-test.js │ │ ├── resource-collection-representations-test.js │ │ ├── null-relationship-test.js │ │ ├── to-many-polymorphic-test.js │ │ ├── to-one-relationships-test.js │ │ ├── multiple-resource-links-test.js │ │ ├── to-many-relationships-test.js │ │ ├── urls-for-resource-collections-test.js │ │ ├── link-with-type.js │ │ ├── href-link-for-resource-collection-test.js │ │ ├── updating-an-individual-resource-test.js │ │ ├── namespace-test.js │ │ └── compound-documents-test.js │ └── serializer-test.js ├── index.html └── runner.js ├── bower.json ├── LICENSE.md ├── package.json ├── README.md ├── Brocfile.js ├── CHANGELOG.md ├── dist ├── ember-json-api.min.js └── ember-json-api.js └── src ├── json-api-adapter.js └── json-api-serializer.js /vendor/no-loader.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /testem.json: -------------------------------------------------------------------------------- 1 | { 2 | "test_page": "tests/index.html" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | bower_components/ 3 | test_build/ 4 | tmp/ 5 | .idea/ 6 | npm-debug.log 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.11" 4 | - "0.10" 5 | before_script: 6 | - npm install -g bower 7 | - bower install 8 | -------------------------------------------------------------------------------- /ember-addon-main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'name': 'ember-json-api', 3 | 4 | init: function () { 5 | this.treePaths.addon = 'src'; 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /tests/helpers/qunit-setup.js: -------------------------------------------------------------------------------- 1 | QUnit.pending = function() { 2 | QUnit.test(arguments[0] + ' (SKIPPED)', function() { 3 | var li = document.getElementById(QUnit.config.current.id); 4 | QUnit.done(function() { 5 | li.style.background = '#FFFF99'; 6 | }); 7 | ok(true); 8 | }); 9 | }; 10 | pending = QUnit.pending; 11 | QUnit.skip = QUnit.pending; 12 | skip = QUnit.pending; 13 | -------------------------------------------------------------------------------- /tests/helpers/setup-polymorphic-models.js: -------------------------------------------------------------------------------- 1 | var Owner, Pet, Cat, Dog; 2 | 3 | function setPolymorphicModels() { 4 | Owner = DS.Model.extend({ 5 | name: DS.attr('string'), 6 | pets: DS.hasMany('pets', {polymorphic: true}) 7 | }); 8 | 9 | Pet = DS.Model.extend({ 10 | paws: DS.attr('number') 11 | }); 12 | 13 | Cat = Pet.extend({ 14 | whiskers: DS.attr('number') 15 | }); 16 | 17 | Dog = Pet.extend({ 18 | spots: DS.attr('number') 19 | }); 20 | 21 | return { 22 | 'owner': Owner, 23 | 'pet': Pet, 24 | 'cat': Cat, 25 | 'dog': Dog 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-json-api", 3 | "version": "0.4.4", 4 | "homepage": "https://github.com/plyfe/ember-json-api", 5 | "authors": [ 6 | "Dali Zheng ", 7 | "Stefan Penner " 8 | ], 9 | "license": "MIT", 10 | "ignore": [ 11 | "**/.*", 12 | "node_modules", 13 | "bower_components", 14 | "test", 15 | "tests" 16 | ], 17 | "dependencies": { 18 | "ember-data": "v1.0.0-beta.16.1", 19 | "ember": "v1.10.0", 20 | "handlebars": "v2.0.0", 21 | "loader.js": "stefanpenner/loader.js#1.0.1" 22 | }, 23 | "devDependencies": { 24 | "qunit": "~1.14.0", 25 | "pretender": "0.1.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/unit/adapter/build-url-test.js: -------------------------------------------------------------------------------- 1 | var adapter; 2 | var User = DS.Model.extend({ 3 | firstName: DS.attr() 4 | }); 5 | 6 | module('unit/ember-json-api-adapter - buildUrl', { 7 | setup: function() { 8 | DS._routes = Ember.create(null); 9 | adapter = DS.JsonApiAdapter.create(); 10 | }, 11 | tearDown: function() { 12 | DS._routes = Ember.create(null); 13 | Ember.run(adapter, 'destroy'); 14 | } 15 | }); 16 | 17 | test('basic', function(){ 18 | equal(adapter.buildURL('user', 1), '/users/1'); 19 | }); 20 | 21 | test("simple replacement", function() { 22 | DS._routes["comment"] = "posts/comments/{id}"; 23 | equal(adapter.buildURL('comment', 1), '/posts/comments/1'); 24 | }); 25 | -------------------------------------------------------------------------------- /tests/helpers/setup-store.js: -------------------------------------------------------------------------------- 1 | window.setupStore = function(options) { 2 | var env = {}; 3 | options = options || {}; 4 | 5 | var container = env.container = new Ember.Container(); 6 | 7 | var adapter = env.adapter = options.adapter || DS.JsonApiAdapter; 8 | var serializer = env.serializer = options.serializer || DS.JsonApiSerializer; 9 | 10 | delete options.adapter; 11 | delete options.serializer; 12 | 13 | for (var prop in options) { 14 | container.register('model:' + prop, options[prop]); 15 | } 16 | 17 | container.register('store:main', DS.Store.extend({ 18 | adapter: adapter 19 | })); 20 | 21 | container.register('serializer:application', serializer); 22 | 23 | container.injection('serializer', 'store', 'store:main'); 24 | 25 | env.serializer = container.lookup('serializer:application'); 26 | env.store = container.lookup('store:main'); 27 | env.adapter = env.store.get('defaultAdapter'); 28 | 29 | return env; 30 | }; 31 | -------------------------------------------------------------------------------- /tests/helpers/setup-models.js: -------------------------------------------------------------------------------- 1 | var Post, Comment, Author; 2 | 3 | function setModels(params) { 4 | var options; 5 | 6 | if (!params) { 7 | params = {} 8 | } 9 | 10 | options = { 11 | authorAsync: params.authorAsync || false, 12 | commentAsync: params.commentAsync || false 13 | }; 14 | 15 | Post = DS.Model.extend({ 16 | title: DS.attr('string'), 17 | postSummary: DS.attr('string'), 18 | comments: DS.hasMany('comment', { async: options.commentAsync }), 19 | author: DS.belongsTo('author', { async: options.authorAsync }) 20 | }); 21 | 22 | Author = DS.Model.extend({ 23 | name: DS.attr('string') 24 | }); 25 | 26 | Comment = DS.Model.extend({ 27 | title: DS.attr('string'), 28 | body: DS.attr('string') 29 | }); 30 | 31 | SomeResource = DS.Model.extend({ 32 | title: DS.attr('string') 33 | }); 34 | 35 | return { 36 | 'post': Post, 37 | 'author': Author, 38 | 'comment': Comment, 39 | 'someResource': SomeResource 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Dali Zheng `` 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /tests/helpers/begin.js: -------------------------------------------------------------------------------- 1 | QUnit.begin(function() { 2 | Ember.testing = true; 3 | Ember.Test.adapter = Ember.Test.QUnitAdapter.create(); 4 | Ember.RSVP.configure('onerror', function(reason) { 5 | // only print error messages if they're exceptions; 6 | // otherwise, let a future turn of the event loop 7 | // handle the error. 8 | if (reason && reason instanceof Error) { 9 | Ember.Logger.log(reason, reason.stack) 10 | throw reason; 11 | } 12 | }); 13 | 14 | var transforms = { 15 | 'boolean': DS.BooleanTransform.create(), 16 | 'date': DS.DateTransform.create(), 17 | 'number': DS.NumberTransform.create(), 18 | 'string': DS.StringTransform.create() 19 | }; 20 | 21 | // Prevent all tests involving serialization to require a container 22 | DS.JSONSerializer.reopen({ 23 | transformFor: function(attributeType) { 24 | return this._super(attributeType, true) || transforms[attributeType]; 25 | } 26 | }); 27 | }); 28 | 29 | // Generate the jQuery expando on window ahead of time 30 | // to make the QUnit global check run clean 31 | jQuery(window).data('testing', true); 32 | -------------------------------------------------------------------------------- /tests/integration/specs/creating-an-individual-resource-test.js: -------------------------------------------------------------------------------- 1 | var get = Ember.get, set = Ember.set; 2 | var models, env; 3 | var responses, fakeServer; 4 | 5 | module('integration/specs/creating-an-individual-resource', { 6 | setup: function() { 7 | fakeServer = stubServer(); 8 | 9 | responses = { 10 | post: { 11 | data: { 12 | type: 'posts', 13 | id: '1', 14 | title: 'Rails is Omakase', 15 | 'post-summary': 'summary' 16 | } 17 | } 18 | }; 19 | 20 | models = setModels(); 21 | env = setupStore(models); 22 | env.store.modelFor('post'); 23 | env.store.modelFor('comment'); 24 | }, 25 | 26 | teardown: function() { 27 | Ember.run(env.store, 'destroy'); 28 | shutdownFakeServer(fakeServer); 29 | } 30 | }); 31 | 32 | asyncTest("POST /posts/1 won't push an array", function() { 33 | var request = { 34 | data: { 35 | title: 'Rails is Omakase', 36 | 'post-summary': null, 37 | links: { 38 | comments: { 39 | linkage: [] 40 | } 41 | }, 42 | type: 'posts' 43 | } 44 | }; 45 | 46 | fakeServer.post('/posts', request, responses.post); 47 | 48 | Em.run(function() { 49 | var post = env.store.createRecord(models.post, { title: 'Rails is Omakase' }); 50 | post.save().then(function(record) { 51 | equal(record.get('id'), '1', 'id is correct'); 52 | equal(record.get('title'), 'Rails is Omakase', 'title is correct'); 53 | equal(record.get('postSummary'), 'summary', 'summary is correct'); 54 | start(); 55 | }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /tests/integration/specs/individual-resource-representations-test.js: -------------------------------------------------------------------------------- 1 | var get = Ember.get, set = Ember.set; 2 | var env; 3 | var responses, fakeServer; 4 | 5 | module('integration/specs/individual-resource-representations', { 6 | setup: function() { 7 | fakeServer = stubServer(); 8 | 9 | responses = { 10 | lone_post_in_singular: { 11 | data: { 12 | type: 'posts', 13 | id: '1', 14 | title: 'Rails is Omakase' 15 | } 16 | }, 17 | lone_post_in_plural: { 18 | data: { 19 | type: 'posts', 20 | id: '2', 21 | title: 'TDD Is Dead lol' 22 | } 23 | } 24 | }; 25 | 26 | env = setupStore(setModels()); 27 | env.store.modelFor('post'); 28 | env.store.modelFor('comment'); 29 | env.store.modelFor('author'); 30 | }, 31 | 32 | teardown: function() { 33 | Ember.run(env.store, 'destroy'); 34 | shutdownFakeServer(fakeServer); 35 | } 36 | }); 37 | 38 | asyncTest('GET /posts/1 with single resource interprets singular root key', function() { 39 | fakeServer.get('/posts/1', responses.lone_post_in_singular); 40 | 41 | Em.run(function() { 42 | env.store.find('post', '1').then(function(record) { 43 | equal(record.get('id'), '1', 'id is correct'); 44 | equal(record.get('title'), 'Rails is Omakase', 'title is correct'); 45 | start(); 46 | }); 47 | }); 48 | }); 49 | 50 | asyncTest('GET /posts/2 with single resource interprets plural root key', function() { 51 | fakeServer.get('/posts/2', responses.lone_post_in_plural); 52 | 53 | Em.run(function() { 54 | env.store.find('post', '2').then(function(record) { 55 | equal(record.get('id'), '2', 'id is correct'); 56 | equal(record.get('title'), 'TDD Is Dead lol', 'title is correct'); 57 | start(); 58 | }); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /tests/integration/specs/resource-collection-representations-test.js: -------------------------------------------------------------------------------- 1 | var get = Ember.get, set = Ember.set; 2 | var env; 3 | var responses, fakeServer; 4 | 5 | module('integration/specs/resource-collection-representations', { 6 | setup: function() { 7 | fakeServer = stubServer(); 8 | 9 | responses = { 10 | posts_list: { 11 | data: [{ 12 | type: 'posts', 13 | id: '1', 14 | title: 'Rails is Omakase' 15 | }, { 16 | type: 'posts', 17 | id: '2', 18 | title: 'Ember.js Handlebars' 19 | }] 20 | }, 21 | empty_list: { 22 | data: [] 23 | } 24 | }; 25 | 26 | env = setupStore(setModels()); 27 | env.store.modelFor('post'); 28 | env.store.modelFor('comment'); 29 | env.store.modelFor('author'); 30 | }, 31 | 32 | teardown: function() { 33 | Ember.run(env.store, 'destroy'); 34 | shutdownFakeServer(fakeServer); 35 | } 36 | }); 37 | 38 | asyncTest('GET /posts', function() { 39 | fakeServer.get('/posts', responses.posts_list); 40 | 41 | env.store.find('post').then(function(record) { 42 | var post1 = record.get("firstObject"), 43 | post2 = record.get("lastObject"); 44 | 45 | equal(record.get('length'), 2, 'length is correct'); 46 | 47 | equal(post1.get('id'), '1', 'id is correct'); 48 | equal(post1.get('title'), 'Rails is Omakase', 'title is correct'); 49 | 50 | equal(post2.get('id'), '2', 'id is correct'); 51 | equal(post2.get('title'), 'Ember.js Handlebars', 'title is correct'); 52 | start(); 53 | }); 54 | }); 55 | 56 | asyncTest('GET empty /posts', function() { 57 | fakeServer.get('/posts', responses.empty_list); 58 | 59 | env.store.find('post').then(function(record) { 60 | equal(record.get('length'), 0, 'length is correct'); 61 | start(); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-json-api", 3 | "description": "Ember Data adapter for JSON API.", 4 | "version": "0.4.4", 5 | "homepage": "http://github.com/kurko/ember-json-api", 6 | "author": "Dali Zheng", 7 | "contributors": [ 8 | { 9 | "name": "Alexandre de Oliveira", 10 | "email": "chavedomundo@gmail.com" 11 | }, 12 | { 13 | "name": "Stefan Penner", 14 | "email": "stefan.penner@gmail.com" 15 | }, 16 | { 17 | "name": "Eric Neuhauser", 18 | "email": "eric.neuhauser@gmail.com" 19 | } 20 | ], 21 | "main": "dist/json-api-adapter.js", 22 | "repository": { 23 | "type": "git", 24 | "url": "http://github.com/kurko/ember-json-api" 25 | }, 26 | "bugs": { 27 | "url": "http://github.com/kurko/ember-json-api/issues" 28 | }, 29 | "keywords": [ 30 | "ember", 31 | "ember-addon" 32 | ], 33 | "ember-addon": { 34 | "main": "ember-addon-main.js" 35 | }, 36 | "scripts": { 37 | "build": "rm -rf dist && BROCCOLI_ENV=production ./node_modules/.bin/broccoli build dist", 38 | "build-test": "rm -rf test_build && ./node_modules/broccoli-cli/bin/broccoli build test_build", 39 | "test": "npm run build-test && phantomjs test_build/tests/runner.js test_build/tests/index.html && rm -rf test_build", 40 | "test-server": "./node_modules/.bin/broccoli serve", 41 | "serve": "./node_modules/.bin/broccoli serve" 42 | }, 43 | "devDependencies": { 44 | "bower": "", 45 | "broccoli": "^0.13.1", 46 | "broccoli-bower": "0.2.0", 47 | "broccoli-cli": "0.0.1", 48 | "broccoli-env": "^0.0.1", 49 | "broccoli-es6-concatenator": "0.1.4", 50 | "broccoli-es6-transpiler": "0.1.0", 51 | "broccoli-merge-trees": "0.1.3", 52 | "broccoli-static-compiler": "0.1.4", 53 | "broccoli-template": "0.1.0", 54 | "broccoli-uglify-js": "0.1.3", 55 | "ember-cli": "^0.1.1", 56 | "testem": "0.6.16" 57 | }, 58 | "license": "MIT" 59 | } 60 | -------------------------------------------------------------------------------- /tests/unit/adapter/ajax-error-test.js: -------------------------------------------------------------------------------- 1 | var JsonApiAdapter = require('src/json-api-adapter').default, 2 | JsonApiSerializer = require('src/json-api-serializer').default; 3 | 4 | var env, store, adapter, SuperUser; 5 | var originalAjax, passedUrl, passedVerb, passedHash; 6 | 7 | module('unit/ember-json-api-adapter - adapter', { 8 | setup: function() { 9 | SuperUser = DS.Model.extend(); 10 | 11 | env = setupStore({ 12 | superUser: SuperUser, 13 | adapter: JsonApiAdapter, 14 | serializer: JsonApiSerializer 15 | }); 16 | 17 | store = env.store; 18 | adapter = env.adapter; 19 | 20 | passedUrl = passedVerb = passedHash = null; 21 | } 22 | }); 23 | 24 | test('ajaxError - returns invalid error if 422 response', function() { 25 | var error = new DS.InvalidError({ 26 | name: "can't be blank" 27 | }); 28 | 29 | var jqXHR = { 30 | status: 422, 31 | responseText: JSON.stringify({ 32 | errors: { 33 | name: "can't be blank" 34 | } 35 | }) 36 | }; 37 | 38 | equal(adapter.ajaxError(jqXHR), error.toString()); 39 | }); 40 | 41 | test('ajaxError - invalid error has camelized keys', function() { 42 | var error = new DS.InvalidError({ 43 | firstName: "can't be blank" 44 | }); 45 | 46 | var jqXHR = { 47 | status: 422, 48 | responseText: JSON.stringify({ 49 | errors: { 50 | first_name: "can't be blank" 51 | } 52 | }) 53 | }; 54 | 55 | equal(adapter.ajaxError(jqXHR), error.toString()); 56 | }); 57 | 58 | test('ajaxError - returns ServerError error if not 422 response', function() { 59 | var error = new JsonApiAdapter.ServerError(500, "Something went wrong"); 60 | 61 | var jqXHR = { 62 | status: 500, 63 | responseText: "Something went wrong" 64 | }; 65 | 66 | var actualError = adapter.ajaxError(jqXHR); 67 | 68 | equal(actualError.message, error.message); 69 | equal(actualError.status, error.status); 70 | equal(actualError.xhr , jqXHR); 71 | }); 72 | -------------------------------------------------------------------------------- /tests/integration/specs/null-relationship-test.js: -------------------------------------------------------------------------------- 1 | var get = Ember.get, set = Ember.set; 2 | var env; 3 | var responses, fakeServer; 4 | 5 | module('integration/specs/null-relationship', { 6 | setup: function() { 7 | fakeServer = stubServer(); 8 | 9 | responses = { 10 | posts_1: { 11 | data: { 12 | type: 'posts', 13 | id: '1', 14 | title: 'Rails is Omakase', 15 | links: { 16 | 'comments': null 17 | } 18 | } 19 | }, 20 | posts_2: { 21 | data: { 22 | type: 'posts', 23 | id: '2', 24 | title: 'Hello world', 25 | links: { 26 | author: null 27 | } 28 | } 29 | } 30 | }; 31 | 32 | env = setupStore(setModels()); 33 | env.store.modelFor('post'); 34 | env.store.modelFor('comment'); 35 | }, 36 | 37 | teardown: function() { 38 | Ember.run(env.store, 'destroy'); 39 | shutdownFakeServer(fakeServer); 40 | } 41 | }); 42 | 43 | asyncTest('GET /posts/1', function() { 44 | var models = setModels({ 45 | authorAsync: true, 46 | commentAsync: true 47 | }); 48 | env = setupStore(models); 49 | 50 | fakeServer.get('/posts/1', responses.posts_1); 51 | 52 | Em.run(function() { 53 | env.store.find('post', '1').then(function(record) { 54 | equal(record.get('id'), '1', 'id is correct'); 55 | equal(record.get('title'), 'Rails is Omakase', 'title is correct'); 56 | 57 | record.get('comments').then(function(comments) { 58 | equal(comments.get('length'), 0, 'there are 0 comments'); 59 | start(); 60 | }); 61 | }); 62 | }); 63 | }); 64 | 65 | asyncTest('GET /posts/2', function() { 66 | var models = setModels({ 67 | authorAsync: true 68 | }); 69 | env = setupStore(models); 70 | 71 | fakeServer.get('/posts/2', responses.posts_2); 72 | 73 | Em.run(function() { 74 | env.store.find('post', '2').then(function(record) { 75 | equal(record.get('id'), '2', 'id is correct'); 76 | equal(record.get('title'), 'Hello world', 'title is correct'); 77 | 78 | record.get('author').then(function(author) { 79 | equal(author, null, 'Author is null'); 80 | start(); 81 | }); 82 | }); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /tests/integration/specs/to-many-polymorphic-test.js: -------------------------------------------------------------------------------- 1 | var get = Ember.get, set = Ember.set; 2 | var env; 3 | var responses, fakeServer; 4 | 5 | module('integration/specs/to-many-polymorphic', { 6 | setup: function() { 7 | fakeServer = stubServer(); 8 | 9 | responses = { 10 | data: { 11 | type: 'owners', 12 | id: '1', 13 | name: 'Luke', 14 | links: { 15 | pets: { 16 | linkage: [ 17 | { 18 | type: 'cats', 19 | id: 'cat_1' 20 | }, 21 | { 22 | type: 'dogs', 23 | id: 'dog_2' 24 | } 25 | ] 26 | } 27 | } 28 | }, 29 | included: [ 30 | { 31 | type: 'cats', 32 | id: 'cat_1', 33 | whiskers: 4, 34 | paws: 3 35 | }, 36 | { 37 | type: 'dogs', 38 | id: 'dog_2', 39 | spots: 7, 40 | paws: 5 41 | } 42 | ] 43 | }; 44 | 45 | env = setupStore(setPolymorphicModels()); 46 | env.store.modelFor('owner'); 47 | env.store.modelFor('pet'); 48 | env.store.modelFor('dog'); 49 | env.store.modelFor('cat'); 50 | }, 51 | 52 | teardown: function() { 53 | Ember.run(env.store, 'destroy'); 54 | shutdownFakeServer(fakeServer); 55 | } 56 | }); 57 | 58 | asyncTest('GET /owners/1 with sync included resources', function() { 59 | var models = setPolymorphicModels(); 60 | env = setupStore(models); 61 | 62 | fakeServer.get('/owners/1', responses); 63 | 64 | Em.run(function() { 65 | env.store.find('owner', '1').then(function(record) { 66 | 67 | equal(record.get('id'), '1', 'id is correct'); 68 | equal(record.get('name'), 'Luke', 'name is correct'); 69 | 70 | var cat = record.get('pets.firstObject'); 71 | var dog = record.get('pets.lastObject'); 72 | 73 | equal(cat.get('paws'), 3, 'common prop from base class correct on cat'); 74 | equal(dog.get('paws'), 5, 'common prop from base class correct on dog'); 75 | equal(cat.get('whiskers'), 4, 'cat has correct whiskers (cat-only prop)'); 76 | equal(dog.get('spots'), 7, 'dog has correct spots (dog-only prop)'); 77 | 78 | start(); 79 | }); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ember-json-api 2 | 3 | ![](https://travis-ci.org/kurko/ember-json-api.svg?branch=master) 4 | 5 | This is a [JSON API](http://jsonapi.org) adapter for [Ember Data](http://github.com/emberjs/data) 1.0 beta 16.1, that extends the built-in REST adapter. Please note that Ember Data and JSON API are both works in progress, use with caution. 6 | 7 | **Important:** this is under heavy development. For the latest stable release, 8 | check the latest tag. 9 | 10 | This follows [JSONAPI v1.0 rc3](https://github.com/json-api/json-api/blob/827ba3c1130408fdb406d9faab645b0db7dd4fe4/index.md), with a primary `data` root, resources linked with `related` property, side loaded data in an `included` array at the root, and consistent linkage with a `linkage` property for a linked resource. 11 | 12 | ### Specification coverage 13 | 14 | To see details on how much of the JSONAPI.org spec this adapter covers, read the 15 | tests under `tests/integration/specs/`. Each field tests one section of the 16 | standard. 17 | 18 | ### Usage 19 | 20 | To install: 21 | 22 | ``` 23 | npm install --save-dev ember-json-api 24 | ``` 25 | 26 | Next, define the adapter and serializer: 27 | 28 | ```js 29 | // app/adapters/application.js 30 | import JsonApiAdapter from 'ember-json-api/json-api-adapter'; 31 | export default JsonApiAdapter; 32 | 33 | // app/serializers/application.js 34 | import JsonApiSerializer from 'ember-json-api/json-api-serializer'; 35 | export default JsonApiSerializer; 36 | ``` 37 | 38 | ### Tests & Build 39 | 40 | First, install depdendencies with `npm install && bower install`. Then run 41 | `npm run serve` and visit `http://localhost:4200/tests`. 42 | 43 | If you prefer, use `npm run test` in your terminal, which will run tests 44 | without a browser. You need to have PhantomJS installed. 45 | 46 | To build a new version, just run `npm run build`. The build will be 47 | available in the `dist/` directory. 48 | 49 | ### Issues 50 | 51 | - This adapter has preliminary support for URL-style JSON API. It currently 52 | only serializes one route per type, so if you have multiple ways to get a 53 | resource, it will not work as expected. 54 | 55 | ### Thanks 56 | 57 | A huge thanks goes to [Dali Zheng](https://github.com/daliwali) who initially 58 | maintained the adapter. 59 | 60 | ### License 61 | 62 | This code abides to the MIT license: http://opensource.org/licenses/MIT 63 | -------------------------------------------------------------------------------- /Brocfile.js: -------------------------------------------------------------------------------- 1 | var uglifyJavaScript = require('broccoli-uglify-js'); 2 | var pickFiles = require('broccoli-static-compiler'); 3 | var mergeTrees = require('broccoli-merge-trees'); 4 | var env = require('broccoli-env').getEnv(); 5 | var compileES6 = require('broccoli-es6-concatenator'); 6 | var findBowerTrees = require('broccoli-bower'); 7 | 8 | var sourceTrees = []; 9 | 10 | if (env === 'production') { 11 | 12 | // Build file 13 | var js = compileES6('src', { 14 | loaderFile: '../vendor/no-loader.js', 15 | inputFiles: [ 16 | '**/*.js' 17 | ], 18 | wrapInEval: false, 19 | outputFile: '/ember-json-api.js' 20 | }); 21 | 22 | var jsMinified = compileES6('src', { 23 | loaderFile: '../vendor/no-loader.js', 24 | inputFiles: [ 25 | '**/*.js' 26 | ], 27 | wrapInEval: false, 28 | outputFile: '/ember-json-api.min.js' 29 | }); 30 | 31 | var ugly = uglifyJavaScript(jsMinified, { 32 | mangle: true, 33 | compress: true 34 | }); 35 | 36 | sourceTrees = sourceTrees.concat(js); 37 | sourceTrees = sourceTrees.concat(ugly); 38 | 39 | } else if (env === 'development') { 40 | 41 | var src, vendor, bowerComponents; 42 | src = pickFiles('src', { 43 | srcDir: '/', 44 | destDir: '/src' 45 | }); 46 | vendor = pickFiles('vendor', { 47 | srcDir: '/', 48 | destDir: '/vendor' 49 | }); 50 | loaderJs = pickFiles('bower_components/loader.js', { 51 | srcDir: '/', 52 | files: ['loader.js'], 53 | destDir: '/vendor/loader.js' 54 | }); 55 | 56 | sourceTrees = sourceTrees.concat(src); 57 | sourceTrees = sourceTrees.concat(findBowerTrees()); 58 | sourceTrees = sourceTrees.concat(vendor); 59 | sourceTrees = sourceTrees.concat(loaderJs); 60 | var js = new mergeTrees(sourceTrees, { overwrite: true }); 61 | 62 | js = compileES6(js, { 63 | loaderFile: 'vendor/loader.js/loader.js', 64 | inputFiles: [ 65 | 'src/**/*.js' 66 | ], 67 | legacyFilesToAppend: [ 68 | 'jquery.js', 69 | 'qunit.js', 70 | 'handlebars.js', 71 | 'ember.debug.js', 72 | 'ember-data.js' 73 | ], 74 | wrapInEval: true, 75 | outputFile: '/assets/app.js' 76 | }); 77 | 78 | sourceTrees = sourceTrees.concat(js); 79 | 80 | var tests = pickFiles('tests', { 81 | srcDir: '/', 82 | destDir: '/tests' 83 | }) 84 | sourceTrees.push(tests) 85 | 86 | sourceTrees = sourceTrees.concat(tests); 87 | 88 | } 89 | module.exports = mergeTrees(sourceTrees, { overwrite: true }); 90 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## CHANGELOG 2 | 3 | ### 0.4.4 4 | 5 | * Made dasherized the default naming convention for resource types, attribute names, and association names per [recommended naming conventions](http://jsonapi.org/recommendations/#naming). To override with camelCase or snake_case, override the following: 6 | 7 | ``` 8 | export default JsonApiSerializer.extend({ 9 | keyForAttribute: function(key) { 10 | return Ember.String.camelize(key); 11 | }, 12 | keyForRelationship: function(key) { 13 | return Ember.String.camelize(key); 14 | }, 15 | keyForSnapshot: function(snapshot) { 16 | return Ember.String.camelize(snapshot.typeKey); 17 | } 18 | }); 19 | ``` 20 | 21 | * Made dasherized the default naming convention for path types. To change, override 22 | 23 | ``` 24 | export default JsonApiAdapter.extend({ 25 | pathForType: function(type) { 26 | var decamelized = Ember.String.decamelize(type); 27 | return Ember.String.pluralize(decamelized); 28 | } 29 | }); 30 | ``` 31 | 32 | ### 0.4.3 33 | 34 | * Replace PUT verb with PATCH. This is a breaking change for some and can be overridden in the application adapter with the following: 35 | 36 | ``` 37 | ajaxOptions: function(url, type, options) { 38 | var methodType = (type === 'PATCH') ? 'PUT' : type; 39 | return this._super(url, methodType, options); 40 | } 41 | ``` 42 | 43 | ### 0.4.2 44 | 45 | * updating to [JSON API RC3](https://github.com/json-api/json-api/blob/827ba3c1130408fdb406d9faab645b0db7dd4fe4/index.md) with the usage of the consistent linkage format. 46 | * Added polymorphic support. 47 | 48 | ### 0.4.1 49 | 50 | * keeping up with JSON API RC2+ to change linked to included and resource to related. 51 | 52 | ### 0.4.0 53 | 54 | * updating to JSON API RC2 55 | 56 | ### 0.3.0 57 | 58 | * removes deprecation warning because of DS' snapshots 59 | * stops overriding `extractMeta` 60 | * FIX: inside a serializer, reuses the same current store instead of relying on 61 | defaultSerializer. This is a fix for apps that use multiple stores. 62 | * FIX: covers null associations 63 | * BREAKING: all keys are camelized, so now define your camelize your model 64 | attributes 65 | * BREAKING: Ember 1.0.0-beta.15 support 66 | 67 | ### 0.2.0 68 | 69 | * ensures that both singular and plural root keys work. #30 70 | * PUT for a single resource won't send array of resources. #30 71 | * createRecord for a single resource won't POST array of resources. #30 72 | * builds URLs with underscores (was building with camelCase before). 73 | * a bunch of tests 74 | -------------------------------------------------------------------------------- /tests/integration/specs/to-one-relationships-test.js: -------------------------------------------------------------------------------- 1 | var get = Ember.get, set = Ember.set; 2 | var env; 3 | var responses, fakeServer; 4 | 5 | module('integration/specs/to-one-relationships', { 6 | setup: function() { 7 | fakeServer = stubServer(); 8 | 9 | responses = { 10 | posts_no_linked: { 11 | data: { 12 | type: 'posts', 13 | id: '1', 14 | title: 'Rails is Omakase', 15 | links: { 16 | author: { 17 | linkage: { 18 | type: 'authors', 19 | id: '2' 20 | } 21 | } 22 | } 23 | } 24 | }, 25 | authors: { 26 | data: { 27 | type: 'authors', 28 | id: '2', 29 | name: 'dhh' 30 | } 31 | } 32 | }; 33 | 34 | env = setupStore(setModels()); 35 | env.store.modelFor('post'); 36 | env.store.modelFor('comment'); 37 | env.store.modelFor('author'); 38 | }, 39 | 40 | teardown: function() { 41 | Ember.run(env.store, 'destroy'); 42 | shutdownFakeServer(fakeServer); 43 | } 44 | }); 45 | 46 | asyncTest('GET /posts/1 with async included resources', function() { 47 | var models = setModels({ 48 | authorAsync: true 49 | }); 50 | env = setupStore(models); 51 | 52 | fakeServer.get('/posts/1', responses.posts_no_linked); 53 | fakeServer.get('/authors/2', responses.authors); 54 | 55 | Em.run(function() { 56 | env.store.find('post', '1').then(function(record) { 57 | equal(record.get('id'), '1', 'id is correct'); 58 | equal(record.get('title'), 'Rails is Omakase', 'title is correct'); 59 | 60 | record.get('author').then(function(author) { 61 | equal(author.get('id'), '2', 'author id is correct'); 62 | equal(author.get('name'), 'dhh', 'author name is correct'); 63 | start(); 64 | }); 65 | }); 66 | }); 67 | }); 68 | 69 | asyncTest("GET /posts/1 with sync included resources won't work", function() { 70 | var models = setModels({ 71 | authorAsync: false 72 | }); 73 | env = setupStore(models); 74 | 75 | fakeServer.get('/posts/1', responses.posts_no_linked); 76 | fakeServer.get('/authors/2', responses.authors); 77 | 78 | Em.run(function() { 79 | env.store.find('post', '1').then(function(record) { 80 | var authorId; 81 | equal(record.get('id'), '1', 'id is correct'); 82 | equal(record.get('title'), 'Rails is Omakase', 'title is correct'); 83 | 84 | throws(function() { 85 | record.get('author').then(function(author) { 86 | equal(author.get('id'), '2', 'author id is correct'); 87 | }); 88 | }); 89 | start(); 90 | }); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ember Data JSONApi Adapter 6 | 7 | 14 | 15 | 16 |
17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /tests/integration/specs/multiple-resource-links-test.js: -------------------------------------------------------------------------------- 1 | var get = Ember.get, set = Ember.set; 2 | var env; 3 | var responses, fakeServer; 4 | 5 | module('integration/specs/multiple-resource-links-test', { 6 | setup: function() { 7 | fakeServer = stubServer(); 8 | 9 | responses = { 10 | posts_not_compound: { 11 | data: [{ 12 | type: 'posts', 13 | id: '1', 14 | title: 'Rails is Omakase', 15 | links: { 16 | author: { 17 | related: '/posts/1/author', 18 | linkage: { 19 | type: 'authors', 20 | id: '2' 21 | } 22 | } 23 | } 24 | }, { 25 | type: 'posts', 26 | id: '2', 27 | title: 'TDD Is Dead lol', 28 | links: { 29 | author: { 30 | related: '/posts/2/author', 31 | linkage: { 32 | type: 'authors', 33 | id: '1' 34 | } 35 | } 36 | } 37 | }] 38 | }, 39 | post_1_author: { 40 | data: { 41 | type: 'authors', 42 | id: '2', 43 | name: 'dhh' 44 | } 45 | }, 46 | post_2_author: { 47 | data: { 48 | type: 'authors', 49 | id: '1', 50 | name: 'ado' 51 | } 52 | } 53 | }; 54 | 55 | env = setupStore(setModels()); 56 | env.store.modelFor('post'); 57 | env.store.modelFor('comment'); 58 | env.store.modelFor('author'); 59 | }, 60 | 61 | teardown: function() { 62 | Ember.run(env.store, 'destroy'); 63 | shutdownFakeServer(fakeServer); 64 | } 65 | }); 66 | 67 | asyncTest('GET /posts/1 calls later GET /posts/1/comments when Posts has async comments', function() { 68 | var models = setModels({ 69 | authorAsync: true 70 | }); 71 | env = setupStore(models); 72 | 73 | fakeServer.get('/posts', responses.posts_not_compound); 74 | fakeServer.get('/posts/1/author', responses.post_1_author); 75 | fakeServer.get('/posts/2/author', responses.post_2_author); 76 | 77 | Em.run(function() { 78 | env.store.find('post').then(function(records) { 79 | equal(records.get('length'), 2, 'there are 2 posts'); 80 | 81 | var post1 = records.objectAt(0); 82 | var post2 = records.objectAt(1); 83 | var promises = []; 84 | 85 | promises.push(post1.get('author').then(function(author) { 86 | equal(author.get('name'), 'dhh', 'post1 author'); 87 | })); 88 | promises.push(post2.get('author').then(function(author) { 89 | equal(author.get('name'), 'ado', 'post2 author'); 90 | })); 91 | 92 | Ember.RSVP.all(promises).then(start); 93 | }); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /tests/unit/serializer/extract-links-test.js: -------------------------------------------------------------------------------- 1 | var serializer; 2 | 3 | module('unit/ember-json-api-adapter - serializer - extract-links-test', { 4 | setup: function() { 5 | // TODO remove global 6 | DS._routes = Ember.create(null); 7 | serializer = DS.JsonApiSerializer.create(); 8 | }, 9 | tearDown: function() { 10 | // TODO remove global 11 | DS._routes = Ember.create(null); 12 | Ember.run(serializer, 'destroy'); 13 | } 14 | }); 15 | 16 | test("no links", function() { 17 | var links = serializer.extractRelationships({ 18 | 19 | }, {}); 20 | 21 | deepEqual(links, {}); 22 | }); 23 | 24 | test("basic", function() { 25 | var links = serializer.extractRelationships({ 26 | "posts.comments": "http://example.com/posts/{posts.id}/comments" 27 | }, {}); 28 | 29 | deepEqual(links, { 30 | "comments": "/posts/{posts.id}/comments" 31 | }); 32 | }); 33 | 34 | test("exploding", function() { 35 | var links = serializer.extractRelationships({ 36 | "posts.comments": "http://example.com/comments/{posts.comments}" 37 | }, {}); 38 | 39 | deepEqual(links, { 40 | "comments": "/comments/{posts.comments}" 41 | }); 42 | }); 43 | 44 | test("self link", function() { 45 | var links = serializer.extractRelationships({ 46 | "author": { 47 | "self": "http://example.com/links/posts/1/author", 48 | "linkage": { 49 | "id": "1", 50 | "type": "authors" 51 | } 52 | } 53 | }, { id:1, type:'posts' }); 54 | 55 | deepEqual(links, { 56 | "author--self": "/links/posts/1/author" 57 | }); 58 | }); 59 | 60 | test("self link with replacement", function() { 61 | var links = serializer.extractRelationships({ 62 | "author": { 63 | "self": "http://example.com/posts/{post.id}/author/{author.id}", 64 | "linkage": { 65 | "id": "1", 66 | "type": "authors" 67 | } 68 | } 69 | }, { id:1, type:'posts' }); 70 | 71 | deepEqual(links, { 72 | "author--self": "/posts/{post.id}/author/{author.id}" 73 | }); 74 | }); 75 | 76 | test("related link", function() { 77 | var links = serializer.extractRelationships({ 78 | "author": { 79 | "related": "http://example.com/authors/1", 80 | "linkage": { 81 | "id": "1", 82 | "type": "authors" 83 | } 84 | } 85 | }, { id:1, type:'posts' }); 86 | 87 | deepEqual(links, { 88 | "author": "/authors/1" 89 | }); 90 | }); 91 | 92 | test("related link with replacement", function() { 93 | var links = serializer.extractRelationships({ 94 | "author": { 95 | "related": "http://example.com/authors/{author.id}", 96 | "id": "1", 97 | "type": "authors" 98 | } 99 | }, { id:1, type:'posts' }); 100 | 101 | deepEqual(links, { 102 | "author": "/authors/{author.id}" 103 | }); 104 | }); 105 | 106 | 107 | test("self and related link", function() { 108 | var links = serializer.extractRelationships({ 109 | "author": { 110 | "self": "http://example.com/posts/1/links/author", 111 | "related": "http://example.com/posts/1/author", 112 | "id": "1", 113 | "type": "authors" 114 | } 115 | }, { id:'1', type:'post' }); 116 | 117 | deepEqual(links, { 118 | "author": "/posts/1/author", 119 | "author--self": "/posts/1/links/author" 120 | }); 121 | }); -------------------------------------------------------------------------------- /tests/integration/specs/to-many-relationships-test.js: -------------------------------------------------------------------------------- 1 | var get = Ember.get, set = Ember.set; 2 | var env; 3 | var responses, fakeServer; 4 | 5 | module('integration/specs/to-many-relationships', { 6 | setup: function() { 7 | fakeServer = stubServer(); 8 | 9 | responses = { 10 | posts_not_compound: { 11 | data: { 12 | type: 'posts', 13 | id: '1', 14 | title: 'Rails is Omakase', 15 | links: { 16 | comments: { 17 | linkage: [{ 18 | id: '2', 19 | type: 'comments' 20 | }, { 21 | id: '3', 22 | type: 'comments' 23 | }] 24 | } 25 | } 26 | } 27 | }, 28 | comments_2: { 29 | data: { 30 | type: 'comments', 31 | id: '2', 32 | title: 'good article', 33 | body: 'ideal for my startup' 34 | } 35 | }, 36 | comments_3: { 37 | data: { 38 | type: 'comments', 39 | id: '3', 40 | title: 'bad article', 41 | body: "doesn't run Crysis" 42 | } 43 | } 44 | }; 45 | 46 | env = setupStore(setModels()); 47 | env.store.modelFor('post'); 48 | env.store.modelFor('comment'); 49 | }, 50 | 51 | teardown: function() { 52 | Ember.run(env.store, 'destroy'); 53 | shutdownFakeServer(fakeServer); 54 | } 55 | }); 56 | 57 | asyncTest('GET /posts/1 with async included resources', function() { 58 | var models = setModels({ 59 | commentAsync: true 60 | }); 61 | env = setupStore(models); 62 | 63 | fakeServer.get('/posts/1', responses.posts_not_compound); 64 | fakeServer.get('/comments/2', responses.comments_2); 65 | fakeServer.get('/comments/3', responses.comments_3); 66 | 67 | Em.run(function() { 68 | env.store.find('post', '1').then(function(record) { 69 | equal(record.get('id'), '1', 'id is correct'); 70 | equal(record.get('title'), 'Rails is Omakase', 'title is correct'); 71 | 72 | record.get('comments').then(function(comments) { 73 | var comment1 = comments.objectAt(0); 74 | var comment2 = comments.objectAt(1); 75 | 76 | equal(comments.get('length'), 2, 'there are 2 comments'); 77 | 78 | equal(comment1.get('title'), 'good article', 'comment1 title'); 79 | equal(comment1.get('body'), 'ideal for my startup', 'comment1 body'); 80 | equal(comment2.get('title'), 'bad article', 'comment2 title'); 81 | equal(comment2.get('body'), "doesn't run Crysis", 'comment2 body'); 82 | start(); 83 | }); 84 | }); 85 | }); 86 | }); 87 | 88 | asyncTest('GET /posts/1 with sync included resources', function() { 89 | var models = setModels({ 90 | commentAsync: false 91 | }); 92 | env = setupStore(models); 93 | 94 | fakeServer.get('/posts/1', responses.posts_not_compound); 95 | 96 | Em.run(function() { 97 | env.store.find('post', '1').then(function(record) { 98 | var comment1, comment2; 99 | equal(record.get('id'), '1', 'id is correct'); 100 | equal(record.get('title'), 'Rails is Omakase', 'title is correct'); 101 | 102 | throws(function() { 103 | record.get('comments'); 104 | }); 105 | 106 | start(); 107 | }); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /tests/integration/specs/urls-for-resource-collections-test.js: -------------------------------------------------------------------------------- 1 | var get = Ember.get, set = Ember.set; 2 | var env; 3 | var responses, fakeServer; 4 | 5 | module('integration/specs/urls-for-resource-collections', { 6 | setup: function() { 7 | fakeServer = stubServer(); 8 | 9 | responses = { 10 | posts_not_compound: { 11 | data: { 12 | type: 'posts', 13 | id: '1', 14 | title: 'Rails is Omakase', 15 | links: { 16 | comments: { 17 | linkage: [{ 18 | type: 'comments', 19 | id: '2' 20 | },{ 21 | type: 'comments', 22 | id: '3' 23 | }] 24 | } 25 | } 26 | } 27 | }, 28 | comments_2: { 29 | data: { 30 | type: 'comments', 31 | 'id': '2', 32 | 'title': 'good article', 33 | 'body': 'ideal for my startup' 34 | } 35 | }, 36 | comments_3: { 37 | data: { 38 | type: 'comments', 39 | id: '3', 40 | title: 'bad article', 41 | body: "doesn't run Crysis" 42 | } 43 | }, 44 | underscore_resource: { 45 | data: { 46 | type: 'some_resource', 47 | id: '1', 48 | title: 'wow' 49 | } 50 | } 51 | }; 52 | 53 | env = setupStore(setModels()); 54 | env.store.modelFor('post'); 55 | env.store.modelFor('comment'); 56 | }, 57 | 58 | teardown: function() { 59 | Ember.run(env.store, 'destroy'); 60 | shutdownFakeServer(fakeServer); 61 | } 62 | }); 63 | 64 | asyncTest('GET /posts/1 calls later GET /comments/2,3 when Posts has async comments', function() { 65 | var models = setModels({ 66 | commentAsync: true 67 | }); 68 | env = setupStore(models); 69 | 70 | fakeServer.get('/posts/1', responses.posts_not_compound); 71 | fakeServer.get('/comments/2', responses.comments_2); 72 | fakeServer.get('/comments/3', responses.comments_3); 73 | 74 | Em.run(function() { 75 | env.store.find('post', '1').then(function(record) { 76 | equal(record.get('id'), '1', 'id is correct'); 77 | equal(record.get('title'), 'Rails is Omakase', 'title is correct'); 78 | 79 | record.get('comments').then(function(comments) { 80 | var comment1 = comments.objectAt(0); 81 | var comment2 = comments.objectAt(1); 82 | 83 | equal(comments.get('length'), 2, 'there are 2 comments'); 84 | 85 | equal(comment1.get('title'), 'good article', 'comment1 title'); 86 | equal(comment1.get('body'), 'ideal for my startup', 'comment1 body'); 87 | equal(comment2.get('title'), 'bad article', 'comment2 title'); 88 | equal(comment2.get('body'), "doesn't run Crysis", 'comment2 body'); 89 | start(); 90 | }); 91 | }); 92 | }); 93 | }); 94 | 95 | asyncTest('GET /some_resource, not camelCase, dasherized', function() { 96 | var models = setModels({ 97 | commentAsync: true 98 | }); 99 | env = setupStore(models); 100 | 101 | fakeServer.get('/some-resources/1', responses.underscore_resource); 102 | 103 | Em.run(function() { 104 | env.store.find('someResource', '1').then(function(record) { 105 | equal(record.get('id'), '1', 'id is correct'); 106 | equal(record.get('title'), 'wow', 'title is correct'); 107 | start(); 108 | }); 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /tests/integration/specs/link-with-type.js: -------------------------------------------------------------------------------- 1 | var get = Ember.get, set = Ember.set; 2 | var Post, Comment, Author, env; 3 | var responses, fakeServer; 4 | 5 | module('integration/specs/link-with-type', { 6 | setup: function() { 7 | fakeServer = stubServer(); 8 | 9 | responses = { 10 | post: { 11 | data: { 12 | type: 'posts', 13 | id: '1', 14 | title: 'Rails is Omakase', 15 | links: { 16 | observations: { 17 | linkage: [{ 18 | id: '2', 19 | type: 'comments' 20 | },{ 21 | id: '3', 22 | type: 'comments' 23 | }] 24 | }, 25 | writer: { 26 | linkage: { 27 | id: '1', 28 | type: 'authors' 29 | } 30 | } 31 | } 32 | } 33 | }, 34 | comments_2: { 35 | data: { 36 | type: 'comments', 37 | id: 2, 38 | title: 'good article' 39 | } 40 | }, 41 | comments_3: { 42 | data: { 43 | type: 'comments', 44 | id: 3, 45 | title: 'bad article' 46 | } 47 | }, 48 | author: { 49 | data: { 50 | type: 'authors', 51 | id: 1, 52 | name: 'Tomster' 53 | } 54 | } 55 | }; 56 | 57 | Post = DS.Model.extend({ 58 | title: DS.attr('string'), 59 | observations: DS.hasMany('comment', {async: true}), 60 | writer: DS.belongsTo('author', {async: true}) 61 | }); 62 | 63 | Comment = DS.Model.extend({ 64 | title: DS.attr('string'), 65 | post: DS.belongsTo('post') 66 | }); 67 | 68 | Author = DS.Model.extend({ 69 | name: DS.attr('string'), 70 | post: DS.belongsTo('post') 71 | }); 72 | 73 | env = setupStore({ 74 | post: Post, 75 | comment: Comment, 76 | author: Author 77 | }); 78 | 79 | env.store.modelFor('post'); 80 | env.store.modelFor('comment'); 81 | env.store.modelFor('author'); 82 | }, 83 | 84 | teardown: function() { 85 | Ember.run(env.store, 'destroy'); 86 | shutdownFakeServer(fakeServer); 87 | } 88 | }); 89 | 90 | asyncTest("GET /posts/1 with array of unmatched named relationship", function() { 91 | fakeServer.get('/posts/1', responses.post); 92 | fakeServer.get('/comments/2', responses.comments_2); 93 | fakeServer.get('/comments/3', responses.comments_3); 94 | 95 | Em.run(function() { 96 | env.store.find('post', 1).then(function(record) { 97 | equal(record.get('id'), '1', 'id is correct'); 98 | equal(record.get('title'), 'Rails is Omakase', 'title is correct'); 99 | record.get('observations').then(function(comments) { 100 | var comment1 = comments.objectAt(0); 101 | var comment2 = comments.objectAt(1); 102 | 103 | equal(comments.get('length'), 2, 'there are 2 comments'); 104 | 105 | equal(comment1.get('title'), 'good article', 'comment1 title'); 106 | equal(comment2.get('title'), 'bad article', 'comment2 title'); 107 | start(); 108 | }); 109 | }); 110 | }); 111 | }); 112 | 113 | asyncTest("GET /posts/1 with single unmatched named relationship", function() { 114 | fakeServer.get('/posts/1', responses.post); 115 | fakeServer.get('/authors/1', responses.author); 116 | 117 | Em.run(function() { 118 | env.store.find('post', 1).then(function(record) { 119 | equal(record.get('id'), '1', 'id is correct'); 120 | equal(record.get('title'), 'Rails is Omakase', 'title is correct'); 121 | record.get('writer').then(function(writer) { 122 | equal(writer.get('name'), 'Tomster', 'author name'); 123 | start(); 124 | }); 125 | }); 126 | }); 127 | }); 128 | -------------------------------------------------------------------------------- /tests/helpers/pretender.js: -------------------------------------------------------------------------------- 1 | var stubServer = function() { 2 | var pretender = new Pretender(); 3 | DS._routes = Ember.create(null); 4 | 5 | pretender.unhandledRequest = function(verb, path, request) { 6 | var string = "Pretender: non-existing "+verb+" "+path, request 7 | console.error(string); 8 | throw(string); 9 | }; 10 | 11 | return { 12 | pretender: pretender, 13 | 14 | availableRequests: { 15 | 'post': [], 16 | 'patch': [] 17 | }, 18 | 19 | get: function(url, response) { 20 | this.validatePayload(response, 'GET', url); 21 | 22 | this.pretender.get(url, function(request){ 23 | var string = JSON.stringify(response); 24 | return [200, {"Content-Type": "application/json"}, string] 25 | }); 26 | }, 27 | 28 | post: function(url, expectedRequest, response) { 29 | var _this = this; 30 | 31 | this.validatePayload(expectedRequest, 'POST', url); 32 | this.validatePayload(response, 'POST', url); 33 | 34 | this.availableRequests.post.push({ 35 | request: expectedRequest, 36 | response: response 37 | }); 38 | 39 | this.pretender.post(url, function(request){ 40 | var responseForRequest = _this.responseForRequest('post', request); 41 | 42 | var string = JSON.stringify(responseForRequest); 43 | return [201, {"Content-Type": "application/json"}, string] 44 | }); 45 | }, 46 | 47 | patch: function(url, expectedRequest, response) { 48 | var _this = this; 49 | 50 | this.validatePayload(expectedRequest, 'PATCH', url); 51 | this.validatePayload(response, 'PATCH', url); 52 | 53 | this.availableRequests.patch.push({ 54 | request: expectedRequest, 55 | response: response 56 | }); 57 | 58 | this.pretender.patch(url, function(request){ 59 | var responseForRequest = _this.responseForRequest('patch', request); 60 | 61 | var string = JSON.stringify(responseForRequest); 62 | return [200, {"Content-Type": "application/json"}, string] 63 | }); 64 | }, 65 | 66 | /** 67 | * We have a set of expected requests. Each one returns a particular 68 | * response. Here, we check that what's being requests exists in 69 | * `this.availableRequests` and then return it. 70 | * 71 | * If it doesn't exist, we throw errors (and rocks). 72 | */ 73 | responseForRequest: function(verb, currentRequest) { 74 | var respectiveResponse; 75 | var availableRequests = this.availableRequests[verb]; 76 | var actualRequest = JSON.stringify(JSON.parse(currentRequest.requestBody)); 77 | 78 | for (requests in availableRequests) { 79 | if (!availableRequests.hasOwnProperty(requests)) 80 | continue; 81 | 82 | var request = JSON.stringify(availableRequests[requests].request); 83 | var response = JSON.stringify(availableRequests[requests].response); 84 | 85 | if (actualRequest === request) { 86 | respectiveResponse = availableRequests[requests].response; 87 | break; 88 | } 89 | } 90 | 91 | if (respectiveResponse) { 92 | return respectiveResponse; 93 | } else { 94 | var error = "No response defined for "+verb+" request"; 95 | console.error(error, actualRequest); 96 | 97 | if (availableRequests.length) { 98 | console.log("Current defined requests:"); 99 | for (requests in availableRequests) { 100 | if (!availableRequests.hasOwnProperty(requests)) 101 | continue; 102 | 103 | console.log(JSON.stringify(availableRequests[requests].request)); 104 | } 105 | } 106 | 107 | throw(error); 108 | } 109 | }, 110 | 111 | validatePayload: function(response, verb, url) { 112 | if (!response) { 113 | var string = "No request or response defined for "+verb+" "+url; 114 | console.warn(string); 115 | throw(string); 116 | } 117 | } 118 | }; 119 | } 120 | 121 | var shutdownFakeServer = function(fakeServer) { 122 | fakeServer.pretender.shutdown(); 123 | DS._routes = Ember.create(null); 124 | } 125 | -------------------------------------------------------------------------------- /tests/integration/specs/href-link-for-resource-collection-test.js: -------------------------------------------------------------------------------- 1 | var get = Ember.get, set = Ember.set; 2 | var env; 3 | var responses, fakeServer; 4 | 5 | module('integration/specs/href-link-for-resource-collection-test', { 6 | setup: function() { 7 | fakeServer = stubServer(); 8 | 9 | responses = { 10 | posts_not_compound: { 11 | data: { 12 | type: 'posts', 13 | id: '1', 14 | title: 'Rails is Omakase', 15 | links: { 16 | comments: { 17 | self: '/posts/1/links/comments', 18 | related: '/posts/1/comments', 19 | linkage: [{ 20 | type: 'comments', 21 | id: '1' 22 | },{ 23 | type: 'comments', 24 | id: '2' 25 | }] 26 | } 27 | } 28 | } 29 | }, 30 | post_1_comments: { 31 | data: [ 32 | { 33 | type: 'comments', 34 | id: '1', 35 | title: 'good article', 36 | body: 'ideal for my startup' 37 | }, 38 | { 39 | type: 'comments', 40 | id: '2', 41 | title: 'bad article', 42 | body: 'doesn\'t run Crysis' 43 | } 44 | ] 45 | } 46 | }; 47 | 48 | env = setupStore(setModels()); 49 | env.store.modelFor('post'); 50 | env.store.modelFor('comment'); 51 | }, 52 | 53 | teardown: function() { 54 | Ember.run(env.store, 'destroy'); 55 | shutdownFakeServer(fakeServer); 56 | } 57 | }); 58 | 59 | asyncTest('GET /posts/1 calls later GET /posts/1/comments when Posts has async comments', function() { 60 | var models = setModels({ 61 | commentAsync: true 62 | }); 63 | env = setupStore(models); 64 | 65 | fakeServer.get('/posts/1', responses.posts_not_compound); 66 | fakeServer.get('/posts/1/comments', responses.post_1_comments); 67 | 68 | Em.run(function() { 69 | env.store.find('post', '1').then(function(record) { 70 | record.get('comments').then(function(comments) { 71 | var comment1 = comments.objectAt(0); 72 | var comment2 = comments.objectAt(1); 73 | 74 | equal(comments.get('length'), 2, 'there are 2 comments'); 75 | 76 | equal(comment1.get('title'), 'good article', 'comment1 title'); 77 | equal(comment1.get('body'), 'ideal for my startup', 'comment1 body'); 78 | equal(comment2.get('title'), 'bad article', 'comment2 title'); 79 | equal(comment2.get('body'), "doesn't run Crysis", 'comment2 body'); 80 | start(); 81 | }); 82 | }); 83 | }); 84 | }); 85 | 86 | asyncTest('GET /posts/1 calls later GET /posts/1/some_resources when Posts has async someResources (camelized)', function() { 87 | var models = setModels(); 88 | // Add hasMany someResources relation to Post 89 | models['post'].reopen({ 90 | someResources: DS.hasMany('someResources', { async: true }) 91 | }) 92 | 93 | env = setupStore(models); 94 | 95 | fakeServer.get('/posts/1', { 96 | data: { 97 | type: 'posts', 98 | id: '1', 99 | title: 'Rails is Omakase', 100 | links: { 101 | 'some_resources': { 102 | related: '/posts/1/some_resources' 103 | } 104 | } 105 | } 106 | }); 107 | 108 | fakeServer.get('/posts/1/some_resources', { 109 | data: [ 110 | { 111 | type: 'some_resources', 112 | id: 1, 113 | title: 'Something 1' 114 | }, 115 | { 116 | type: 'some_resources', 117 | id: 2, 118 | title: 'Something 2' 119 | } 120 | ] 121 | }); 122 | 123 | Em.run(function() { 124 | env.store.find('post', '1').then(function(record) { 125 | record.get('someResources').then(function(someResources) { 126 | var something1 = someResources.objectAt(0); 127 | var something2 = someResources.objectAt(1); 128 | 129 | equal(someResources.get('length'), 2, 'there are 2 someResources'); 130 | 131 | equal(something1.get('title'), 'Something 1', 'something1 title'); 132 | equal(something2.get('title'), 'Something 2', 'something2 title'); 133 | start(); 134 | }); 135 | }); 136 | }); 137 | }); 138 | -------------------------------------------------------------------------------- /tests/runner.js: -------------------------------------------------------------------------------- 1 | /* 2 | * QtWebKit-powered headless test runner using PhantomJS 3 | * 4 | * PhantomJS binaries: http://phantomjs.org/download.html 5 | * Requires PhantomJS 1.6+ (1.7+ recommended) 6 | * 7 | * Run with: 8 | * phantomjs runner.js [url-of-your-qunit-testsuite] 9 | * 10 | * e.g. 11 | * phantomjs runner.js http://localhost/qunit/test/index.html 12 | */ 13 | 14 | /*global phantom:false, require:false, console:false, window:false, QUnit:false */ 15 | 16 | (function() { 17 | 'use strict'; 18 | 19 | var url, page, timeout, 20 | args = require('system').args; 21 | 22 | // arg[0]: scriptName, args[1...]: arguments 23 | if (args.length < 2 || args.length > 3) { 24 | console.error('Usage:\n phantomjs runner.js [url-of-your-qunit-testsuite] [timeout-in-seconds]'); 25 | phantom.exit(1); 26 | } 27 | 28 | url = args[1]; 29 | page = require('webpage').create(); 30 | page.settings.clearMemoryCaches = true; 31 | if (args[2] !== undefined) { 32 | timeout = parseInt(args[2], 10); 33 | } 34 | 35 | // Route `console.log()` calls from within the Page context to the main Phantom context (i.e. current `this`) 36 | page.onConsoleMessage = function(msg) { 37 | console.log(msg); 38 | }; 39 | 40 | page.onInitialized = function() { 41 | page.evaluate(addLogging); 42 | }; 43 | 44 | page.onCallback = function(message) { 45 | var result, 46 | failed; 47 | 48 | if (message) { 49 | if (message.name === 'QUnit.done') { 50 | result = message.data; 51 | failed = !result || !result.total || result.failed; 52 | 53 | if (!result.total) { 54 | console.error('No tests were executed. Are you loading tests asynchronously?'); 55 | } 56 | 57 | phantom.exit(failed ? 1 : 0); 58 | } 59 | } 60 | }; 61 | 62 | page.open(url, function(status) { 63 | if (status !== 'success') { 64 | console.error('Unable to access network: ' + status); 65 | phantom.exit(1); 66 | } else { 67 | // Cannot do this verification with the 'DOMContentLoaded' handler because it 68 | // will be too late to attach it if a page does not have any script tags. 69 | var qunitMissing = page.evaluate(function() { return (typeof QUnit === 'undefined' || !QUnit); }); 70 | if (qunitMissing) { 71 | console.error('The `QUnit` object is not present on this page.'); 72 | phantom.exit(1); 73 | } 74 | 75 | // Set a timeout on the test running, otherwise tests with async problems will hang forever 76 | if (typeof timeout === 'number') { 77 | setTimeout(function() { 78 | console.error('The specified timeout of ' + timeout + ' seconds has expired. Aborting...'); 79 | phantom.exit(1); 80 | }, timeout * 1000); 81 | } 82 | 83 | // Do nothing... the callback mechanism will handle everything! 84 | } 85 | }); 86 | 87 | function addLogging() { 88 | window.document.addEventListener('DOMContentLoaded', function() { 89 | var currentTestAssertions = []; 90 | 91 | QUnit.log(function(details) { 92 | var response; 93 | 94 | // Ignore passing assertions 95 | if (details.result) { 96 | return; 97 | } 98 | 99 | response = details.message || ''; 100 | 101 | if (typeof details.expected !== 'undefined') { 102 | if (response) { 103 | response += ', '; 104 | } 105 | 106 | response += 'expected: ' + details.expected + ', but was: ' + details.actual; 107 | } 108 | 109 | if (details.source) { 110 | response += "\n" + details.source; 111 | } 112 | 113 | currentTestAssertions.push('Failed assertion: ' + response); 114 | }); 115 | 116 | QUnit.testDone(function(result) { 117 | var i, 118 | len, 119 | name = result.module + ': ' + result.name; 120 | 121 | if (result.failed) { 122 | console.log('Test failed: ' + name); 123 | 124 | for (i = 0, len = currentTestAssertions.length; i < len; i++) { 125 | console.log(' ' + currentTestAssertions[i]); 126 | } 127 | } 128 | 129 | currentTestAssertions.length = 0; 130 | }); 131 | 132 | QUnit.done(function(result) { 133 | console.log('Took ' + result.runtime + 'ms to run ' + result.total + ' tests. ' + result.passed + ' passed, ' + result.failed + ' failed.'); 134 | 135 | if (typeof window.callPhantom === 'function') { 136 | window.callPhantom({ 137 | 'name': 'QUnit.done', 138 | 'data': result 139 | }); 140 | } 141 | }); 142 | }, false); 143 | } 144 | })(); 145 | -------------------------------------------------------------------------------- /tests/integration/specs/updating-an-individual-resource-test.js: -------------------------------------------------------------------------------- 1 | var get = Ember.get, set = Ember.set; 2 | var env; 3 | var responses, fakeServer; 4 | 5 | module('integration/specs/updating-an-individual-resource', { 6 | setup: function() { 7 | fakeServer = stubServer(); 8 | 9 | responses = { 10 | post: { 11 | data: { 12 | type: 'posts', 13 | id: '1', 14 | title: 'Rails is Omakase', 15 | links: { 16 | author: { 17 | self: '/posts/1/links/author', 18 | related: '/posts/1/author', 19 | linkage: {} 20 | } 21 | } 22 | } 23 | }, 24 | author: { 25 | data: { 26 | type: 'authors', 27 | id: '1', 28 | name: 'dhh' 29 | } 30 | }, 31 | postAfterUpdate: { 32 | data: { 33 | type: 'posts', 34 | id: '1', 35 | title: 'TDD Is Dead lol', 36 | 'post-summary': 'summary' 37 | } 38 | }, 39 | postAfterUpdateAuthor: { 40 | data: { 41 | type: 'posts', 42 | id: '1', 43 | title: 'TDD Is Dead lol', 44 | 'post-summary': 'summary', 45 | links: { 46 | author: { 47 | self: '/posts/1/links/author', 48 | related: '/posts/1/author', 49 | linkage: { 50 | type: 'authors', 51 | id: '1' 52 | } 53 | } 54 | } 55 | } 56 | } 57 | }; 58 | 59 | env = setupStore(setModels({ 60 | authorAsync: true, 61 | commentAsync: true 62 | })); 63 | env.store.modelFor('post'); 64 | env.store.modelFor('author'); 65 | env.store.modelFor('comment'); 66 | }, 67 | 68 | teardown: function() { 69 | Ember.run(env.store, 'destroy'); 70 | shutdownFakeServer(fakeServer); 71 | } 72 | }); 73 | 74 | asyncTest("PATCH /posts/1 won't push an array", function() { 75 | var request = { 76 | data: { 77 | id: '1', 78 | title: 'TDD Is Dead lol', 79 | 'post-summary': null, 80 | links: { 81 | comments: { 82 | linkage: [] 83 | } 84 | }, 85 | type: 'posts' 86 | } 87 | }; 88 | 89 | fakeServer.get('/posts/1', responses.post); 90 | fakeServer.patch('/posts/1', request, responses.postAfterUpdate); 91 | 92 | Em.run(function() { 93 | env.store.find('post', '1').then(function(post) { 94 | equal(post.get('title'), 'Rails is Omakase', 'title is correct'); 95 | post.set('title', 'TDD Is Dead lol'); 96 | post.save().then(function(record) { 97 | equal(record.get('id'), '1', 'id is correct'); 98 | equal(record.get('title'), 'TDD Is Dead lol', 'title is correct'); 99 | equal(record.get('postSummary'), 'summary', 'summary is correct'); 100 | 101 | start(); 102 | }); 103 | }); 104 | }); 105 | }); 106 | 107 | asyncTest("Update a post with an author", function() { 108 | var request = { 109 | data: { 110 | id: '1', 111 | title: 'TDD Is Dead lol', 112 | 'post-summary': null, 113 | links: { 114 | comments: { 115 | linkage: [] 116 | }, 117 | author: { 118 | linkage: { 119 | id: '1', 120 | type: 'authors' 121 | } 122 | } 123 | }, 124 | type: 'posts' 125 | } 126 | }; 127 | 128 | fakeServer.get('/posts/1', responses.post); 129 | fakeServer.get('/authors/1', responses.author); 130 | // FIXME This call shouldn't have to be made since it already exists 131 | fakeServer.get('/posts/1/author', responses.author); 132 | // FIXME Need a way to PATCH to /posts/1/links/author 133 | fakeServer.patch('/posts/1', request, responses.postAfterUpdateAuthor); 134 | 135 | Em.run(function() { 136 | var findPost = env.store.find('post', '1'), 137 | findAuthor = env.store.find('author', '1'); 138 | 139 | findPost.then(function(post) { 140 | equal(post.get('title'), 'Rails is Omakase', 'title is correct'); 141 | findAuthor.then(function(author) { 142 | equal(author.get('name'), 'dhh', 'author name is correct'); 143 | post.set('title', 'TDD Is Dead lol'); 144 | post.set('author', author); 145 | post.save().then(function(record) { 146 | equal(record.get('id'), '1', 'id is correct'); 147 | equal(record.get('title'), 'TDD Is Dead lol', 'title is correct'); 148 | equal(record.get('postSummary'), 'summary', 'summary is correct'); 149 | equal(record.get('author.id'), '1', 'author ID is correct'); 150 | equal(record.get('author.name'), 'dhh', 'author name is correct'); 151 | start(); 152 | }); 153 | }); 154 | }); 155 | }); 156 | }); 157 | -------------------------------------------------------------------------------- /dist/ember-json-api.min.js: -------------------------------------------------------------------------------- 1 | define("json-api-adapter",["exports"],function(e){"use strict";function r(e,r,t){this.status=e,this.message=r,this.xhr=t,this.stack=(new Error).stack}var t=Ember.get;DS._routes=Ember.create(null),DS.JsonApiAdapter=DS.RESTAdapter.extend({defaultSerializer:"DS/jsonApi",contentType:"application/vnd.api+json; charset=utf-8",accepts:"application/vnd.api+json, application/json, text/javascript, */*; q=0.01",ajaxOptions:function(e,r,t){var i=this._super(e,r,t);return i.data&&"GET"!==r&&(i.contentType=this.contentType),i.hasOwnProperty("headers")||(i.headers={}),i.headers.Accept=this.accepts,i},getRoute:function(e,r){return DS._routes[e]},buildURL:function(e,r,i){var n=this.getRoute(e,r,i);if(!n)return this._super(e,r,i);var a=[],s=t(this,"host"),o=this.urlPrefix(),l=/\{(.*?)\}/g;return a.push(r?l.test(n)?n.replace(l,r):n:n.replace(l,"")),o&&a.unshift(o),a=a.join("/"),!s&&a&&(a="/"+a),a},findMany:function(e,r,t,i){return this.ajax(this.buildURL(r.typeKey,t.join(","),i,"findMany"),"GET")},createRecord:function(e,r,t){var i=this._serializeData(e,r,t);return this.ajax(this.buildURL(r.typeKey),"POST",{data:i})},findBelongsTo:function(e,r,t,i){var n=r.belongsTo(i.key),a=n&&!n.record.get("currentState.isEmpty");return a?void 0:this._super(e,r,t,i)},findHasMany:function(e,r,i,n){var a=r.hasMany(n.key).filter(function(e){return!e.record.get("currentState.isEmpty")});return t(a,"length")?new Ember.RSVP.Promise(function(e,r){r()}):this._super(e,r,i,n)},updateRecord:function(e,r,i){var n=this._serializeData(e,r,i),a=t(i,"id");return this.ajax(this.buildURL(r.typeKey,a,i),"PATCH",{data:n})},_serializeData:function(e,r,t){var i=e.serializerFor(r.typeKey),n=Ember.isArray(t)?"serializeArray":"serialize",a={data:i[n](t,{includeId:!0,type:r.typeKey})};return a},_tryParseErrorResponse:function(e){try{return Ember.$.parseJSON(e)}catch(r){return"Something went wrong"}},ajaxError:function(e){var t,i=this._super(e);if(e&&"object"==typeof e){t=this._tryParseErrorResponse(e.responseText);var n={};return t&&"object"==typeof t&&void 0!==t.errors&&Ember.A(Ember.keys(t.errors)).forEach(function(e){n[Ember.String.camelize(e)]=t.errors[e]}),422===e.status?new DS.InvalidError(n):new r(e.status,t,e)}return i},pathForType:function(e){var r=Ember.String.dasherize(e);return Ember.String.pluralize(r)}}),r.prototype=Ember.create(Error.prototype),r.constructor=r,DS.JsonApiAdapter.ServerError=r,e["default"]=DS.JsonApiAdapter}),define("json-api-serializer",["exports"],function(e){"use strict";function r(e,r,t){return t?{linkage:{id:t,type:Ember.String.pluralize(r)}}:t}function t(e,r,t,i){var n,a,s=Ember.A(t.hasMany(i)).mapBy("id")||[],o=Ember.String.pluralize(r),l=[];for(n=0,a=s.length;a>n;++n)l.push({id:s[n],type:o});return{linkage:l}}function i(e){return e.type?{id:e.id,type:Ember.String.camelize(Ember.String.singularize(e.type))}:e.id}function n(e){return Ember.isEmpty(e)?null:Ember.isArray(e)?a(e):i(e)}function a(e){if(Ember.isEmpty(e))return null;var r,t,n=[];for(r=0,t=e.length;t>r;++r)n.push(i(e[r]));return n}var s=Ember.get,o=Ember.isNone,l=/(^https?:\/\/.*?)(\/.*)/;DS.JsonApiSerializer=DS.RESTSerializer.extend({primaryRecordKey:"data",sideloadedRecordsKey:"included",relationshipKey:"self",relatedResourceKey:"related",keyForAttribute:function(e){return Ember.String.dasherize(e)},keyForRelationship:function(e){return Ember.String.dasherize(e)},keyForSnapshot:function(e){return Ember.String.dasherize(e.typeKey)},normalize:function(e,r,t){var i={};for(var n in r)if("links"!==n){var a=Ember.String.camelize(n);i[a]=r[n]}else i[n]=r[n];return this._super(e,i,t)},normalizePayload:function(e){if(!e)return{};var r=e[this.primaryRecordKey];return r&&(Ember.isArray(r)?this.extractArrayData(r,e):this.extractSingleData(r,e),delete e[this.primaryRecordKey]),e.meta&&(this.extractMeta(e.meta),delete e.meta),e.links&&delete e.links,e[this.sideloadedRecordsKey]&&(this.extractSideloaded(e[this.sideloadedRecordsKey]),delete e[this.sideloadedRecordsKey]),e},extractArray:function(e,r,t,i,n){return Ember.isEmpty(t[this.primaryRecordKey])?Ember.A():this._super(e,r,t,i,n)},extractSingleData:function(e,r){e.links&&this.extractRelationships(e.links,e),r[e.type]=e,delete e.type},extractArrayData:function(e,r){var t=e.length>0?e[0].type:null,i=this;e.forEach(function(e){e.links&&i.extractRelationships(e.links,e)}),r[t]=e},extractSideloaded:function(e){var r=s(this,"store"),t={},i=this;e.forEach(function(e){var r=e.type;e.links&&i.extractRelationships(e.links,e),delete e.type,t[r]||(t[r]=[]),t[r].push(e)}),this.pushPayload(r,t)},extractRelationships:function(e,r){var t,i,a,s,o,l;r.links={};for(t in e)i=e[t],t=Ember.String.camelize(t.split(".").pop()),i&&("string"==typeof i?(i.indexOf("/")>-1?(s=i,a=null):(s=null,a=i),o=null):(o=i[this.relationshipKey],s=i[this.relatedResourceKey],a=n(i.linkage)),s&&(l=this.removeHost(s),r.links[t]=l,l.indexOf("{")>-1&&(DS._routes[t]=l.replace(/^\//,""))),a&&(r[t]=a),o&&(r.links[t+"--self"]=this.removeHost(o)));return r.links},removeHost:function(e){return e.replace(l,"$2")},serialize:function(e,r){var t=this._super(e,r);return!t.hasOwnProperty("type")&&r&&r.type&&(t.type=Ember.String.pluralize(this.keyForRelationship(r.type))),t},serializeArray:function(e,r){var t=Ember.A(),i=this;return e?(e.forEach(function(e){t.push(i.serialize(e,r))}),t):t},serializeIntoHash:function(e,r,t,i){var n=this.serialize(t,i);n.hasOwnProperty("type")||(n.type=Ember.String.pluralize(this.keyForRelationship(r.typeKey))),e[this.keyForAttribute(r.typeKey)]=n},serializeBelongsTo:function(e,t,i){var n,a,l=i.key,u=e.belongsTo(l);o(u)||(n=this.keyForSnapshot(u),a=this.keyForRelationship(l),t.links=t.links||{},t.links[a]=r(a,n,s(u,"id")))},serializeHasMany:function(e,r,i){var n=i.key,a=this.keyForRelationship(i.type.typeKey),s=this.keyForRelationship(n);"hasMany"===i.kind&&(r.links=r.links||{},r.links[s]=t(s,a,e,n))}}),e["default"]=DS.JsonApiSerializer}); -------------------------------------------------------------------------------- /src/json-api-adapter.js: -------------------------------------------------------------------------------- 1 | /* global Ember, DS */ 2 | var get = Ember.get; 3 | 4 | /** 5 | * Keep a record of routes to resources by type. 6 | */ 7 | 8 | // null prototype in es5 browsers wont allow collisions with things on the 9 | // global Object.prototype. 10 | DS._routes = Ember.create(null); 11 | 12 | DS.JsonApiAdapter = DS.RESTAdapter.extend({ 13 | defaultSerializer: 'DS/jsonApi', 14 | 15 | contentType: 'application/vnd.api+json; charset=utf-8', 16 | accepts: 'application/vnd.api+json, application/json, text/javascript, */*; q=0.01', 17 | 18 | ajaxOptions: function(url, type, options) { 19 | var hash = this._super(url, type, options); 20 | if (hash.data && type !== 'GET') { 21 | hash.contentType = this.contentType; 22 | } 23 | // Does not work 24 | //hash.accepts = this.accepts; 25 | if(!hash.hasOwnProperty('headers')) { hash.headers = {}; } 26 | hash.headers.Accept = this.accepts; 27 | return hash; 28 | }, 29 | 30 | getRoute: function(typeName, id/*, record */) { 31 | return DS._routes[typeName]; 32 | }, 33 | 34 | /** 35 | * Look up routes based on top-level links. 36 | */ 37 | buildURL: function(typeName, id, snapshot) { 38 | // FIXME If there is a record, try and look up the self link 39 | // - Need to use the function from the serializer to build the self key 40 | // TODO: this basically only works in the simplest of scenarios 41 | var route = this.getRoute(typeName, id, snapshot); 42 | if(!route) { 43 | return this._super(typeName, id, snapshot); 44 | } 45 | 46 | var url = []; 47 | var host = get(this, 'host'); 48 | var prefix = this.urlPrefix(); 49 | var param = /\{(.*?)\}/g; 50 | 51 | if (id) { 52 | if (param.test(route)) { 53 | url.push(route.replace(param, id)); 54 | } else { 55 | url.push(route); 56 | } 57 | } else { 58 | url.push(route.replace(param, '')); 59 | } 60 | 61 | if (prefix) { url.unshift(prefix); } 62 | 63 | url = url.join('/'); 64 | if (!host && url) { url = '/' + url; } 65 | 66 | return url; 67 | }, 68 | 69 | /** 70 | * Fix query URL. 71 | */ 72 | findMany: function(store, type, ids, snapshots) { 73 | return this.ajax(this.buildURL(type.typeKey, ids.join(','), snapshots, 'findMany'), 'GET'); 74 | }, 75 | 76 | /** 77 | * Cast individual record to array, 78 | * and match the root key to the route 79 | */ 80 | createRecord: function(store, type, snapshot) { 81 | var data = this._serializeData(store, type, snapshot); 82 | 83 | return this.ajax(this.buildURL(type.typeKey), 'POST', { 84 | data: data 85 | }); 86 | }, 87 | 88 | /** 89 | * Suppress additional API calls if the relationship was already loaded via an `included` section 90 | */ 91 | findBelongsTo: function(store, snapshot, url, relationship) { 92 | var belongsTo = snapshot.belongsTo(relationship.key); 93 | var belongsToLoaded = belongsTo && !belongsTo.record.get('currentState.isEmpty'); 94 | 95 | if(belongsToLoaded) { return; } 96 | 97 | return this._super(store, snapshot, url, relationship); 98 | }, 99 | 100 | /** 101 | * Suppress additional API calls if the relationship was already loaded via an `included` section 102 | */ 103 | findHasMany: function(store, snapshot, url, relationship) { 104 | var hasManyLoaded = snapshot.hasMany(relationship.key).filter(function(item) { return !item.record.get('currentState.isEmpty'); }); 105 | 106 | if(get(hasManyLoaded, 'length')) { 107 | return new Ember.RSVP.Promise(function (resolve, reject) { reject(); }); 108 | } 109 | 110 | return this._super(store, snapshot, url, relationship); 111 | }, 112 | 113 | /** 114 | * Cast individual record to array, 115 | * and match the root key to the route 116 | */ 117 | updateRecord: function(store, type, snapshot) { 118 | var data = this._serializeData(store, type, snapshot); 119 | var id = get(snapshot, 'id'); 120 | 121 | return this.ajax(this.buildURL(type.typeKey, id, snapshot), 'PATCH', { 122 | data: data 123 | }); 124 | }, 125 | 126 | _serializeData: function(store, type, snapshot) { 127 | var serializer = store.serializerFor(type.typeKey); 128 | var fn = Ember.isArray(snapshot) ? 'serializeArray' : 'serialize'; 129 | var json = { 130 | data: serializer[fn](snapshot, { includeId:true, type:type.typeKey }) 131 | }; 132 | 133 | return json; 134 | }, 135 | 136 | _tryParseErrorResponse: function(responseText) { 137 | try { 138 | return Ember.$.parseJSON(responseText); 139 | } catch(e) { 140 | return "Something went wrong"; 141 | } 142 | }, 143 | 144 | ajaxError: function(jqXHR) { 145 | var error = this._super(jqXHR); 146 | var response; 147 | 148 | if (jqXHR && typeof jqXHR === 'object') { 149 | response = this._tryParseErrorResponse(jqXHR.responseText); 150 | var errors = {}; 151 | 152 | if (response && 153 | typeof response === 'object' && 154 | response.errors !== undefined) { 155 | 156 | Ember.A(Ember.keys(response.errors)).forEach(function(key) { 157 | errors[Ember.String.camelize(key)] = response.errors[key]; 158 | }); 159 | } 160 | 161 | if (jqXHR.status === 422) { 162 | return new DS.InvalidError(errors); 163 | } else{ 164 | return new ServerError(jqXHR.status, response, jqXHR); 165 | } 166 | } else { 167 | return error; 168 | } 169 | }, 170 | 171 | pathForType: function(type) { 172 | var dasherized = Ember.String.dasherize(type); 173 | return Ember.String.pluralize(dasherized); 174 | } 175 | }); 176 | 177 | function ServerError(status, message, xhr) { 178 | this.status = status; 179 | this.message = message; 180 | this.xhr = xhr; 181 | 182 | this.stack = new Error().stack; 183 | } 184 | 185 | ServerError.prototype = Ember.create(Error.prototype); 186 | ServerError.constructor = ServerError; 187 | 188 | DS.JsonApiAdapter.ServerError = ServerError; 189 | 190 | export default DS.JsonApiAdapter; 191 | -------------------------------------------------------------------------------- /tests/integration/specs/namespace-test.js: -------------------------------------------------------------------------------- 1 | var get = Ember.get, set = Ember.set; 2 | var env; 3 | var responses, fakeServer; 4 | 5 | module('integration/specs/namespace', { 6 | setup: function() { 7 | fakeServer = stubServer(); 8 | 9 | responses = { 10 | posts1_id: { 11 | data: { 12 | type: 'posts', 13 | id: '1', 14 | title: 'Rails is Omakase', 15 | links: { 16 | author: { 17 | linkage: { 18 | type: 'authors', 19 | id: '2' 20 | } 21 | }, 22 | comments: { 23 | linkage: [{ 24 | type: 'comments', 25 | id: '2' 26 | }] 27 | } 28 | } 29 | } 30 | }, 31 | posts2_id: { 32 | data: { 33 | type: 'posts', 34 | id: '2', 35 | title: 'TDD Is Dead lol', 36 | links: { 37 | author: { 38 | linkage: { 39 | type: 'authors', 40 | id: '2' 41 | } 42 | }, 43 | comments: { 44 | linkage: [{ 45 | type: 'comments', 46 | id: '3' 47 | }] 48 | } 49 | } 50 | } 51 | }, 52 | posts1_related: { 53 | data: { 54 | type: 'posts', 55 | id: '1', 56 | title: 'Rails is Omakase', 57 | links: { 58 | author: { 59 | related: '/api/posts/1/author', 60 | linkage: { 61 | type: 'authors', 62 | id: '2' 63 | } 64 | }, 65 | comments: { 66 | related: '/api/posts/1/comments' 67 | } 68 | } 69 | } 70 | }, 71 | posts2_related: { 72 | data: { 73 | type: 'posts', 74 | id: '2', 75 | title: 'TDD Is Dead lol', 76 | links: { 77 | author: { 78 | related: '/api/posts/2/author', 79 | linkage: { 80 | type: 'authors', 81 | id: '2' 82 | } 83 | }, 84 | comments: { 85 | related: '/api/posts/2/comments' 86 | } 87 | } 88 | } 89 | }, 90 | author: { 91 | data: { 92 | type: 'authors', 93 | id: '2', 94 | name: 'dhh' 95 | } 96 | }, 97 | post1_comments: { 98 | data: [{ 99 | type: 'comments', 100 | 'id': '2', 101 | 'title': 'good article', 102 | 'body': 'ideal for my startup' 103 | }] 104 | }, 105 | post2_comments: { 106 | data: [{ 107 | type: 'comments', 108 | id: '3', 109 | title: 'bad article', 110 | body: "doesn't run Crysis" 111 | }] 112 | }, 113 | comments_2: { 114 | data: { 115 | type: 'comments', 116 | 'id': '2', 117 | 'title': 'good article', 118 | 'body': 'ideal for my startup' 119 | } 120 | }, 121 | comments_3: { 122 | data: { 123 | type: 'comments', 124 | id: '3', 125 | title: 'bad article', 126 | body: "doesn't run Crysis" 127 | } 128 | } 129 | }; 130 | 131 | env = setupStore($.extend({ 132 | adapter: DS.JsonApiAdapter.extend({ 133 | namespace: 'api' 134 | }) 135 | }, setModels({ 136 | authorAsync: true, 137 | commentAsync: true 138 | }))); 139 | env.store.modelFor('post'); 140 | env.store.modelFor('author'); 141 | env.store.modelFor('comment'); 142 | }, 143 | 144 | teardown: function() { 145 | Ember.run(env.store, 'destroy'); 146 | shutdownFakeServer(fakeServer); 147 | } 148 | }); 149 | 150 | asyncTest('GET /api/posts/1 calls with type and id to comments', function() { 151 | fakeServer.get('/api/posts/1', responses.posts1_id); 152 | fakeServer.get('/api/posts/2', responses.posts2_id); 153 | fakeServer.get('/api/authors/2', responses.author); 154 | fakeServer.get('/api/comments/2', responses.comments_2); 155 | fakeServer.get('/api/comments/3', responses.comments_3); 156 | 157 | runTests(); 158 | }); 159 | 160 | asyncTest('GET /api/posts/1 calls with related URLs', function() { 161 | fakeServer.get('/api/posts/1', responses.posts1_related); 162 | fakeServer.get('/api/posts/2', responses.posts2_related); 163 | fakeServer.get('/api/posts/1/author', responses.author); 164 | fakeServer.get('/api/posts/2/author', responses.author); 165 | fakeServer.get('/api/posts/1/comments', responses.post1_comments); 166 | fakeServer.get('/api/posts/2/comments', responses.post2_comments); 167 | runTests(); 168 | }); 169 | 170 | function runTests() { 171 | Em.run(function() { 172 | var promises = []; 173 | promises.push(testPost1()); 174 | promises.push(testPost2()); 175 | 176 | Ember.RSVP.all(promises).then(start); 177 | }); 178 | } 179 | 180 | function testPost1() { 181 | return env.store.find('post', '1').then(function(record) { 182 | equal(record.get('id'), '1', 'id is correct'); 183 | equal(record.get('title'), 'Rails is Omakase', 'title is correct'); 184 | 185 | record.get('author').then(function(author) { 186 | equal(author.get('id'), '2', 'author id is correct'); 187 | equal(author.get('name'), 'dhh', 'author name is correct'); 188 | 189 | record.get('comments').then(function(comments) { 190 | var comment = comments.objectAt(0); 191 | 192 | equal(comments.get('length'), 1, 'there is 1 comment'); 193 | 194 | equal(comment.get('title'), 'good article', 'comment1 title'); 195 | equal(comment.get('body'), 'ideal for my startup', 'comment1 body'); 196 | }); 197 | }); 198 | }); 199 | } 200 | 201 | function testPost2() { 202 | return env.store.find('post', '2').then(function(record) { 203 | equal(record.get('id'), '2', 'id is correct'); 204 | equal(record.get('title'), 'TDD Is Dead lol', 'title is correct'); 205 | 206 | record.get('author').then(function(author) { 207 | equal(author.get('id'), '2', 'author id is correct'); 208 | equal(author.get('name'), 'dhh', 'author name is correct'); 209 | 210 | record.get('comments').then(function(comments) { 211 | var comment = comments.objectAt(0); 212 | 213 | equal(comments.get('length'), 1, 'there is 1 comment'); 214 | 215 | equal(comment.get('title'), 'bad article', 'comment2 title'); 216 | equal(comment.get('body'), "doesn't run Crysis", 'comment2 body'); 217 | }); 218 | }); 219 | }); 220 | } -------------------------------------------------------------------------------- /src/json-api-serializer.js: -------------------------------------------------------------------------------- 1 | /* global Ember,DS */ 2 | var get = Ember.get; 3 | var isNone = Ember.isNone; 4 | var HOST = /(^https?:\/\/.*?)(\/.*)/; 5 | 6 | DS.JsonApiSerializer = DS.RESTSerializer.extend({ 7 | 8 | primaryRecordKey: 'data', 9 | sideloadedRecordsKey: 'included', 10 | relationshipKey: 'self', 11 | relatedResourceKey: 'related', 12 | 13 | keyForAttribute: function(key) { 14 | return Ember.String.dasherize(key); 15 | }, 16 | keyForRelationship: function(key) { 17 | return Ember.String.dasherize(key); 18 | }, 19 | keyForSnapshot: function(snapshot) { 20 | return Ember.String.dasherize(snapshot.typeKey); 21 | }, 22 | 23 | /** 24 | * Flatten links 25 | */ 26 | normalize: function(type, hash, prop) { 27 | var json = {}; 28 | for (var key in hash) { 29 | // This is already normalized 30 | if (key === 'links') { 31 | json[key] = hash[key]; 32 | continue; 33 | } 34 | 35 | var camelizedKey = Ember.String.camelize(key); 36 | json[camelizedKey] = hash[key]; 37 | } 38 | 39 | return this._super(type, json, prop); 40 | }, 41 | 42 | /** 43 | * Extract top-level "meta" & "links" before normalizing. 44 | */ 45 | normalizePayload: function(payload) { 46 | if(!payload) { return {}; } 47 | var data = payload[this.primaryRecordKey]; 48 | if (data) { 49 | if(Ember.isArray(data)) { 50 | this.extractArrayData(data, payload); 51 | } else { 52 | this.extractSingleData(data, payload); 53 | } 54 | delete payload[this.primaryRecordKey]; 55 | } 56 | if (payload.meta) { 57 | this.extractMeta(payload.meta); 58 | delete payload.meta; 59 | } 60 | if (payload.links) { 61 | // FIXME Need to handle top level links, like pagination 62 | //this.extractRelationships(payload.links, payload); 63 | delete payload.links; 64 | } 65 | if (payload[this.sideloadedRecordsKey]) { 66 | this.extractSideloaded(payload[this.sideloadedRecordsKey]); 67 | delete payload[this.sideloadedRecordsKey]; 68 | } 69 | 70 | return payload; 71 | }, 72 | 73 | extractArray: function(store, type, arrayPayload, id, requestType) { 74 | if(Ember.isEmpty(arrayPayload[this.primaryRecordKey])) { return Ember.A(); } 75 | return this._super(store, type, arrayPayload, id, requestType); 76 | }, 77 | 78 | /** 79 | * Extract top-level "data" containing a single primary data 80 | */ 81 | extractSingleData: function(data, payload) { 82 | if(data.links) { 83 | this.extractRelationships(data.links, data); 84 | //delete data.links; 85 | } 86 | payload[data.type] = data; 87 | delete data.type; 88 | }, 89 | 90 | /** 91 | * Extract top-level "data" containing a single primary data 92 | */ 93 | extractArrayData: function(data, payload) { 94 | var type = data.length > 0 ? data[0].type : null; 95 | var serializer = this; 96 | data.forEach(function(item) { 97 | if(item.links) { 98 | serializer.extractRelationships(item.links, item); 99 | //delete data.links; 100 | } 101 | }); 102 | 103 | payload[type] = data; 104 | }, 105 | 106 | /** 107 | * Extract top-level "included" containing associated objects 108 | */ 109 | extractSideloaded: function(sideloaded) { 110 | var store = get(this, 'store'); 111 | var models = {}; 112 | var serializer = this; 113 | 114 | sideloaded.forEach(function(link) { 115 | var type = link.type; 116 | if(link.links) { 117 | serializer.extractRelationships(link.links, link); 118 | } 119 | delete link.type; 120 | if(!models[type]) { 121 | models[type] = []; 122 | } 123 | models[type].push(link); 124 | }); 125 | 126 | this.pushPayload(store, models); 127 | }, 128 | 129 | /** 130 | * Parse the top-level "links" object. 131 | */ 132 | extractRelationships: function(links, resource) { 133 | var link, association, id, route, relationshipLink, cleanedRoute; 134 | 135 | // Clear the old format 136 | resource.links = {}; 137 | 138 | for (link in links) { 139 | association = links[link]; 140 | link = Ember.String.camelize(link.split('.').pop()); 141 | if(!association) { continue; } 142 | if (typeof association === 'string') { 143 | if (association.indexOf('/') > -1) { 144 | route = association; 145 | id = null; 146 | } else { // This is no longer valid in JSON API. Potentially remove. 147 | route = null; 148 | id = association; 149 | } 150 | relationshipLink = null; 151 | } else { 152 | relationshipLink = association[this.relationshipKey]; 153 | route = association[this.relatedResourceKey]; 154 | id = getLinkageId(association.linkage); 155 | } 156 | 157 | if (route) { 158 | cleanedRoute = this.removeHost(route); 159 | resource.links[link] = cleanedRoute; 160 | 161 | // Need clarification on how this is used 162 | if(cleanedRoute.indexOf('{') > -1) { 163 | DS._routes[link] = cleanedRoute.replace(/^\//, ''); 164 | } 165 | } 166 | if(id) { 167 | resource[link] = id; 168 | } 169 | if(relationshipLink) { 170 | resource.links[link + '--self'] = this.removeHost(relationshipLink); 171 | } 172 | } 173 | return resource.links; 174 | }, 175 | 176 | removeHost: function(url) { 177 | return url.replace(HOST, '$2'); 178 | }, 179 | 180 | // SERIALIZATION 181 | 182 | serialize: function(snapshot, options) { 183 | var data = this._super(snapshot, options); 184 | if(!data.hasOwnProperty('type') && options && options.type) { 185 | data.type = Ember.String.pluralize(this.keyForRelationship(options.type)); 186 | } 187 | return data; 188 | }, 189 | 190 | serializeArray: function(snapshots, options) { 191 | var data = Ember.A(); 192 | var serializer = this; 193 | if(!snapshots) { return data; } 194 | snapshots.forEach(function(snapshot) { 195 | data.push(serializer.serialize(snapshot, options)); 196 | }); 197 | return data; 198 | }, 199 | 200 | serializeIntoHash: function(hash, type, snapshot, options) { 201 | var data = this.serialize(snapshot, options); 202 | if(!data.hasOwnProperty('type')) { 203 | data.type = Ember.String.pluralize(this.keyForRelationship(type.typeKey)); 204 | } 205 | hash[this.keyForAttribute(type.typeKey)] = data; 206 | }, 207 | 208 | /** 209 | * Use "links" key, remove support for polymorphic type 210 | */ 211 | serializeBelongsTo: function(record, json, relationship) { 212 | var attr = relationship.key; 213 | var belongsTo = record.belongsTo(attr); 214 | var type, key; 215 | 216 | if (isNone(belongsTo)) { return; } 217 | 218 | type = this.keyForSnapshot(belongsTo); 219 | key = this.keyForRelationship(attr); 220 | 221 | json.links = json.links || {}; 222 | json.links[key] = belongsToLink(key, type, get(belongsTo, 'id')); 223 | }, 224 | 225 | /** 226 | * Use "links" key 227 | */ 228 | serializeHasMany: function(record, json, relationship) { 229 | var attr = relationship.key; 230 | var type = this.keyForRelationship(relationship.type.typeKey); 231 | var key = this.keyForRelationship(attr); 232 | 233 | if (relationship.kind === 'hasMany') { 234 | json.links = json.links || {}; 235 | json.links[key] = hasManyLink(key, type, record, attr); 236 | } 237 | } 238 | }); 239 | 240 | function belongsToLink(key, type, value) { 241 | if(!value) { return value; } 242 | 243 | return { 244 | linkage: { 245 | id: value, 246 | type: Ember.String.pluralize(type) 247 | } 248 | }; 249 | } 250 | 251 | function hasManyLink(key, type, record, attr) { 252 | var links = Ember.A(record.hasMany(attr)).mapBy('id') || []; 253 | var typeName = Ember.String.pluralize(type); 254 | var linkages = []; 255 | var index, total; 256 | 257 | for(index=0, total=links.length; index 0 ? data[0].type : null; 293 | var serializer = this; 294 | data.forEach(function(item) { 295 | if(item.links) { 296 | serializer.extractRelationships(item.links, item); 297 | //delete data.links; 298 | } 299 | }); 300 | 301 | payload[type] = data; 302 | }, 303 | 304 | /** 305 | * Extract top-level "included" containing associated objects 306 | */ 307 | extractSideloaded: function(sideloaded) { 308 | var store = get(this, 'store'); 309 | var models = {}; 310 | var serializer = this; 311 | 312 | sideloaded.forEach(function(link) { 313 | var type = link.type; 314 | if(link.links) { 315 | serializer.extractRelationships(link.links, link); 316 | } 317 | delete link.type; 318 | if(!models[type]) { 319 | models[type] = []; 320 | } 321 | models[type].push(link); 322 | }); 323 | 324 | this.pushPayload(store, models); 325 | }, 326 | 327 | /** 328 | * Parse the top-level "links" object. 329 | */ 330 | extractRelationships: function(links, resource) { 331 | var link, association, id, route, relationshipLink, cleanedRoute; 332 | 333 | // Clear the old format 334 | resource.links = {}; 335 | 336 | for (link in links) { 337 | association = links[link]; 338 | link = Ember.String.camelize(link.split('.').pop()); 339 | if(!association) { continue; } 340 | if (typeof association === 'string') { 341 | if (association.indexOf('/') > -1) { 342 | route = association; 343 | id = null; 344 | } else { // This is no longer valid in JSON API. Potentially remove. 345 | route = null; 346 | id = association; 347 | } 348 | relationshipLink = null; 349 | } else { 350 | relationshipLink = association[this.relationshipKey]; 351 | route = association[this.relatedResourceKey]; 352 | id = getLinkageId(association.linkage); 353 | } 354 | 355 | if (route) { 356 | cleanedRoute = this.removeHost(route); 357 | resource.links[link] = cleanedRoute; 358 | 359 | // Need clarification on how this is used 360 | if(cleanedRoute.indexOf('{') > -1) { 361 | DS._routes[link] = cleanedRoute.replace(/^\//, ''); 362 | } 363 | } 364 | if(id) { 365 | resource[link] = id; 366 | } 367 | if(relationshipLink) { 368 | resource.links[link + '--self'] = this.removeHost(relationshipLink); 369 | } 370 | } 371 | return resource.links; 372 | }, 373 | 374 | removeHost: function(url) { 375 | return url.replace(HOST, '$2'); 376 | }, 377 | 378 | // SERIALIZATION 379 | 380 | serialize: function(snapshot, options) { 381 | var data = this._super(snapshot, options); 382 | if(!data.hasOwnProperty('type') && options && options.type) { 383 | data.type = Ember.String.pluralize(this.keyForRelationship(options.type)); 384 | } 385 | return data; 386 | }, 387 | 388 | serializeArray: function(snapshots, options) { 389 | var data = Ember.A(); 390 | var serializer = this; 391 | if(!snapshots) { return data; } 392 | snapshots.forEach(function(snapshot) { 393 | data.push(serializer.serialize(snapshot, options)); 394 | }); 395 | return data; 396 | }, 397 | 398 | serializeIntoHash: function(hash, type, snapshot, options) { 399 | var data = this.serialize(snapshot, options); 400 | if(!data.hasOwnProperty('type')) { 401 | data.type = Ember.String.pluralize(this.keyForRelationship(type.typeKey)); 402 | } 403 | hash[this.keyForAttribute(type.typeKey)] = data; 404 | }, 405 | 406 | /** 407 | * Use "links" key, remove support for polymorphic type 408 | */ 409 | serializeBelongsTo: function(record, json, relationship) { 410 | var attr = relationship.key; 411 | var belongsTo = record.belongsTo(attr); 412 | var type, key; 413 | 414 | if (isNone(belongsTo)) { return; } 415 | 416 | type = this.keyForSnapshot(belongsTo); 417 | key = this.keyForRelationship(attr); 418 | 419 | json.links = json.links || {}; 420 | json.links[key] = belongsToLink(key, type, get(belongsTo, 'id')); 421 | }, 422 | 423 | /** 424 | * Use "links" key 425 | */ 426 | serializeHasMany: function(record, json, relationship) { 427 | var attr = relationship.key; 428 | var type = this.keyForRelationship(relationship.type.typeKey); 429 | var key = this.keyForRelationship(attr); 430 | 431 | if (relationship.kind === 'hasMany') { 432 | json.links = json.links || {}; 433 | json.links[key] = hasManyLink(key, type, record, attr); 434 | } 435 | } 436 | }); 437 | 438 | function belongsToLink(key, type, value) { 439 | if(!value) { return value; } 440 | 441 | return { 442 | linkage: { 443 | id: value, 444 | type: Ember.String.pluralize(type) 445 | } 446 | }; 447 | } 448 | 449 | function hasManyLink(key, type, record, attr) { 450 | var links = Ember.A(record.hasMany(attr)).mapBy('id') || []; 451 | var typeName = Ember.String.pluralize(type); 452 | var linkages = []; 453 | var index, total; 454 | 455 | for(index=0, total=links.length; index