huridocs/uwazi

View on GitHub
app/shared/JSONRequest.js

Summary

Maintainability
A
0 mins
Test Coverage
A
95%
import 'isomorphic-fetch';
import superagent from 'superagent';

import rison from 'rison-node';
import { assign } from 'lodash';
import { getResponseType } from 'shared/apiClient/httpResponses';

let cookie;

class FetchResponseError extends Error {
  // eslint-disable-next-line no-shadow, max-statements
  constructor(message, details = {}) {
    super(message);
    const { json, status, cookie: cookieRes, headers, endpoint, ...rest } = details;
    const errorType = getResponseType(status);
    this.name = errorType === 'ClientError' ? 'Client Error' : 'Server Error';
    this.json = json;
    this.status = status;
    this.cookie = cookieRes;
    this.headers = headers;
    this.endpoint = endpoint;
    this.requestId = json?.requestId;
    this.additionalInfo = rest;
  }
}

const attemptRisonDecode = string => {
  const errcb = e => {
    throw Error(`rison decoder error: ${e}`);
  };

  const risonParser = new rison.parser(errcb); // eslint-disable-line new-cap
  risonParser.error = message => {
    this.message = message;
  };

  risonParser.parse(string);
};

export function toUrlParams(_data) {
  if (typeof _data === 'string') {
    return `?${_data}`;
  }

  const data = { ..._data };
  if (!data || Object.keys(data).length === 0) {
    return '';
  }

  return `?${Object.keys(data)
    .map(key => {
      if (typeof data[key] === 'undefined' || data[key] === null) {
        return;
      }

      if (typeof data[key] === 'object') {
        data[key] = JSON.stringify(data[key]);
      }

      let encodedValue = encodeURIComponent(data[key]);

      if (encodeURIComponent(key) === 'q') {
        try {
          attemptRisonDecode(data[key]);
          encodedValue = data[key];
        } catch (err) {
          encodedValue = encodeURIComponent(data[key]);
        }
      }
      return `${encodeURIComponent(key)}=${encodedValue}`;
    })
    .filter(param => param)
    .join('&')}`;
}

const removeUndefinedKeys = obj => {
  //eslint-disable-next-line no-param-reassign
  Object.keys(obj).forEach(key => (obj[key] === undefined ? delete obj[key] : {}));
};

const _fetch = (url, data, method, _headers) => {
  let response;
  let params = '';
  let body;

  const headers = {
    Accept: 'application/json',
    'Content-Type': 'application/json',
    'X-Requested-With': 'XMLHttpRequest',
    Cookie: cookie,
    ..._headers,
  };

  if (method === 'GET' || method === 'DELETE') {
    params = toUrlParams(data);
  }

  if (method === 'POST' || method === 'PUT') {
    body = JSON.stringify(data);
  }

  if (URLSearchParams && data instanceof URLSearchParams) {
    body = data;
  }

  removeUndefinedKeys(headers);

  return fetch(url + params, {
    method,
    headers,
    credentials: 'same-origin',
    body,
  })
    .then(res => {
      let setCookie;
      if (res.headers.get('set-cookie')) {
        setCookie = res.headers.get('set-cookie');
      }
      response = res;
      // Failed .json() parsing usually indicates a non-success http status,
      // so we rather return that failure status than throw our own parsin
      // error.
      return Promise.all([res.json().catch(() => ({})), setCookie, res.headers]);
    })
    .then(([json, setCookie, responseHeaders]) => {
      const processedResponse = assign(
        {
          json,
          status: response.status,
          cookie: setCookie,
          headers: responseHeaders,
          endpoint: { method, url },
        },
        !response.ok
          ? {
              ok: response.ok,
              message: json.message || response.statusText,
            }
          : {}
      );
      if (!response.ok) {
        throw new FetchResponseError(
          `Request failed with status code ${processedResponse.status}: ${processedResponse.message}`,
          processedResponse
        );
      }

      return processedResponse;
    });
};

export default {
  post: (url, data, headers) => _fetch(url, data, 'POST', headers),

  put: (url, data, headers) => _fetch(url, data, 'PUT', headers),

  get: (url, data, headers) => _fetch(url, data, 'GET', headers),

  delete: (url, data, headers) => _fetch(url, data, 'DELETE', headers),

  head: (url, data, headers) => _fetch(url, data, 'HEAD', headers),

  // TEST!!!!! Fully untested function
  uploadFile: (url, filename, file, _cookie) =>
    new Promise((resolve, reject) => {
      superagent
        .post(url)
        .set('Accept', 'application/json')
        .set('X-Requested-With', 'XMLHttpRequest')
        .set('Cookie', _cookie || cookie || '')
        .attach('file', file, filename)
        .then(response => {
          resolve(response);
        })
        .catch(err => {
          reject(err);
        });
    }),

  cookie: c => {
    cookie = c;
  },
};

export { FetchResponseError };