github.com/hernad/nomad@v1.6.112/ui/app/adapters/watchable.js (about)

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