Source: platform.js

'use strict'

/**
 * @module platform
 */
export const platform = {};

/**
 * The HttpClient type.
 *
 * @memberOf module:platform
 * @constructor
 * @struct
 * @final
 */
platform.HttpClient = function () {

  let baseUrl_ = '';
  let baseUrlAutodetect_ = false;
  let token_ = '';
  let tokenAutodetect_ = false;

  const reset = () => {
    baseUrl_ = '';
    baseUrlAutodetect_ = false;
    token_ = '';
    tokenAutodetect_ = false;
  }

  const findTokenFromQueryString = () => {
    const urlParams = new URLSearchParams(window?.location?.search);
    const token = urlParams.get('token');
    return token ? token : '';
  }

  const findBaseUrlFromReferrer = () => {
    let origin = '';
    if (window && window.document && window.document.referrer) {
      const url = new URL(window.document.referrer);
      origin = url.origin;
    }
    return origin;
  }

  /**
   * Execute a Http request to a given platform endpoint.
   *
   * @param {string} endpoint the platform endpoint.
   * @param {Object} body the request payload.
   * @param {...*} customConfig the Http request configuration.
   * @return {Promise<Object>} the platform response.
   */
  const fetch = (endpoint, {body, ...customConfig} = {}) => {
    const headers = {'Content-Type': 'application/json'}
    const config = {
      method: 'GET', ...customConfig, headers: {
        ...headers, ...customConfig.headers,
      },
    }
    if (body) {
      if (config.method === 'GET') {
        endpoint += '?' + new URLSearchParams(body);
      } else {
        config.body = JSON.stringify(body);
      }
    }
    return window.fetch(endpoint, config).then(async response => {
      if (response.ok) {
        return await response.json();
      } else {
        const errorMessage = await response.json();
        return Promise.reject(new Error(errorMessage.error));
      }
    });
  }

  /**
   * Returns the API token.
   *
   * @return {string} the API token.
   */
  this.getToken = function () {
    return token_;
  }

  /**
   * Set the API token.
   *
   * @param {string} token The API token.
   */
  this.setToken = function (token) {
    tokenAutodetect_ = false;
    token_ = token;
  }

  /**
   * Checks if the API token is set.
   *
   * @returns {boolean} returns true iif the API token is set, false otherwise.
   */
  this.hasToken = function () {
    return token_ !== '';
  }

  /**
   * Returns the API base URL.
   *
   * @return {string} the API base URL.
   */
  this.getBaseUrl = function () {
    return baseUrl_;
  }

  /**
   * Set the API base URL.
   *
   * @param {string} url the API base URL.
   */
  this.setBaseUrl = function (url) {
    baseUrlAutodetect_ = false;
    baseUrl_ = url;
  }

  /**
   * Checks if the API base URL is set.
   *
   * @returns {boolean} true iif the API base URL is set, false otherwise.
   */
  this.hasBaseUrl = function () {
    return baseUrl_ !== '';
  }

  /**
   * Initializes the Http client.
   *
   * If you omit a parameter, we will try to autodetect it.
   * For `token`, we try to find it on the query string. Ex: `?token=your_api_token`.
   * For `baseUrl`, we try to find it from the referrer.
   *
   * @param {string} baseUrl the base URL eg. https://www.company.computablefacts.com
   * @param {string} token the token.
   */
  this.init = function (baseUrl, token) {

    reset();

    if (typeof token === 'undefined') {
      token_ = findTokenFromQueryString();
      tokenAutodetect_ = this.hasToken();
      // console.log('init-autodetect-token token=', token, '_tokenAutodetect=', _tokenAutodetect)
    } else {
      this.setToken(token)
    }

    if (typeof baseUrl === 'undefined') {
      baseUrl_ = findBaseUrlFromReferrer();
      baseUrlAutodetect_ = this.hasBaseUrl();
      // console.log('init-autodetect-baseUrl baseUrl=', baseUrl, 'baseUrlAutodetect_=', baseUrlAutodetect_)
    } else {
      this.setBaseUrl(baseUrl);
    }
  }

  /**
   * Checks if the API token and base URL have been automatically set.
   *
   * @return `true` if the API token and base URL have been automatically set during [[`init`]].
   */
  this.hasAutodetect = function () {
    return tokenAutodetect_ && baseUrlAutodetect_;
  }

  /**
   * Returns the user information based on the API token.
   *
   * @return {Promise<Object>} the user permissions and authorizations.
   */
  this.whoAmI = function () {
    return fetch(`${baseUrl_}/api/v2/public/whoami`, {
      headers: {
        Authorization: `Bearer ${token_}`
      }
    });
  }

  /**
   * Call the platform JSON-RPC endpoint.
   *
   * @param {Object} payload the request payload.
   * @return {Promise<Object>} the platform response.
   * @preserve The specification can be found at https://www.jsonrpc.org/specification.
   */
  this.fetch = function (payload) {
    return fetch(`${baseUrl_}/api/v2/public/json-rpc?api_token=${token_}`, {body: payload, method: 'POST'}).then(
        response => {
          if ('error' in response) {
            const error = response['error'];
            const message = '(' + error.code + ') ' + error.message + '\n' + JSON.stringify(error.data);
            return Promise.reject(new Error(message));
          }
          return response['result'];
        });
  }

  /**
   * Call the `execute-problog-query` platform endpoint.
   *
   * @param {Object} params the request payload.
   * @return {Promise<Object>} the platform response.
   */
  this.executeProblogQuery = function (params) {
    return this.fetch({
      jsonrpc: '2.0', id: Date.now(), method: 'execute-problog-query', params: params
    });
  }

  /**
   * Call the `execute-sql-query` platform endpoint.
   *
   * @param {Object} params the request payload.
   * @return {Promise<Object>} the platform response.
   */
  this.executeSqlQuery = function (params) {
    return this.fetch({
      jsonrpc: '2.0', id: Date.now(), method: 'execute-sql-query', params: params
    });
  }

  /**
   * Call the `find-objects` platform endpoint.
   *
   * @param params the request payload.
   * @return {Promise<Object>} the platform response.
   */
  this.findObjects = function (params) {
    return this.fetch({
      jsonrpc: '2.0', id: Date.now(), method: 'find-objects', params: params
    });
  }

  /**
   * Call the `get-objects` platform endpoint.
   *
   * @param params the request payload.
   * @return {Promise<Array<Object>>} the platform response.
   */
  this.getObjects = function (params) {
    return this.fetch({
      jsonrpc: '2.0', id: Date.now(), method: 'get-objects', params: params
    });
  }

  /**
   * Call the `get-flattened-objects` platform endpoint.
   *
   * @param params the request payload.
   * @return {Promise<Array<Object>>} the platform response.
   */
  this.getFlattenedObjects = function (params) {
    return this.fetch({
      jsonrpc: '2.0', id: Date.now(), method: 'get-flattened-objects', params: params
    });
  }

  /**
   * Call the `find-terms` platform endpoint.
   *
   * @param params the request payload.
   * @return {Promise<Object>} the platform response.
   */
  this.findTerms = function (params) {
    return this.fetch({
      jsonrpc: '2.0', id: Date.now(), method: 'find-terms', params: params
    });
  }

  /**
   * Sink a single event.
   *
   * @param {string} type the event type.
   * @param {Array<string>} propNames the event property names.
   * @param {Array<string>} propValues the event property values.
   * @return {Promise<Object>} the created fact.
   */
  this.sinkEvent = function (type, propNames, propValues) {

    if (propNames.length !== propValues.length) {
      throw "Mismatch between the number of names and values"
    }

    const typeNormalized = 'event_' + type.replace(/-/g, '_').toLowerCase();
    const startDate = new Date();

    return fetch(`${baseUrl_}/api/v2/facts`, {
      body: {
        data: [{
          type: typeNormalized,
          values: propValues.map(prop => '' + prop),
          is_valid: true,
          start_date: startDate.toISOString(),
        }]
      }, method: 'POST', headers: {
        Authorization: `Bearer ${token_}`
      }
    });
  }

  /**
   * Source one or more events.
   *
   * @param {string} type the event type.
   * @param {Array<string>} propNames the event property names.
   * @param {Array<Object>} propPatterns the list of patterns to match.
   * @param {number} maxNbResults the maximum number of events to return.
   * @return {Promise<Array<Object>>} an array of events.
   */
  this.sourceEventsAsObjects = function (type, propNames, propPatterns, maxNbResults) {
    return this.sourceEvents(type, propNames, propPatterns, maxNbResults, 'objects');
  }

  /**
   * Source one or more events.
   *
   * @param {string} type the event type.
   * @param {Array<string>} propNames the event property names.
   * @param {Array<Object>} propPatterns the list of patterns to match.
   * @param {number} maxNbResults the maximum number of events to return.
   * @return {Promise<Array<Array<string>>>} an array of events.
   */
  this.sourceEventsAsArrays = function (type, propNames, propPatterns, maxNbResults) {
    return this.sourceEvents(type, propNames, propPatterns, maxNbResults, 'arrays_with_header');
  }

  /**
   * Source one or more events.
   *
   * @param {string} type the event type.
   * @param {Array<string>} propNames the event property names.
   * @param {Object} propPatterns the list of patterns to match.
   * @param {number} maxNbResults the maximum number of events to return.
   * @param {string} format the returned events format. 'objects' returns an `Array<Object>`. Both 'arrays' and 'arrays_with_header' return an `Array<Array<string>>`.
   * @return {Promise<Array<Object>|Array<Array<string>>>} an array of events.
   */
  this.sourceEvents = function (type, propNames, propPatterns, maxNbResults, format) {

    const newRule = (eventType, eventPropertyNames, patterns) => {

      let result = eventType + '(';
      result += eventPropertyNames.map(prop => prop.toUpperCase()).join(', ');
      result += ') :- ';
      result += 'fn_mysql_materialize_facts("{{ app_url }}/api/v3/facts/no_namespace/';
      result += eventType;
      result += '?alea=' + Math.random().toString(36).substring(2, 12);
      const filtersQuery = Object.entries(patterns).map(entry => entry[0] + '=' + entry[1]).join('&');
      result += filtersQuery ? '&' + filtersQuery : '';
      result += '", "{{ client }}", "{{ env }}", "{{ sftp_host }}", "{{ sftp_username }}", "{{ sftp_password }}", ';
      result += eventPropertyNames.map((prop, i) => '"value_' + i + '", _, ' + prop.toUpperCase()).join(', ');
      result += ').';

      // console.log('newRule = ', result);
      return result.trim();
    }

    const pattern = {};

    for (let i = 0; i < propNames.length; i++) {
      if (propPatterns[propNames[i]]) {
        pattern['value_' + i] = propPatterns[propNames[i]];
      }
    }

    const typeNormalized = 'event_' + type.replace(/-/g, '_').toLowerCase();
    const rule = newRule(typeNormalized, propNames, pattern);
    const alea = Math.random().toString(36).substring(2, 8);

    return this.executeProblogQuery({
      problog_rules: [alea + '_' + rule],
      problog_query: alea + '_' + (rule.substring(0, rule.indexOf(':-')).trim()) + '?',
      format: format ? format : 'objects',
      sample_size: maxNbResults ? maxNbResults : 15,
    });
  }
}