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