├── .bowerrc ├── .editorconfig ├── .ember-cli ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .travis.yml ├── .watchmanconfig ├── LICENSE.md ├── README.md ├── addon ├── .gitkeep └── mixins │ ├── model.js │ └── nested-relations.js ├── app └── .gitkeep ├── bower.json ├── config ├── ember-try.js └── environment.js ├── ember-cli-build.js ├── index.js ├── package.json ├── testem.js ├── tests ├── .eslintrc.js ├── acceptance │ ├── create-nested-relations-test.js │ ├── delete-nested-object-test.js │ └── update-nested-relations-test.js ├── dummy │ ├── app │ │ ├── app.js │ │ ├── components │ │ │ └── .gitkeep │ │ ├── controllers │ │ │ ├── .gitkeep │ │ │ └── post-form.js │ │ ├── edit-post │ │ │ └── route.js │ │ ├── helpers │ │ │ └── .gitkeep │ │ ├── index.html │ │ ├── mixins │ │ │ └── post-form-route.js │ │ ├── models │ │ │ ├── .gitkeep │ │ │ ├── author.js │ │ │ ├── post.js │ │ │ └── tag.js │ │ ├── new-post │ │ │ └── route.js │ │ ├── post │ │ │ ├── route.js │ │ │ └── template.hbs │ │ ├── resolver.js │ │ ├── router.js │ │ ├── routes │ │ │ └── .gitkeep │ │ ├── serializers │ │ │ └── application.js │ │ ├── styles │ │ │ └── app.css │ │ └── templates │ │ │ ├── components │ │ │ └── .gitkeep │ │ │ └── post-form.hbs │ ├── config │ │ └── environment.js │ ├── mirage │ │ ├── config.js │ │ ├── models │ │ │ ├── author.js │ │ │ ├── post.js │ │ │ └── tag.js │ │ ├── scenarios │ │ │ └── default.js │ │ └── serializers │ │ │ ├── application.js │ │ │ └── post.js │ └── public │ │ ├── crossdomain.xml │ │ └── robots.txt ├── helpers │ ├── destroy-app.js │ ├── get-owner.js │ ├── module-for-acceptance.js │ ├── resolver.js │ └── start-app.js ├── index.html ├── integration │ └── .gitkeep ├── pages │ ├── post-form.js │ └── show-post.js ├── test-helper.js └── unit │ ├── .gitkeep │ └── mixins │ ├── model-test.js │ └── nested-relations-test.js ├── vendor └── .gitkeep └── yarn.lock /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components", 3 | "analytics": false 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.hbs] 17 | insert_final_newline = false 18 | 19 | [*.{diff,md}] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Ember CLI sends analytics information by default. The data is completely 4 | anonymous, but there are times when you might want to disable this behavior. 5 | 6 | Setting `disableAnalytics` to true will prevent any data from being sent. 7 | */ 8 | "disableAnalytics": false 9 | } 10 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parserOptions: { 4 | ecmaVersion: 2017, 5 | sourceType: 'module' 6 | }, 7 | extends: [ 8 | 'eslint:recommended', 9 | 'plugin:ember/recommended' 10 | ], 11 | env: { 12 | browser: true 13 | }, 14 | rules: { 15 | }, 16 | globals: { 17 | module: true 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | 7 | # dependencies 8 | /node_modules 9 | /bower_components 10 | 11 | # misc 12 | /.sass-cache 13 | /connect.lock 14 | /coverage/* 15 | /libpeerconnection.log 16 | npm-debug.log 17 | testem.log 18 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /bower_components 2 | /config/ember-try.js 3 | /dist 4 | /tests 5 | /tmp 6 | **/.gitkeep 7 | .bowerrc 8 | .editorconfig 9 | .ember-cli 10 | .gitignore 11 | .jshintrc 12 | .watchmanconfig 13 | .travis.yml 14 | bower.json 15 | ember-cli-build.js 16 | .eslintrc.js 17 | testem.js 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | node_js: 4 | - "4" 5 | 6 | sudo: false 7 | 8 | cache: 9 | directories: 10 | - node_modules 11 | 12 | env: 13 | - EMBER_TRY_SCENARIO=default 14 | - EMBER_TRY_SCENARIO=ember-1.13 15 | - EMBER_TRY_SCENARIO=ember-release 16 | - EMBER_TRY_SCENARIO=ember-beta 17 | - EMBER_TRY_SCENARIO=ember-canary 18 | 19 | matrix: 20 | fast_finish: true 21 | allow_failures: 22 | - env: EMBER_TRY_SCENARIO=ember-canary 23 | 24 | before_install: 25 | - npm config set spin false 26 | - npm install -g bower 27 | - bower --version 28 | - npm install phantomjs-prebuilt 29 | - phantomjs --version 30 | 31 | install: 32 | - npm install 33 | - bower install 34 | 35 | script: 36 | # Usually, it's ok to finish the test scenario without reverting 37 | # to the addon's original dependency state, skipping "cleanup". 38 | - ember try:one $EMBER_TRY_SCENARIO test --skip-cleanup 39 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp", "dist"] 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 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-data-extensions 2 | 3 | This is a collection of helpers/customizations to ember-data to make it work for our use cases. 4 | 5 | View [official documentation](http://jsonapi-suite.github.io/ember-data-extensions) 6 | 7 | ### Installation 8 | 9 | `ember install ember-data-jsonapi-extensions` 10 | 11 | ### Nested Forms 12 | 13 | Run the dummy app to see nested forms in action. The following `Post` has an `Author` and many `Tags`: 14 | 15 | 16 | -------------------------------------------------------------------------------- /addon/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsonapi-suite/ember-data-extensions/b2d91ffe8372c3ddd07b9ffb8bff63f820d04e02/addon/.gitkeep -------------------------------------------------------------------------------- /addon/mixins/model.js: -------------------------------------------------------------------------------- 1 | import Mixin from '@ember/object/mixin'; 2 | import { computed } from '@ember/object'; 3 | import EmberObject from '@ember/object'; 4 | import { A } from '@ember/array'; 5 | import { defineProperty } from '@ember/object'; 6 | import { guidFor } from '@ember/object/internals'; 7 | 8 | const resetRelations = function(record) { 9 | Object.keys(record.get('__recordsJustSaved')).forEach((relationName) => { 10 | let relationRecords = record.get('__recordsJustSaved')[relationName]; 11 | 12 | relationRecords.forEach((r) => { 13 | let shouldUnload = r.get('isNew') || r.get('markedForDestruction'); 14 | if (shouldUnload) { 15 | r.unloadRecord(); 16 | } else if (r.get('markedForDeletion')) { 17 | record.get(relationName).removeObject(r); 18 | r.set('markedForDeletion', false); 19 | } 20 | }); 21 | }); 22 | record.set('__recordsJustSaved', []); 23 | }; 24 | 25 | const defaultOptions = function(options) { 26 | if (options.resetRelations !== false) { 27 | options.resetRelations = true; 28 | } 29 | }; 30 | 31 | export default Mixin.create({ 32 | hasDirtyAttributes: computed('currentState.isDirty', 'markedForDestruction', 'markedForDeletion', '_manyToOneDeleted.[]', function() { 33 | let original = this._super(...arguments); 34 | return original || this.get('markedForDestruction') || this.get('markedForDeletion') || this.get('_manyToOneDeleted.length') > 0; 35 | }), 36 | 37 | markedForDeletion: computed('_markedForDeletion', function() { 38 | return this.get('_markedForDeletion') || false; 39 | }), 40 | 41 | markedForDestruction: computed('_markedForDestruction', function() { 42 | return this.get('_markedForDestruction') || false; 43 | }), 44 | 45 | markForDeletion() { 46 | this.set('_markedForDeletion', true); 47 | }, 48 | 49 | unmarkForDeletion() { 50 | this.set('_markedForDeletion', false); 51 | }, 52 | 53 | markForDestruction() { 54 | this.set('_markedForDestruction', true); 55 | }, 56 | 57 | unmarkForDestruction() { 58 | this.set('_markedForDestruction', false); 59 | }, 60 | 61 | markManyToOneDeletion(relation) { 62 | let deletedRelations = this.get('_manyToOneDeleted'); 63 | 64 | if (!deletedRelations) { 65 | this.set('_manyToOneDeleted', A()); 66 | deletedRelations = this.get('_manyToOneDeleted'); 67 | } 68 | 69 | if (!deletedRelations.includes(relation)) { 70 | deletedRelations.pushObject(relation); 71 | } 72 | }, 73 | 74 | unmarkManyToOneDeletion(relation) { 75 | return this.markedForManyToOneDeletion(relation) && this.get('_manyToOneDeleted').removeObject(relation); 76 | }, 77 | 78 | markedForManyToOneDeletion(relation) { 79 | return this.get('_manyToOneDeleted') && this.get('_manyToOneDeleted').includes(relation); 80 | }, 81 | 82 | markManyToManyDeletion(relation, model) { 83 | let deletedRelations = this.get('_manyToManyDeleted'); 84 | if(!deletedRelations) { 85 | this.set('_manyToManyDeleted', EmberObject.create()); 86 | deletedRelations = this.get('_manyToManyDeleted'); 87 | } 88 | 89 | if(!deletedRelations.get(relation)) { 90 | deletedRelations.set(relation, A()); 91 | defineProperty( 92 | this, 93 | `manyToManyDeleted${relation}`, computed.readOnly(`_manyToManyDeleted.${relation}`) 94 | ); 95 | } 96 | 97 | if(!deletedRelations.get(relation).includes(model)) { 98 | deletedRelations.get(relation).pushObject(model); 99 | } 100 | }, 101 | 102 | manyToManyMarkedForDeletionModels(relation) { 103 | const relationModels = this.get('_manyToManyDeleted') && 104 | this.get(`_manyToManyDeleted.${relation}`); 105 | return relationModels && relationModels.toArray() || []; 106 | }, 107 | 108 | unmarkManyToManyDeletion(relation, model) { 109 | return this.get('_manyToManyDeleted') && 110 | this.get(`_manyToManyDeleted.${relation}`) && 111 | this.get(`_manyToManyDeleted.${relation}`).removeObject(model); 112 | }, 113 | 114 | tempId() { 115 | if (!this._tempId) { 116 | this._tempId = guidFor(this); 117 | } 118 | return this._tempId; 119 | }, 120 | 121 | jsonapiType() { 122 | return this.store 123 | .adapterFor(this.constructor.modelName) 124 | .pathForType(this.constructor.modelName); 125 | }, 126 | 127 | // Blank out all relations after saving 128 | // We will use the server response includes to 'reset' 129 | // these relations 130 | save(options = {}) { 131 | defaultOptions(options); 132 | let promise = this._super(...arguments); 133 | if (options.resetRelations === true) { 134 | promise.then(resetRelations); 135 | } 136 | return promise; 137 | } 138 | }); 139 | -------------------------------------------------------------------------------- /addon/mixins/nested-relations.js: -------------------------------------------------------------------------------- 1 | import Mixin from '@ember/object/mixin'; 2 | import { copy } from '@ember/object/internals'; 3 | import { merge } from '@ember/polyfills'; 4 | 5 | // This is for reference in our post-save promise 6 | // We need to unload these records after save, otherwise 7 | // we will be left with 2 of the same object - one persisted 8 | // and one not. 9 | // This is only required for hasMany's 10 | let savedRecords = {}; 11 | 12 | const iterateRelations = function(record, relations, callback) { 13 | Object.keys(relations).forEach((relationName) => { 14 | let subRelations = relations[relationName]; 15 | 16 | let metadata = record.relationshipFor(relationName); 17 | let kind = metadata.kind; 18 | let relatedRecord = record.get(relationName); 19 | let manyToManyDeleted = record.manyToManyMarkedForDeletionModels(relationName); 20 | let isManyToOneDelete = record.markedForManyToOneDeletion(relationName); 21 | 22 | if (metadata.options.async !== false) { 23 | relatedRecord = relatedRecord.get('content'); 24 | } 25 | 26 | if (relatedRecord) { 27 | callback(relationName, kind, relatedRecord, subRelations, manyToManyDeleted, isManyToOneDelete); 28 | } 29 | }); 30 | }; 31 | 32 | const isPresentObject = function(val) { 33 | return val && Object.keys(val).length > 0; 34 | }; 35 | 36 | const attributesFor = function(record) { 37 | let attrs = {}; 38 | 39 | let changes = record.changedAttributes(); 40 | let serializer = record.store.serializerFor(record.constructor.modelName); 41 | 42 | record.eachAttribute((name/* meta */) => { 43 | let keyName = serializer.keyForAttribute(name); 44 | 45 | if (record.get('isNew') || changes[name]) { 46 | let value = record.get(name); 47 | 48 | if (value !== undefined) { 49 | attrs[keyName] = record.get(name); 50 | } 51 | } 52 | }); 53 | 54 | return record.transformJsonapiAttrs ? record.transformJsonapiAttrs(attrs) : attrs; 55 | }; 56 | 57 | const jsonapiPayload = function(record, relationshipMarkedForDeletion) { 58 | let attributes = attributesFor(record); 59 | 60 | let payload = { type: record.jsonapiType() }; 61 | 62 | if (isPresentObject(attributes)) { 63 | payload.attributes = attributes; 64 | } 65 | 66 | if (record.get('isNew')) { 67 | payload['temp-id'] = record.tempId(); 68 | payload['method'] = 'create'; 69 | } 70 | else if (record.get('markedForDestruction')) { 71 | payload['method'] = 'destroy'; 72 | } 73 | else if (record.get('markedForDeletion') || relationshipMarkedForDeletion) { 74 | payload['method'] = 'disassociate'; 75 | } 76 | else if (record.get('currentState.isDirty')) { 77 | payload['method'] = 'update'; 78 | } 79 | 80 | if (record.id) { 81 | payload.id = record.id; 82 | } 83 | 84 | return payload; 85 | }; 86 | 87 | const payloadForInclude = function(payload) { 88 | let payloadCopy = copy(payload, true); 89 | delete(payloadCopy.method); 90 | 91 | return payloadCopy; 92 | }; 93 | 94 | const payloadForRelationship = function(payload) { 95 | let payloadCopy = copy(payload, true); 96 | delete(payloadCopy.attributes); 97 | delete(payloadCopy.relationships); 98 | 99 | return payloadCopy; 100 | }; 101 | 102 | const addToIncludes = function(payload, includedRecords) { 103 | let includedPayload = payloadForInclude(payload); 104 | 105 | if (!includedPayload.attributes && !isPresentObject(includedPayload.relationships)) { 106 | return; 107 | } 108 | 109 | const alreadyIncluded = includedRecords.find((includedRecord) => 110 | includedPayload['type'] === includedRecord['type'] && 111 | ((includedPayload['temp-id'] && includedPayload['temp-id'] === includedRecord['temp-id']) || 112 | (includedPayload['id'] && includedPayload['id'] === includedRecord['id'])) 113 | ) !== undefined; 114 | 115 | if (!alreadyIncluded) { 116 | includedRecords.push(includedPayload); 117 | } 118 | }; 119 | 120 | const hasManyData = function(relationName, relatedRecords, subRelations, manyToManyDeleted, includedRecords) { 121 | let payloads = []; 122 | savedRecords[relationName] = []; 123 | 124 | relatedRecords.forEach((relatedRecord) => { 125 | let payload = jsonapiPayload(relatedRecord, manyToManyDeleted && manyToManyDeleted.includes(relatedRecord)); 126 | processRelationships(subRelations, payload, relatedRecord, includedRecords); 127 | addToIncludes(payload, includedRecords); 128 | 129 | payloads.push(payloadForRelationship(payload)); 130 | savedRecords[relationName].push(relatedRecord); 131 | }); 132 | return { data: payloads }; 133 | }; 134 | 135 | const belongsToData = function(relatedRecord, subRelations, isManyToOneDelete, includedRecords) { 136 | let payload = jsonapiPayload(relatedRecord, isManyToOneDelete); 137 | processRelationships(subRelations, payload, relatedRecord, includedRecords); 138 | addToIncludes(payload, includedRecords); 139 | 140 | return { data: payloadForRelationship(payload) }; 141 | }; 142 | 143 | const processRelationship = function(name, kind, relationData, subRelations, manyToManyDeleted, isManyToOneDelete, includedRecords, callback) { 144 | let payload = null; 145 | 146 | if (kind === 'hasMany') { 147 | payload = hasManyData(name, relationData, subRelations, manyToManyDeleted, includedRecords); 148 | } else { 149 | payload = belongsToData(relationData, subRelations, isManyToOneDelete, includedRecords); 150 | } 151 | 152 | if (payload && payload.data) { 153 | callback(payload); 154 | } 155 | }; 156 | 157 | const processRelationships = function(relationshipHash, jsonData, record, includedRecords) { 158 | if (isPresentObject(relationshipHash)) { 159 | jsonData.relationships = {}; 160 | 161 | iterateRelations(record, relationshipHash, (name, kind, related, subRelations, manyToManyDeleted, isManyToOneDelete) => { 162 | processRelationship(name, kind, related, subRelations, manyToManyDeleted, isManyToOneDelete, includedRecords, (payload) => { 163 | let serializer = record.store.serializerFor(record.constructor.modelName); 164 | let serializedName = serializer.keyForRelationship(name); 165 | jsonData.relationships[serializedName] = payload; 166 | }); 167 | }); 168 | } 169 | }; 170 | 171 | const relationshipsDirective = function(value) { 172 | let directive = {}; 173 | 174 | if (value) { 175 | if (typeof(value) === 'string') { 176 | directive[value] = {}; 177 | } else if(Array.isArray(value)) { 178 | value.forEach((key) => { 179 | merge(directive, relationshipsDirective(key)); 180 | }); 181 | } else { 182 | Object.keys(value).forEach((key) => { 183 | directive[key] = relationshipsDirective(value[key]); 184 | }); 185 | } 186 | } else { 187 | return {}; 188 | } 189 | 190 | return directive; 191 | }; 192 | 193 | export default Mixin.create({ 194 | serialize(snapshot/*, options */) { 195 | savedRecords = []; 196 | 197 | let json = this._super(...arguments); 198 | let includedRecords = []; 199 | 200 | if (snapshot.record.get('emberDataExtensions') !== false) { 201 | delete(json.data.relationships); 202 | delete(json.data.attributes); 203 | 204 | let adapterOptions = snapshot.adapterOptions || {}; 205 | 206 | let attributes = attributesFor(snapshot.record); 207 | if (isPresentObject(attributes)) { 208 | json.data.attributes = attributes; 209 | } 210 | 211 | if (snapshot.record.id) { 212 | json.data.id = snapshot.record.id.toString(); 213 | } 214 | 215 | if (adapterOptions.attributes === false) { 216 | delete(json.data.attributes); 217 | } 218 | 219 | if (adapterOptions.attributes) { 220 | if (!json.data.attributes) { 221 | json.data.attributes = {}; 222 | } 223 | 224 | Object.keys(adapterOptions.attributes).forEach((k) => { 225 | json.data.attributes[k] = adapterOptions.attributes[k]; 226 | }); 227 | } 228 | 229 | let relationships = relationshipsDirective(adapterOptions.relationships); 230 | processRelationships(relationships, json.data, snapshot.record, includedRecords); 231 | if (includedRecords && includedRecords.length > 0) { 232 | json.included = includedRecords; 233 | } 234 | snapshot.record.set('__recordsJustSaved', savedRecords); 235 | } 236 | 237 | return json; 238 | } 239 | }); 240 | -------------------------------------------------------------------------------- /app/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsonapi-suite/ember-data-extensions/b2d91ffe8372c3ddd07b9ffb8bff63f820d04e02/app/.gitkeep -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-data-extensions", 3 | "dependencies": { 4 | "ember": "~2.7.0", 5 | "ember-cli-shims": "0.1.1", 6 | "ember-qunit-notifications": "0.1.0", 7 | "pretender": "~1.1.0", 8 | "Faker": "~3.1.0", 9 | "materialize": "0.97.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /config/ember-try.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true*/ 2 | module.exports = { 3 | scenarios: [ 4 | { 5 | name: 'default', 6 | bower: { 7 | dependencies: { } 8 | } 9 | }, 10 | { 11 | name: 'ember-1.13', 12 | bower: { 13 | dependencies: { 14 | 'ember': '~1.13.0' 15 | }, 16 | resolutions: { 17 | 'ember': '~1.13.0' 18 | } 19 | } 20 | }, 21 | { 22 | name: 'ember-release', 23 | bower: { 24 | dependencies: { 25 | 'ember': 'components/ember#release' 26 | }, 27 | resolutions: { 28 | 'ember': 'release' 29 | } 30 | } 31 | }, 32 | { 33 | name: 'ember-beta', 34 | bower: { 35 | dependencies: { 36 | 'ember': 'components/ember#beta' 37 | }, 38 | resolutions: { 39 | 'ember': 'beta' 40 | } 41 | } 42 | }, 43 | { 44 | name: 'ember-canary', 45 | bower: { 46 | dependencies: { 47 | 'ember': 'components/ember#canary' 48 | }, 49 | resolutions: { 50 | 'ember': 'canary' 51 | } 52 | } 53 | } 54 | ] 55 | }; 56 | -------------------------------------------------------------------------------- /config/environment.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true*/ 2 | 'use strict'; 3 | 4 | module.exports = function(/* environment, appConfig */) { 5 | return { }; 6 | }; 7 | -------------------------------------------------------------------------------- /ember-cli-build.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true*/ 2 | /* global require, module */ 3 | var EmberAddon = require('ember-cli/lib/broccoli/ember-addon'); 4 | 5 | module.exports = function(defaults) { 6 | var app = new EmberAddon(defaults, { 7 | // Add options here 8 | }); 9 | 10 | app.import('bower_components/materialize/dist/css/materialize.css', { prepend: true }); 11 | 12 | /* 13 | This build file specifies the options for the dummy test app of this 14 | addon, located in `/tests/dummy` 15 | This build file does *not* influence how the addon or the app using it 16 | behave. You most likely want to be modifying `./index.js` or app's build file 17 | */ 18 | 19 | return app.toTree(); 20 | }; 21 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 'use strict'; 3 | 4 | module.exports = { 5 | name: 'ember-data-extensions', 6 | isDevelopingAddon() { 7 | return true; 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-data-jsonapi-extensions", 3 | "version": "0.8.0", 4 | "description": "The default blueprint for ember-cli addons.", 5 | "directories": { 6 | "doc": "doc", 7 | "test": "tests" 8 | }, 9 | "scripts": { 10 | "build": "ember build", 11 | "start": "ember server", 12 | "test": "ember try:each" 13 | }, 14 | "repository": "https://github.com/jsonapi-suite/ember-data-extensions", 15 | "engines": { 16 | "node": ">= 0.10.0" 17 | }, 18 | "author": "", 19 | "license": "MIT", 20 | "devDependencies": { 21 | "broccoli-asset-rev": "^2.4.2", 22 | "ember-ajax": "^3.0.1", 23 | "ember-cli": "^2.7.0", 24 | "ember-cli-app-version": "^2.0.0", 25 | "ember-cli-dependency-checker": "^1.2.0", 26 | "ember-cli-eslint": "4.2.3", 27 | "ember-cli-htmlbars": "^1.0.3", 28 | "ember-cli-htmlbars-inline-precompile": "^0.4.0", 29 | "ember-cli-inject-live-reload": "^1.4.0", 30 | "ember-cli-mirage": "^0.2.1", 31 | "ember-cli-page-object": "^1.6.0", 32 | "ember-cli-qunit": "^3.0.0", 33 | "ember-cli-release": "^0.2.9", 34 | "ember-cli-sri": "^2.1.0", 35 | "ember-cli-test-loader": "^2.0.0", 36 | "ember-cli-uglify": "^1.2.0", 37 | "ember-disable-prototype-extensions": "^1.1.0", 38 | "ember-export-application-global": "^2.0.0", 39 | "ember-load-initializers": "^0.5.1", 40 | "ember-resolver": "^5.0.0", 41 | "eslint-plugin-ember": "^5.2.0", 42 | "loader.js": "^4.0.1" 43 | }, 44 | "keywords": [ 45 | "ember-addon" 46 | ], 47 | "dependencies": { 48 | "ember-cli-babel": "^6.6.0", 49 | "ember-data": "^2.7.0" 50 | }, 51 | "ember-addon": { 52 | "configPath": "tests/dummy/config" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /testem.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true*/ 2 | module.exports = { 3 | "framework": "qunit", 4 | "test_page": "tests/index.html?hidepassed", 5 | "disable_watching": true, 6 | "launch_in_ci": [ 7 | "Chrome" 8 | ], 9 | "launch_in_dev": [ 10 | "Chrome" 11 | ] 12 | }; 13 | -------------------------------------------------------------------------------- /tests/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | embertest: true 4 | }, 5 | globals: { 6 | server: true, 7 | QUnit: true 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /tests/acceptance/create-nested-relations-test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'qunit'; 2 | import moduleForAcceptance from '../../tests/helpers/module-for-acceptance'; 3 | import page from 'dummy/tests/pages/post-form'; 4 | import detailPage from 'dummy/tests/pages/show-post'; 5 | 6 | moduleForAcceptance('Acceptance | create nested relations'); 7 | 8 | test('creating a record with nested relations', function(assert) { 9 | page 10 | .visit() 11 | .title.fillIn('my post'); 12 | 13 | page.addTag().tags(0).setName('new tag 1'); 14 | page.addTag().tags(1).setName('new tag 2'); 15 | page.addTag(); 16 | page.authorName.fillIn('John Doe'); 17 | page.submit(); 18 | 19 | andThen(function() { 20 | assert.equal(detailPage.title, 'my post', 'saves basic attributes correctly'); 21 | assert.equal(detailPage.tagList, 'new tag 1, new tag 2', 'saves one-to-many correctly'); 22 | assert.equal(detailPage.tagIds, '1, 2', 'maintains one-to-many ids correctly'); 23 | assert.equal(detailPage.authorName, 'John Doe', 'saves one-to-one correctly'); 24 | assert.equal(detailPage.authorId, '1', 'maintains one-to-one ids correctly'); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /tests/acceptance/delete-nested-object-test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'qunit'; 2 | import moduleForAcceptance from '../../tests/helpers/module-for-acceptance'; 3 | import page from 'dummy/tests/pages/post-form'; 4 | import detailPage from 'dummy/tests/pages/show-post'; 5 | 6 | moduleForAcceptance('Acceptance | delete nested objects'); 7 | 8 | test('deleting a nested object', function(assert) { 9 | page.visit(); 10 | page.addTag().tags(0).setName('a'); 11 | page.submit(); 12 | 13 | andThen(function() { 14 | assert.equal(detailPage.tagList, 'a'); 15 | detailPage.edit(); 16 | 17 | andThen(function() { 18 | page.addTag().tags(1).setName('b'); 19 | page.submit(); 20 | 21 | andThen(function() { 22 | detailPage.edit(); 23 | 24 | andThen(function() { 25 | page.tags(0).remove(); 26 | 27 | andThen(function() { 28 | assert.equal(page.tags().count, 1, 'should not show removed tag'); 29 | page.submit(); 30 | 31 | andThen(function() { 32 | assert.equal(detailPage.tagList, 'b', 'should delete removed tag'); 33 | }); 34 | }); 35 | }); 36 | }); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /tests/acceptance/update-nested-relations-test.js: -------------------------------------------------------------------------------- 1 | import { test } from 'qunit'; 2 | import moduleForAcceptance from '../../tests/helpers/module-for-acceptance'; 3 | import page from 'dummy/tests/pages/post-form'; 4 | import detailPage from 'dummy/tests/pages/show-post'; 5 | 6 | moduleForAcceptance('Acceptance | update nested relations'); 7 | 8 | test('updating nested relations', function(assert) { 9 | let author = server.create('author', { name: 'Joe Author' }); 10 | let tag1 = server.create('tag', { name: 'tag1' }); 11 | let tag2 = server.create('tag', { name: 'tag2' }); 12 | let post = server.create('post', { 13 | author: author, 14 | tags: [tag1, tag2], 15 | title: 'test title' 16 | }); 17 | visit(`/posts/${post.id}/edit`); 18 | 19 | andThen(function() { 20 | assert.equal(page.title.val, 'test title'); 21 | assert.equal(page.authorName.val, 'Joe Author'); 22 | assert.equal(page.tags().count, 2); 23 | assert.equal(page.tags(0).name, 'tag1'); 24 | assert.equal(page.tags(1).name, 'tag2'); 25 | 26 | page.authorName.fillIn('new author'); 27 | page.tags(1).setName('tag2 changed'); 28 | page.addTag(); 29 | page.tags(2).setName('new tag'); 30 | page.submit(); 31 | 32 | andThen(function() { 33 | post.reload(); 34 | assert.equal(post.author.id, author.id, 'updates existing author'); 35 | assert.equal(detailPage.authorName, 'new author', 'updates author name'); 36 | assert.equal(detailPage.tagList, 'tag1, tag2 changed, new tag', 'updates one-to-many correctly'); 37 | }); 38 | }); 39 | }); 40 | 41 | test('updating only one member of a hasMany relation', function(assert) { 42 | server.foo = 'bar'; 43 | let tag1 = server.create('tag', { name: 'tag1' }); 44 | let tag2 = server.create('tag', { name: 'tag2' }); 45 | let post = server.create('post', { 46 | tags: [tag1, tag2], 47 | }); 48 | visit(`/posts/${post.id}/edit`); 49 | 50 | andThen(function() { 51 | assert.equal(page.tags().count, 2); 52 | page.tags(0).setName('tag1 changed'); 53 | page.submit(); 54 | 55 | andThen(function() { 56 | assert.equal(detailPage.tagList, 'tag1 changed, tag2'); 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /tests/dummy/app/app.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Application from '@ember/application'; 3 | import Resolver from './resolver'; 4 | import loadInitializers from 'ember-load-initializers'; 5 | import config from './config/environment'; 6 | 7 | let App; 8 | 9 | Ember.MODEL_FACTORY_INJECTIONS = true; 10 | 11 | App = Application.extend({ 12 | modulePrefix: config.modulePrefix, 13 | podModulePrefix: config.podModulePrefix, 14 | Resolver 15 | }); 16 | 17 | loadInitializers(App, config.modulePrefix); 18 | 19 | export default App; 20 | -------------------------------------------------------------------------------- /tests/dummy/app/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsonapi-suite/ember-data-extensions/b2d91ffe8372c3ddd07b9ffb8bff63f820d04e02/tests/dummy/app/components/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/controllers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsonapi-suite/ember-data-extensions/b2d91ffe8372c3ddd07b9ffb8bff63f820d04e02/tests/dummy/app/controllers/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/controllers/post-form.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import { filterBy } from '@ember/object/computed'; 3 | 4 | export default Controller.extend({ 5 | tags: filterBy('model.tags', 'markedForDeletion', false) 6 | }); 7 | -------------------------------------------------------------------------------- /tests/dummy/app/edit-post/route.js: -------------------------------------------------------------------------------- 1 | import PostFormMixin from 'dummy/mixins/post-form-route'; 2 | import Route from '@ember/routing/route'; 3 | 4 | export default Route.extend(PostFormMixin, { 5 | model(params) { 6 | return this.store.findRecord('post', params.post_id, { 7 | include: 'tags,author' 8 | }); 9 | } 10 | }); 11 | -------------------------------------------------------------------------------- /tests/dummy/app/helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsonapi-suite/ember-data-extensions/b2d91ffe8372c3ddd07b9ffb8bff63f820d04e02/tests/dummy/app/helpers/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | 12 | 13 | 14 | 15 | {{content-for "head-footer"}} 16 | 17 | 18 | {{content-for "body"}} 19 | 20 | 21 | 22 | 23 | {{content-for "body-footer"}} 24 | 25 | 26 | -------------------------------------------------------------------------------- /tests/dummy/app/mixins/post-form-route.js: -------------------------------------------------------------------------------- 1 | import Mixin from '@ember/object/mixin'; 2 | 3 | export default Mixin.create({ 4 | renderTemplate(controller, model) { 5 | this.render('post-form', { 6 | controller: 'post-form', 7 | model: model 8 | }); 9 | }, 10 | 11 | actions: { 12 | submit(model) { 13 | model.save({ adapterOptions: { relationships: { 'tags': {}, 'author': {} }}}).then((m) => { 14 | this.transitionTo('post', m.id); 15 | }); 16 | }, 17 | 18 | addTag(model) { 19 | let tag = this.store.createRecord('tag'); 20 | model.get('tags').pushObject(tag); 21 | }, 22 | 23 | removeTag(model, tag) { 24 | tag.markForDeletion(); 25 | } 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /tests/dummy/app/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsonapi-suite/ember-data-extensions/b2d91ffe8372c3ddd07b9ffb8bff63f820d04e02/tests/dummy/app/models/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/models/author.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | import ModelMixin from 'ember-data-extensions/mixins/model'; 3 | 4 | export default DS.Model.extend(ModelMixin, { 5 | name: DS.attr('string') 6 | }); 7 | -------------------------------------------------------------------------------- /tests/dummy/app/models/post.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | import ModelMixin from 'ember-data-extensions/mixins/model'; 3 | import { computed } from '@ember/object'; 4 | 5 | export default DS.Model.extend(ModelMixin, { 6 | title: DS.attr('string'), 7 | publishedDate: DS.attr('date'), 8 | 9 | author: DS.belongsTo('author'), 10 | tags: DS.hasMany('tag'), 11 | 12 | tagNames: computed('tags.@each.name', function() { 13 | return this.get('tags').mapBy('name').join(', '); 14 | }), 15 | 16 | tagIds: computed('tags.@each.name', function() { 17 | return this.get('tags').mapBy('id').join(', '); 18 | }) 19 | }); 20 | -------------------------------------------------------------------------------- /tests/dummy/app/models/tag.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | import ModelMixin from 'ember-data-extensions/mixins/model'; 3 | 4 | export default DS.Model.extend(ModelMixin, { 5 | name: DS.attr('string') 6 | }); 7 | -------------------------------------------------------------------------------- /tests/dummy/app/new-post/route.js: -------------------------------------------------------------------------------- 1 | import PostFormMixin from 'dummy/mixins/post-form-route'; 2 | import Route from '@ember/routing/route'; 3 | 4 | export default Route.extend(PostFormMixin, { 5 | model() { 6 | return this.store.createRecord('post', { 7 | author: this.store.createRecord('author') 8 | }); 9 | } 10 | }); 11 | -------------------------------------------------------------------------------- /tests/dummy/app/post/route.js: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | 3 | export default Route.extend({ 4 | model(params) { 5 | return this.store.peekRecord('post', params.post_id); 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /tests/dummy/app/post/template.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
Post Detail
9 | {{#link-to 'edit-post' model.id class='edit'}} 10 | Edit 11 | {{/link-to}} 12 | 13 |
14 | 15 | {{model.title}} 16 |
17 |
18 | 19 | {{model.author.name}} 20 |
21 | 22 |
23 | 24 | {{model.author.id}} 25 |
26 | 27 |
28 | 29 | {{model.tagNames}} 30 |
31 | 32 |
33 | 34 | {{model.tagIds}} 35 |
36 |
37 |
38 |
39 |
40 | 41 |
42 |
43 |
44 | -------------------------------------------------------------------------------- /tests/dummy/app/resolver.js: -------------------------------------------------------------------------------- 1 | import Resolver from 'ember-resolver'; 2 | 3 | export default Resolver; 4 | -------------------------------------------------------------------------------- /tests/dummy/app/router.js: -------------------------------------------------------------------------------- 1 | import config from './config/environment'; 2 | import EmberRouter from '@ember/routing/router'; 3 | 4 | const Router = EmberRouter.extend({ 5 | location: config.locationType, 6 | rootURL: config.rootURL 7 | }); 8 | 9 | Router.map(function() { 10 | this.route('new-post', { path: '/' }); 11 | this.route('edit-post', { path: '/posts/:post_id/edit' }); 12 | this.route('post', { path: '/posts/:post_id' }); 13 | }); 14 | 15 | export default Router; 16 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsonapi-suite/ember-data-extensions/b2d91ffe8372c3ddd07b9ffb8bff63f820d04e02/tests/dummy/app/routes/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/serializers/application.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | import NestedRelationsMixin from 'ember-data-extensions/mixins/nested-relations'; 3 | import { underscore } from '@ember/string'; 4 | 5 | export default DS.JSONAPISerializer.extend(NestedRelationsMixin, { 6 | keyForAttribute(key /* relationship, method */) { 7 | return underscore(key); 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /tests/dummy/app/styles/app.css: -------------------------------------------------------------------------------- 1 | .clearfix:before, 2 | .clearfix:after { 3 | content: " "; 4 | display: table; 5 | } 6 | 7 | .clearfix:after { 8 | clear: both; 9 | } 10 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsonapi-suite/ember-data-extensions/b2d91ffe8372c3ddd07b9ffb8bff63f820d04e02/tests/dummy/app/templates/components/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/templates/post-form.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
6 |
7 |
8 |
9 |
New Post
10 | 11 |
12 |
13 | {{input id='title' type='text' value=model.title placeholder='Title'}} 14 |
15 | 16 |
17 | {{input type='text' value=model.author.name placeholder='Author Name'}} 18 |
19 | 20 |
21 | Add Tag 22 |
    23 | {{#each tags as |tag|}} 24 |
  • 25 | {{input class='col s7' type='text' value=tag.name placeholder='Enter Tag Name'}} 26 | x 27 |
  • 28 | {{/each}} 29 |
30 |
31 | 32 |
33 | 34 |
35 |
36 |
37 |
38 |
39 |
40 | 41 |
42 |
43 |
44 | -------------------------------------------------------------------------------- /tests/dummy/config/environment.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 3 | module.exports = function(environment) { 4 | var ENV = { 5 | modulePrefix: 'dummy', 6 | exportApplicationGlobal: true, 7 | environment: environment, 8 | rootURL: '/', 9 | locationType: 'auto', 10 | EmberENV: { 11 | FEATURES: { 12 | // Here you can enable experimental features on an ember canary build 13 | // e.g. 'with-controller': true 14 | } 15 | }, 16 | 17 | APP: { 18 | // Here you can pass flags/options to your application instance 19 | // when it is created 20 | } 21 | }; 22 | 23 | if (environment === 'development') { 24 | // ENV.APP.LOG_RESOLVER = true; 25 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 26 | // ENV.APP.LOG_TRANSITIONS = true; 27 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 28 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 29 | } 30 | 31 | if (environment === 'test') { 32 | // Testem prefers this... 33 | ENV.locationType = 'none'; 34 | 35 | // keep test console output quieter 36 | ENV.APP.LOG_ACTIVE_GENERATION = false; 37 | ENV.APP.LOG_VIEW_LOOKUPS = false; 38 | 39 | ENV.APP.rootElement = '#ember-testing'; 40 | } 41 | 42 | return ENV; 43 | }; 44 | -------------------------------------------------------------------------------- /tests/dummy/mirage/config.js: -------------------------------------------------------------------------------- 1 | import { A } from '@ember/array'; 2 | import { isPresent } from '@ember/utils'; 3 | 4 | const attributes = function(request) { 5 | return JSON.parse(request.requestBody).data.attributes || {}; 6 | }; 7 | 8 | const iterateRelations = function(request, callback) { 9 | let relationships = JSON.parse(request.requestBody).data.relationships || {}; 10 | Object.keys(relationships).forEach((relationName) => { 11 | let data = relationships[relationName].data; 12 | callback(relationName, data); 13 | }); 14 | }; 15 | 16 | // Omit anything that has all blank attributes 17 | // Akin to rails' accepts_nested_attributes_for :foos, reject_if: :all_blank 18 | const recordFromJson = function(db, data, includedData, callback) { 19 | 20 | let found; 21 | 22 | if (data['temp-id']) { 23 | found = includedData.filter(item => (item['temp-id'] === data['temp-id']))[0]; 24 | } 25 | else { 26 | found = includedData.filter(item => (item.id === data.id))[0]; 27 | } 28 | 29 | let attributes = found ? found.attributes : {}; 30 | 31 | if (data.id) { 32 | let record = db[data.type].find(data.id); 33 | 34 | if (data['method'] === 'update') { 35 | record.update(attributes); 36 | } 37 | callback(record); 38 | return; 39 | } 40 | 41 | let notNull = false; 42 | Object.keys(attributes).forEach((key) => { 43 | if (isPresent(attributes[key])) { 44 | notNull = true; 45 | } 46 | }); 47 | 48 | if (notNull) { 49 | callback(db[data.type].new(attributes)); 50 | } 51 | }; 52 | 53 | const mapBy = function(array, attribute) { 54 | return A(array).mapBy(attribute); 55 | }; 56 | 57 | const contains = function(array, element) { 58 | return A(array).contains(element); 59 | }; 60 | 61 | const hasRecord = function(array, record) { 62 | if (record.id) { 63 | let ids = mapBy(array, 'id'); 64 | return contains(ids, record.id); 65 | } else { 66 | return false; 67 | } 68 | }; 69 | 70 | const buildOneToMany = function(db, relationData, includedRecords, originalRecords) { 71 | relationData.forEach((data) => { 72 | let method = data.method; 73 | 74 | recordFromJson(db, data, includedRecords, (record) => { 75 | if (method === 'disassociate' || method === 'destroy') { 76 | let index = originalRecords.indexOf(record); 77 | originalRecords.splice(index, 1); 78 | } 79 | else { 80 | if (!hasRecord(originalRecords, record)) { 81 | originalRecords.push(record); 82 | } 83 | } 84 | }); 85 | }); 86 | return originalRecords; 87 | }; 88 | 89 | const processRelations = function(record, db, request) { 90 | let includedRecords = JSON.parse(request.requestBody).included || []; 91 | 92 | iterateRelations(request, (relationName, relationData) => { 93 | if (Array.isArray(relationData)) { 94 | let originals = record[relationName].models; 95 | record[relationName] = buildOneToMany(db, relationData, includedRecords, originals); 96 | } else { 97 | recordFromJson(db, relationData, includedRecords, (relationRecord, remove) => { 98 | record[relationName] = relationRecord; 99 | if (remove) { 100 | delete record[relationName]; 101 | } 102 | }); 103 | } 104 | }); 105 | record.save(); 106 | }; 107 | 108 | export default function() { 109 | this.logging = true; 110 | 111 | this.post('/posts', function(db, request) { 112 | let post = db.posts.create(attributes(request)); 113 | processRelations(post, db, request); 114 | 115 | return post; 116 | }); 117 | 118 | this.patch('/posts/:id', function(db, request) { 119 | let post = db.posts.find(request.params.id); 120 | processRelations(post, db, request); 121 | return post; 122 | }); 123 | 124 | this.get('/posts/:id', function(db, request) { 125 | let post = db.posts.find(request.params.id); 126 | return post; 127 | }); 128 | 129 | this.get('/authors/:id'); 130 | this.get('/tags/:id'); 131 | } 132 | -------------------------------------------------------------------------------- /tests/dummy/mirage/models/author.js: -------------------------------------------------------------------------------- 1 | import { Model, hasMany } from 'ember-cli-mirage'; 2 | 3 | export default Model.extend({ 4 | posts: hasMany() 5 | }); 6 | -------------------------------------------------------------------------------- /tests/dummy/mirage/models/post.js: -------------------------------------------------------------------------------- 1 | import { Model, belongsTo, hasMany } from 'ember-cli-mirage'; 2 | 3 | export default Model.extend({ 4 | author: belongsTo(), 5 | tags: hasMany() 6 | }); 7 | -------------------------------------------------------------------------------- /tests/dummy/mirage/models/tag.js: -------------------------------------------------------------------------------- 1 | import { Model, belongsTo } from 'ember-cli-mirage'; 2 | 3 | export default Model.extend({ 4 | post: belongsTo() 5 | }); 6 | -------------------------------------------------------------------------------- /tests/dummy/mirage/scenarios/default.js: -------------------------------------------------------------------------------- 1 | export default function(/* server */) { 2 | 3 | /* 4 | Seed your development database using your factories. 5 | This data will not be loaded in your tests. 6 | 7 | Make sure to define a factory for each model you want to create. 8 | */ 9 | 10 | // server.createList('post', 10); 11 | } 12 | -------------------------------------------------------------------------------- /tests/dummy/mirage/serializers/application.js: -------------------------------------------------------------------------------- 1 | import { JSONAPISerializer } from 'ember-cli-mirage'; 2 | 3 | export default JSONAPISerializer.extend({ 4 | }); 5 | -------------------------------------------------------------------------------- /tests/dummy/mirage/serializers/post.js: -------------------------------------------------------------------------------- 1 | import ApplicationSerializer from './application'; 2 | 3 | export default ApplicationSerializer.extend({ 4 | init() { 5 | this._super(...arguments); 6 | this.set('include', ['tags', 'author']); 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /tests/dummy/public/crossdomain.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /tests/dummy/public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /tests/helpers/destroy-app.js: -------------------------------------------------------------------------------- 1 | import { run } from '@ember/runloop'; 2 | 3 | export default function destroyApp(application) { 4 | run(application, 'destroy'); 5 | server.shutdown(); 6 | } 7 | -------------------------------------------------------------------------------- /tests/helpers/get-owner.js: -------------------------------------------------------------------------------- 1 | // Ripped from ember-model-fragments 2 | // Ideally this uses Ember.getOwner... 3 | export default function(context) { 4 | let _context = context.application.__deprecatedInstance__; 5 | if (!_context || !_context.lookup) { 6 | _context = context.application.__container__; 7 | } 8 | return _context; 9 | } 10 | -------------------------------------------------------------------------------- /tests/helpers/module-for-acceptance.js: -------------------------------------------------------------------------------- 1 | import { module } from 'qunit'; 2 | import startApp from '../helpers/start-app'; 3 | import destroyApp from '../helpers/destroy-app'; 4 | import { Promise } from 'rsvp'; 5 | 6 | export default function(name, options = {}) { 7 | module(name, { 8 | beforeEach() { 9 | this.application = startApp(); 10 | 11 | if (options.beforeEach) { 12 | return options.beforeEach.apply(this, arguments); 13 | } 14 | }, 15 | 16 | afterEach() { 17 | let afterEach = options.afterEach && options.afterEach.apply(this, arguments); 18 | return Promise.resolve(afterEach).then(() => destroyApp(this.application)); 19 | } 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /tests/helpers/resolver.js: -------------------------------------------------------------------------------- 1 | import Resolver from '../../resolver'; 2 | import config from '../../config/environment'; 3 | 4 | const resolver = Resolver.create(); 5 | 6 | resolver.namespace = { 7 | modulePrefix: config.modulePrefix, 8 | podModulePrefix: config.podModulePrefix 9 | }; 10 | 11 | export default resolver; 12 | -------------------------------------------------------------------------------- /tests/helpers/start-app.js: -------------------------------------------------------------------------------- 1 | import Application from '../../app'; 2 | import config from '../../config/environment'; 3 | import { merge } from '@ember/polyfills'; 4 | import { run } from '@ember/runloop'; 5 | 6 | export default function startApp(attrs) { 7 | let application; 8 | 9 | let attributes = merge({}, config.APP); 10 | attributes = merge(attributes, attrs); // use defaults, but you can override; 11 | 12 | run(() => { 13 | application = Application.create(attributes); 14 | application.setupForTesting(); 15 | application.injectTestHelpers(); 16 | }); 17 | 18 | return application; 19 | } 20 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy Tests 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | {{content-for "test-head"}} 12 | 13 | 14 | 15 | 16 | 17 | {{content-for "head-footer"}} 18 | {{content-for "test-head-footer"}} 19 | 20 | 21 | {{content-for "body"}} 22 | {{content-for "test-body"}} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {{content-for "body-footer"}} 31 | {{content-for "test-body-footer"}} 32 | 33 | 34 | -------------------------------------------------------------------------------- /tests/integration/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsonapi-suite/ember-data-extensions/b2d91ffe8372c3ddd07b9ffb8bff63f820d04e02/tests/integration/.gitkeep -------------------------------------------------------------------------------- /tests/pages/post-form.js: -------------------------------------------------------------------------------- 1 | import { 2 | create, 3 | visitable, 4 | fillable, 5 | clickable, 6 | collection, 7 | value 8 | } from 'ember-cli-page-object'; 9 | 10 | export default create({ 11 | visit: visitable('/'), 12 | submit: clickable('button[type="submit"]'), 13 | addTag: clickable('.add-tag'), 14 | 15 | title: { 16 | scope: '.title', 17 | fillIn: fillable('input'), 18 | val: value('input') 19 | }, 20 | 21 | authorName: { 22 | scope: '.author-name', 23 | fillIn: fillable('input'), 24 | val: value('input') 25 | }, 26 | 27 | tags: collection({ 28 | itemScope: '.tags .tag', 29 | 30 | item: { 31 | name: value('input'), 32 | setName: fillable('input'), 33 | remove: clickable('a') 34 | } 35 | }) 36 | }); 37 | -------------------------------------------------------------------------------- /tests/pages/show-post.js: -------------------------------------------------------------------------------- 1 | import { 2 | create, 3 | visitable, 4 | text, 5 | clickable 6 | } from 'ember-cli-page-object'; 7 | 8 | export default create({ 9 | visit: visitable('/posts/:id'), 10 | title: text('.title span'), 11 | authorName: text('.author-name span'), 12 | authorId: text('.author-id span'), 13 | 14 | tagList: text('.tag-names span'), 15 | tagIds: text('.tag-ids span'), 16 | 17 | edit: clickable('.edit') 18 | }); 19 | -------------------------------------------------------------------------------- /tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import resolver from './helpers/resolver'; 2 | import { 3 | setResolver 4 | } from 'ember-qunit'; 5 | 6 | setResolver(resolver); 7 | -------------------------------------------------------------------------------- /tests/unit/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsonapi-suite/ember-data-extensions/b2d91ffe8372c3ddd07b9ffb8bff63f820d04e02/tests/unit/.gitkeep -------------------------------------------------------------------------------- /tests/unit/mixins/model-test.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | import ModelMixin from 'ember-data-extensions/mixins/model'; 3 | import moduleForAcceptance from '../../../tests/helpers/module-for-acceptance'; 4 | import getOwner from '../../../tests/helpers/get-owner'; 5 | import { run } from '@ember/runloop'; 6 | import { test } from 'qunit'; 7 | 8 | let store = null; 9 | let TestStore = DS.Store.extend(); 10 | 11 | const Author = DS.Model.extend(ModelMixin, { 12 | name: DS.attr('string') 13 | }); 14 | 15 | const Post = DS.Model.extend(ModelMixin, { 16 | title: DS.attr('string'), 17 | author: DS.belongsTo({ async: false }), 18 | tags: DS.hasMany({ async: true }) 19 | }); 20 | 21 | const Tag = DS.Model.extend(ModelMixin, { 22 | name: DS.attr('string') 23 | }); 24 | 25 | moduleForAcceptance('Unit | Mixin | model', { 26 | beforeEach() { 27 | getOwner(this).register('service:store', TestStore); 28 | store = getOwner(this).lookup('service:store'); 29 | getOwner(this).register('model:post', Post); 30 | getOwner(this).register('model:author', Author); 31 | getOwner(this).register('model:tag', Tag); 32 | } 33 | }); 34 | 35 | const seedPost = function() { 36 | store.pushPayload({ 37 | data: { 38 | type: 'posts', 39 | id: '1', 40 | attributes: { title: 'test title' } 41 | } 42 | }); 43 | return store.peekRecord('post', 1); 44 | }; 45 | 46 | test('#hasDirtyAttributes', function(assert) { 47 | run(() => { 48 | let post = store.createRecord('post'); 49 | assert.ok(post.get('hasDirtyAttributes'), 'should be true when new record'); 50 | post = seedPost(); 51 | assert.notOk(post.get('hasDirtyAttributes'), 'should be false when persisted record with no changes'); 52 | post.set('title', 'changed'); 53 | assert.ok(post.get('hasDirtyAttributes'), 'should be true when persisted record with changes'); 54 | post.set('title', 'test title'); 55 | assert.notOk(post.get('hasDirtyAttributes'), 'should be false when attributes reset'); 56 | post.set('markedForDestruction', true); 57 | assert.ok(post.get('hasDirtyAttributes'), 'should be true when marked for destruction'); 58 | post.set('markedForDestruction', false); 59 | post.set('markedForDeletion', true); 60 | assert.ok(post.get('hasDirtyAttributes'), 'should be true when marked for deletion'); 61 | }); 62 | }); 63 | 64 | // note tag with id 3 does not send attributes 65 | test('resetting relations when only sending dirty relations', function(assert) { 66 | let done = assert.async(); 67 | server.patch('/posts/:id', (db, request) => { 68 | let relationships = JSON.parse(request.requestBody).data.relationships; 69 | assert.deepEqual(relationships, { 70 | tags: { 71 | data: [ 72 | { 73 | id: '2', 74 | method: 'update', 75 | type: 'tags', 76 | }, 77 | { 78 | id: '3', 79 | type: 'tags' 80 | } 81 | ] 82 | } 83 | }); 84 | 85 | let included = JSON.parse(request.requestBody).included; 86 | assert.deepEqual(included, [ 87 | { 88 | id: '2', 89 | type: 'tags', 90 | attributes: { name: 'tag1 changed' } 91 | } 92 | ]); 93 | 94 | 95 | done(); 96 | let post = db.posts.find(request.params.id); 97 | post.tags.models[0].update({ name: 'tag1 changed' }); 98 | return post; 99 | }); 100 | 101 | let post = server.create('post'); 102 | post.createTag({ name: 'tag1' }); 103 | post.createTag({ name: 'tag2' }); 104 | run(() => { 105 | store.pushPayload({ 106 | data: { 107 | type: 'posts', 108 | id: 1, 109 | relationships: { 110 | tags: { 111 | data: [ 112 | { type: 'tags', id: '2' }, 113 | { type: 'tags', id: '3' } 114 | ] 115 | } 116 | } 117 | }, 118 | included: [ 119 | { type: 'tags', id: '2', attributes: { name: 'tag1' } }, 120 | { type: 'tags', id: '3', attributes: { name: 'tag2' } } 121 | ] 122 | }); 123 | }); 124 | 125 | post = store.peekRecord('post', 1); 126 | assert.equal(post.get('tags.length'), 2); 127 | post.set('tags.firstObject.name', 'tag1 changed'); 128 | 129 | let done2 = assert.async(); 130 | run(() => { 131 | post.save({ adapterOptions: { relationships: 'tags' } }).then((p) => { 132 | assert.equal(p.get('tags.length'), 2); 133 | done2(); 134 | }); 135 | }); 136 | }); 137 | -------------------------------------------------------------------------------- /tests/unit/mixins/nested-relations-test.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | import moduleForAcceptance from '../../../tests/helpers/module-for-acceptance'; 3 | import NestedRelationsMixin from 'ember-data-extensions/mixins/nested-relations'; 4 | import ModelMixin from 'ember-data-extensions/mixins/model'; 5 | import getOwner from '../../../tests/helpers/get-owner'; 6 | import { begin, end } from '@ember/runloop'; 7 | import { test } from 'qunit'; 8 | 9 | QUnit.dump.maxDepth = 999999999; 10 | 11 | let serializer = null; 12 | let store = null; 13 | 14 | const State = DS.Model.extend(ModelMixin, NestedRelationsMixin, { 15 | name: DS.attr('string') 16 | }); 17 | 18 | const Author = DS.Model.extend(ModelMixin, NestedRelationsMixin, { 19 | name: DS.attr('string'), 20 | state: DS.belongsTo() 21 | }); 22 | 23 | const User = DS.Model.extend(ModelMixin, NestedRelationsMixin, { 24 | name: DS.attr('string') 25 | }); 26 | 27 | const Tag = DS.Model.extend(ModelMixin, NestedRelationsMixin, { 28 | name: DS.attr('string'), 29 | creator: DS.belongsTo('user') 30 | }); 31 | 32 | const Genre = DS.Model.extend(ModelMixin, NestedRelationsMixin, { 33 | name: DS.attr('string') 34 | }); 35 | 36 | const Post = DS.Model.extend(ModelMixin, NestedRelationsMixin, { 37 | title: DS.attr('string'), 38 | publishedDate: DS.attr('date'), 39 | genre: DS.belongsTo(), 40 | author: DS.belongsTo(), 41 | asyncFalseAuthor: DS.belongsTo('author', { async: false }), 42 | tags: DS.hasMany() 43 | }); 44 | 45 | let TestSerializer = DS.JSONAPISerializer.extend(NestedRelationsMixin); 46 | let TestStore = DS.Store.extend(); 47 | 48 | moduleForAcceptance('Unit | Mixin | nested-relations', { 49 | beforeEach() { 50 | getOwner(this).register('service:store', TestStore); 51 | store = getOwner(this).lookup('service:store'); 52 | 53 | getOwner(this).register('test-container:test-serializer', TestSerializer); 54 | serializer = getOwner(this).lookup('test-container:test-serializer'); 55 | serializer.store = store; 56 | 57 | getOwner(this).register('model:post', Post); 58 | getOwner(this).register('model:tag', Tag); 59 | getOwner(this).register('model:author', Author); 60 | getOwner(this).register('model:genre', Genre); 61 | getOwner(this).register('model:state', State); 62 | getOwner(this).register('model:user', User); 63 | 64 | begin(); 65 | }, 66 | 67 | afterEach() { 68 | end(); 69 | } 70 | }); 71 | 72 | const serialize = function(record, adapterOptions) { 73 | let snapshot = record._internalModel.createSnapshot({ 74 | adapterOptions: adapterOptions 75 | }); 76 | 77 | let json = serializer.serialize(snapshot, {}); 78 | return json; 79 | }; 80 | 81 | const seedPostWithAuthor = function() { 82 | store.pushPayload({ 83 | data: { 84 | type: 'posts', 85 | id: 1, 86 | relationships: { 87 | author: { 88 | data: { 89 | type: 'authors', 90 | id: 2 91 | } 92 | } 93 | } 94 | }, 95 | included: [ 96 | { 97 | type: 'authors', 98 | id: 2, 99 | attributes: { name: 'Joe Author' } 100 | } 101 | ] 102 | }); 103 | }; 104 | 105 | const seedPostWithTags = function() { 106 | store.pushPayload({ 107 | data: { 108 | type: 'posts', 109 | id: 1, 110 | relationships: { 111 | tags: { 112 | data: [ 113 | { type: 'tags', id: 2 }, 114 | { type: 'tags', id: 3 }, 115 | { type: 'tags', id: 4 } 116 | ] 117 | } 118 | } 119 | }, 120 | included: [ 121 | { 122 | type: 'tags', 123 | id: 2, 124 | attributes: { name: 'tag1' } 125 | }, 126 | { 127 | type: 'tags', 128 | id: 3, 129 | attributes: { name: 'tag2' } 130 | }, 131 | { 132 | type: 'tags', 133 | id: 4, 134 | attributes: { name: 'tag3' } 135 | } 136 | ] 137 | }); 138 | }; 139 | 140 | test('it serializes basic attributes correctly', function(assert) { 141 | let post = store.createRecord('post', { title: 'test post' }); 142 | let json = serialize(post, {}); 143 | 144 | let expectedJSON = { 145 | data: { 146 | type: 'posts', 147 | attributes: { 148 | title: 'test post' 149 | } 150 | } 151 | }; 152 | 153 | assert.deepEqual(json, expectedJSON, 'has correct json'); 154 | }); 155 | 156 | test('it uses transformJsonapiAttrs to modify attributes', function(assert) { 157 | let post = store.createRecord('post', { title: 'test post' }); 158 | 159 | post.transformJsonapiAttrs = function(attrs) { 160 | return { 161 | titleUpcase: attrs.title.toUpperCase(), 162 | publishedDate: this.get('publishedDate') 163 | }; 164 | }; 165 | 166 | let json = serialize(post, {}); 167 | 168 | let expectedJSON = { 169 | data: { 170 | type: 'posts', 171 | attributes: { 172 | titleUpcase: 'test post'.toUpperCase(), 173 | publishedDate: post.get('publishedDate') 174 | } 175 | } 176 | }; 177 | 178 | assert.deepEqual(json, expectedJSON, 'has correct json'); 179 | }); 180 | 181 | test('it respects custom keyForAttribute settings in serializer', function(assert) { 182 | let date = new Date(); 183 | let post = store.createRecord('post', { publishedDate: date }); 184 | let json = serialize(post, {}); 185 | 186 | let expectedJSON = { 187 | data: { 188 | type: 'posts', 189 | attributes: { 190 | published_date: date // serializer transforms to underscores 191 | } 192 | } 193 | }; 194 | 195 | assert.deepEqual(json, expectedJSON, 'has correct json'); 196 | }); 197 | 198 | test('it does not serialize undefined attributes', function(assert) { 199 | let post = store.createRecord('post'); 200 | let json = serialize(post, {}); 201 | 202 | let expectedJSON = { data: { type: 'posts' } }; 203 | assert.deepEqual(json, expectedJSON, 'has correct json'); 204 | }); 205 | 206 | test('it does not serialize non-dirty attributes', function(assert) { 207 | store.pushPayload({ 208 | data: { 209 | type: 'posts', 210 | id: '1', 211 | attributes: { title: 'test title' } 212 | } 213 | }); 214 | let post = store.peekRecord('post', 1); 215 | 216 | let json = serialize(post, {}); 217 | let expectedJSON = { 218 | data: { 219 | type: 'posts', 220 | id: '1' 221 | } 222 | }; 223 | assert.deepEqual(json, expectedJSON); 224 | }); 225 | 226 | test('excluding attributes', function(assert) { 227 | let post = store.createRecord('post', { title: 'test post' }); 228 | let json = serialize(post, { attributes: false }); 229 | assert.notOk(json.data.hasOwnProperty('attributes'), 'attributes not included'); 230 | json = serialize(post, {}); 231 | assert.deepEqual(json.data.attributes, { title: 'test post' }, 'attributes included'); 232 | }); 233 | 234 | test('it serializes one-to-one correctly', function(assert) { 235 | let author = store.createRecord('author', { name: 'Joe Author' }); 236 | let post = store.createRecord('post', { 237 | author: author 238 | }); 239 | let json = serialize(post, { attributes: false, relationships: 'author' }); 240 | let expectedJSON = { 241 | data: { 242 | type: 'posts', 243 | relationships: { 244 | author: { 245 | data: { 246 | type: 'authors', 247 | 'temp-id': author.tempId(), 248 | method: 'create' 249 | } 250 | } 251 | } 252 | }, 253 | included: [ 254 | {type: 'authors', 'temp-id': author.tempId(), attributes: { name: 'Joe Author' }} 255 | ] 256 | }; 257 | assert.deepEqual(json, expectedJSON, 'has correct json'); 258 | }); 259 | 260 | test('it serializes async: false relationships correctly', function(assert) { 261 | 262 | // NOTE: Not sure if I'm doing this right with the async thing 263 | let author = store.createRecord('author', { name: 'Joe Author' }); 264 | let post = store.createRecord('post', { 265 | asyncFalseAuthor: author 266 | }); 267 | let json = serialize(post, { attributes: false, relationships: 'asyncFalseAuthor' }); 268 | let expectedJSON = { 269 | data: { 270 | type: 'posts', 271 | relationships: { 272 | 'async-false-author': { 273 | data: { 274 | type: 'authors', 275 | method: 'create', 276 | 'temp-id': author.tempId() 277 | } 278 | } 279 | } 280 | }, 281 | included: [ 282 | { type: 'authors', 'temp-id': author.tempId(), attributes: { name: 'Joe Author' }} 283 | ] 284 | }; 285 | assert.deepEqual(json, expectedJSON, 'has correct json'); 286 | }); 287 | 288 | test('it serializes has one marked for deletion correctly', function(assert) { 289 | seedPostWithAuthor(); 290 | 291 | let post = store.peekRecord('post', 1); 292 | post.get('author').set('markedForDeletion', true); 293 | 294 | let json = serialize(post, { attributes: false, relationships: 'author' }); 295 | let expectedJSON = { 296 | data: { 297 | id: '1', 298 | type: 'posts', 299 | relationships: { 300 | author: { 301 | data: { 302 | id: '2', 303 | type: 'authors', 304 | method: 'disassociate' 305 | } 306 | } 307 | } 308 | } 309 | }; 310 | 311 | assert.deepEqual(json, expectedJSON, 'has correct json'); 312 | }); 313 | 314 | test('it serializes has one marked for destruction correctly', function(assert) { 315 | seedPostWithAuthor(); 316 | 317 | let post = store.peekRecord('post', 1); 318 | post.get('author').set('markedForDestruction', true); 319 | 320 | let json = serialize(post, { attributes: false, relationships: 'author' }); 321 | let expectedJSON = { 322 | data: { 323 | id: '1', 324 | type: 'posts', 325 | relationships: { 326 | author: { 327 | data: { 328 | id: '2', 329 | type: 'authors', 330 | method: 'destroy' 331 | } 332 | } 333 | } 334 | } 335 | }; 336 | 337 | assert.deepEqual(json, expectedJSON, 'has correct json'); 338 | }); 339 | 340 | test('it serializes one-to-many correctly', function(assert) { 341 | let tag = store.createRecord('tag', { name: 'tag1' }); 342 | let post = store.createRecord('post', { 343 | tags: [ 344 | tag 345 | ] 346 | }); 347 | 348 | let json = serialize(post, { attributes: false, relationships: 'tags' }); 349 | 350 | let expectedJSON = { 351 | data: { 352 | type: 'posts', 353 | relationships: { 354 | tags: { 355 | data: [ 356 | { 357 | type: 'tags', 358 | 'temp-id': tag.tempId(), 359 | method: 'create' 360 | } 361 | ] 362 | } 363 | } 364 | }, 365 | included: [ 366 | { type: 'tags', 'temp-id': tag.tempId(), attributes: { name: 'tag1' } } 367 | ] 368 | }; 369 | assert.deepEqual(json, expectedJSON, 'has correct json'); 370 | }); 371 | 372 | // note tag 2 does not pass attributes 373 | test('one-to-many deletion/destruction', function(assert) { 374 | seedPostWithTags(); 375 | let post = store.peekRecord('post', 1); 376 | post.get('tags').objectAt(1).set('markedForDeletion', true); 377 | post.get('tags').objectAt(2).set('markedForDestruction', true); 378 | let json = serialize(post, { attributes: false, relationships: 'tags' }); 379 | let expectedJSON = { 380 | data: { 381 | id: '1', 382 | type: 'posts', 383 | relationships: { 384 | tags: { 385 | data: [ 386 | { type: 'tags', id: '2' }, 387 | { type: 'tags', id: '3', method: 'disassociate' }, 388 | { type: 'tags', id: '4', method: 'destroy' } 389 | ] 390 | } 391 | } 392 | } 393 | }; 394 | assert.deepEqual(json, expectedJSON, 'it has correct json'); 395 | }); 396 | 397 | test('relationship specified but not present', function(assert) { 398 | let post = store.createRecord('post'); 399 | let json = serialize(post, { attributes: false, relationships: 'author' }); 400 | let expectedJSON = { 401 | data: { 402 | type: 'posts', 403 | relationships: {} 404 | } 405 | }; 406 | assert.deepEqual(json, expectedJSON, 'it does not blow up'); 407 | }); 408 | 409 | test('does not serialize attributes of non-dirty relations', function(assert) { 410 | store.pushPayload({ 411 | data: { 412 | id: '1', 413 | type: 'posts', 414 | relationships: { 415 | genre: { 416 | data: { 417 | type: 'genres', 418 | id: '88' 419 | } 420 | }, 421 | author: { 422 | data: { 423 | type: 'authors', 424 | id: '99' 425 | } 426 | }, 427 | tags: { 428 | data: [ 429 | { id: '2', type: 'tags' }, 430 | { id: '3', type: 'tags' } 431 | ] 432 | } 433 | } 434 | }, 435 | included: [ 436 | { type: 'tags', id: '2', attributes: { name: 'tag1' } }, 437 | { type: 'tags', id: '3', attributes: { name: 'tag2' } }, 438 | { type: 'authors', id: '99', attributes: { name: 'Joe Author' } }, 439 | { type: 'genres', id: '88', attributes: { name: 'comedy' } } 440 | ] 441 | }); 442 | 443 | let post = store.peekRecord('post', 1); 444 | post.set('genre.name', 'drama'); 445 | post.set('tags.firstObject.name', 'tag1 change'); 446 | let newTag = store.createRecord('tag', { name: 'new tag' }); 447 | post.get('tags').addObject(newTag); 448 | let json = serialize(post, { 449 | attributes: false, 450 | relationships: ['author', 'tags', 'genre'] 451 | }); 452 | let expectedJSON = { 453 | data: { 454 | id: '1', 455 | type: 'posts', 456 | relationships: { 457 | author: { 458 | data: { 459 | id: '99', 460 | type: 'authors' 461 | } 462 | }, 463 | genre: { 464 | data: { 465 | type: 'genres', 466 | id: '88', 467 | method: 'update' 468 | } 469 | }, 470 | tags: { 471 | data: [ 472 | { type: 'tags', id: '2', method: 'update' }, 473 | { type: 'tags', id: '3' }, 474 | { type: 'tags', 'temp-id': newTag.tempId(), method: 'create' } 475 | ] 476 | } 477 | } 478 | }, 479 | included: [ 480 | {type: 'tags', id:'2', attributes: { name: 'tag1 change' }}, 481 | {type: 'tags', 'temp-id': newTag.tempId(), attributes: { name: 'new tag' }}, 482 | {type: 'genres', id: '88', attributes: { name: 'drama' }} 483 | ] 484 | }; 485 | assert.deepEqual(json, expectedJSON); 486 | }); 487 | 488 | test('nested one-to-one', function(assert) { 489 | let state = store.createRecord('state', { 490 | name: 'New York' 491 | }); 492 | 493 | let author = store.createRecord('author', { 494 | name: 'Joe Author', 495 | state: state 496 | }); 497 | 498 | let post = store.createRecord('post', { 499 | author: author 500 | }); 501 | 502 | let json = serialize(post, { 503 | attributes: false, 504 | relationships: { author: 'state' } 505 | }); 506 | 507 | let expectedJSON = { 508 | data: { 509 | type: 'posts', 510 | relationships: { 511 | author: { 512 | data: { 513 | type: 'authors', 514 | method: 'create', 515 | 'temp-id': author.tempId(), 516 | } 517 | } 518 | } 519 | }, 520 | included: [ 521 | { 522 | type: 'states', 523 | 'temp-id': state.tempId(), 524 | attributes: { name: 'New York' } 525 | }, 526 | { 527 | type: 'authors', 528 | 'temp-id': author.tempId(), 529 | attributes: { name: 'Joe Author' }, 530 | relationships: { 531 | state: { 532 | data: { 533 | type: 'states', 534 | 'temp-id': state.tempId(), 535 | method: 'create', 536 | } 537 | } 538 | } 539 | } 540 | ] 541 | }; 542 | assert.deepEqual(json, expectedJSON); 543 | }); 544 | 545 | test('nested one-to-many', function(assert) { 546 | let user = store.createRecord('user', { 547 | name: 'Joe User' 548 | }); 549 | 550 | let tag = store.createRecord('tag', { 551 | name: 'tag1', 552 | creator: user 553 | }); 554 | 555 | let post = store.createRecord('post', { 556 | tags: [tag] 557 | }); 558 | 559 | let json = serialize(post, { 560 | attributes: false, 561 | relationships: { tags: 'creator' } 562 | }); 563 | 564 | let expectedJSON = { 565 | data: { 566 | type: 'posts', 567 | relationships: { 568 | tags: { 569 | data: [ 570 | { 571 | type: 'tags', 572 | 'temp-id': tag.tempId(), 573 | method: 'create' 574 | } 575 | ] 576 | } 577 | } 578 | }, 579 | included: [ 580 | { 581 | type: 'users', 582 | 'temp-id': user.tempId(), 583 | attributes: { name: 'Joe User' } 584 | }, 585 | { 586 | type: 'tags', 587 | 'temp-id': tag.tempId(), 588 | attributes: { name: 'tag1' }, 589 | relationships: { 590 | creator: { 591 | data: { 592 | type: 'users', 593 | 'temp-id': user.tempId(), 594 | method: 'create' 595 | } 596 | } 597 | } 598 | } 599 | ] 600 | }; 601 | assert.deepEqual(json, expectedJSON); 602 | }); 603 | 604 | // tests a relationship hash like ['foo', { bar: 'baz' }] 605 | test('array with nesting', function(assert) { 606 | let state = store.createRecord('state', { 607 | name: 'New York' 608 | }); 609 | 610 | let author = store.createRecord('author', { 611 | name: 'Joe Author', 612 | state: state 613 | }); 614 | 615 | let tag = store.createRecord('tag', { 616 | name: 'tag1' 617 | }); 618 | 619 | let post = store.createRecord('post', { 620 | tags: [tag], 621 | author: author 622 | }); 623 | 624 | let json = serialize(post, { 625 | attributes: false, 626 | relationships: ['tags', { author: 'state' }] 627 | }); 628 | 629 | let expectedJSON = { 630 | data: { 631 | type: 'posts', 632 | relationships: { 633 | tags: { 634 | data: [ 635 | { type: 'tags', 'temp-id': tag.tempId(), method: 'create' } 636 | ] 637 | }, 638 | author: { 639 | data: { 640 | type: 'authors', 641 | 'temp-id': author.tempId(), 642 | method: 'create' 643 | } 644 | } 645 | } 646 | }, 647 | included: [ 648 | { type: 'tags', 'temp-id': tag.tempId(), attributes: { name: 'tag1' } }, 649 | { type: 'states', 'temp-id': state.tempId(), attributes: { name: 'New York' } }, 650 | { 651 | type: 'authors', 652 | 'temp-id': author.tempId(), 653 | attributes: { name: 'Joe Author' }, 654 | relationships: { 655 | state: { 656 | data: { 657 | method: 'create', 658 | type: 'states', 659 | 'temp-id': state.tempId(), 660 | } 661 | } 662 | } 663 | } 664 | ] 665 | }; 666 | assert.deepEqual(json, expectedJSON); 667 | }); 668 | -------------------------------------------------------------------------------- /vendor/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsonapi-suite/ember-data-extensions/b2d91ffe8372c3ddd07b9ffb8bff63f820d04e02/vendor/.gitkeep --------------------------------------------------------------------------------