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  }