├── .bowerrc ├── .gitignore ├── .travis.yml ├── Brocfile.js ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── bower.json ├── dist ├── ember-json-api.js └── ember-json-api.min.js ├── ember-addon-main.js ├── package.json ├── src ├── json-api-adapter.js └── json-api-serializer.js ├── testem.json ├── tests ├── helpers │ ├── begin.js │ ├── pretender.js │ ├── qunit-setup.js │ ├── setup-models.js │ ├── setup-polymorphic-models.js │ └── setup-store.js ├── index.html ├── integration │ ├── serializer-test.js │ └── specs │ │ ├── compound-documents-test.js │ │ ├── creating-an-individual-resource-test.js │ │ ├── href-link-for-resource-collection-test.js │ │ ├── individual-resource-representations-test.js │ │ ├── link-with-type.js │ │ ├── multiple-resource-links-test.js │ │ ├── namespace-test.js │ │ ├── null-relationship-test.js │ │ ├── resource-collection-representations-test.js │ │ ├── to-many-polymorphic-test.js │ │ ├── to-many-relationships-test.js │ │ ├── to-one-relationships-test.js │ │ ├── updating-an-individual-resource-test.js │ │ └── urls-for-resource-collections-test.js ├── runner.js └── unit │ ├── adapter │ ├── ajax-error-test.js │ └── build-url-test.js │ └── serializer │ └── extract-links-test.js └── vendor └── no-loader.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components" 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 | -------------------------------------------------------------------------------- /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.5.1 4 | 5 | * Updated to JSON API 1.0 Spec 6 | 7 | ### 0.5.0 8 | 9 | * Updated to ember data v1.0.0.beta-19 10 | 11 | ### 0.4.4 12 | 13 | * 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: 14 | 15 | ``` 16 | export default JsonApiSerializer.extend({ 17 | keyForAttribute: function(key) { 18 | return Ember.String.camelize(key); 19 | }, 20 | keyForRelationship: function(key) { 21 | return Ember.String.camelize(key); 22 | }, 23 | keyForSnapshot: function(snapshot) { 24 | return Ember.String.camelize(snapshot.typeKey); 25 | } 26 | }); 27 | ``` 28 | 29 | * Made dasherized the default naming convention for path types. To change, override 30 | 31 | ``` 32 | export default JsonApiAdapter.extend({ 33 | pathForType: function(type) { 34 | var decamelized = Ember.String.decamelize(type); 35 | return Ember.String.pluralize(decamelized); 36 | } 37 | }); 38 | ``` 39 | 40 | ### 0.4.3 41 | 42 | * Replace PUT verb with PATCH. This is a breaking change for some and can be overridden in the application adapter with the following: 43 | 44 | ``` 45 | ajaxOptions: function(url, type, options) { 46 | var methodType = (type === 'PATCH') ? 'PUT' : type; 47 | return this._super(url, methodType, options); 48 | } 49 | ``` 50 | 51 | ### 0.4.2 52 | 53 | * updating to [JSON API RC3](https://github.com/json-api/json-api/blob/827ba3c1130408fdb406d9faab645b0db7dd4fe4/index.md) with the usage of the consistent linkage format. 54 | * Added polymorphic support. 55 | 56 | ### 0.4.1 57 | 58 | * keeping up with JSON API RC2+ to change linked to included and resource to related. 59 | 60 | ### 0.4.0 61 | 62 | * updating to JSON API RC2 63 | 64 | ### 0.3.0 65 | 66 | * removes deprecation warning because of DS' snapshots 67 | * stops overriding `extractMeta` 68 | * FIX: inside a serializer, reuses the same current store instead of relying on 69 | defaultSerializer. This is a fix for apps that use multiple stores. 70 | * FIX: covers null associations 71 | * BREAKING: all keys are camelized, so now define your camelize your model 72 | attributes 73 | * BREAKING: Ember 1.0.0-beta.15 support 74 | 75 | ### 0.2.0 76 | 77 | * ensures that both singular and plural root keys work. #30 78 | * PUT for a single resource won't send array of resources. #30 79 | * createRecord for a single resource won't POST array of resources. #30 80 | * builds URLs with underscores (was building with camelCase before). 81 | * a bunch of tests 82 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ember-json-api 2 | 3 | > **Deprecation Notice:** This addon was developed to add support for JSON API in Ember Data. 4 | > With the release of Ember Data v1.13 support for JSON API was built-in, making this addon unnecessary. 5 | > Since Ember Data now offers a similar feature set, we have decided to deprecate this addon. 6 | 7 | > For more information on how to use the offical Ember Data solution see the [Ember 1.13 release notes](http://emberjs.com/blog/2015/06/18/ember-data-1-13-released.html). 8 | 9 | ![](https://travis-ci.org/kurko/ember-json-api.svg?branch=master) 10 | 11 | This is a [JSON API](http://jsonapi.org) adapter for [Ember Data](http://github.com/emberjs/data) 1.0 beta 19, that extends the built-in REST adapter. Please note that Ember Data and JSON API are both works in progress, use with caution. 12 | 13 | This supports JSON API v1.0. 14 | 15 | ### Specification coverage 16 | 17 | To see details on how much of the JSONAPI.org spec this adapter covers, read the 18 | tests under `tests/integration/specs/`. Each field tests one section of the 19 | standard. 20 | 21 | ### Usage 22 | 23 | To install: 24 | 25 | ``` 26 | npm install --save-dev ember-json-api 27 | ``` 28 | 29 | Next, define the adapter and serializer: 30 | 31 | ```js 32 | // app/adapters/application.js 33 | import JsonApiAdapter from 'ember-json-api/json-api-adapter'; 34 | export default JsonApiAdapter; 35 | 36 | // app/serializers/application.js 37 | import JsonApiSerializer from 'ember-json-api/json-api-serializer'; 38 | export default JsonApiSerializer; 39 | ``` 40 | 41 | ### Tests & Build 42 | 43 | First, install depdendencies with `npm install && bower install`. Then run 44 | `npm run serve` and visit `http://localhost:4200/tests`. 45 | 46 | If you prefer, use `npm run test` in your terminal, which will run tests 47 | without a browser. You need to have PhantomJS installed. 48 | 49 | To build a new version, just run `npm run build`. The build will be 50 | available in the `dist/` directory. 51 | 52 | ### Issues 53 | 54 | - This adapter has preliminary support for URL-style JSON API. It currently 55 | only serializes one route per type, so if you have multiple ways to get a 56 | resource, it will not work as expected. 57 | 58 | ### Thanks 59 | 60 | A huge thanks goes to [Dali Zheng](https://github.com/daliwali) who initially 61 | maintained the adapter. 62 | 63 | ### License 64 | 65 | This code abides to the MIT license: http://opensource.org/licenses/MIT 66 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-json-api", 3 | "version": "0.6.0", 4 | "homepage": "https://github.com/kurko/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.19", 19 | "ember": "v1.12.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 | -------------------------------------------------------------------------------- /dist/ember-json-api.js: -------------------------------------------------------------------------------- 1 | define("json-api-adapter", 2 | ["exports"], 3 | function(__exports__) { 4 | "use strict"; 5 | /* global Ember, DS */ 6 | var get = Ember.get; 7 | 8 | /** 9 | * Keep a record of routes to resources by type. 10 | */ 11 | 12 | // null prototype in es5 browsers wont allow collisions with things on the 13 | // global Object.prototype. 14 | DS._routes = Ember.create(null); 15 | 16 | DS.JsonApiAdapter = DS.RESTAdapter.extend({ 17 | defaultSerializer: 'DS/jsonApi', 18 | 19 | contentType: 'application/vnd.api+json; charset=utf-8', 20 | accepts: 'application/vnd.api+json, application/json, text/javascript, */*; q=0.01', 21 | 22 | ajaxOptions: function(url, type, options) { 23 | var hash = this._super(url, type, options); 24 | if (hash.data && type !== 'GET') { 25 | hash.contentType = this.contentType; 26 | } 27 | // Does not work 28 | //hash.accepts = this.accepts; 29 | if(!hash.hasOwnProperty('headers')) { 30 | hash.headers = {}; 31 | } 32 | 33 | hash.headers.Accept = this.accepts; 34 | return hash; 35 | }, 36 | 37 | getRoute: function(typeName, id/*, record */) { 38 | return DS._routes[typeName]; 39 | }, 40 | 41 | /** 42 | * Look up routes based on top-level links. 43 | */ 44 | buildURL: function(typeName, id, snapshot) { 45 | // FIXME If there is a record, try and look up the self link 46 | // - Need to use the function from the serializer to build the self key 47 | // TODO: this basically only works in the simplest of scenarios 48 | var route = this.getRoute(typeName, id, snapshot); 49 | if(!route) { 50 | return this._super(typeName, id, snapshot); 51 | } 52 | 53 | var url = []; 54 | var host = get(this, 'host'); 55 | var prefix = this.urlPrefix(); 56 | var param = /\{(.*?)\}/g; 57 | 58 | if (id) { 59 | if (param.test(route)) { 60 | url.push(route.replace(param, id)); 61 | } else { 62 | url.push(route); 63 | } 64 | } else { 65 | url.push(route.replace(param, '')); 66 | } 67 | 68 | if (prefix) { 69 | url.unshift(prefix); 70 | } 71 | 72 | url = url.join('/'); 73 | 74 | if (!host && url) { 75 | url = '/' + url; 76 | } 77 | 78 | return url; 79 | }, 80 | 81 | /** 82 | * Fix query URL. 83 | */ 84 | findMany: function(store, type, ids, snapshots) { 85 | return this.ajax(this.buildURL(type.modelName, ids.join(','), snapshots, 'findMany'), 'GET'); 86 | }, 87 | 88 | /** 89 | * Cast individual record to array, 90 | * and match the root key to the route 91 | */ 92 | createRecord: function(store, type, snapshot) { 93 | var data = this._serializeData(store, type, snapshot); 94 | 95 | return this.ajax(this.buildURL(type.modelName), 'POST', { 96 | data: data 97 | }); 98 | }, 99 | 100 | /** 101 | * Suppress additional API calls if the relationship was already loaded via an `included` section 102 | */ 103 | findBelongsTo: function(store, snapshot, url, relationship) { 104 | var belongsTo = snapshot.belongsTo(relationship.key); 105 | var belongsToLoaded = belongsTo && !belongsTo.record.get('_internalModel.currentState.isEmpty'); 106 | 107 | if(belongsToLoaded) { 108 | return; 109 | } 110 | 111 | return this._super(store, snapshot, url, relationship); 112 | }, 113 | 114 | /** 115 | * Suppress additional API calls if the relationship was already loaded via an `included` section 116 | */ 117 | findHasMany: function(store, snapshot, url, relationship) { 118 | var hasManyLoaded = snapshot.hasMany(relationship.key); 119 | 120 | if (hasManyLoaded) { 121 | hasManyLoaded = hasManyLoaded.filter(function(item) { 122 | return !item.record.get('_internalModel.currentState.isEmpty'); 123 | }); 124 | 125 | if (get(hasManyLoaded, 'length')) { 126 | return new Ember.RSVP.Promise(function(resolve, reject) { 127 | reject(); 128 | }); 129 | } 130 | } 131 | 132 | return this._super(store, snapshot, url, relationship); 133 | }, 134 | 135 | /** 136 | * Cast individual record to array, 137 | * and match the root key to the route 138 | */ 139 | updateRecord: function(store, type, snapshot) { 140 | var data = this._serializeData(store, type, snapshot); 141 | if (data.data.links) { 142 | delete data.data.links; 143 | } 144 | var id = get(snapshot, 'id'); 145 | return this.ajax(this.buildURL(type.modelName, id, snapshot), 'PATCH', { 146 | data: data 147 | }); 148 | }, 149 | 150 | _serializeData: function(store, type, snapshot) { 151 | var serializer = store.serializerFor(type.modelName); 152 | var fn = Ember.isArray(snapshot) ? 'serializeArray' : 'serialize'; 153 | var json = { 154 | data: serializer[fn](snapshot, { includeId: true, type:type.modelName }) 155 | }; 156 | 157 | return json; 158 | }, 159 | 160 | _tryParseErrorResponse: function(responseText) { 161 | try { 162 | return Ember.$.parseJSON(responseText); 163 | } catch (e) { 164 | return 'Something went wrong'; 165 | } 166 | }, 167 | 168 | ajaxError: function(jqXHR) { 169 | var error = this._super(jqXHR); 170 | var response; 171 | 172 | if (jqXHR && typeof jqXHR === 'object') { 173 | response = this._tryParseErrorResponse(jqXHR.responseText); 174 | var errors = {}; 175 | 176 | if (response && 177 | typeof response === 'object' && 178 | response.errors !== undefined) { 179 | 180 | Ember.A(Ember.keys(response.errors)).forEach(function(key) { 181 | errors[Ember.String.camelize(key)] = response.errors[key]; 182 | }); 183 | } 184 | 185 | if (jqXHR.status === 422) { 186 | return new DS.InvalidError(errors); 187 | } else{ 188 | return new ServerError(jqXHR.status, error.statusText || response, jqXHR); 189 | } 190 | } else { 191 | return error; 192 | } 193 | }, 194 | 195 | pathForType: function(type) { 196 | var dasherized = Ember.String.dasherize(type); 197 | return Ember.String.pluralize(dasherized); 198 | } 199 | }); 200 | 201 | function ServerError(status, message, xhr) { 202 | this.status = status; 203 | this.message = message; 204 | this.xhr = xhr; 205 | 206 | this.stack = new Error().stack; 207 | } 208 | 209 | ServerError.prototype = Ember.create(Error.prototype); 210 | ServerError.constructor = ServerError; 211 | 212 | DS.JsonApiAdapter.ServerError = ServerError; 213 | 214 | __exports__["default"] = DS.JsonApiAdapter; 215 | });define("json-api-serializer", 216 | ["exports"], 217 | function(__exports__) { 218 | "use strict"; 219 | /* global Ember,DS */ 220 | var get = Ember.get; 221 | var isNone = Ember.isNone; 222 | var HOST = /(^https?:\/\/.*?)(\/.*)/; 223 | 224 | DS.JsonApiSerializer = DS.RESTSerializer.extend({ 225 | 226 | primaryRecordKey: 'data', 227 | sideloadedRecordsKey: 'included', 228 | relationshipKey: 'self', 229 | relatedResourceKey: 'related', 230 | 231 | keyForAttribute: function(key) { 232 | return Ember.String.dasherize(key); 233 | }, 234 | keyForRelationship: function(key) { 235 | return Ember.String.dasherize(key); 236 | }, 237 | keyForSnapshot: function(snapshot) { 238 | return snapshot.modelName; 239 | }, 240 | 241 | /** 242 | * Flatten links 243 | */ 244 | normalize: function(type, hash, prop) { 245 | var json = {}; 246 | for (var key in hash) { 247 | // This is already normalized 248 | if (key === 'relationships') { 249 | json[key] = hash[key]; 250 | continue; 251 | } 252 | 253 | if (key === 'attributes') { 254 | for (var attributeKey in hash[key]) { 255 | var camelizedKey = Ember.String.camelize(attributeKey); 256 | json[camelizedKey] = hash[key][attributeKey]; 257 | } 258 | continue; 259 | } 260 | var camelizedKey = Ember.String.camelize(key); 261 | json[camelizedKey] = hash[key]; 262 | } 263 | 264 | return this._super(type, json, prop); 265 | }, 266 | 267 | /** 268 | * Extract top-level "meta" & "links" before normalizing. 269 | */ 270 | normalizePayload: function(payload) { 271 | if(!payload) { 272 | return {}; 273 | } 274 | 275 | var data = payload[this.primaryRecordKey]; 276 | if (data) { 277 | if (Ember.isArray(data)) { 278 | this.extractArrayData(data, payload); 279 | } else { 280 | this.extractSingleData(data, payload); 281 | } 282 | delete payload[this.primaryRecordKey]; 283 | } 284 | if (payload.meta) { 285 | this.extractMeta(payload.meta); 286 | delete payload.meta; 287 | } 288 | if (payload.links) { 289 | // FIXME Need to handle top level links, like pagination 290 | delete payload.links; 291 | } 292 | if (payload[this.sideloadedRecordsKey]) { 293 | this.extractSideloaded(payload[this.sideloadedRecordsKey]); 294 | delete payload[this.sideloadedRecordsKey]; 295 | } 296 | 297 | return payload; 298 | }, 299 | 300 | extractArray: function(store, type, arrayPayload, id, requestType) { 301 | if (Ember.isEmpty(arrayPayload[this.primaryRecordKey])) { 302 | return Ember.A(); 303 | } 304 | return this._super(store, type, arrayPayload, id, requestType); 305 | }, 306 | 307 | /** 308 | * Extract top-level "data" containing a single primary data 309 | */ 310 | extractSingleData: function(data, payload) { 311 | if (data.relationships) { 312 | this.extractRelationships(data.relationships, data); 313 | } 314 | payload[data.type] = data; 315 | delete data.type; 316 | }, 317 | 318 | /** 319 | * Extract top-level "data" containing a single primary data 320 | */ 321 | extractArrayData: function(data, payload) { 322 | var type = data.length > 0 ? data[0].type : null; 323 | var serializer = this; 324 | data.forEach(function(item) { 325 | if(item.relationships) { 326 | serializer.extractRelationships(item.relationships, item); 327 | } 328 | }); 329 | 330 | payload[type] = data; 331 | }, 332 | 333 | /** 334 | * Extract top-level "included" containing associated objects 335 | */ 336 | extractSideloaded: function(sideloaded) { 337 | var store = get(this, 'store'); 338 | var models = {}; 339 | var serializer = this; 340 | 341 | sideloaded.forEach(function(link) { 342 | var type = link.type; 343 | if (link.relationships) { 344 | serializer.extractRelationships(link.relationships, link); 345 | } 346 | delete link.type; 347 | if (!models[type]) { 348 | models[type] = []; 349 | } 350 | models[type].push(link); 351 | }); 352 | 353 | this.pushPayload(store, models); 354 | }, 355 | 356 | /** 357 | * Parse the top-level "links" object. 358 | */ 359 | extractRelationships: function(links, resource) { 360 | var link, association, id, route, relationshipLink, cleanedRoute; 361 | 362 | // Clear the old format 363 | resource.links = {}; 364 | 365 | for (link in links) { 366 | association = links[link]; 367 | link = Ember.String.camelize(link.split('.').pop()); 368 | 369 | if(!association) { 370 | continue; 371 | } 372 | 373 | if (typeof association === 'string') { 374 | if (association.indexOf('/') > -1) { 375 | route = association; 376 | id = null; 377 | } else { // This is no longer valid in JSON API. Potentially remove. 378 | route = null; 379 | id = association; 380 | } 381 | relationshipLink = null; 382 | } else { 383 | if (association.links) { 384 | relationshipLink = association.links[this.relationshipKey]; 385 | route = association.links[this.relatedResourceKey]; 386 | } 387 | id = getLinkageId(association.data); 388 | } 389 | 390 | if (route) { 391 | cleanedRoute = this.removeHost(route); 392 | resource.links[link] = cleanedRoute; 393 | 394 | // Need clarification on how this is used 395 | if (cleanedRoute.indexOf('{') > -1) { 396 | DS._routes[link] = cleanedRoute.replace(/^\//, ''); 397 | } 398 | } 399 | if (id) { 400 | resource[link] = id; 401 | } 402 | } 403 | return resource.links; 404 | }, 405 | 406 | removeHost: function(url) { 407 | return url.replace(HOST, '$2'); 408 | }, 409 | 410 | // SERIALIZATION 411 | 412 | serialize: function(snapshot, options) { 413 | var data = this._super(snapshot, options); 414 | var type = (options ? options.type : null) || snapshot.modelName; 415 | data['attributes'] = {}; 416 | for (var key in data) { 417 | if (key === 'links' || key === 'attributes' || key === 'id' || key === 'type' || key === 'relationships') { 418 | if (key === 'links') { 419 | if (!data.relationships) { 420 | data.relationships = {}; 421 | } 422 | for (var k in data[key]) { 423 | data.relationships[k] = data[key][k]; 424 | } 425 | delete data.links; 426 | } 427 | continue; 428 | } 429 | data['attributes'][key] = data[key]; 430 | delete data[key]; 431 | } 432 | if (!data.hasOwnProperty('type') && type) { 433 | data.type = Ember.String.pluralize(this.keyForRelationship(type)); 434 | } 435 | return data; 436 | }, 437 | 438 | serializeArray: function(snapshots, options) { 439 | var data = Ember.A(); 440 | var serializer = this; 441 | 442 | if(!snapshots) { 443 | return data; 444 | } 445 | 446 | snapshots.forEach(function(snapshot) { 447 | data.push(serializer.serialize(snapshot, options)); 448 | }); 449 | return data; 450 | }, 451 | 452 | serializeIntoHash: function(hash, type, snapshot, options) { 453 | var data = this.serialize(snapshot, options); 454 | if (!data.hasOwnProperty('type')) { 455 | data.type = Ember.String.pluralize(this.keyForRelationship(type.modelName)); 456 | } 457 | hash[this.keyForAttribute(type.modelName)] = data; 458 | }, 459 | 460 | /** 461 | * Use "links" key, remove support for polymorphic type 462 | */ 463 | serializeBelongsTo: function(record, json, relationship) { 464 | var attr = relationship.key; 465 | var belongsTo = record.belongsTo(attr); 466 | var type, key; 467 | 468 | if (isNone(belongsTo)) { 469 | return; 470 | } 471 | 472 | type = this.keyForSnapshot(belongsTo); 473 | key = this.keyForRelationship(attr); 474 | 475 | if (!json.links) { 476 | json.links = json.relationships || {}; 477 | } 478 | json.links[key] = belongsToLink(key, type, get(belongsTo, 'id')); 479 | }, 480 | 481 | /** 482 | * Use "links" key 483 | */ 484 | serializeHasMany: function(record, json, relationship) { 485 | var attr = relationship.key; 486 | var type = this.keyForRelationship(relationship.type); 487 | var key = this.keyForRelationship(attr); 488 | 489 | if (relationship.kind === 'hasMany') { 490 | json.relationships = json.relationships || {}; 491 | json.relationships[key] = hasManyLink(key, type, record, attr); 492 | } 493 | } 494 | }); 495 | 496 | function belongsToLink(key, type, value) { 497 | if (!value) { 498 | return value; 499 | } 500 | 501 | return { 502 | data: { 503 | id: value, 504 | type: Ember.String.pluralize(type) 505 | } 506 | }; 507 | } 508 | 509 | function hasManyLink(key, type, record, attr) { 510 | var links = Ember.A(record.hasMany(attr)).mapBy('id') || []; 511 | var typeName = Ember.String.pluralize(type); 512 | var linkages = []; 513 | var index, total; 514 | 515 | for (index = 0, total = links.length; index < total; ++index) { 516 | linkages.push({ 517 | id: links[index], 518 | type: typeName 519 | }); 520 | } 521 | 522 | return { data: linkages }; 523 | } 524 | 525 | function normalizeLinkage(linkage) { 526 | if (!linkage.type) { 527 | return linkage.id; 528 | } 529 | 530 | return { 531 | id: linkage.id, 532 | type: Ember.String.camelize(Ember.String.singularize(linkage.type)) 533 | }; 534 | } 535 | function getLinkageId(linkage) { 536 | if (Ember.isEmpty(linkage)) { 537 | return null; 538 | } 539 | 540 | return (Ember.isArray(linkage)) ? getLinkageIds(linkage) : normalizeLinkage(linkage); 541 | } 542 | function getLinkageIds(linkage) { 543 | if (Ember.isEmpty(linkage)) { 544 | return null; 545 | } 546 | 547 | var ids = []; 548 | var index, total; 549 | for (index = 0, total = linkage.length; index < total; ++index) { 550 | ids.push(normalizeLinkage(linkage[index])); 551 | } 552 | return ids; 553 | } 554 | 555 | __exports__["default"] = DS.JsonApiSerializer; 556 | }); -------------------------------------------------------------------------------- /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 r?l.test(n)?a.push(n.replace(l,r)):a.push(n):a.push(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.modelName,t.join(","),i,"findMany"),"GET")},createRecord:function(e,r,t){var i=this._serializeData(e,r,t);return this.ajax(this.buildURL(r.modelName),"POST",{data:i})},findBelongsTo:function(e,r,t,i){var n=r.belongsTo(i.key),a=n&&!n.record.get("_internalModel.currentState.isEmpty");return a?void 0:this._super(e,r,t,i)},findHasMany:function(e,r,i,n){var a=r.hasMany(n.key);return a&&(a=a.filter(function(e){return!e.record.get("_internalModel.currentState.isEmpty")}),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);n.data.links&&delete n.data.links;var a=t(i,"id");return this.ajax(this.buildURL(r.modelName,a,i),"PATCH",{data:n})},_serializeData:function(e,r,t){var i=e.serializerFor(r.modelName),n=Ember.isArray(t)?"serializeArray":"serialize",a={data:i[n](t,{includeId:!0,type:r.modelName})};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,i.statusText||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?{data:{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{data: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 e.modelName},normalize:function(e,r,t){var i={};for(var n in r)if("relationships"!==n)if("attributes"!==n){var a=Ember.String.camelize(n);i[a]=r[n]}else for(var s in r[n]){var a=Ember.String.camelize(s);i[a]=r[n][s]}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.relationships&&this.extractRelationships(e.relationships,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.relationships&&i.extractRelationships(e.relationships,e)}),r[t]=e},extractSideloaded:function(e){var r=s(this,"store"),t={},i=this;e.forEach(function(e){var r=e.type;e.relationships&&i.extractRelationships(e.relationships,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):(i.links&&(o=i.links[this.relationshipKey],s=i.links[this.relatedResourceKey]),a=n(i.data)),s&&(l=this.removeHost(s),r.links[t]=l,l.indexOf("{")>-1&&(DS._routes[t]=l.replace(/^\//,""))),a&&(r[t]=a));return r.links},removeHost:function(e){return e.replace(l,"$2")},serialize:function(e,r){var t=this._super(e,r),i=(r?r.type:null)||e.modelName;t.attributes={};for(var n in t)if("links"!==n&&"attributes"!==n&&"id"!==n&&"type"!==n&&"relationships"!==n)t.attributes[n]=t[n],delete t[n];else if("links"===n){t.relationships||(t.relationships={});for(var a in t[n])t.relationships[a]=t[n][a];delete t.links}return!t.hasOwnProperty("type")&&i&&(t.type=Ember.String.pluralize(this.keyForRelationship(i))),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.modelName))),e[this.keyForAttribute(r.modelName)]=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.relationships||{}),t.links[a]=r(a,n,s(u,"id")))},serializeHasMany:function(e,r,i){var n=i.key,a=this.keyForRelationship(i.type),s=this.keyForRelationship(n);"hasMany"===i.kind&&(r.relationships=r.relationships||{},r.relationships[s]=t(s,a,e,n))}}),e["default"]=DS.JsonApiSerializer}); -------------------------------------------------------------------------------- /ember-addon-main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'name': 'ember-json-api', 3 | 4 | init: function () { 5 | this.treePaths.addon = 'src'; 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-json-api", 3 | "description": "Ember Data adapter for JSON API.", 4 | "version": "0.6.0", 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": "ember-addon-main.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 | -------------------------------------------------------------------------------- /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')) { 26 | hash.headers = {}; 27 | } 28 | 29 | hash.headers.Accept = this.accepts; 30 | return hash; 31 | }, 32 | 33 | getRoute: function(typeName, id/*, record */) { 34 | return DS._routes[typeName]; 35 | }, 36 | 37 | /** 38 | * Look up routes based on top-level links. 39 | */ 40 | buildURL: function(typeName, id, snapshot) { 41 | // FIXME If there is a record, try and look up the self link 42 | // - Need to use the function from the serializer to build the self key 43 | // TODO: this basically only works in the simplest of scenarios 44 | var route = this.getRoute(typeName, id, snapshot); 45 | if(!route) { 46 | return this._super(typeName, id, snapshot); 47 | } 48 | 49 | var url = []; 50 | var host = get(this, 'host'); 51 | var prefix = this.urlPrefix(); 52 | var param = /\{(.*?)\}/g; 53 | 54 | if (id) { 55 | if (param.test(route)) { 56 | url.push(route.replace(param, id)); 57 | } else { 58 | url.push(route); 59 | } 60 | } else { 61 | url.push(route.replace(param, '')); 62 | } 63 | 64 | if (prefix) { 65 | url.unshift(prefix); 66 | } 67 | 68 | url = url.join('/'); 69 | 70 | if (!host && url) { 71 | url = '/' + url; 72 | } 73 | 74 | return url; 75 | }, 76 | 77 | /** 78 | * Fix query URL. 79 | */ 80 | findMany: function(store, type, ids, snapshots) { 81 | return this.ajax(this.buildURL(type.modelName, ids.join(','), snapshots, 'findMany'), 'GET'); 82 | }, 83 | 84 | /** 85 | * Cast individual record to array, 86 | * and match the root key to the route 87 | */ 88 | createRecord: function(store, type, snapshot) { 89 | var data = this._serializeData(store, type, snapshot); 90 | 91 | return this.ajax(this.buildURL(type.modelName), 'POST', { 92 | data: data 93 | }); 94 | }, 95 | 96 | /** 97 | * Suppress additional API calls if the relationship was already loaded via an `included` section 98 | */ 99 | findBelongsTo: function(store, snapshot, url, relationship) { 100 | var belongsTo = snapshot.belongsTo(relationship.key); 101 | var belongsToLoaded = belongsTo && !belongsTo.record.get('_internalModel.currentState.isEmpty'); 102 | 103 | if(belongsToLoaded) { 104 | return; 105 | } 106 | 107 | return this._super(store, snapshot, url, relationship); 108 | }, 109 | 110 | /** 111 | * Suppress additional API calls if the relationship was already loaded via an `included` section 112 | */ 113 | findHasMany: function(store, snapshot, url, relationship) { 114 | var hasManyLoaded = snapshot.hasMany(relationship.key); 115 | 116 | if (hasManyLoaded) { 117 | hasManyLoaded = hasManyLoaded.filter(function(item) { 118 | return !item.record.get('_internalModel.currentState.isEmpty'); 119 | }); 120 | 121 | if (get(hasManyLoaded, 'length')) { 122 | return new Ember.RSVP.Promise(function(resolve, reject) { 123 | reject(); 124 | }); 125 | } 126 | } 127 | 128 | return this._super(store, snapshot, url, relationship); 129 | }, 130 | 131 | /** 132 | * Cast individual record to array, 133 | * and match the root key to the route 134 | */ 135 | updateRecord: function(store, type, snapshot) { 136 | var data = this._serializeData(store, type, snapshot); 137 | if (data.data.links) { 138 | delete data.data.links; 139 | } 140 | var id = get(snapshot, 'id'); 141 | return this.ajax(this.buildURL(type.modelName, id, snapshot), 'PATCH', { 142 | data: data 143 | }); 144 | }, 145 | 146 | _serializeData: function(store, type, snapshot) { 147 | var serializer = store.serializerFor(type.modelName); 148 | var fn = Ember.isArray(snapshot) ? 'serializeArray' : 'serialize'; 149 | var json = { 150 | data: serializer[fn](snapshot, { includeId: true, type:type.modelName }) 151 | }; 152 | 153 | return json; 154 | }, 155 | 156 | _tryParseErrorResponse: function(responseText) { 157 | try { 158 | return Ember.$.parseJSON(responseText); 159 | } catch (e) { 160 | return 'Something went wrong'; 161 | } 162 | }, 163 | 164 | ajaxError: function(jqXHR) { 165 | var error = this._super(jqXHR); 166 | var response; 167 | 168 | if (jqXHR && typeof jqXHR === 'object') { 169 | response = this._tryParseErrorResponse(jqXHR.responseText); 170 | var errors = {}; 171 | 172 | if (response && 173 | typeof response === 'object' && 174 | response.errors !== undefined) { 175 | 176 | Ember.A(Ember.keys(response.errors)).forEach(function(key) { 177 | errors[Ember.String.camelize(key)] = response.errors[key]; 178 | }); 179 | } 180 | 181 | if (jqXHR.status === 422) { 182 | return new DS.InvalidError(errors); 183 | } else{ 184 | return new ServerError(jqXHR.status, error.statusText || response, jqXHR); 185 | } 186 | } else { 187 | return error; 188 | } 189 | }, 190 | 191 | pathForType: function(type) { 192 | var dasherized = Ember.String.dasherize(type); 193 | return Ember.String.pluralize(dasherized); 194 | } 195 | }); 196 | 197 | function ServerError(status, message, xhr) { 198 | this.status = status; 199 | this.message = message; 200 | this.xhr = xhr; 201 | 202 | this.stack = new Error().stack; 203 | } 204 | 205 | ServerError.prototype = Ember.create(Error.prototype); 206 | ServerError.constructor = ServerError; 207 | 208 | DS.JsonApiAdapter.ServerError = ServerError; 209 | 210 | export default DS.JsonApiAdapter; 211 | -------------------------------------------------------------------------------- /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 snapshot.modelName; 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 === 'relationships') { 31 | json[key] = hash[key]; 32 | continue; 33 | } 34 | 35 | if (key === 'attributes') { 36 | for (var attributeKey in hash[key]) { 37 | var camelizedKey = Ember.String.camelize(attributeKey); 38 | json[camelizedKey] = hash[key][attributeKey]; 39 | } 40 | continue; 41 | } 42 | var camelizedKey = Ember.String.camelize(key); 43 | json[camelizedKey] = hash[key]; 44 | } 45 | 46 | return this._super(type, json, prop); 47 | }, 48 | 49 | /** 50 | * Extract top-level "meta" & "links" before normalizing. 51 | */ 52 | normalizePayload: function(payload) { 53 | if(!payload) { 54 | return {}; 55 | } 56 | 57 | var data = payload[this.primaryRecordKey]; 58 | if (data) { 59 | if (Ember.isArray(data)) { 60 | this.extractArrayData(data, payload); 61 | } else { 62 | this.extractSingleData(data, payload); 63 | } 64 | delete payload[this.primaryRecordKey]; 65 | } 66 | if (payload.meta) { 67 | this.extractMeta(payload.meta); 68 | delete payload.meta; 69 | } 70 | if (payload.links) { 71 | // FIXME Need to handle top level links, like pagination 72 | delete payload.links; 73 | } 74 | if (payload[this.sideloadedRecordsKey]) { 75 | this.extractSideloaded(payload[this.sideloadedRecordsKey]); 76 | delete payload[this.sideloadedRecordsKey]; 77 | } 78 | 79 | return payload; 80 | }, 81 | 82 | extractArray: function(store, type, arrayPayload, id, requestType) { 83 | if (Ember.isEmpty(arrayPayload[this.primaryRecordKey])) { 84 | return Ember.A(); 85 | } 86 | return this._super(store, type, arrayPayload, id, requestType); 87 | }, 88 | 89 | /** 90 | * Extract top-level "data" containing a single primary data 91 | */ 92 | extractSingleData: function(data, payload) { 93 | if (data.relationships) { 94 | this.extractRelationships(data.relationships, data); 95 | } 96 | payload[data.type] = data; 97 | delete data.type; 98 | }, 99 | 100 | /** 101 | * Extract top-level "data" containing a single primary data 102 | */ 103 | extractArrayData: function(data, payload) { 104 | var type = data.length > 0 ? data[0].type : null; 105 | var serializer = this; 106 | data.forEach(function(item) { 107 | if(item.relationships) { 108 | serializer.extractRelationships(item.relationships, item); 109 | } 110 | }); 111 | 112 | payload[type] = data; 113 | }, 114 | 115 | /** 116 | * Extract top-level "included" containing associated objects 117 | */ 118 | extractSideloaded: function(sideloaded) { 119 | var store = get(this, 'store'); 120 | var models = {}; 121 | var serializer = this; 122 | 123 | sideloaded.forEach(function(link) { 124 | var type = link.type; 125 | if (link.relationships) { 126 | serializer.extractRelationships(link.relationships, link); 127 | } 128 | delete link.type; 129 | if (!models[type]) { 130 | models[type] = []; 131 | } 132 | models[type].push(link); 133 | }); 134 | 135 | this.pushPayload(store, models); 136 | }, 137 | 138 | /** 139 | * Parse the top-level "links" object. 140 | */ 141 | extractRelationships: function(links, resource) { 142 | var link, association, id, route, relationshipLink, cleanedRoute; 143 | 144 | // Clear the old format 145 | resource.links = {}; 146 | 147 | for (link in links) { 148 | association = links[link]; 149 | link = Ember.String.camelize(link.split('.').pop()); 150 | 151 | if(!association) { 152 | continue; 153 | } 154 | 155 | if (typeof association === 'string') { 156 | if (association.indexOf('/') > -1) { 157 | route = association; 158 | id = null; 159 | } else { // This is no longer valid in JSON API. Potentially remove. 160 | route = null; 161 | id = association; 162 | } 163 | relationshipLink = null; 164 | } else { 165 | if (association.links) { 166 | relationshipLink = association.links[this.relationshipKey]; 167 | route = association.links[this.relatedResourceKey]; 168 | } 169 | id = getLinkageId(association.data); 170 | } 171 | 172 | if (route) { 173 | cleanedRoute = this.removeHost(route); 174 | resource.links[link] = cleanedRoute; 175 | 176 | // Need clarification on how this is used 177 | if (cleanedRoute.indexOf('{') > -1) { 178 | DS._routes[link] = cleanedRoute.replace(/^\//, ''); 179 | } 180 | } 181 | if (id) { 182 | resource[link] = id; 183 | } 184 | } 185 | return resource.links; 186 | }, 187 | 188 | removeHost: function(url) { 189 | return url.replace(HOST, '$2'); 190 | }, 191 | 192 | // SERIALIZATION 193 | 194 | serialize: function(snapshot, options) { 195 | var data = this._super(snapshot, options); 196 | var type = (options ? options.type : null) || snapshot.modelName; 197 | data['attributes'] = {}; 198 | for (var key in data) { 199 | if (key === 'links' || key === 'attributes' || key === 'id' || key === 'type' || key === 'relationships') { 200 | if (key === 'links') { 201 | if (!data.relationships) { 202 | data.relationships = {}; 203 | } 204 | for (var k in data[key]) { 205 | data.relationships[k] = data[key][k]; 206 | } 207 | delete data.links; 208 | } 209 | continue; 210 | } 211 | data['attributes'][key] = data[key]; 212 | delete data[key]; 213 | } 214 | if (!data.hasOwnProperty('type') && type) { 215 | data.type = Ember.String.pluralize(this.keyForRelationship(type)); 216 | } 217 | return data; 218 | }, 219 | 220 | serializeArray: function(snapshots, options) { 221 | var data = Ember.A(); 222 | var serializer = this; 223 | 224 | if(!snapshots) { 225 | return data; 226 | } 227 | 228 | snapshots.forEach(function(snapshot) { 229 | data.push(serializer.serialize(snapshot, options)); 230 | }); 231 | return data; 232 | }, 233 | 234 | serializeIntoHash: function(hash, type, snapshot, options) { 235 | var data = this.serialize(snapshot, options); 236 | if (!data.hasOwnProperty('type')) { 237 | data.type = Ember.String.pluralize(this.keyForRelationship(type.modelName)); 238 | } 239 | hash[this.keyForAttribute(type.modelName)] = data; 240 | }, 241 | 242 | /** 243 | * Use "links" key, remove support for polymorphic type 244 | */ 245 | serializeBelongsTo: function(record, json, relationship) { 246 | var attr = relationship.key; 247 | var belongsTo = record.belongsTo(attr); 248 | var type, key; 249 | 250 | if (isNone(belongsTo)) { 251 | return; 252 | } 253 | 254 | type = this.keyForSnapshot(belongsTo); 255 | key = this.keyForRelationship(attr); 256 | 257 | if (!json.links) { 258 | json.links = json.relationships || {}; 259 | } 260 | json.links[key] = belongsToLink(key, type, get(belongsTo, 'id')); 261 | }, 262 | 263 | /** 264 | * Use "links" key 265 | */ 266 | serializeHasMany: function(record, json, relationship) { 267 | var attr = relationship.key; 268 | var type = this.keyForRelationship(relationship.type); 269 | var key = this.keyForRelationship(attr); 270 | 271 | if (relationship.kind === 'hasMany') { 272 | json.relationships = json.relationships || {}; 273 | json.relationships[key] = hasManyLink(key, type, record, attr); 274 | } 275 | } 276 | }); 277 | 278 | function belongsToLink(key, type, value) { 279 | if (!value) { 280 | return value; 281 | } 282 | 283 | return { 284 | data: { 285 | id: value, 286 | type: Ember.String.pluralize(type) 287 | } 288 | }; 289 | } 290 | 291 | function hasManyLink(key, type, record, attr) { 292 | var links = Ember.A(record.hasMany(attr)).mapBy('id') || []; 293 | var typeName = Ember.String.pluralize(type); 294 | var linkages = []; 295 | var index, total; 296 | 297 | for (index = 0, total = links.length; index < total; ++index) { 298 | linkages.push({ 299 | id: links[index], 300 | type: typeName 301 | }); 302 | } 303 | 304 | return { data: linkages }; 305 | } 306 | 307 | function normalizeLinkage(linkage) { 308 | if (!linkage.type) { 309 | return linkage.id; 310 | } 311 | 312 | return { 313 | id: linkage.id, 314 | type: Ember.String.camelize(Ember.String.singularize(linkage.type)) 315 | }; 316 | } 317 | function getLinkageId(linkage) { 318 | if (Ember.isEmpty(linkage)) { 319 | return null; 320 | } 321 | 322 | return (Ember.isArray(linkage)) ? getLinkageIds(linkage) : normalizeLinkage(linkage); 323 | } 324 | function getLinkageIds(linkage) { 325 | if (Ember.isEmpty(linkage)) { 326 | return null; 327 | } 328 | 329 | var ids = []; 330 | var index, total; 331 | for (index = 0, total = linkage.length; index < total; ++index) { 332 | ids.push(normalizeLinkage(linkage[index])); 333 | } 334 | return ids; 335 | } 336 | 337 | export default DS.JsonApiSerializer; 338 | -------------------------------------------------------------------------------- /testem.json: -------------------------------------------------------------------------------- 1 | { 2 | "test_page": "tests/index.html" 3 | } 4 | -------------------------------------------------------------------------------- /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/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 sortString = function(s) { 75 | var c = []; 76 | var l = s.length; 77 | for (var i = 0; i < l; i++) { 78 | c.push(s[i]); 79 | } 80 | return c.sort().join(''); 81 | }; 82 | var respectiveResponse; 83 | var availableRequests = this.availableRequests[verb]; 84 | var actualRequest = sortString(JSON.stringify(JSON.parse(currentRequest.requestBody))); 85 | 86 | for (requests in availableRequests) { 87 | if (!availableRequests.hasOwnProperty(requests)) 88 | continue; 89 | 90 | var request = sortString(JSON.stringify(availableRequests[requests].request)); 91 | var response = JSON.stringify(availableRequests[requests].response); 92 | 93 | if (request === actualRequest) { 94 | respectiveResponse = availableRequests[requests].response; 95 | break; 96 | } 97 | } 98 | 99 | if (respectiveResponse) { 100 | return respectiveResponse; 101 | } else { 102 | var error = "No response defined for "+verb+" request"; 103 | console.error(error, JSON.stringify(JSON.parse(currentRequest.requestBody))); 104 | 105 | if (availableRequests.length) { 106 | console.log("Current defined requests:"); 107 | for (requests in availableRequests) { 108 | if (!availableRequests.hasOwnProperty(requests)) 109 | continue; 110 | 111 | console.log(JSON.stringify(availableRequests[requests].request)); 112 | } 113 | } 114 | 115 | throw(error); 116 | } 117 | }, 118 | 119 | validatePayload: function(response, verb, url) { 120 | if (!response) { 121 | var string = "No request or response defined for "+verb+" "+url; 122 | console.warn(string); 123 | throw(string); 124 | } 125 | } 126 | }; 127 | } 128 | 129 | var shutdownFakeServer = function(fakeServer) { 130 | fakeServer.pretender.shutdown(); 131 | DS._routes = Ember.create(null); 132 | } 133 | -------------------------------------------------------------------------------- /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-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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/helpers/setup-store.js: -------------------------------------------------------------------------------- 1 | window.setupStore = function(options) { 2 | var container, registry; 3 | var env = {}; 4 | options = options || {}; 5 | 6 | if (Ember.Registry) { 7 | registry = env.registry = new Ember.Registry(); 8 | container = env.container = registry.container(); 9 | } else { 10 | container = env.container = new Ember.Container(); 11 | registry = env.registry = container; 12 | } 13 | 14 | var adapter = env.adapter = options.adapter || DS.JsonApiAdapter; 15 | var serializer = env.serializer = options.serializer || DS.JsonApiSerializer; 16 | 17 | delete options.adapter; 18 | delete options.serializer; 19 | 20 | for (var prop in options) { 21 | registry.register('model:' + Ember.String.dasherize(prop), options[prop]); 22 | } 23 | 24 | registry.register('adapter:-custom', adapter); 25 | registry.register('store:main', DS.Store.extend({ 26 | adapter: '-custom' 27 | })); 28 | 29 | registry.register('serializer:application', serializer); 30 | 31 | registry.injection('serializer', 'store', 'store:main'); 32 | 33 | env.serializer = container.lookup('serializer:application'); 34 | env.store = container.lookup('store:main'); 35 | env.adapter = env.store.get('defaultAdapter'); 36 | 37 | return env; 38 | }; -------------------------------------------------------------------------------- /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/serializer-test.js: -------------------------------------------------------------------------------- 1 | var get = Ember.get, set = Ember.set; 2 | var HomePlanet, league, SuperVillain, superVillain, Minion, EvilMinion, YellowMinion, MaleMinion, FemaleMinion, env; 3 | module('integration/ember-json-api-adapter - serializer', { 4 | setup: function() { 5 | SuperVillain = DS.Model.extend({ 6 | firstName: DS.attr('string'), 7 | lastName: DS.attr('string'), 8 | homePlanet: DS.belongsTo('homePlanet'), 9 | evilMinions: DS.hasMany('evilMinion') 10 | }); 11 | 12 | MegaVillain = DS.Model.extend({ 13 | firstName: DS.attr('string'), 14 | lastName: DS.attr('string'), 15 | minions: DS.hasMany('blueMinion') 16 | }); 17 | 18 | HomePlanet = DS.Model.extend({ 19 | name: DS.attr('string'), 20 | superVillains: DS.hasMany('superVillain', { async: true }) 21 | }); 22 | 23 | Minion = DS.Model.extend({ 24 | name: DS.attr('string') 25 | }); 26 | 27 | EvilMinion = Minion.extend({ 28 | superVillain: DS.belongsTo('superVillain') 29 | }); 30 | 31 | YellowMinion = EvilMinion.extend(); 32 | BlueMinion = DS.Model.extend({ 33 | superVillain: DS.belongsTo('megaVillain') 34 | }); 35 | 36 | MaleMinion = Minion.extend({ 37 | wife: DS.belongsTo('femaleMinion', {inverse: 'husband'}), 38 | spouse: DS.belongsTo('minion', {polymorphic: true}) 39 | }); 40 | 41 | FemaleMinion = Minion.extend({ 42 | husband: DS.belongsTo('maleMinion', {inverse: 'wife'}) 43 | }); 44 | 45 | env = setupStore({ 46 | superVillain: SuperVillain, 47 | megaVillain: MegaVillain, 48 | homePlanet: HomePlanet, 49 | minion: Minion, 50 | evilMinion: EvilMinion, 51 | yellowMinion: YellowMinion, 52 | blueMinion: BlueMinion, 53 | maleMinion: MaleMinion, 54 | femaleMinion: FemaleMinion 55 | }); 56 | 57 | env.store.modelFor('superVillain'); 58 | env.store.modelFor('homePlanet'); 59 | env.store.modelFor('evilMinion'); 60 | env.store.modelFor('yellowMinion'); 61 | }, 62 | 63 | teardown: function() { 64 | Ember.run(env.store, 'destroy'); 65 | } 66 | }); 67 | 68 | test('serialize dasherized', function() { 69 | var tom; 70 | 71 | Ember.run(function() { 72 | league = env.store.createRecord('homePlanet', { 73 | name: 'Villain League', 74 | id: '123' 75 | }); 76 | 77 | tom = env.store.createRecord('superVillain', { 78 | id: '666', 79 | firstName: 'Tom', 80 | lastName: 'Dale', 81 | homePlanet: league 82 | }); 83 | }); 84 | 85 | var json = Ember.run(function() { 86 | var snapshot = tom._createSnapshot(); 87 | return env.serializer.serialize(snapshot, { includeId: true, type: 'super-villian' }); 88 | }); 89 | 90 | deepEqual(json, { 91 | id: '666', 92 | type: 'super-villians', 93 | attributes: { 94 | 'first-name': 'Tom', 95 | 'last-name': 'Dale' 96 | }, 97 | relationships: { 98 | 'evil-minions': { 99 | data: [] 100 | }, 101 | 'home-planet': { 102 | data: { 103 | id: get(league, 'id'), 104 | type: 'home-planets' 105 | } 106 | } 107 | } 108 | }); 109 | }); 110 | 111 | test('serialize camelcase', function() { 112 | var tom; 113 | 114 | env.serializer.keyForAttribute = function(key) { 115 | return Ember.String.camelize(key); 116 | }; 117 | 118 | env.serializer.keyForRelationship = function(key, relationshipKind) { 119 | return Ember.String.camelize(key); 120 | }; 121 | 122 | env.serializer.keyForSnapshot = function(snapshot) { 123 | return Ember.String.camelize(snapshot.modelName); 124 | }; 125 | 126 | Ember.run(function() { 127 | league = env.store.createRecord('homePlanet', { 128 | name: 'Villain League', 129 | id: '123' 130 | }); 131 | 132 | tom = env.store.createRecord('superVillain', { 133 | firstName: 'Tom', 134 | lastName: 'Dale', 135 | homePlanet: league 136 | }); 137 | }); 138 | 139 | var json = Ember.run(function(){ 140 | var snapshot = tom._createSnapshot(); 141 | return env.serializer.serialize(snapshot); 142 | }); 143 | 144 | deepEqual(json, { 145 | type: 'superVillains', 146 | attributes: { 147 | firstName: 'Tom', 148 | lastName: 'Dale' 149 | }, 150 | relationships: { 151 | evilMinions: { 152 | data: [] 153 | }, 154 | homePlanet: { 155 | data: { 156 | id: get(league, 'id'), 157 | type: 'homePlanets' 158 | } 159 | } 160 | } 161 | }); 162 | }); 163 | 164 | test('serialize into snake_case', function() { 165 | var tom; 166 | 167 | Ember.run(function() { 168 | league = env.store.createRecord('homePlanet', { 169 | name: 'Villain League', 170 | id: '123' 171 | }); 172 | 173 | tom = env.store.createRecord('superVillain', { 174 | firstName: 'Tom', 175 | lastName: 'Dale', 176 | homePlanet: league 177 | }); 178 | }); 179 | 180 | env.serializer.keyForAttribute = function(key) { 181 | return Ember.String.underscore(key); 182 | }; 183 | 184 | env.serializer.keyForRelationship = function(key, relationshipKind) { 185 | return Ember.String.underscore(key); 186 | }; 187 | 188 | env.serializer.keyForSnapshot = function(snapshot) { 189 | return Ember.String.underscore(snapshot.modelName); 190 | }; 191 | 192 | var json = Ember.run(function() { 193 | var snapshot = tom._createSnapshot(); 194 | return env.serializer.serialize(snapshot); 195 | }); 196 | 197 | deepEqual(json, { 198 | type: 'super_villains', 199 | attributes: { 200 | first_name: 'Tom', 201 | last_name: 'Dale' 202 | }, 203 | relationships: { 204 | evil_minions: { 205 | data: [] 206 | }, 207 | home_planet: { 208 | data: { 209 | id: get(league, 'id'), 210 | type: 'home_planets' 211 | } 212 | } 213 | } 214 | }); 215 | }); 216 | 217 | test('serializeIntoHash', function() { 218 | var actual = {}; 219 | 220 | Ember.run(function(){ 221 | var league = env.store.createRecord('homePlanet', { 222 | name: 'Umber', 223 | id: '123' 224 | }); 225 | 226 | var snapshot = league._createSnapshot(); 227 | env.serializer.serializeIntoHash(actual, HomePlanet, snapshot); 228 | }); 229 | 230 | var expected = { 231 | 'home-planet': { 232 | type: 'home-planets', 233 | relationships: { 234 | 'super-villains': { 235 | data: [] 236 | } 237 | }, 238 | attributes: { 239 | 'name': 'Umber' 240 | } 241 | } 242 | }; 243 | 244 | deepEqual(actual, expected); 245 | }); 246 | 247 | test('serializeIntoHash with decamelized types', function() { 248 | HomePlanet.modelName = 'home-planet'; 249 | var json = {}; 250 | 251 | Ember.run(function() { 252 | league = env.store.createRecord('homePlanet', { 253 | name: 'Umber', 254 | id: '123' 255 | }); 256 | 257 | var snapshot = league._createSnapshot(); 258 | env.serializer.serializeIntoHash(json, HomePlanet, snapshot); 259 | }); 260 | 261 | deepEqual(json, { 262 | 'home-planet': { 263 | attributes: { 264 | name: 'Umber' 265 | }, 266 | relationships: { 267 | 'super-villains': { 268 | data: [] 269 | } 270 | }, 271 | type: 'home-planets' 272 | } 273 | }); 274 | }); 275 | 276 | test('serialize has many relationships', function() { 277 | var minime, minime2, drevil; 278 | 279 | Ember.run(function() { 280 | drevil = env.store.createRecord('megaVillain', { 281 | firstName: 'Dr', 282 | lastName: 'Evil' 283 | }); 284 | 285 | minime = env.store.createRecord('blueMinion', { 286 | id: '123', 287 | name: 'Mini me', 288 | superVillain: drevil 289 | }); 290 | 291 | minime2 = env.store.createRecord('blueMinion', { 292 | id: '345', 293 | name: 'Mini me 2', 294 | superVillain: drevil 295 | }); 296 | }); 297 | 298 | var json = Ember.run(function() { 299 | var snapshot = drevil._createSnapshot(); 300 | return env.serializer.serialize(snapshot); 301 | }); 302 | 303 | deepEqual(json, { 304 | type: 'mega-villains', 305 | attributes: { 306 | 'first-name': 'Dr', 307 | 'last-name': 'Evil' 308 | }, 309 | relationships: { 310 | minions: { 311 | data: [{ 312 | id: '123', 313 | type: 'blue-minions' 314 | }, { 315 | id: '345', 316 | type: 'blue-minions' 317 | }] 318 | } 319 | } 320 | }); 321 | }); 322 | 323 | test('serialize belongs to relationships', function() { 324 | var male, female; 325 | 326 | Ember.run(function() { 327 | // Of course they belong to each other 328 | female = env.store.createRecord('femaleMinion', { 329 | name: 'Bobbie Sue' 330 | }); 331 | male = env.store.createRecord('maleMinion', { 332 | id: 2, 333 | wife: female 334 | }); 335 | }); 336 | 337 | var json = Ember.run(function() { 338 | var snapshot = female._createSnapshot(); 339 | return env.serializer.serialize(snapshot); 340 | }); 341 | 342 | deepEqual(json, { 343 | type: 'female-minions', 344 | relationships: { 345 | husband: { 346 | data: { 347 | id: '2', 348 | type: 'male-minions' 349 | } 350 | } 351 | }, 352 | attributes: { 353 | name: 'Bobbie Sue' 354 | } 355 | }); 356 | }); 357 | 358 | test('serialize polymorphic belongs to relationships', function() { 359 | var male, female; 360 | 361 | Ember.run(function() { 362 | // Of course they belong to each other 363 | female = env.store.createRecord('femaleMinion', { 364 | id: 1, 365 | name: 'Bobbie Sue' 366 | }); 367 | male = env.store.createRecord('maleMinion', { 368 | id: 2, 369 | spouse: female, 370 | name: 'Billy Joe' 371 | }); 372 | }); 373 | 374 | var json = Ember.run(function(){ 375 | var snapshot = male._createSnapshot(); 376 | return env.serializer.serialize(snapshot); 377 | }); 378 | 379 | deepEqual(json, { 380 | type: 'male-minions', 381 | relationships: { 382 | spouse: { 383 | data: { 384 | id: '1', 385 | type: 'female-minions' 386 | } 387 | } 388 | }, 389 | attributes: { 390 | name: 'Billy Joe' 391 | } 392 | }); 393 | }); 394 | 395 | test('extractSingle snake_case', function() { 396 | env.registry.register('adapter:superVillain', DS.ActiveModelAdapter); 397 | 398 | var json_hash = { 399 | data: { 400 | id: '1', 401 | name: 'Umber', 402 | links: { 403 | super_villains: { 404 | linkage: [{ 405 | id: 1, 406 | type: 'super_villains' 407 | }] 408 | } 409 | }, 410 | type: 'home_planets' 411 | }, 412 | included: [{ 413 | id: '1', 414 | first_name: 'Tom', 415 | last_name: 'Dale', 416 | links: { 417 | home_planet: { 418 | linkage: { 419 | id: '1', 420 | type: 'home_planets' 421 | } 422 | } 423 | }, 424 | type: 'super_villains' 425 | }] 426 | }; 427 | 428 | env.serializer.keyForAttribute = function(key) { 429 | return Ember.String.decamelize(key); 430 | }; 431 | 432 | env.serializer.keyForRelationship = function(key, relationshipKind) { 433 | return Ember.String.decamelize(key); 434 | }; 435 | 436 | Ember.run(function() { 437 | return env.serializer.extractSingle(env.store, HomePlanet, json_hash); 438 | }); 439 | 440 | env.store.find('superVillain', 1).then(function(minion) { 441 | equal(minion.get('firstName'), 'Tom'); 442 | }); 443 | }); 444 | 445 | test('extractSingle camelCase', function() { 446 | env.registry.register('adapter:superVillain', DS.ActiveModelAdapter); 447 | 448 | var json_hash = { 449 | data: { 450 | id: '1', 451 | name: 'Umber', 452 | links: { 453 | super_villains: { 454 | linkage: [{ 455 | id: 1, 456 | type: 'super_villains' 457 | }] 458 | } 459 | }, 460 | type: 'home_planets' 461 | }, 462 | included: [{ 463 | id: '1', 464 | first_name: 'Tom', 465 | last_name: 'Dale', 466 | links: { 467 | home_planet: { 468 | linkage: { 469 | id: '1', 470 | type: 'home_planets' 471 | } 472 | } 473 | }, 474 | type: 'super_villains' 475 | }] 476 | }; 477 | 478 | Ember.run(function() { 479 | return env.serializer.extractSingle(env.store, HomePlanet, json_hash); 480 | }); 481 | 482 | env.store.find('superVillain', 1).then(function(minion) { 483 | equal(minion.get('firstName'), 'Tom'); 484 | }); 485 | }); 486 | 487 | test('extractArray snake_case', function() { 488 | env.registry.register('adapter:superVillain', DS.ActiveModelAdapter); 489 | 490 | var json_hash = { 491 | data: [{ 492 | id: '1', 493 | name: 'Umber', 494 | links: { 495 | super_villains: { 496 | linkage: [{ 497 | id: 1, 498 | type: 'super_villains' 499 | }] 500 | } 501 | }, 502 | type: 'home_planets' 503 | }], 504 | included: [{ 505 | id: '1', 506 | first_name: 'Tom', 507 | last_name: 'Dale', 508 | links: { 509 | home_planet: { 510 | linkage: { 511 | id: '1', 512 | type: 'home_planet' 513 | } 514 | } 515 | }, 516 | type: 'super_villains' 517 | }] 518 | }; 519 | 520 | env.serializer.keyForAttribute = function(key) { 521 | return Ember.String.decamelize(key); 522 | }; 523 | 524 | env.serializer.keyForRelationship = function(key, relationshipKind) { 525 | return Ember.String.decamelize(key); 526 | }; 527 | 528 | Ember.run(function() { 529 | env.serializer.extractArray(env.store, HomePlanet, json_hash); 530 | }); 531 | 532 | env.store.find('superVillain', 1).then(function(minion) { 533 | equal(minion.get('firstName'), 'Tom'); 534 | }); 535 | }); 536 | // TODO: test something that utilizes the flattening of links in normalize 537 | 538 | test('extractArray', function() { 539 | env.registry.register('adapter:superVillain', DS.ActiveModelAdapter); 540 | 541 | var json_hash = { 542 | data: [{ 543 | id: '1', 544 | name: 'Umber', 545 | links: { 546 | super_villains: { 547 | linkage: [{ 548 | id: 1, 549 | type: 'super_villains' 550 | }] 551 | } 552 | }, 553 | type: 'home_planets' 554 | }], 555 | included: [{ 556 | id: '1', 557 | first_name: 'Tom', 558 | last_name: 'Dale', 559 | links: { 560 | home_planet: { 561 | linkage: { 562 | id: '1', 563 | type: 'home_planets' 564 | } 565 | } 566 | }, 567 | type: 'super_villains' 568 | }] 569 | }; 570 | 571 | env.serializer.keyForAttribute = function(key) { 572 | return Ember.String.decamelize(key); 573 | }; 574 | 575 | env.serializer.keyForRelationship = function(key, relationshipKind) { 576 | return Ember.String.decamelize(key); 577 | }; 578 | 579 | Ember.run(function() { 580 | env.serializer.extractArray(env.store, HomePlanet, json_hash); 581 | }); 582 | 583 | env.store.find('superVillain', 1).then(function(minion){ 584 | equal(minion.get('firstName'), 'Tom'); 585 | }); 586 | }); 587 | 588 | test('looking up a belongsTo association', function() { 589 | env.registry.register('adapter:superVillain', DS.ActiveModelAdapter); 590 | 591 | var json_hash = { 592 | data: [{ 593 | id: '1', 594 | name: 'Umber', 595 | relationships: { 596 | super_villains: { 597 | data: [{ 598 | id: 1, 599 | type: 'super_villains' 600 | }] 601 | } 602 | }, 603 | type: 'home_planets' 604 | }], 605 | included: [{ 606 | id: '1', 607 | attributes: { 608 | first_name: 'Tom', 609 | last_name: 'Dale' 610 | }, 611 | relationships: { 612 | home_planet: { 613 | data: { 614 | id: '1', 615 | type: 'home_planets' 616 | } 617 | } 618 | }, 619 | type: 'super_villains' 620 | }] 621 | }; 622 | 623 | env.serializer.keyForAttribute = function(key) { 624 | return Ember.String.decamelize(key); 625 | }; 626 | 627 | env.serializer.keyForRelationship = function(key, relationshipKind) { 628 | return Ember.String.decamelize(key); 629 | }; 630 | 631 | Ember.run(function() { 632 | env.store.pushMany('homePlanet', env.serializer.extractArray(env.store, HomePlanet, json_hash)); 633 | }); 634 | 635 | Ember.run(function() { 636 | env.store.find('homePlanet', 1).then(function(planet) { 637 | return planet.get('superVillains').then(function(villains) { 638 | equal(villains.get('firstObject').get('id'), 1); 639 | }); 640 | }); 641 | }); 642 | }); 643 | -------------------------------------------------------------------------------- /tests/integration/specs/compound-documents-test.js: -------------------------------------------------------------------------------- 1 | var get = Ember.get, set = Ember.set; 2 | var env; 3 | var responses, fakeServer; 4 | 5 | module('integration/specs/compound-documents', { 6 | setup: function() { 7 | fakeServer = stubServer(); 8 | 9 | responses = { 10 | posts_compound_document: { 11 | data: { 12 | type: 'posts', 13 | id: '1', 14 | attributes: { 15 | title: 'Rails is Omakase' 16 | }, 17 | relationships: { 18 | comments: { 19 | data: [{ 20 | type: 'comments', 21 | id: '2' 22 | },{ 23 | type: 'comments', 24 | id: '3' 25 | }] 26 | }, 27 | author: { 28 | data: { 29 | type: 'authors', 30 | id: '4' 31 | } 32 | } 33 | } 34 | }, 35 | included: [{ 36 | type: 'comments', 37 | id: '2', 38 | attributes: { 39 | title: 'good article', 40 | body: 'ideal for my startup' 41 | } 42 | }, { 43 | type: 'comments', 44 | id: '3', 45 | attributes: { 46 | title: 'bad article', 47 | body: "doesn't run Crysis" 48 | } 49 | }, { 50 | type: 'authors', 51 | id: '4', 52 | attributes: { 53 | name: 'dhh' 54 | } 55 | }] 56 | }, 57 | posts_nested_compound_document: { 58 | data: { 59 | type: 'posts', 60 | id: '1', 61 | attributes: { 62 | title: 'Rails is Omakase' 63 | }, 64 | relationships: { 65 | comments: { 66 | data: [{ 67 | type: 'comments', 68 | id: '2' 69 | },{ 70 | type: 'comments', 71 | id: '3' 72 | }] 73 | }, 74 | author: { 75 | data: { 76 | type: 'authors', 77 | id: '4' 78 | } 79 | } 80 | } 81 | }, 82 | included: [{ 83 | type: 'comments', 84 | id: '2', 85 | attributes: { 86 | title: 'good article', 87 | body: 'ideal for my startup' 88 | }, 89 | relationships: { 90 | writer: { 91 | data: { 92 | id: 5, 93 | type: 'authors' 94 | } 95 | } 96 | } 97 | }, { 98 | type: 'comments', 99 | id: '3', 100 | attributes: { 101 | title: 'bad article', 102 | body: "doesn't run Crysis" 103 | }, 104 | relationships: { 105 | writer: { 106 | data: { 107 | id: 4, 108 | type: 'authors' 109 | } 110 | } 111 | } 112 | }, { 113 | type: 'authors', 114 | id: '4', 115 | attributes: { 116 | name: 'dhh' 117 | } 118 | }, { 119 | type: 'authors', 120 | id: '5', 121 | attributes: { 122 | name: 'ado' 123 | } 124 | }] 125 | } 126 | }; 127 | 128 | }, 129 | 130 | teardown: function() { 131 | Ember.run(env.store, 'destroy'); 132 | shutdownFakeServer(fakeServer); 133 | } 134 | }); 135 | 136 | function setupCompoundModels(async) { 137 | var models = setModels({ 138 | commentAsync: async, 139 | authorAsync: async 140 | }); 141 | env = setupStore(models); 142 | env.store.modelFor('post'); 143 | env.store.modelFor('comment'); 144 | env.store.modelFor('author'); 145 | } 146 | 147 | function setupNestedCompoundModels(async) { 148 | var Post, Comment, Author; 149 | Post = DS.Model.extend({ 150 | title: DS.attr('string'), 151 | postSummary: DS.attr('string'), 152 | comments: DS.hasMany('comment', { async: async }), 153 | author: DS.belongsTo('author', { async: async }) 154 | }); 155 | 156 | Author = DS.Model.extend({ 157 | name: DS.attr('string') 158 | }); 159 | 160 | Comment = DS.Model.extend({ 161 | title: DS.attr('string'), 162 | body: DS.attr('string'), 163 | writer: DS.belongsTo('author', { async: async }) 164 | }); 165 | 166 | env = setupStore({ 167 | post: Post, 168 | comment: Comment, 169 | author: Author 170 | }); 171 | 172 | env.store.modelFor('post'); 173 | env.store.modelFor('comment'); 174 | env.store.modelFor('author'); 175 | } 176 | 177 | asyncTest('Post with sync comments uses included resources', function() { 178 | setupCompoundModels(false); 179 | 180 | fakeServer.get('/posts/1', responses.posts_compound_document); 181 | 182 | Em.run(function() { 183 | env.store.find('post', '1').then(function(record) { 184 | equal(record.get('id'), '1', 'id is correct'); 185 | equal(record.get('title'), 'Rails is Omakase', 'title is correct'); 186 | 187 | var comments = record.get('comments'); 188 | var author = record.get('author'); 189 | var comment1 = comments.objectAt(0); 190 | var comment2 = comments.objectAt(1); 191 | 192 | equal(comments.get('length'), 2, 'there are 2 comments'); 193 | 194 | equal(comment1.get('title'), 'good article', 'comment1 title'); 195 | equal(comment1.get('body'), 'ideal for my startup', 'comment1 body'); 196 | equal(comment2.get('title'), 'bad article', 'comment2 title'); 197 | equal(comment2.get('body'), "doesn't run Crysis", 'comment2 body'); 198 | equal(author.get('id'), '4', 'author id'); 199 | equal(author.get('name'), 'dhh', 'author name'); 200 | start(); 201 | }); 202 | }); 203 | }); 204 | 205 | asyncTest('Post with async comments uses included resources', function() { 206 | setupCompoundModels(true); 207 | 208 | fakeServer.get('/posts/1', responses.posts_compound_document); 209 | 210 | Em.run(function() { 211 | env.store.find('post', '1').then(function(record) { 212 | equal(record.get('id'), '1', 'id is correct'); 213 | equal(record.get('title'), 'Rails is Omakase', 'title is correct'); 214 | 215 | record.get('comments').then(function(comments) { 216 | var comment1 = comments.objectAt(0); 217 | var comment2 = comments.objectAt(1); 218 | 219 | equal(comments.get('length'), 2, 'there are 2 comments'); 220 | 221 | equal(comment1.get('title'), 'good article', 'comment1 title'); 222 | equal(comment1.get('body'), 'ideal for my startup', 'comment1 body'); 223 | equal(comment2.get('title'), 'bad article', 'comment2 title'); 224 | equal(comment2.get('body'), "doesn't run Crysis", 'comment2 body'); 225 | return record.get('author') 226 | }).then(function(author) { 227 | equal(author.get('id'), '4', 'author id'); 228 | equal(author.get('name'), 'dhh', 'author name'); 229 | start(); 230 | }); 231 | }); 232 | }); 233 | }); 234 | 235 | asyncTest('Post with sync comments uses included resources and nested included resource', function() { 236 | setupNestedCompoundModels(false); 237 | 238 | fakeServer.get('/posts/1', responses.posts_nested_compound_document); 239 | 240 | Em.run(function() { 241 | env.store.find('post', '1').then(function(record) { 242 | equal(record.get('id'), '1', 'id is correct'); 243 | equal(record.get('title'), 'Rails is Omakase', 'title is correct'); 244 | 245 | var comments = record.get('comments'); 246 | var author = record.get('author'); 247 | var comment1 = comments.objectAt(0); 248 | var comment2 = comments.objectAt(1); 249 | var writer1 = comment1.get('writer'); 250 | var writer2 = comment2.get('writer'); 251 | 252 | equal(comments.get('length'), 2, 'there are 2 comments'); 253 | 254 | equal(comment1.get('title'), 'good article', 'comment1 title'); 255 | equal(comment1.get('body'), 'ideal for my startup', 'comment1 body'); 256 | equal(comment2.get('title'), 'bad article', 'comment2 title'); 257 | equal(comment2.get('body'), "doesn't run Crysis", 'comment2 body'); 258 | equal(author.get('id'), '4', 'author id'); 259 | equal(author.get('name'), 'dhh', 'author name'); 260 | equal(writer1.get('id'), '5', 'writer1 id'); 261 | equal(writer1.get('name'), 'ado', 'writer1 name'); 262 | equal(writer2.get('id'), '4', 'writer2 id'); 263 | equal(writer2.get('name'), 'dhh', 'writer2 name'); 264 | start(); 265 | }); 266 | }); 267 | }); 268 | 269 | asyncTest('Post with async comments uses included resources and nested included resource', function() { 270 | setupNestedCompoundModels(true); 271 | 272 | fakeServer.get('/posts/1', responses.posts_nested_compound_document); 273 | 274 | Em.run(function() { 275 | env.store.find('post', '1').then(function(record) { 276 | equal(record.get('id'), '1', 'id is correct'); 277 | equal(record.get('title'), 'Rails is Omakase', 'title is correct'); 278 | 279 | record.get('comments').then(function(comments) { 280 | var comment1 = comments.objectAt(0); 281 | var comment2 = comments.objectAt(1); 282 | 283 | equal(comments.get('length'), 2, 'there are 2 comments'); 284 | 285 | equal(comment1.get('title'), 'good article', 'comment1 title'); 286 | equal(comment1.get('body'), 'ideal for my startup', 'comment1 body'); 287 | equal(comment2.get('title'), 'bad article', 'comment2 title'); 288 | equal(comment2.get('body'), "doesn't run Crysis", 'comment2 body'); 289 | 290 | return comment1.get('writer') 291 | }).then(function(author) { 292 | equal(author.get('id'), '5', 'author id'); 293 | equal(author.get('name'), 'ado', 'author name'); 294 | start(); 295 | }); 296 | }); 297 | }); 298 | }); 299 | -------------------------------------------------------------------------------- /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 | attributes: { 15 | title: 'Rails is Omakase', 16 | 'post-summary': 'summary' 17 | } 18 | } 19 | } 20 | }; 21 | 22 | models = setModels(); 23 | env = setupStore(models); 24 | env.store.modelFor('post'); 25 | env.store.modelFor('comment'); 26 | }, 27 | 28 | teardown: function() { 29 | Ember.run(env.store, 'destroy'); 30 | shutdownFakeServer(fakeServer); 31 | } 32 | }); 33 | 34 | asyncTest("POST /posts/1 won't push an array", function() { 35 | var request = { 36 | data: { 37 | attributes: { 38 | title: 'Rails is Omakase', 39 | 'post-summary': null 40 | }, 41 | relationships: { 42 | comments: { 43 | data: [] 44 | } 45 | }, 46 | type: 'posts' 47 | } 48 | }; 49 | 50 | fakeServer.post('/posts', request, responses.post); 51 | 52 | Em.run(function() { 53 | var post = env.store.createRecord('post', { title: 'Rails is Omakase' }); 54 | post.save().then(function(record) { 55 | equal(record.get('id'), '1', 'id is correct'); 56 | equal(record.get('title'), 'Rails is Omakase', 'title is correct'); 57 | equal(record.get('postSummary'), 'summary', 'summary is correct'); 58 | start(); 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /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 | attributes: { 15 | title: 'Rails is Omakase' 16 | }, 17 | relationships: { 18 | comments: { 19 | links: { 20 | self: '/posts/1/links/comments', 21 | related: '/posts/1/comments' 22 | }, 23 | data: [{ 24 | type: 'comments', 25 | id: '1' 26 | },{ 27 | type: 'comments', 28 | id: '2' 29 | }] 30 | } 31 | } 32 | } 33 | }, 34 | post_1_comments: { 35 | data: [ 36 | { 37 | type: 'comments', 38 | id: '1', 39 | attributes: { 40 | title: 'good article', 41 | body: 'ideal for my startup' 42 | } 43 | }, 44 | { 45 | type: 'comments', 46 | id: '2', 47 | attributes: { 48 | title: 'bad article', 49 | body: 'doesn\'t run Crysis' 50 | } 51 | } 52 | ] 53 | } 54 | }; 55 | 56 | env = setupStore(setModels()); 57 | env.store.modelFor('post'); 58 | env.store.modelFor('comment'); 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 | commentAsync: true 70 | }); 71 | env = setupStore(models); 72 | 73 | fakeServer.get('/posts/1', responses.posts_not_compound); 74 | fakeServer.get('/posts/1/comments', responses.post_1_comments); 75 | 76 | Em.run(function() { 77 | env.store.find('post', '1').then(function(record) { 78 | record.get('comments').then(function(comments) { 79 | var comment1 = comments.objectAt(0); 80 | var comment2 = comments.objectAt(1); 81 | 82 | equal(comments.get('length'), 2, 'there are 2 comments'); 83 | 84 | equal(comment1.get('title'), 'good article', 'comment1 title'); 85 | equal(comment1.get('body'), 'ideal for my startup', 'comment1 body'); 86 | equal(comment2.get('title'), 'bad article', 'comment2 title'); 87 | equal(comment2.get('body'), "doesn't run Crysis", 'comment2 body'); 88 | start(); 89 | }); 90 | }); 91 | }); 92 | }); 93 | 94 | asyncTest('GET /posts/1 calls later GET /posts/1/some_resources when Posts has async someResources (camelized)', function() { 95 | var models = setModels(); 96 | // Add hasMany someResources relation to Post 97 | models['post'].reopen({ 98 | someResources: DS.hasMany('someResources', { async: true }) 99 | }) 100 | 101 | env = setupStore(models); 102 | 103 | fakeServer.get('/posts/1', { 104 | data: { 105 | type: 'posts', 106 | id: '1', 107 | attributes: { 108 | title: 'Rails is Omakase', 109 | }, 110 | relationships: { 111 | 'some_resources': { 112 | links: { 113 | related: '/posts/1/some_resources' 114 | } 115 | } 116 | } 117 | } 118 | }); 119 | 120 | fakeServer.get('/posts/1/some_resources', { 121 | data: [ 122 | { 123 | type: 'some_resources', 124 | id: 1, 125 | attributes: { 126 | title: 'Something 1' 127 | } 128 | }, 129 | { 130 | type: 'some_resources', 131 | id: 2, 132 | attributes: { 133 | title: 'Something 2' 134 | } 135 | } 136 | ] 137 | }); 138 | 139 | Em.run(function() { 140 | env.store.find('post', '1').then(function(record) { 141 | record.get('someResources').then(function(someResources) { 142 | var something1 = someResources.objectAt(0); 143 | var something2 = someResources.objectAt(1); 144 | 145 | equal(someResources.get('length'), 2, 'there are 2 someResources'); 146 | 147 | equal(something1.get('title'), 'Something 1', 'something1 title'); 148 | equal(something2.get('title'), 'Something 2', 'something2 title'); 149 | start(); 150 | }); 151 | }); 152 | }); 153 | }); 154 | -------------------------------------------------------------------------------- /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/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 | attributes: { 15 | title: 'Rails is Omakase', 16 | }, 17 | relationships: { 18 | observations: { 19 | data: [{ 20 | id: '2', 21 | type: 'comments' 22 | },{ 23 | id: '3', 24 | type: 'comments' 25 | }] 26 | }, 27 | writer: { 28 | data: { 29 | id: '1', 30 | type: 'authors' 31 | } 32 | } 33 | } 34 | } 35 | }, 36 | comments_2: { 37 | data: { 38 | type: 'comments', 39 | id: 2, 40 | attributes: { 41 | title: 'good article' 42 | } 43 | } 44 | }, 45 | comments_3: { 46 | data: { 47 | type: 'comments', 48 | id: 3, 49 | attributes: { 50 | title: 'bad article' 51 | } 52 | } 53 | }, 54 | author: { 55 | data: { 56 | type: 'authors', 57 | id: 1, 58 | attributes: { 59 | name: 'Tomster' 60 | } 61 | } 62 | } 63 | }; 64 | 65 | Post = DS.Model.extend({ 66 | title: DS.attr('string'), 67 | observations: DS.hasMany('comment', {async: true}), 68 | writer: DS.belongsTo('author', {async: true}) 69 | }); 70 | 71 | Comment = DS.Model.extend({ 72 | title: DS.attr('string'), 73 | post: DS.belongsTo('post') 74 | }); 75 | 76 | Author = DS.Model.extend({ 77 | name: DS.attr('string'), 78 | post: DS.belongsTo('post') 79 | }); 80 | 81 | env = setupStore({ 82 | post: Post, 83 | comment: Comment, 84 | author: Author 85 | }); 86 | 87 | env.store.modelFor('post'); 88 | env.store.modelFor('comment'); 89 | env.store.modelFor('author'); 90 | }, 91 | 92 | teardown: function() { 93 | Ember.run(env.store, 'destroy'); 94 | shutdownFakeServer(fakeServer); 95 | } 96 | }); 97 | 98 | asyncTest("GET /posts/1 with array of unmatched named relationship", function() { 99 | fakeServer.get('/posts/1', responses.post); 100 | fakeServer.get('/comments/2', responses.comments_2); 101 | fakeServer.get('/comments/3', responses.comments_3); 102 | 103 | Em.run(function() { 104 | env.store.find('post', 1).then(function(record) { 105 | equal(record.get('id'), '1', 'id is correct'); 106 | equal(record.get('title'), 'Rails is Omakase', 'title is correct'); 107 | record.get('observations').then(function(comments) { 108 | var comment1 = comments.objectAt(0); 109 | var comment2 = comments.objectAt(1); 110 | 111 | equal(comments.get('length'), 2, 'there are 2 comments'); 112 | 113 | equal(comment1.get('title'), 'good article', 'comment1 title'); 114 | equal(comment2.get('title'), 'bad article', 'comment2 title'); 115 | start(); 116 | }); 117 | }); 118 | }); 119 | }); 120 | 121 | asyncTest("GET /posts/1 with single unmatched named relationship", function() { 122 | fakeServer.get('/posts/1', responses.post); 123 | fakeServer.get('/authors/1', responses.author); 124 | 125 | Em.run(function() { 126 | env.store.find('post', 1).then(function(record) { 127 | equal(record.get('id'), '1', 'id is correct'); 128 | equal(record.get('title'), 'Rails is Omakase', 'title is correct'); 129 | record.get('writer').then(function(writer) { 130 | equal(writer.get('name'), 'Tomster', 'author name'); 131 | start(); 132 | }); 133 | }); 134 | }); 135 | }); 136 | -------------------------------------------------------------------------------- /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 | attributes: { 15 | title: 'Rails is Omakase', 16 | }, 17 | relationships: { 18 | author: { 19 | links: { 20 | self: '/posts/1/relationships/author', 21 | related: '/posts/1/author' 22 | }, 23 | data: { 24 | type: 'authors', 25 | id: '2' 26 | } 27 | } 28 | } 29 | }, { 30 | type: 'posts', 31 | id: '2', 32 | attributes: { 33 | title: 'TDD Is Dead lol' 34 | }, 35 | relationships: { 36 | author: { 37 | links: { 38 | self: '/posts/2/relationships/author', 39 | related: '/posts/2/author' 40 | }, 41 | data: { 42 | type: 'authors', 43 | id: '1' 44 | } 45 | } 46 | } 47 | }] 48 | }, 49 | post_1_author: { 50 | data: { 51 | type: 'authors', 52 | id: '2', 53 | attributes: { 54 | name: 'dhh' 55 | } 56 | } 57 | }, 58 | post_2_author: { 59 | data: { 60 | type: 'authors', 61 | id: '1', 62 | attributes: { 63 | name: 'ado' 64 | } 65 | } 66 | } 67 | }; 68 | 69 | env = setupStore(setModels()); 70 | env.store.modelFor('post'); 71 | env.store.modelFor('comment'); 72 | env.store.modelFor('author'); 73 | }, 74 | 75 | teardown: function() { 76 | Ember.run(env.store, 'destroy'); 77 | shutdownFakeServer(fakeServer); 78 | } 79 | }); 80 | 81 | asyncTest('GET /posts/1 calls later GET /posts/1/comments when Posts has async comments', function() { 82 | var models = setModels({ 83 | authorAsync: true 84 | }); 85 | env = setupStore(models); 86 | 87 | fakeServer.get('/posts', responses.posts_not_compound); 88 | fakeServer.get('/posts/1/author', responses.post_1_author); 89 | fakeServer.get('/posts/2/author', responses.post_2_author); 90 | 91 | Em.run(function() { 92 | env.store.find('post').then(function(records) { 93 | equal(records.get('length'), 2, 'there are 2 posts'); 94 | 95 | var post1 = records.objectAt(0); 96 | var post2 = records.objectAt(1); 97 | var promises = []; 98 | 99 | promises.push(new Ember.RSVP.Promise(function(resolve, reject) { resolve('asdf'); })); 100 | promises.push(post1.get('author')); 101 | promises.push(post1.get('author').then(function(author) { 102 | equal(author.get('name'), 'dhh', 'post1 author'); 103 | })); 104 | promises.push(post2.get('author').then(function(author) { 105 | equal(author.get('name'), 'ado', 'post2 author'); 106 | })); 107 | 108 | Ember.RSVP.all(promises).then(start); 109 | }); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /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 | relationships: { 16 | author: { 17 | data: { 18 | type: 'authors', 19 | id: '2' 20 | } 21 | }, 22 | comments: { 23 | data: [{ 24 | type: 'comments', 25 | id: '2' 26 | }] 27 | } 28 | } 29 | } 30 | }, 31 | posts2_id: { 32 | data: { 33 | type: 'posts', 34 | id: '2', 35 | attributes: { 36 | title: 'TDD Is Dead lol', 37 | }, 38 | relationships: { 39 | author: { 40 | data: { 41 | type: 'authors', 42 | id: '2' 43 | } 44 | }, 45 | comments: { 46 | data: [{ 47 | type: 'comments', 48 | id: '3' 49 | }] 50 | } 51 | } 52 | } 53 | }, 54 | posts1_related: { 55 | data: { 56 | type: 'posts', 57 | id: '1', 58 | attributes: { 59 | title: 'Rails is Omakase', 60 | }, 61 | relationships: { 62 | author: { 63 | links: { 64 | related: '/api/posts/1/author' 65 | }, 66 | data: { 67 | type: 'authors', 68 | id: '2' 69 | } 70 | }, 71 | comments: { 72 | links: { 73 | related: '/api/posts/1/comments' 74 | } 75 | } 76 | } 77 | } 78 | }, 79 | posts2_related: { 80 | data: { 81 | type: 'posts', 82 | id: '2', 83 | attributes: { 84 | title: 'TDD Is Dead lol' 85 | }, 86 | relationships: { 87 | author: { 88 | links: { 89 | related: '/api/posts/2/author' 90 | }, 91 | data: { 92 | type: 'authors', 93 | id: '2' 94 | } 95 | }, 96 | comments: { 97 | links: { 98 | related: '/api/posts/2/comments' 99 | } 100 | } 101 | } 102 | } 103 | }, 104 | author: { 105 | data: { 106 | type: 'authors', 107 | id: '2', 108 | attributes: { 109 | name: 'dhh' 110 | } 111 | } 112 | }, 113 | post1_comments: { 114 | data: [{ 115 | type: 'comments', 116 | 'id': '2', 117 | attributes: { 118 | 'title': 'good article', 119 | 'body': 'ideal for my startup' 120 | } 121 | }] 122 | }, 123 | post2_comments: { 124 | data: [{ 125 | type: 'comments', 126 | id: '3', 127 | attributes: { 128 | title: 'bad article', 129 | body: "doesn't run Crysis" 130 | } 131 | }] 132 | }, 133 | comments_2: { 134 | data: { 135 | type: 'comments', 136 | 'id': '2', 137 | attributes: { 138 | 'title': 'good article', 139 | 'body': 'ideal for my startup' 140 | } 141 | } 142 | }, 143 | comments_3: { 144 | data: { 145 | type: 'comments', 146 | id: '3', 147 | title: 'bad article', 148 | body: "doesn't run Crysis" 149 | } 150 | } 151 | }; 152 | 153 | env = setupStore($.extend({ 154 | adapter: DS.JsonApiAdapter.extend({ 155 | namespace: 'api' 156 | }) 157 | }, setModels({ 158 | authorAsync: true, 159 | commentAsync: true 160 | }))); 161 | env.store.modelFor('post'); 162 | env.store.modelFor('author'); 163 | env.store.modelFor('comment'); 164 | }, 165 | 166 | teardown: function() { 167 | Ember.run(env.store, 'destroy'); 168 | shutdownFakeServer(fakeServer); 169 | } 170 | }); 171 | 172 | asyncTest('GET /api/posts/1 calls with type and id to comments', function() { 173 | fakeServer.get('/api/posts/1', responses.posts1_id); 174 | fakeServer.get('/api/posts/2', responses.posts2_id); 175 | fakeServer.get('/api/authors/2', responses.author); 176 | fakeServer.get('/api/comments/2', responses.comments_2); 177 | fakeServer.get('/api/comments/3', responses.comments_3); 178 | 179 | runTests(); 180 | }); 181 | 182 | asyncTest('GET /api/posts/1 calls with related URLs', function() { 183 | fakeServer.get('/api/posts/1', responses.posts1_related); 184 | fakeServer.get('/api/posts/2', responses.posts2_related); 185 | fakeServer.get('/api/posts/1/author', responses.author); 186 | fakeServer.get('/api/posts/2/author', responses.author); 187 | fakeServer.get('/api/posts/1/comments', responses.post1_comments); 188 | fakeServer.get('/api/posts/2/comments', responses.post2_comments); 189 | runTests(); 190 | }); 191 | 192 | function runTests() { 193 | Em.run(function() { 194 | var promises = []; 195 | promises.push(testPost1()); 196 | promises.push(testPost2()); 197 | 198 | Ember.RSVP.all(promises).then(start); 199 | }); 200 | } 201 | 202 | function testPost1() { 203 | return env.store.find('post', '1').then(function(record) { 204 | equal(record.get('id'), '1', 'id is correct'); 205 | equal(record.get('title'), 'Rails is Omakase', 'title is correct'); 206 | 207 | record.get('author').then(function(author) { 208 | equal(author.get('id'), '2', 'author id is correct'); 209 | equal(author.get('name'), 'dhh', 'author name is correct'); 210 | 211 | record.get('comments').then(function(comments) { 212 | var comment = comments.objectAt(0); 213 | 214 | equal(comments.get('length'), 1, 'there is 1 comment'); 215 | 216 | equal(comment.get('title'), 'good article', 'comment1 title'); 217 | equal(comment.get('body'), 'ideal for my startup', 'comment1 body'); 218 | }); 219 | }); 220 | }); 221 | } 222 | 223 | function testPost2() { 224 | return env.store.find('post', '2').then(function(record) { 225 | equal(record.get('id'), '2', 'id is correct'); 226 | equal(record.get('title'), 'TDD Is Dead lol', 'title is correct'); 227 | 228 | record.get('author').then(function(author) { 229 | equal(author.get('id'), '2', 'author id is correct'); 230 | equal(author.get('name'), 'dhh', 'author name is correct'); 231 | 232 | record.get('comments').then(function(comments) { 233 | var comment = comments.objectAt(0); 234 | 235 | equal(comments.get('length'), 1, 'there is 1 comment'); 236 | 237 | equal(comment.get('title'), 'bad article', 'comment2 title'); 238 | equal(comment.get('body'), "doesn't run Crysis", 'comment2 body'); 239 | }); 240 | }); 241 | }); 242 | } 243 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | attributes: { 14 | name: 'Luke' 15 | }, 16 | relationships: { 17 | pets: { 18 | links: { 19 | self: '/owners/1/relationships/pets', 20 | related: '/owners/1/pets' 21 | }, 22 | data: [ 23 | { 24 | type: 'cats', 25 | id: 'cat_1' 26 | }, 27 | { 28 | type: 'dogs', 29 | id: 'dog_2' 30 | } 31 | ] 32 | } 33 | } 34 | }, 35 | included: [ 36 | { 37 | type: 'cats', 38 | id: 'cat_1', 39 | attributes: { 40 | whiskers: 4, 41 | paws: 3 42 | } 43 | }, 44 | { 45 | type: 'dogs', 46 | id: 'dog_2', 47 | attributes: { 48 | spots: 7, 49 | paws: 5 50 | } 51 | } 52 | ] 53 | }; 54 | 55 | env = setupStore(setPolymorphicModels()); 56 | env.store.modelFor('owner'); 57 | env.store.modelFor('pet'); 58 | env.store.modelFor('dog'); 59 | env.store.modelFor('cat'); 60 | }, 61 | 62 | teardown: function() { 63 | Ember.run(env.store, 'destroy'); 64 | shutdownFakeServer(fakeServer); 65 | } 66 | }); 67 | 68 | asyncTest('GET /owners/1 with sync included resources', function() { 69 | var models = setPolymorphicModels(); 70 | env = setupStore(models); 71 | 72 | fakeServer.get('/owners/1', responses); 73 | 74 | Em.run(function() { 75 | env.store.find('owner', '1').then(function(record) { 76 | 77 | equal(record.get('id'), '1', 'id is correct'); 78 | equal(record.get('name'), 'Luke', 'name is correct'); 79 | 80 | var cat = record.get('pets.firstObject'); 81 | var dog = record.get('pets.lastObject'); 82 | 83 | equal(cat.get('paws'), 3, 'common prop from base class correct on cat'); 84 | equal(dog.get('paws'), 5, 'common prop from base class correct on dog'); 85 | equal(cat.get('whiskers'), 4, 'cat has correct whiskers (cat-only prop)'); 86 | equal(dog.get('spots'), 7, 'dog has correct spots (dog-only prop)'); 87 | 88 | start(); 89 | }); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /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 | attributes: { 15 | title: 'Rails is Omakase', 16 | }, 17 | relationships: { 18 | comments: { 19 | data: [{ 20 | id: '2', 21 | type: 'comments' 22 | }, { 23 | id: '3', 24 | type: 'comments' 25 | }] 26 | } 27 | } 28 | } 29 | }, 30 | comments_2: { 31 | data: { 32 | type: 'comments', 33 | id: '2', 34 | attributes: { 35 | title: 'good article', 36 | body: 'ideal for my startup' 37 | } 38 | } 39 | }, 40 | comments_3: { 41 | data: { 42 | type: 'comments', 43 | id: '3', 44 | attributes: { 45 | title: 'bad article', 46 | body: "doesn't run Crysis" 47 | } 48 | } 49 | } 50 | }; 51 | 52 | env = setupStore(setModels()); 53 | env.store.modelFor('post'); 54 | env.store.modelFor('comment'); 55 | }, 56 | 57 | teardown: function() { 58 | Ember.run(env.store, 'destroy'); 59 | shutdownFakeServer(fakeServer); 60 | } 61 | }); 62 | 63 | asyncTest('GET /posts/1 with async included resources', function() { 64 | var models = setModels({ 65 | commentAsync: true 66 | }); 67 | env = setupStore(models); 68 | 69 | fakeServer.get('/posts/1', responses.posts_not_compound); 70 | fakeServer.get('/comments/2', responses.comments_2); 71 | fakeServer.get('/comments/3', responses.comments_3); 72 | 73 | Em.run(function() { 74 | env.store.find('post', '1').then(function(record) { 75 | equal(record.get('id'), '1', 'id is correct'); 76 | equal(record.get('title'), 'Rails is Omakase', 'title is correct'); 77 | 78 | record.get('comments').then(function(comments) { 79 | var comment1 = comments.objectAt(0); 80 | var comment2 = comments.objectAt(1); 81 | 82 | equal(comments.get('length'), 2, 'there are 2 comments'); 83 | 84 | equal(comment1.get('title'), 'good article', 'comment1 title'); 85 | equal(comment1.get('body'), 'ideal for my startup', 'comment1 body'); 86 | equal(comment2.get('title'), 'bad article', 'comment2 title'); 87 | equal(comment2.get('body'), "doesn't run Crysis", 'comment2 body'); 88 | start(); 89 | }); 90 | }); 91 | }); 92 | }); 93 | 94 | asyncTest('GET /posts/1 with sync included resources', function() { 95 | var models = setModels({ 96 | commentAsync: false 97 | }); 98 | env = setupStore(models); 99 | 100 | fakeServer.get('/posts/1', responses.posts_not_compound); 101 | 102 | Em.run(function() { 103 | env.store.find('post', '1').then(function(record) { 104 | var comment1, comment2; 105 | equal(record.get('id'), '1', 'id is correct'); 106 | equal(record.get('title'), 'Rails is Omakase', 'title is correct'); 107 | 108 | throws(function() { 109 | record.get('comments'); 110 | }); 111 | 112 | start(); 113 | }); 114 | }); 115 | }); 116 | -------------------------------------------------------------------------------- /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 | attributes: { 15 | title: 'Rails is Omakase', 16 | }, 17 | relationships: { 18 | author: { 19 | data: { 20 | type: 'authors', 21 | id: '2' 22 | } 23 | } 24 | }, 25 | links: { 26 | self: "/posts/1" 27 | } 28 | } 29 | }, 30 | authors: { 31 | data: { 32 | type: 'authors', 33 | id: '2', 34 | attributes: { 35 | name: 'dhh' 36 | }, 37 | relationships: { 38 | }, 39 | links: { 40 | self: "/authors/2" 41 | } 42 | } 43 | } 44 | }; 45 | 46 | env = setupStore(setModels()); 47 | env.store.modelFor('post'); 48 | env.store.modelFor('comment'); 49 | env.store.modelFor('author'); 50 | }, 51 | 52 | teardown: function() { 53 | Ember.run(env.store, 'destroy'); 54 | shutdownFakeServer(fakeServer); 55 | } 56 | }); 57 | 58 | asyncTest('GET /posts/1 with async included resources', function() { 59 | var models = setModels({ 60 | authorAsync: true 61 | }); 62 | env = setupStore(models); 63 | 64 | fakeServer.get('/posts/1', responses.posts_no_linked); 65 | fakeServer.get('/authors/2', responses.authors); 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('author').then(function(author) { 73 | equal(author.get('id'), '2', 'author id is correct'); 74 | equal(author.get('name'), 'dhh', 'author name is correct'); 75 | start(); 76 | }); 77 | }); 78 | }); 79 | }); 80 | 81 | asyncTest("GET /posts/1 with sync included resources won't work", function() { 82 | var models = setModels({ 83 | authorAsync: false 84 | }); 85 | env = setupStore(models); 86 | 87 | fakeServer.get('/posts/1', responses.posts_no_linked); 88 | fakeServer.get('/authors/2', responses.authors); 89 | 90 | Em.run(function() { 91 | env.store.find('post', '1').then(function(record) { 92 | var authorId; 93 | equal(record.get('id'), '1', 'id is correct'); 94 | equal(record.get('title'), 'Rails is Omakase', 'title is correct'); 95 | 96 | throws(function() { 97 | record.get('author').then(function(author) { 98 | equal(author.get('id'), '2', 'author id is correct'); 99 | }); 100 | }); 101 | start(); 102 | }); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /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 | attributes: { 15 | title: 'Rails is Omakase' 16 | }, 17 | relationships: { 18 | author: { 19 | links: { 20 | self: '/posts/1/links/author', 21 | related: '/posts/1/author', 22 | }, 23 | data: { 24 | } 25 | } 26 | } 27 | } 28 | }, 29 | author: { 30 | data: { 31 | type: 'authors', 32 | id: '1', 33 | attributes: { 34 | name: 'dhh' 35 | } 36 | } 37 | }, 38 | postAfterUpdate: { 39 | data: { 40 | type: 'posts', 41 | id: '1', 42 | attributes: { 43 | title: 'TDD Is Dead lol', 44 | 'post-summary': 'summary' 45 | } 46 | } 47 | }, 48 | postAfterUpdateAuthor: { 49 | data: { 50 | type: 'posts', 51 | id: '1', 52 | attributes: { 53 | title: 'TDD Is Dead lol', 54 | 'post-summary': 'summary' 55 | }, 56 | relationships: { 57 | author: { 58 | links: { 59 | self: '/posts/1/links/author', 60 | related: '/posts/1/author' 61 | }, 62 | data: { 63 | type: 'authors', 64 | id: '1' 65 | } 66 | } 67 | } 68 | } 69 | } 70 | }; 71 | 72 | env = setupStore(setModels({ 73 | authorAsync: true, 74 | commentAsync: true 75 | })); 76 | env.store.modelFor('post'); 77 | env.store.modelFor('author'); 78 | env.store.modelFor('comment'); 79 | }, 80 | 81 | teardown: function() { 82 | Ember.run(env.store, 'destroy'); 83 | shutdownFakeServer(fakeServer); 84 | } 85 | }); 86 | 87 | asyncTest("PATCH /posts/1 won't push an array", function() { 88 | var request = { 89 | data: { 90 | id: '1', 91 | attributes: { 92 | title: 'TDD Is Dead lol', 93 | 'post-summary': null 94 | }, 95 | relationships: { 96 | comments: { 97 | data: [] 98 | } 99 | }, 100 | type: 'posts' 101 | } 102 | }; 103 | 104 | fakeServer.get('/posts/1', responses.post); 105 | fakeServer.patch('/posts/1', request, responses.postAfterUpdate); 106 | 107 | Em.run(function() { 108 | env.store.find('post', '1').then(function(post) { 109 | equal(post.get('title'), 'Rails is Omakase', 'title is correct'); 110 | post.set('title', 'TDD Is Dead lol'); 111 | post.save().then(function(record) { 112 | equal(record.get('id'), '1', 'id is correct'); 113 | equal(record.get('title'), 'TDD Is Dead lol', 'title is correct'); 114 | equal(record.get('postSummary'), 'summary', 'summary is correct'); 115 | 116 | start(); 117 | }); 118 | }); 119 | }); 120 | }); 121 | 122 | asyncTest("Update a post with an author", function() { 123 | var request = { 124 | data: { 125 | id: '1', 126 | attributes: { 127 | title: 'TDD Is Dead lol', 128 | 'post-summary': null, 129 | }, 130 | relationships: { 131 | comments: { 132 | data: [] 133 | }, 134 | author: { 135 | data: { 136 | id: '1', 137 | type: 'authors' 138 | } 139 | } 140 | }, 141 | type: 'posts' 142 | } 143 | }; 144 | 145 | fakeServer.get('/posts/1', responses.post); 146 | fakeServer.get('/authors/1', responses.author); 147 | // FIXME This call shouldn't have to be made since it already exists 148 | fakeServer.get('/posts/1/author', responses.author); 149 | // FIXME Need a way to PATCH to /posts/1/links/author 150 | fakeServer.patch('/posts/1', request, responses.postAfterUpdateAuthor); 151 | 152 | Em.run(function() { 153 | var findPost = env.store.find('post', '1'), 154 | findAuthor = env.store.find('author', '1'); 155 | 156 | findPost.then(function(post) { 157 | equal(post.get('title'), 'Rails is Omakase', 'title is correct'); 158 | findAuthor.then(function(author) { 159 | equal(author.get('name'), 'dhh', 'author name is correct'); 160 | post.set('title', 'TDD Is Dead lol'); 161 | post.set('author', author); 162 | post.save().then(function(record) { 163 | equal(record.get('id'), '1', 'id is correct'); 164 | equal(record.get('title'), 'TDD Is Dead lol', 'title is correct'); 165 | equal(record.get('postSummary'), 'summary', 'summary is correct'); 166 | equal(record.get('author.id'), '1', 'author ID is correct'); 167 | equal(record.get('author.name'), 'dhh', 'author name is correct'); 168 | start(); 169 | }); 170 | }); 171 | }); 172 | }); 173 | }); 174 | -------------------------------------------------------------------------------- /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 | attributes: { 15 | title: 'Rails is Omakase', 16 | }, 17 | relationships: { 18 | comments: { 19 | data: [{ 20 | type: 'comments', 21 | id: '2' 22 | },{ 23 | type: 'comments', 24 | id: '3' 25 | }] 26 | } 27 | } 28 | } 29 | }, 30 | comments_2: { 31 | data: { 32 | type: 'comments', 33 | 'id': '2', 34 | attributes: { 35 | 'title': 'good article', 36 | 'body': 'ideal for my startup' 37 | } 38 | } 39 | }, 40 | comments_3: { 41 | data: { 42 | type: 'comments', 43 | id: '3', 44 | attributes: { 45 | title: 'bad article', 46 | body: "doesn't run Crysis" 47 | } 48 | } 49 | }, 50 | underscore_resource: { 51 | data: { 52 | type: 'some_resource', 53 | id: '1', 54 | attributes: { 55 | title: 'wow' 56 | } 57 | } 58 | } 59 | }; 60 | 61 | env = setupStore(setModels()); 62 | env.store.modelFor('post'); 63 | env.store.modelFor('comment'); 64 | }, 65 | 66 | teardown: function() { 67 | Ember.run(env.store, 'destroy'); 68 | shutdownFakeServer(fakeServer); 69 | } 70 | }); 71 | 72 | asyncTest('GET /posts/1 calls later GET /comments/2,3 when Posts has async comments', function() { 73 | var models = setModels({ 74 | commentAsync: true 75 | }); 76 | env = setupStore(models); 77 | 78 | fakeServer.get('/posts/1', responses.posts_not_compound); 79 | fakeServer.get('/comments/2', responses.comments_2); 80 | fakeServer.get('/comments/3', responses.comments_3); 81 | 82 | Em.run(function() { 83 | env.store.find('post', '1').then(function(record) { 84 | equal(record.get('id'), '1', 'id is correct'); 85 | equal(record.get('title'), 'Rails is Omakase', 'title is correct'); 86 | 87 | record.get('comments').then(function(comments) { 88 | var comment1 = comments.objectAt(0); 89 | var comment2 = comments.objectAt(1); 90 | 91 | equal(comments.get('length'), 2, 'there are 2 comments'); 92 | 93 | equal(comment1.get('title'), 'good article', 'comment1 title'); 94 | equal(comment1.get('body'), 'ideal for my startup', 'comment1 body'); 95 | equal(comment2.get('title'), 'bad article', 'comment2 title'); 96 | equal(comment2.get('body'), "doesn't run Crysis", 'comment2 body'); 97 | start(); 98 | }); 99 | }); 100 | }); 101 | }); 102 | 103 | asyncTest('GET /some_resource, not camelCase, dasherized', function() { 104 | var models = setModels({ 105 | commentAsync: true 106 | }); 107 | env = setupStore(models); 108 | 109 | fakeServer.get('/some-resources/1', responses.underscore_resource); 110 | 111 | Em.run(function() { 112 | env.store.find('someResource', '1').then(function(record) { 113 | equal(record.get('id'), '1', 'id is correct'); 114 | equal(record.get('title'), 'wow', 'title is correct'); 115 | start(); 116 | }); 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /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/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/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/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("related link", function() { 25 | var links = serializer.extractRelationships({ 26 | "author": { 27 | "links": { 28 | "related": "http://example.com/authors/1" 29 | }, 30 | "data": { 31 | "id": "1", 32 | "type": "authors" 33 | } 34 | } 35 | }, { id:1, type:'posts' }); 36 | 37 | deepEqual(links, { 38 | "author": "/authors/1" 39 | }); 40 | }); 41 | 42 | test("related link with replacement", function() { 43 | var links = serializer.extractRelationships({ 44 | "author": { 45 | "links": { 46 | "related": "http://example.com/authors/{author.id}", 47 | }, 48 | "data": { 49 | "id": "1", 50 | "type": "authors" 51 | } 52 | } 53 | }, { id:1, type:'posts' }); 54 | 55 | deepEqual(links, { 56 | "author": "/authors/{author.id}" 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /vendor/no-loader.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurko/ember-json-api/752003a26df5e5cb6ddfcce3526f9925d7cdb3d9/vendor/no-loader.js --------------------------------------------------------------------------------