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  }