Source: business_endpoint.js

import queryString from 'qs';
import Query from './query';

export function InvalidCriteriaException(message) {
  this.message = message;
  this.name = 'InvalidCriteriaException';
}

/**
 * BusinessEndpoint is the main interaction class against non-resourceful sequoia endpoints.
 *
 * Business endpoints are simple rest-like endpoints. Due to the nature of them, this SDK
 * is not (currently) intending to provide anything other than simple validation for
 * paths and query strings. Objects are just the plain JSON returned from the service,
 * unlike {@link ResourcefulEndpoint} which will send back {@link ResourceCollection} and
 * {@link Resource}s.
 *
 * This class should not be used directly, but should instead be obtained
 * from {@link ServiceDescriptor#businessEndpoint}
 *
 * @param {Transport} transport - Transport instance to use for fetching
 * @param {Object} business - JSON object describing this business endpoint
 * (fetched from sequoia services 'descriptor')
 *
 * @example
 * // `contents` is used in the rest of the examples as our reference
 * // to a ResourcefulEndpoint
 * let feeds;
 *
 * client.generate().then(() => {
 *   client.service('feed').then((service) => {
 *     // Get a business endpoint (this is synchronous as the service passed
 *     // all the necessary data):
 *     feeds = service.businessEndpoint('feeds', { name: 'UTV-15246' });
 *     // whatever
 *   });
 * });
 *
 * @private
 */
class BusinessEndpoint {
  constructor(transport, endpoint, pathOptions) {
    this.transport = transport;
    this.endpoint = endpoint;

    let { path } = endpoint;
    const errors = [];
    const pathParams = Object.assign({ owner: endpoint.tenant }, pathOptions);

    (path.match(/([^{]*?).(?=\})/gim) || []).forEach((pathParam) => {
      if (!(pathParam in pathParams) && !pathParam.endsWith('?')) {
        errors.push(`Required path parameter '${pathParam}' was not supplied`);
      }
    });

    if (errors.length) {
      throw new Error(errors.join('\n'));
    }

    Object.keys(pathParams).forEach((key) => {
      path = path.replace(new RegExp(`{${key}[?]?}`), pathParams[key]);
    });

    // Strip out any optional params:
    path = path.replace(/{[^?]+\?}/g, '');
    this.endpoint.location = `${this.endpoint.location}${path}`;
  }

  /**
   * Perform an action against the business endpoint. Note, the HTTP method comes from
   * the `routes['name'].method` portion of the descriptor
   *
   * @param {(string|Query)} criteria - A query string to append to the request
   * @param {object} options - [fetch options]{@link https://github.github.io/fetch/#options}
   *
   * @see {@link Transport#fetchWithDefaults}
   *
   * @throws InvalidCriteriaException
   *
   * @returns {Promise} - JSON returned from the endpoint
   */
  fetch(criteria, options) {
    const { method } = this.endpoint;
    const fetchOptions = Object.assign({ method }, options);

    return this.transport.fetchWithDefaults(
      this.endPointUrl(criteria),
      fetchOptions
    );
  }

  /**
   * Required query parameters for the business endpoint from the
   * `routes['name'].inputs.query` portion of the descriptor
   *
   * Returns the raw objects
   *
   * @example
   * // Returns e.g.
   * [{
   *   "type": "name",
   *   "meta": [{
   *     "sequoiaType": "name"
   *     }],
   *   "invalids": ["" ],
   *   "name": "count",
   *   "required": false
   *  },
   *  {
   *    "type": "string",
   *    "description": "language to localise response to",
   *    "invalids": [""],
   *    "name": "lang",
   *    "required": false
   *  }]
   *
   * @type {object[]}
   */
  get requiredQueryParameters() {
    const { inputs } = this.endpoint;

    if (inputs && inputs.query) {
      return inputs.query.filter(query => query.required === true);
    }

    return [];
  }

  /**
   * Returns an array of query parameter names
   *
   * @example
   * // Returns e.g.
   * ['currency', 'price']
   *
   * @type {string[]}
   */
  get requiredQueryParameterNames() {
    return this.requiredQueryParameters.map(q => q.name);
  }

  // Private methods

  /**
   * Return a query string to append to the HTTP call. Will default to appending
   * `?owner=<owner>`
   *
   * @param {(string|Query)?} criteria - A (potential) query string to append to the request
   *
   * @private
   *
   * @throws InvalidCriteriaException
   *
   * @returns {string}
   */
  criteriaToQuery(criteria) {
    let query = '';

    if (typeof criteria === 'string') {
      if (criteria !== '') {
        query += `?${criteria}`;
      }
    } else if (criteria instanceof Query) {
      query += `?${criteria.toQueryString()}`;
    }

    const requestQueryParams = Object.keys(
      queryString.parse(query, { ignoreQueryPrefix: true })
    );

    this.requiredQueryParameterNames.forEach((requiredValue) => {
      if (!requestQueryParams.includes(requiredValue)) {
        throw new InvalidCriteriaException(
          `Required query parameter '${requiredValue}' was not passed`
        );
      }
    });

    return query;
  }

  /**
   * Get the full URL to the business endpoint/item we will send the request to
   *
   * @param {(string|Query)?} criteria - A (potential) query string to append to the request
   *
   * @see {@link BusinessEndpoint#criteriaToQuery}
   *
   * @private
   *
   * @throws InvalidCriteriaException
   *
   * @returns {string}
   */
  endPointUrl(criteria) {
    return `${this.endpoint.location}${this.criteriaToQuery(criteria)}`;
  }
}

export default BusinessEndpoint;