Source: hooks.js

/**
 * Custom React hook for ease of state management with use of SlashDB and SDK.
 */
// import React built-in hooks to use under the hood.
import { useState, useEffect, useContext, useRef } from 'react';



// import custom context for retrieving primary SlashDB instance config
import { SlashDBContext } from './Context';

// required JavaScript SDK classes
import { SlashDBClient, DataDiscoveryResource, SQLPassThruQuery } from '@slashdb/js-slashdb';

// list of SlashDB clients for use by hooks
const sdbClientRegistry = {}
/**
 * Create a SlashDB client object for use with hooks and store in a local registry.  If no parameters are provided, create a client object using
 * the parameters configured with the SlashDBProvider component
 * @param {string} [instanceName] a name to identify the client in the registry; if not provided, will be named 'default'
 * @param {Object} [config] holds the setup parameter: config is an object of key value pairs.
 * @param {string} [config.host] hostname of the SlashDB instance to connect to, with protocol/port
 * @param {string} [config.apiKey] API key for the username
 * @param {Object} [config.sso] optional settings to login with Single Sign-On
 * @param {Object} [config.sso.idpId] optional identity provider id configured in SlashDB
 * @param {Object} [config.sso.redirectUri] optional redirect uri to redirect browser after sign in
 * @param {Object} [config.sso.popUp] optional flag to sign in against the identity provider with a Pop Up window (false by default)
 * @returns {SlashDBClient} a reference to the SlashDB client that was created
  */
const useSetUp = (instanceName = 'default', config = undefined) => {
  
  // // if instance already exists and host parameter is provided, warn about overwrite
  // if (sdbClientRegistry.hasOwnProperty(instanceName) && host) {
  //   console.warn(`A SlashDB client with the name '${instanceName}' already exists, overwriting`);
  // } 

  // handling for the special default instanceName 
  if (instanceName === 'default') {
    // try and get SlashDBProvider config, if it exists
    const { setUpOptions } = useContext(SlashDBContext);

    // when the default hasn't been created yet
    if (!sdbClientRegistry.hasOwnProperty('default')) {
      // if SlashDBProvider configured, create default using its config
      if (setUpOptions) {
        sdbClientRegistry['default'] = new SlashDBClient(setUpOptions);
      }
      else {
        sdbClientRegistry['default'] = new SlashDBClient(config);
      }     
    }      

    else {
      if (config) {
        // if SlashDBProvider component configured and and attempting to overwrite default, don't allow
        if (setUpOptions) {
          console.warn(`SlashDB client 'default' was previously configured with SlashDBProvider - cannot overwrite`); 
        }
        else {
          sdbClientRegistry['default'] = new SlashDBClient(config);
        }
      }
    }
    return sdbClientRegistry['default'];        
  }

  // when host parameter provided, overwrite client object
  else if (config) {
    sdbClientRegistry[instanceName] = new SlashDBClient(config);
  }
  
  return sdbClientRegistry[instanceName];
};


/**
 * Hook for accessing SlashDB Data Discovery feature
 *
 * @param {string} database name of the database containing the desired resource/table
 * @param {string} resource the resource/table to discover
 * @param {string | DataDiscoveryFilter} [defaultFilter] optional string/DataDiscovery filter object containing a default filter for the resource
 * @param {string} [instanceName] optional name of SlashDB client that has been previously created with useSetUp() hook; default is 'default'
 * @returns {array} array, first element is data for given resource/table and subsequent elements are function references to execute on-demand GET/POST/PUT/DELETE calls on the resource
 */
const useDataDiscovery = (
  database,
  resource,
  defaultFilter = '',
  instanceName = 'default'
) => {
  
  const sdbClient = sdbClientRegistry[instanceName];

  const isMountedRef = useRef(null);

  const [data, setData] = useState([]);
  const [didUpdate, setDidUpdate] = useState(new Date().getTime());

  const handleSetData = (data) => {
    setData(data);
  };

  const handleUpdate = () => {
    setDidUpdate(new Date().getTime());
  };

  const dbResource = new DataDiscoveryResource(database, resource, sdbClient);
 
  
  /**
   * executes GET HTTP requests on resource
   * 
   * @param {string | DataDiscoveryFilter} [filter] optional string/DataDiscovery filter object containing a filter for the resource
   * @param {Object} [headers] object of key/value pairs containing headers for the HTTP request
   */
  const _get = async (filter, headers) => {
    filter = filter ? filter : defaultFilter;
    
    if (headers) {
      dbResource.setExtraHeaders(headers);
    }

    try {
      const r = await dbResource.get(filter);
      handleSetData(r.data);
      dbResource.extraHeaders = {};
    }
    catch(e) {
      dbResource.extraHeaders = {};
      console.error(e);
	  throw Error(e);
    }
  };

  /**
   * executes POST HTTP requests on resource
   * 
   * @param {string | Object} body payload for POST request (usually a JavaScript object)
   * @param {string | DataDiscoveryFilter} [filter] optional string/DataDiscovery filter object containing a filter for the resource
   * @param {Object} [headers] object of key/value pairs containing headers for the HTTP request
   */
  const _post = async (body, filter, headers) => {
    filter = filter ? filter : defaultFilter;

    if (headers) {
      dbResource.setExtraHeaders(headers);
    }

    try {
      await dbResource.post(body, filter)
        .then(handleUpdate);
      dbResource.extraHeaders = {};
    }
    catch(e) {
      dbResource.extraHeaders = {};
      console.error(e);
	  throw Error(e);
    }
  };

  /**
   * executes PUT HTTP requests on resource
   * 
   * @param {string | DataDiscoveryFilter} [filter] optional string/DataDiscovery filter object containing a filter for the resource
   * @param {string | Object} body payload for POST request (usually a JavaScript object) 
   * @param {Object} [headers] object of key/value pairs containing headers for the HTTP request
   */
    const _put = async (filter, body, headers) => {
      filter = filter ? filter : defaultFilter;

      if (headers) {
        dbResource.setExtraHeaders(headers);
      }

      try {
        await dbResource.put(filter, body)
          .then(handleUpdate);
        dbResource.extraHeaders = {};
      }
      catch(e) {
        dbResource.extraHeaders = {};
        console.error(e);
		throw Error(e);
      }
    }

  /**
   * executes DELETE HTTP requests on resource
   * 
   * @param {string | DataDiscoveryFilter} [filter] optional string/DataDiscovery filter object containing a filter for the resource
   * @param {Object} [headers] object of key/value pairs containing headers for the HTTP request
   */
  const _delete = async (filter, headers) => {
    filter = filter ? filter : defaultFilter;

    if (headers) {
      dbResource.setExtraHeaders(headers);
    }

    try {
      await dbResource.delete(filter)
        .then(handleUpdate);
      dbResource.extraHeaders = {};
    }
    catch(e) {
      dbResource.extraHeaders = {};
      console.error(e);
	  throw Error(e);
    }
  };


  useEffect( () => {
    isMountedRef.current = true;
    if (isMountedRef.current) {
      
      async function getData() {
        const r = await dbResource.get(defaultFilter);
        handleSetData(r.data);
      };

      try {
        getData();
      }
      catch(e) {
        console.error(e);
		throw Error(e);
      }
    }
    return () => (isMountedRef.current = false);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [didUpdate]);

  return [data, _get, _post, _put, _delete];
};



/**
 * Hook for accessing SlashDB SQL Pass-Thru feature
 *
 * @param {string} queryName name of the query to execute
 * @param {string | SQLPassThruFilter} [defaultParams] optional string/SQLPassThruFilter filter object containing parameters for the query
 * @param {string} defHttpMethod optional parameter to set the HTTP method used by the query; default is 'get'
 * @param {string} [instanceName] optional name of SlashDB client that has been previously created with useSetUp() hook; default is 'default'
 * @returns {array} array, first element is data for given resource/table and second element is function reference to execute query on demand
 */

const useExecuteQuery = (
  queryName,
  defaultParams,
  defHttpMethod = 'get',
  instanceName = 'default'
) => {
  
  const sdbClient = sdbClientRegistry[instanceName];
  const sqlQuery = new SQLPassThruQuery(queryName, sdbClient);

  const isMountedRef = useRef(null);

  const [data, setData] = useState([{}]);

  const handleDataSet = (data) => {
    setData(data);
  };

  /**
   * Function to be used for query execution after initial useExecuteQuery has been called
   *
   * @param {String} httpMethod GET, POST, PUT or DELETE - HTTP method to be used.
   * @param {array} parameters Any params user may wish to pass for query.
   * @param {Object} queryStrParameters Query params in key value pairs format to be send via url eg. {limit: 29} => ?limit=29
   * @param {Object} headers Any headers user may wish to pass.
   */
  const _executeQuery = async (
    params,
    body,
    httpMethod = undefined,
    headers = undefined
  ) => {

    params = params ? params : defaultParams;
    httpMethod = httpMethod ? httpMethod : defHttpMethod;

    if (headers) {
      sqlQuery.setExtraHeaders(headers);
    }

    try {
      const method = httpMethod.toLowerCase();
      // post method has body as required arg in position 1, and parameter is optional
      // so flip them around.  also, post method in SQL Pass Thru doesn't accept URL parameters and a body data
      // so set the parameters arg to undefined
      if (method === 'post') {
        [params,body] = [body,params];
        body = undefined;
      }
      const r = await sqlQuery[method](params, body);
      handleDataSet(r.data);
      sqlQuery.extraHeaders = {};
    }
    catch(e) {
      sqlQuery.extraHeaders = {};
      console.error(e);
	  throw Error(e);
    }
  };

  useEffect(() => {
    isMountedRef.current = true;
    if (isMountedRef.current) {
      _executeQuery(defaultParams);
    }
    return () => (isMountedRef.current = false);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [defHttpMethod]);

  return [data, _executeQuery];
};

/** Helper to logout and remove SlashDB clients - used in Auth logout method
 * 
 * @param {string} [instanceName] the SlashDB client instance to log out; if undefined, all clients are logged out
*/
export function removeSdbClients(instanceName = undefined) {
  if (instanceName) {
    if (!sdbClientRegistry.hasOwnProperty(instanceName)) {
      throw Error(`Client '${instanceName}' does not exist`);
    }
    sdbClientRegistry[instanceName].logout();
    delete sdbClientRegistry[instanceName];
  }
  else {
    Object.keys(sdbClientRegistry).forEach( instanceName => {
      sdbClientRegistry[instanceName].logout();
      delete sdbClientRegistry[instanceName];
    });
  }
}

/** Helper to check client authentication status - used in Auth clientIsAuthenticated method
 * @param {string} instanceName the SlashDB client instance check
 * @returns {boolean} flag indicating if client is currently authenticated to SlashDB server
*/
export async function checkClientAuthStatus(instanceName) {
  if (!sdbClientRegistry.hasOwnProperty(instanceName)) {
    console.warn(`Client '${instanceName}' does not exist`);
    return false;
  }
  const status = await sdbClientRegistry[instanceName].isAuthenticated();
  return status;
}


export { useSetUp, useDataDiscovery, useExecuteQuery };