Manual Reference Source Test

src/services/JsonApiService.js

import Client from '../Client';
import AbstractService from './AbstractService';
import { objectToURI, stringToSnake } from '../utilities/FormatUtility';
import {
  hasAttribute,
  hasToken,
  isA,
  isRequired,
  isValid,
  validate,
} from '../utilities/ValidationUtility';
import { helperFactory } from '../utilities/factories/HelperFactory';

/**
 * An abstract class to handle generic get, insert, update, delete json api calls.
 *
 * Note: This class is intended to be extended and not used directly.
 */
class JsonApiService extends AbstractService {
  constructor(type, options = { title: 'JsonApiService', obj: 'obj', tokenRequired: [] }) {
    super(type);
    this.options = options;
  }

  /**
   * Call a DELETE request on the main obj.type expecting JSON API information.
   *
   * @param  {Object} [obj]               - Object to be deleted
   * @param  {String} [options]           - Modify the nature of the call and response
   * @param  {String} [options.url]       - Override the url for this call
   * @param  {String} [options.cacheMode] - Override the caching method for this call
   * @return {Promise}                    - Message
   */
  async delete(obj, options = {}) {
    // Validation
    const action = `delete`;
    const title = `${this.options.title}.${action}`;
    if (this.options.tokenRequired.indexOf(action) >= 0) {
      validate(title, hasToken());
    }
    const objMap = {};
    objMap[`${this.options.obj || 'obj'}`] = obj;
    validate(title,
      isRequired(objMap),
      hasAttribute(objMap, 'id'),
      hasAttribute(objMap, 'type'));

    // set defaults
    const mode = options.cacheMode || this.cacheMode;
    const type = obj.type.replace('__', '/');
    let url = options.url || `/v3/${type}/${obj.id}`;
    ['url', 'cacheMode'].forEach(v => delete options[v]);

    // set url options
    if (Object.keys(options).length > 0) {
      url = `${url}?${objectToURI(options, 'options')}`;
    }
    return Client.instance.request(obj.type, Client.instance.fetch(url, 'delete'), { mode, id: obj.id, clear: true });
  }

  /**
   * Call a GET request expecting JSON API information.
   *
   * @param  {Object} [params]              - Parameters used to get information from server
   * @param  {Number} [params.id]           - Id to get data from the server
   * @param  {String} [params.type]         - Type to be used for storage
   * @param  {Object} [params.filters]      - Filters to be used for get
   * @param  {Object} [params.page]         - Pagination object
   * @param  {Object} [params.page.size]    - Size of each page
   * @param  {Object} [params.page.number]  - The current page
   * @param  {Object} [params.page.first]   - The first page of data
   * @param  {Object} [params.page.last]    - The last page of data
   * @param  {Object} [params.page.prev]    - The previous page of data
   * @param  {Object} [params.page.next]    - The next page of data
   * @param  {String} [params.sort]         - The string on how things should be sorted, such as sort1,-sort2,sort3
   * @param  {String} [options]             - Modify the nature of the call and response
   * @param  {String} [options.url]         - Override the url for this call
   * @param  {String} [options.cacheMode]   - Override the caching method for this call
   * @param  {String} [options.meta]        - Pass and retrieve through reference meta data
   * @return {Promise}                      - Promise with data (array or object)
   */
  async get(params = {}, options = {}) {
    // Validation
    const action = `get`;
    const title = `${this.options.title}.${action}`;
    if (this.options.tokenRequired.indexOf(action) >= 0) {
      validate(title, hasToken());
    }

    // set defaults
    let key = 'id';
    if (options.key && params[options.key]) {
      ({ key } = options);
    } else if (this.options.key && params[this.options.key]) {
      ({ key } = this.options);
    }
    const id = (params[key] !== undefined) ? `/${params[key]}` : '';
    const type = (params.type !== undefined) ? params.type : this.type;
    let url = options.url || `/v3/${type.replace('__', '/')}${id}`;

    // update caching to return an array if there is no id
    if (params[key] === undefined) {
      options = Object.assign({ asArray: true }, options);
    }

    // check filters
    if (params.filters) {
      validate(title, isA({ filters: params.filters }, Object));
    }

    // check page
    if (params.page) {
      validate(title, isA(params.page, 'number'));
    }

    // check sort
    if (params.sort) {
      validate(title,
        isA({ sort: stringToSnake(params.sort) }, 'string'),
        isValid(/^(((-?\w)(\..*)?){1},?)+[^,.]$/.test(params.sort), 'sort format must be like "sort1,-sort2,sort3,sort4.property"'));
    }

    // check search
    if (params.search) {
      validate(title, isA({ search: params.search }, 'string'));
    }

    const urlParams = objectToURI(
      ['filters', 'page', 'sort', 'search']
        .filter(f => params[f])
        .reduce((o, k) => { o[k] = params[k]; return o; }, {})
    );
    if (urlParams !== '') {
      url = `${url}?${urlParams}`;
    }

    // Call request, store, return to calling command
    return Client.instance.request(type,
      Client.instance.fetch(url).then(response => helperFactory.get(response, undefined, options.meta)),
      {
        mode: options.cacheMode || this.cacheMode,
        asArray: options.asArray,
        id: params[key]
      });
  }

  /**
   * Call a GET request expecting JSON API information for children given a parent.
   *
   * @param  {Object} parent              - Parameters used to get information from server
   * @param  {Number} parent.id           - Id to get data from the server
   * @param  {String} parent.type         - Type to be used for storage
   * @param  {Object} [child]             - Parameters used to get information from server
   * @param  {Number} [child.id]          - Id to get data from the server
   * @param  {String} [child.type]        - Type to be used for storage
   * @param  {Object} [child.filters]     - Filters to be used for get
   * @param  {Object} [child.page]        - Pagination object
   * @param  {Number} [child.page.size]   - Size of each page
   * @param  {Number} [child.page.number] - The current page
   * @param  {Number} [child.page.first]  - The first page of data
   * @param  {Number} [child.page.last]   - The last page of data
   * @param  {Number} [child.page.prev]   - The previous page of data
   * @param  {Number} [child.page.next]   - The next page of data
   * @param  {String} [options]           - Modify the nature of the call and response
   * @param  {String} [options.url]       - Override the url for this call
   * @param  {String} [options.cacheMode] - Override the caching method for this call
   * @param  {String} [options.meta]      - Pass and retrieve through reference meta data
   * @return {Promise}                    - Promise with data (array or object)
   *
   * @example
   * import { siteService, mobileUserService, SiteMember } from 'clinical6';
   *
   * siteService.getChildren(site, { type: 'trials/site_members' });
   * siteService.getChildren(site, new SiteMember());
   * mobileUserService.getChildren(user, { type: 'ediary/entries' });  // generates /v3/mobile_users/:id/ediary/entries
   */
  async getChildren(parent, child = {}, options = {}) {
    // Validation
    const action = `getChildren`;
    const title = `${this.options.title}.${action}`;
    if (this.options.tokenRequired.indexOf(action) >= 0) {
      validate(title, hasToken());
    }
    validate(title,
      hasAttribute({ parent }, 'id'),
      hasAttribute({ child }, 'type'));

    // set defaults
    let key = 'id';
    if (options.key && parent[options.key]) {
      ({ key } = options);
    } else if (this.options.key && parent[this.options.key]) {
      ({ key } = this.options);
    }
    const parentId = (parent[key] !== undefined) ? `/${parent[key]}` : '';
    const childId = (child[key] !== undefined) ? `/${child[key]}` : '';
    const parentType = (parent.type !== undefined) ? parent.type : this.type;
    let url = options.url || `/v3/${parentType.replace('__', '/')}${parentId}`;
    url += `/${child.type.replace('__', '/')}${childId}`;

    // update caching to return an array if there is no id
    if (parent[key] === undefined) {
      options = Object.assign({ asArray: true }, options);
    }

    // check filters
    if (child.filters) {
      validate(title, isA({ filters: child.filters }, Object));
    }

    // check page
    if (child.page) {
      validate(title, isA(child.page, 'number'));
    }

    // check sort
    if (child.sort) {
      validate(title,
        isA({ sort: child.sort }, 'string'),
        isValid(/^(((-?\w)(\..*)?){1},?)+[^,.]$/.test(child.sort), 'sort format must be like "sort1,-sort2,sort3"'));
    }

    const urlParams = objectToURI(
      ['filters', 'page', 'sort']
        .filter(f => child[f])
        .reduce((o, k) => { o[k] = child[k]; return o; }, {})
    );
    if (urlParams !== '') {
      url = `${url}?${urlParams}`;
    }


    // Call request, store, return to calling command
    return Client.instance.request(child.type,
      Client.instance.fetch(url).then(response => helperFactory.get(response, undefined, options.meta)),
      {
        mode: options.cacheMode || this.cacheMode,
        asArray: options.asArray,
        id: child[key]
      });
  }

  /**
   * Call a POST request on the main obj.type expecting JSON API information.
   *
   * @param  {Object} [obj]               - Object to be inserted
   * @param  {String} [options]           - Modify the nature of the call and response
   * @param  {String} [options.url]       - Override the url for this call
   * @param  {String} [options.cacheMode] - Override the caching method for this call
   * @param  {Object} [options.meta]      - Pass and retrieve through reference meta data
   * @return {Promise}                    - Promise with object
   */
  async insert(obj, options = {}) {
    // Validation
    const action = `insert`;
    const title = `${this.options.title}.${action}`;
    if (this.options.tokenRequired.indexOf(action) >= 0) {
      validate(title, hasToken());
    }
    const objMap = {};
    objMap[`${this.options.obj || 'obj'}`] = obj;
    validate(title,
      isRequired(objMap),
      hasAttribute(objMap, 'type'));

    // set defaults
    const type = (obj.type || this.type).replace('__', '/');
    const url = options.url || `/v3/${type}`;

    const headers = options.headers || {};

    const params = { data: obj };
    if (options.meta && Object.keys(options.meta).length > 0) {
      params.meta = options.meta;
    } else if (!options.meta) {
      options.meta = {};
    }

    return Client.instance.request(obj.type,
      Client.instance.fetch(url, 'post', params, headers).then((response) => {
        if (response && (response.id || (response.data && response.data.id))) {
          obj.id = parseInt(response.id || response.data.id, 10);
        }
        return helperFactory.get(response, undefined, options.meta);
      }),
      { mode: options.cacheMode || this.cacheMode });
  }

  /**
   * Call a PATCH request on the main obj.type expecting JSON API information.
   *
   * @param  {Object} [obj]               - Object to be updated
   * @param  {String} [options]           - Modify the nature of the call and response
   * @param  {String} [options.url]       - Override the url for this call
   * @param  {String} [options.cacheMode] - Override the caching method for this call
   * @return {Promise}                    - Promise with object
   */
  async update(obj, options = {}) {
    // Validation
    const action = `update`;
    const title = `${this.options.title}.${action}`;
    if (this.options.tokenRequired.indexOf(action) >= 0) {
      validate(title, hasToken());
    }
    const objMap = {};
    objMap[`${this.options.obj || 'obj'}`] = obj;
    validate(title,
      isRequired(objMap),
      hasAttribute(objMap, 'type'));


    // set defaults
    const type = obj.type.replace('__', '/');
    const url = options.url || `/v3/${type}/${obj.id}`;
    return Client.instance.request(obj.type,
      Client.instance.fetch(url, 'patch', { data: obj, meta: options.meta }).then(response => helperFactory.get(response)),
      { mode: options.cacheMode || this.cacheMode, id: obj.id });
  }
}

export default JsonApiService;