loomio/loomio

View on GitHub
vue/src/shared/record_store/restful_client.js

Summary

Maintainability
A
3 hrs
Test Coverage
import { encodeParams } from '@/shared/helpers/encode_params';
import { omitBy, snakeCase, compact, isString, defaults, pickBy, isNil } from 'lodash-es';

function __guard__(value, transform) {
  return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined;
}

const getCSRF = () => decodeURIComponent(__guard__(document.cookie.match("(^|;)\\s*csrftoken\\s*=\\s*([^;]+)"), x => x.pop()) || '');

export default class RestfulClient {

  constructor(resourcePlural = null) {
    this.defaultParams = {};
    this.currentUpload = null;
    this.apiPrefix = "/api/v1";
    this.onResponse = this.onResponse.bind(this);
    this.defaultParams.unsubscribe_token = new URLSearchParams(location.search).get('unsubscribe_token');
    this.defaultParams.membership_token = new URLSearchParams(location.search).get('membership_token');
    this.defaultParams.stance_token = new URLSearchParams(location.search).get('stance_token');
    this.defaultParams.discussion_reader_token = new URLSearchParams(location.search).get('discussion_reader_token');
    this.defaultParams = omitBy(this.defaultParams, isNil);
    this.processing = [];
    if (resourcePlural) { this.resourcePlural = snakeCase(resourcePlural); }
  }

  onPrepare(request)  { return request; }
  onCleanup(response) { return response; }
  onResponse(response) {
    if (response.ok) {
      return response.json().then(this.onSuccess);
    } else {
      return this.onFailure(response);
    }
  }

  onSuccess(data) { return data; }

  onFailure(response) {
    if (response.json) {
      return response.json().then(function(data) {
        data.status = response.status;
        data.statusText = response.statusText;
        data.ok = response.ok;
        throw data;
      });
    } else {
      throw response;
    }
  }
  
  onUploadSuccess(response) { return response; }

  buildUrl(path, params) {
    path = compact([this.apiPrefix, this.resourcePlural, path]).join('/');
    if (params == null) { return path; }
    return path + "?" + encodeParams(params);
  }

  memberPath(id, action) {
    return compact([id, action]).join('/');
  }

  fetchById(id, params) {
    if (params == null) { params = {}; }
    return this.getMember(id, '', params);
  }

  fetch({params, path}) {
    return this.get(path || '', params);
  }

  post(path, params) {
    return this.request(this.buildUrl(path), 'POST', this.paramsFor(params));
  }

  patch(path, params) {
    return this.request(this.buildUrl(path), 'PATCH', this.paramsFor(params));
  }

  delete(path, params) {
    return this.request(this.buildUrl(path), 'DELETE', this.paramsFor(params));
  }

  // NB: get requests place their params into the query string, rather than the request body
  get(path, params) {
    return this.request(this.buildUrl(path, this.paramsFor(params)), 'GET');
  }

  request(path, method, body) {
    if (body == null) { body = {}; }
    const opts = {
      method,
      credentials: 'same-origin',
      headers: { 
        'Content-Type': 'application/json',
        'X-CSRF-TOKEN': getCSRF()
      },
      body: JSON.stringify(body)
    };
    if (method === 'GET') { delete opts.body; }
    this.onPrepare();
    return fetch(path, opts)
    .then(this.onResponse, this.onFailure)
    .finally(this.onCleanup);
  }

  postMember(keyOrId, action, params) {
    return this.post(this.memberPath(keyOrId, action), params);
  }

  patchMember(keyOrId, action, params) {
    return this.patch(this.memberPath(keyOrId, action), params);
  }

  getMember(keyOrId, action, params) {
    if (action == null) { action = ''; }
    return this.get(this.memberPath(keyOrId, action), params);
  }

  create(params) {
    return this.post('', params);
  }

  update(id, params) {
    return this.patch(id, params);
  }

  destroy(id, params) {
    return this.delete(id, params);
  }

  discard(id, params) {
    return this.delete(id+'/discard', params);
  }

  undiscard(id, params) {
    return this.post(id+'/undiscard', params);
  }

  upload(path, file, options, onProgress) {
    if (options == null) { options = {}; }
    if (!file) { return; }
    return new Promise((resolve, reject) => {
      const data = new FormData();
      data.append(options.fileField     || 'file',     file);
      data.append(options.filenameField || 'filename', file.name.replace(/[^a-z0-9_\-\.]/gi, '_'));

      this.currentUpload = new XMLHttpRequest();
      this.currentUpload.open('POST', this.buildUrl(path), true);
      this.currentUpload.setRequestHeader('X-CSRF-TOKEN', getCSRF());
      this.currentUpload.responseType = 'json';
      this.currentUpload.addEventListener('load', () => {
        if ((this.currentUpload.status >= 200) && (this.currentUpload.status < 300)) {
          if (isString(this.currentUpload.response)) { this.currentUpload.response = JSON.parse(this.currentUpload.response); }
          this.onUploadSuccess(this.currentUpload.response);
          resolve(this.currentUpload.response);
          return this.currentUpload = null;
        }
      });
      if (onProgress) { this.currentUpload.upload.addEventListener('progress', onProgress); }
      this.currentUpload.addEventListener('error', reject);
      this.currentUpload.addEventListener('abort', reject);
      return this.currentUpload.send(data);
    });
  }

  abort() {
    if (this.currentUpload) { return this.currentUpload.abort(); }
  }

  paramsFor(params) {
    return defaults({}, this.defaultParams, pickBy(params, v => !isNil(v)));
  }
};