github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/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 class Watchable extends ApplicationAdapter { 10 @service watchList; 11 @service store; 12 13 // Overriding ajax is not advised, but this is a minimal modification 14 // that sets off a series of events that results in query params being 15 // available in handleResponse below. Unfortunately, this is the only 16 // place where what becomes requestData can be modified. 17 // 18 // It's either this weird side-effecting thing that also requires a change 19 // to ajaxOptions or overriding ajax completely. 20 ajax(url, type, options) { 21 const hasParams = hasNonBlockingQueryParams(options); 22 if (!hasParams || type !== 'GET') return super.ajax(url, type, options); 23 24 const params = { ...options.data }; 25 delete params.index; 26 27 // Options data gets appended as query params as part of ajaxOptions. 28 // In order to prevent doubling params, data should only include index 29 // at this point since everything else is added to the URL in advance. 30 options.data = options.data.index ? { index: options.data.index } : {}; 31 32 return super.ajax(`${url}?${queryString.stringify(params)}`, type, options); 33 } 34 35 findAll(store, type, sinceToken, snapshotRecordArray, additionalParams = {}) { 36 const params = assign(this.buildQuery(), additionalParams); 37 const url = this.urlForFindAll(type.modelName); 38 39 if (get(snapshotRecordArray || {}, 'adapterOptions.watch')) { 40 params.index = this.watchList.getIndexFor(url); 41 } 42 43 const signal = get(snapshotRecordArray || {}, 'adapterOptions.abortController.signal'); 44 return this.ajax(url, 'GET', { 45 signal, 46 data: params, 47 }); 48 } 49 50 findRecord(store, type, id, snapshot, additionalParams = {}) { 51 let [url, params] = this.buildURL(type.modelName, id, snapshot, 'findRecord').split('?'); 52 params = assign(queryString.parse(params) || {}, this.buildQuery(), additionalParams); 53 54 if (get(snapshot || {}, 'adapterOptions.watch')) { 55 params.index = this.watchList.getIndexFor(url); 56 } 57 58 const signal = get(snapshot || {}, 'adapterOptions.abortController.signal'); 59 return this.ajax(url, 'GET', { 60 signal, 61 data: params, 62 }).catch(error => { 63 if (error instanceof AbortError || error.name == 'AbortError') { 64 return; 65 } 66 throw error; 67 }); 68 } 69 70 query(store, type, query, snapshotRecordArray, options, additionalParams = {}) { 71 const url = this.buildURL(type.modelName, null, null, 'query', query); 72 let [urlPath, params] = url.split('?'); 73 params = assign(queryString.parse(params) || {}, this.buildQuery(), additionalParams, query); 74 75 if (get(options, 'adapterOptions.watch')) { 76 // The intended query without additional blocking query params is used 77 // to track the appropriate query index. 78 params.index = this.watchList.getIndexFor(`${urlPath}?${queryString.stringify(query)}`); 79 } 80 81 const signal = get(options, 'adapterOptions.abortController.signal'); 82 return this.ajax(urlPath, 'GET', { 83 signal, 84 data: params, 85 }).then(payload => { 86 const adapter = store.adapterFor(type.modelName); 87 88 // Query params may not necessarily map one-to-one to attribute names. 89 // Adapters are responsible for declaring param mappings. 90 const queryParamsToAttrs = Object.keys(adapter.queryParamsToAttrs || {}).map(key => ({ 91 queryParam: key, 92 attr: adapter.queryParamsToAttrs[key], 93 })); 94 95 // Remove existing records that match this query. This way if server-side 96 // deletes have occurred, the store won't have stale records. 97 store 98 .peekAll(type.modelName) 99 .filter(record => 100 queryParamsToAttrs.some( 101 mapping => get(record, mapping.attr) === query[mapping.queryParam] 102 ) 103 ) 104 .forEach(record => { 105 removeRecord(store, record); 106 }); 107 108 return payload; 109 }); 110 } 111 112 reloadRelationship(model, relationshipName, options = { watch: false, abortController: null }) { 113 const { watch, abortController } = options; 114 const relationship = model.relationshipFor(relationshipName); 115 if (relationship.kind !== 'belongsTo' && relationship.kind !== 'hasMany') { 116 throw new Error( 117 `${relationship.key} must be a belongsTo or hasMany, instead it was ${relationship.kind}` 118 ); 119 } else { 120 const url = model[relationship.kind](relationship.key).link(); 121 let params = {}; 122 123 if (watch) { 124 params.index = this.watchList.getIndexFor(url); 125 } 126 127 // Avoid duplicating existing query params by passing them to ajax 128 // in the URL and in options.data 129 if (url.includes('?')) { 130 const paramsInUrl = queryString.parse(url.split('?')[1]); 131 Object.keys(paramsInUrl).forEach(key => { 132 delete params[key]; 133 }); 134 } 135 136 return this.ajax(url, 'GET', { 137 signal: abortController && abortController.signal, 138 data: params, 139 }).then( 140 json => { 141 const store = this.store; 142 const normalizeMethod = 143 relationship.kind === 'belongsTo' 144 ? 'normalizeFindBelongsToResponse' 145 : 'normalizeFindHasManyResponse'; 146 const serializer = store.serializerFor(relationship.type); 147 const modelClass = store.modelFor(relationship.type); 148 const normalizedData = serializer[normalizeMethod](store, modelClass, json); 149 store.push(normalizedData); 150 }, 151 error => { 152 if (error instanceof AbortError || error.name === 'AbortError') { 153 return relationship.kind === 'belongsTo' ? {} : []; 154 } 155 throw error; 156 } 157 ); 158 } 159 } 160 161 handleResponse(status, headers, payload, requestData) { 162 // Some browsers lowercase all headers. Others keep them 163 // case sensitive. 164 const newIndex = headers['x-nomad-index'] || headers['X-Nomad-Index']; 165 if (newIndex) { 166 this.watchList.setIndexFor(requestData.url, newIndex); 167 } 168 169 return super.handleResponse(...arguments); 170 } 171 } 172 173 function hasNonBlockingQueryParams(options) { 174 if (!options || !options.data) return false; 175 const keys = Object.keys(options.data); 176 if (!keys.length) return false; 177 if (keys.length === 1 && keys[0] === 'index') return false; 178 179 return true; 180 }