github.com/aminovpavel/nomad@v0.11.8/ui/app/adapters/watchable.js (about) 1 import { get } from '@ember/object'; 2 import { assign } from '@ember/polyfills'; 3 import { inject as service } from '@ember/service'; 4 import { AbortError } from '@ember-data/adapter/error'; 5 import queryString from 'query-string'; 6 import ApplicationAdapter from './application'; 7 import removeRecord from '../utils/remove-record'; 8 9 export default ApplicationAdapter.extend({ 10 watchList: service(), 11 store: service(), 12 13 ajaxOptions(url, type, options) { 14 const ajaxOptions = this._super(url, type, options); 15 16 // Since ajax has been changed to include query params in the URL, 17 // we have to remove query params that are in the URL from the data 18 // object so they don't get passed along twice. 19 const [newUrl, params] = ajaxOptions.url.split('?'); 20 const queryParams = queryString.parse(params); 21 ajaxOptions.url = !params ? newUrl : `${newUrl}?${queryString.stringify(queryParams)}`; 22 Object.keys(queryParams).forEach(key => { 23 delete ajaxOptions.data[key]; 24 }); 25 26 const abortToken = (options || {}).abortToken; 27 if (abortToken) { 28 delete options.abortToken; 29 30 const previousBeforeSend = ajaxOptions.beforeSend; 31 ajaxOptions.beforeSend = function(jqXHR) { 32 abortToken.capture(jqXHR); 33 if (previousBeforeSend) { 34 previousBeforeSend(...arguments); 35 } 36 }; 37 } 38 39 return ajaxOptions; 40 }, 41 42 // Overriding ajax is not advised, but this is a minimal modification 43 // that sets off a series of events that results in query params being 44 // available in handleResponse below. Unfortunately, this is the only 45 // place where what becomes requestData can be modified. 46 // 47 // It's either this weird side-effecting thing that also requires a change 48 // to ajaxOptions or overriding ajax completely. 49 ajax(url, type, options) { 50 const hasParams = hasNonBlockingQueryParams(options); 51 if (!hasParams || type !== 'GET') return this._super(url, type, options); 52 53 const params = { ...options.data }; 54 delete params.index; 55 56 return this._super(`${url}?${queryString.stringify(params)}`, type, options); 57 }, 58 59 findAll(store, type, sinceToken, snapshotRecordArray, additionalParams = {}) { 60 const params = assign(this.buildQuery(), additionalParams); 61 const url = this.urlForFindAll(type.modelName); 62 63 if (get(snapshotRecordArray || {}, 'adapterOptions.watch')) { 64 params.index = this.watchList.getIndexFor(url); 65 } 66 67 const abortToken = get(snapshotRecordArray || {}, 'adapterOptions.abortToken'); 68 return this.ajax(url, 'GET', { 69 abortToken, 70 data: params, 71 }); 72 }, 73 74 findRecord(store, type, id, snapshot, additionalParams = {}) { 75 let [url, params] = this.buildURL(type.modelName, id, snapshot, 'findRecord').split('?'); 76 params = assign(queryString.parse(params) || {}, this.buildQuery(), additionalParams); 77 78 if (get(snapshot || {}, 'adapterOptions.watch')) { 79 params.index = this.watchList.getIndexFor(url); 80 } 81 82 const abortToken = get(snapshot || {}, 'adapterOptions.abortToken'); 83 return this.ajax(url, 'GET', { 84 abortToken, 85 data: params, 86 }).catch(error => { 87 if (error instanceof AbortError) { 88 return; 89 } 90 throw error; 91 }); 92 }, 93 94 query(store, type, query, snapshotRecordArray, options, additionalParams = {}) { 95 const url = this.buildURL(type.modelName, null, null, 'query', query); 96 let [, params] = url.split('?'); 97 params = assign(queryString.parse(params) || {}, this.buildQuery(), additionalParams, query); 98 99 if (get(options, 'adapterOptions.watch')) { 100 // The intended query without additional blocking query params is used 101 // to track the appropriate query index. 102 params.index = this.watchList.getIndexFor(`${url}?${queryString.stringify(query)}`); 103 } 104 105 const abortToken = get(options, 'adapterOptions.abortToken'); 106 return this.ajax(url, 'GET', { 107 abortToken, 108 data: params, 109 }).then(payload => { 110 const adapter = store.adapterFor(type.modelName); 111 112 // Query params may not necessarily map one-to-one to attribute names. 113 // Adapters are responsible for declaring param mappings. 114 const queryParamsToAttrs = Object.keys(adapter.queryParamsToAttrs || {}).map(key => ({ 115 queryParam: key, 116 attr: adapter.queryParamsToAttrs[key], 117 })); 118 119 // Remove existing records that match this query. This way if server-side 120 // deletes have occurred, the store won't have stale records. 121 store 122 .peekAll(type.modelName) 123 .filter(record => 124 queryParamsToAttrs.some( 125 mapping => get(record, mapping.attr) === query[mapping.queryParam] 126 ) 127 ) 128 .forEach(record => { 129 removeRecord(store, record); 130 }); 131 132 return payload; 133 }); 134 }, 135 136 reloadRelationship(model, relationshipName, options = { watch: false, abortToken: null }) { 137 const { watch, abortToken } = options; 138 const relationship = model.relationshipFor(relationshipName); 139 if (relationship.kind !== 'belongsTo' && relationship.kind !== 'hasMany') { 140 throw new Error( 141 `${relationship.key} must be a belongsTo or hasMany, instead it was ${relationship.kind}` 142 ); 143 } else { 144 const url = model[relationship.kind](relationship.key).link(); 145 let params = {}; 146 147 if (watch) { 148 params.index = this.watchList.getIndexFor(url); 149 } 150 151 // Avoid duplicating existing query params by passing them to ajax 152 // in the URL and in options.data 153 if (url.includes('?')) { 154 const paramsInUrl = queryString.parse(url.split('?')[1]); 155 Object.keys(paramsInUrl).forEach(key => { 156 delete params[key]; 157 }); 158 } 159 160 return this.ajax(url, 'GET', { 161 abortToken, 162 data: params, 163 }).then( 164 json => { 165 const store = this.store; 166 const normalizeMethod = 167 relationship.kind === 'belongsTo' 168 ? 'normalizeFindBelongsToResponse' 169 : 'normalizeFindHasManyResponse'; 170 const serializer = store.serializerFor(relationship.type); 171 const modelClass = store.modelFor(relationship.type); 172 const normalizedData = serializer[normalizeMethod](store, modelClass, json); 173 store.push(normalizedData); 174 }, 175 error => { 176 if (error instanceof AbortError) { 177 return relationship.kind === 'belongsTo' ? {} : []; 178 } 179 throw error; 180 } 181 ); 182 } 183 }, 184 185 handleResponse(status, headers, payload, requestData) { 186 // Some browsers lowercase all headers. Others keep them 187 // case sensitive. 188 const newIndex = headers['x-nomad-index'] || headers['X-Nomad-Index']; 189 if (newIndex) { 190 this.watchList.setIndexFor(requestData.url, newIndex); 191 } 192 193 return this._super(...arguments); 194 }, 195 }); 196 197 function hasNonBlockingQueryParams(options) { 198 if (!options || !options.data) return false; 199 const keys = Object.keys(options.data); 200 if (!keys.length) return false; 201 if (keys.length === 1 && keys[0] === 'index') return false; 202 203 return true; 204 }