Source: search.js

/* eslint no-underscore-dangle:
  ["error", { "allow": ["_parametersByName", "_parametersByType"] }]
*/

import { getFormat } from './formats';
import { createRequest, createXHR } from './utils';
import { getErrorFromXml } from './error';
import { config } from './config';

/**
 * @module opensearch/search
 */


 /**
  * @typedef module:opensearch/search.BaseRequest
  * @type Object
  * @property {string} url The URL of the search service.
  * @property {string} method The ussed HTTP method.
  * @property {object} [headers] Optional HTTP headers.
  * @property {string|FormData} [body] The body to be sent to the search service.
  *                                    (For HTTP POST requests)
  */

/**
 * Returns an object that can be transformed into a fetch Request or an
 * XMLHttpRequest.
 * @param {string} url The url to send the search request to.
 * @param {object} parameterValues The search parameter values.
 * @returns {module:opensearch/search.BaseRequest} The constructed base request object.
 */
export function createBaseRequest(url, parameterValues) {
  // check parameters
  Object.keys(parameterValues).forEach((key) => {
    if (!Object.prototype.hasOwnProperty.call(url._parametersByType, key)
        && !Object.prototype.hasOwnProperty.call(url._parametersByName, key)) {
      throw new Error(`Invalid parameter '${key}'.`);
    }
  });

  const missingMandatoryParameters = url.parameters.filter(
    parameter => parameter.mandatory
      && !Object.prototype.hasOwnProperty.call(parameterValues, parameter.name)
      && !Object.prototype.hasOwnProperty.call(parameterValues, parameter.type)
  ).map(parameter => parameter.type);

  const missingOptionalParameters = url.parameters.filter(
    parameter => !parameter.mandatory
      && !Object.prototype.hasOwnProperty.call(parameterValues, parameter.name)
      && !Object.prototype.hasOwnProperty.call(parameterValues, parameter.type)
  ).map(parameter => parameter.type);

  if (missingMandatoryParameters.length) {
    throw new Error(`Missing mandatory parameters: ${missingMandatoryParameters.join(', ')}`);
  }

  if (url.method === 'GET') {
    // insert parameters into URL template
    let urlString = url.url;

    Object.keys(parameterValues).forEach((key) => {
      const parameter = url._parametersByType[key] || url._parametersByName[key];
      urlString = urlString.replace(
        new RegExp(`{${parameter.type}[?]?}`),
        parameter.serializeValue(parameterValues[key])
      );
    });

    missingOptionalParameters.forEach((type) => {
      urlString = urlString.replace(new RegExp(`{${type}[?]?}`), '');
    });

    return {
      method: url.method,
      url: urlString,
    };
  }

  // for POST
  const enctype = url.enctype || 'application/x-www-form-urlencoded';
  let body = null;
  if (enctype === 'application/x-www-form-urlencoded') {
    body = Object.keys(parameterValues).map((key) => {
      const param = (url._parametersByType[key] || url._parametersByName[key]);
      const k = encodeURIComponent(param.name);
      const v = encodeURIComponent(param.serializeValue(parameterValues[key]));
      return `${k}=${v}`;
    }).join('&');
  } else if (enctype === 'multipart/form-data') {
    body = new FormData();
    Object.keys(parameterValues).forEach((key) => {
      const param = (url._parametersByType[key] || url._parametersByName[key]);
      body.append(param.name, param.serializeValue(parameterValues[key]));
    });
  } else {
    throw new Error(`Unsupported enctype '${enctype}'.`);
  }

  return {
    method: url.method,
    url: url.url,
    headers: {
      'Content-Type': enctype,
    },
    body,
  };
}

/**
 * Performs a search for the given URL and parameters.
 * @param {OpenSearchUrl} url The URL to search on.
 * @param {object} [parameters={}] The search parameters.
 * @param {string} [type=null] The response format.
 * @param {boolean} [raw=false] Whether the response shall be parsed or returned raw.
 * @param {number} [maxUrlLength=undefined] The maximum URL length. URLs longer than that
                                            will result in errors.
 * @returns {Promise<SearchResult>|Promise<Response>} The search result as a Promise
 */
export function search(url, parameters = {}, type = null, raw = false, maxUrlLength = undefined) {
  const baseRequest = createBaseRequest(url, parameters);
  const { useXHR } = config();

  if (typeof maxUrlLength !== 'undefined' && baseRequest.url.length > maxUrlLength) {
    return Promise.reject(new Error(`Search URL too long: ${baseRequest.url.length}, maximum: ${maxUrlLength}`));
  }

  let request = null;

  // Decide whether to use XHR or fetch
  if (useXHR) {
    request = new Promise((resolve, reject, onCancel) => {
      const xhr = createXHR(baseRequest.url, baseRequest);
      xhr.onload = () => {
        if (raw) {
          resolve(xhr);
        }
        resolve([xhr.responseText, xhr.status]);
      };

      xhr.onerror = () => {
        reject(new TypeError('Failed to fetch'));
      };

      // set up cancellation if available
      if (onCancel && typeof onCancel === 'function') {
        onCancel(() => {
          xhr.abort();
        });
      }
    });
  } else {
    // fetch API
    request = fetch(createRequest(baseRequest.url, baseRequest));
    if (raw) {
      return request;
    }
    request = request.then(response => response.text().then(text => [text, response.status]));
  }

  // postprocess, check for error and parse result
  return request
    .then(([text, status]) => {
      if (status >= 400) {
        const error = getErrorFromXml(text);
        throw (error || new Error(text));
      }
      const format = getFormat(type || url.type);
      if (!format) {
        throw new Error(`Could not parse response of type '${type}'.`);
      }
      return format.parse(text);
    });
}