04 - Browser JavaScript REST API implementation

04 - Browser JavaScript REST API implementation

Get startedπŸ”—

The files for Lesson 04 are api_training_javascript_1.html and api_training_javascript_2.html

01 - Basics of browser-side JavaScriptπŸ”—

Many actions in JavaScript are asynchronous, that is to say, code does not execute in the order it is written.

Instead, you specify code to run after a task, like a request to another server, has completed.

JS is also a functional language. Functions (blocks of code) can be passed to and returned from other functions, and this is a very common design pattern often referred to as callbacks.

// Need to know what types a and b
function callbackFunction(a, b){
   return a + b;
}

function doOtherFunction(a, b, otherFunc){
 // We assume the other function will be passing a and b of some type here
    return otherFunc(a,b);
}

// Send only the name of the first function as an argument of the second function
let result = doOtherFunction(2, 2, callbackFunction);

Modern JavaScript uses the Promise concept to help with structuring asynchronous code.

Promises provide .then() and .catch() methods, where each .then() takes a handleFulfilled and an optional handleRejected callback function as an argument:

myPromise
  .then(handleFulfilledA, handleRejectedA)
  .catch((err) => {
      console.error(err);
    });

Configure ThoughtSpot CORSπŸ”—

ThoughtSpot instances are locked down by default, rejecting API requests from web servers that have not been added to the CORS allowlist.

CORS is applied by web browsers, which is why we did not look at these settings in any previous lessons where Python called the REST API directly.

Set global scope variablesπŸ”—

JavaScript has variables defined with let or var and constants defined with const. We will use variable to refer to either throughout.

Variables that are declared outside of functions, directly within <script> tags, have global scope.

Global scoped variables can be referenced in all other functions, as long as there is no variable with the same name declared within the function (local scoped).

let tsHost = 'https://{yourdomain}.thoughtspot.cloud';
const publicApiUrl = 'api/rest/2.0/';


const endpoint = "/{}/{}"

Scope within module blocksπŸ”—

The ThoughtSpot Visual Embed SDK must be imported as a module, and the scoping rules for modules are slightly different than other <script> blocks.

<script type = 'module'>
    import {
        LiveboardEmbed,
        AuthType,
        init,
        prefetch,
        tokenizedFetch
    }
from 'https://cdn.jsdelivr.net/npm/@thoughtspot/visual-embed-sdk/dist/tsembed.es.js';

The simple form is that things declared within a module block are not available in other blocks, while things that are declared in other blocks are available even within the module block.

If you are using the Visual Embed SDK, keep all ThoughtSpot related code within the block where you import the Visual Embed SDK module.

Copy requests directly from PlaygroundπŸ”—

The request and response format for the V2.0 REST API is JSON, the native object format of JavaScript.

You can copy any request from the REST API V2.0 Playground directly into your code:

JSON request format in Playground

// Copy in directly from the V2.0 REST API Playground
const apiRequestObject = {
  "record_offset": 0,
  "record_size": 10,
  "include_favorite_metadata": false,
  "privileges": [
    "DATADOWNLOADING"
  ]
}

// Can later use the following to send as part of a request body:
// JSON.stringify(apiRequestObject);

02 - Use fetch() and tokenizedFetch() to issue REST API requestsπŸ”—

The fetch() function in JavaScript uses the browser itself as a store of cookies and other details when you use the credentials: "include" parameter.

fetch is an asynchronous call that returns a Promise, which has .then() and .error() methods for calling the next code to run once the REST API response is returned:

...
const apiFullEndpoint = tsHost + "/" + publicApiUrl + endpoint;

await fetch(
  apiFullEndpoint, // URL
    {
      method: β€˜POST’,
      headers: {
          "Accept": "application/json",
          "X-Requested-By": "ThoughtSpot",
          "Content-Type": "application/json"
         },
       credentials: "include",
       body: JSON.stringify(apiRequestObject)
    }
) .then(response =>  response.json())
  .catch(error => {
        console.error("Unable to get the" + endpoint + "response: " + error);
  });

The standard JavaScript fetch() works if you are using any of the cookie-based authentication methods.

If you are using Cookieless Trusted Authentication, you’ll need to import the tokenizedFetch() function from the ThoughtSpot Visual Embed SDK. The tokenizedFetch() function can access the current Bearer Token used by the SDK when making requests, while using the exact syntax as the standard fetch() function:

<script type = 'module'>
    import {
        LiveboardEmbed,
        AuthType,
        init,
        prefetch,
        tokenizedFetch
    }
from 'https://cdn.jsdelivr.net/npm/@thoughtspot/visual-embed-sdk/dist/tsembed.es.js';

const apiFullEndpoint = tsHost + "/" + publicApiUrl + endpoint;

await tokenizedFetch(
  apiFullEndpoint, // URL
    {
      method: β€˜POST’,
      headers: {
          "Accept": "application/json",
          "X-Requested-By": "ThoughtSpot",
          "Content-Type": "application/json"
         },
       credentials: "include",
       body: JSON.stringify(apiRequestObject)
    }
) .then(response =>  response.json())
  .catch(error => {
        console.error("Unable to get the" + endpoint + "response: " + error);
  });

</script>

As noted before, you need these function to be within the module block where the Visual Embed SDK is imported, and you must import the tokenizedFetch function explicitly to make it available.

03 - Generic function for all callsπŸ”—

By making this a function, we can make the code generic, where it can work for any call by changing the arguments

async function restApiCallV2(endpoint, httpVerb, apiRequestObj){
  const tsApiVersion = '2.0';
  const baseUrl = `${tsHost}/api/rest/${tsApiVersion}/`;  // Forward ticks allow variables in strings
  const apiFullEndpoint = baseUrl + endpoint;
  console.log("Executing fetch");
  /*
  * Fetch is asynchronous and returns a Promise, which always has a .then() and .catch() method so you can chain
  * additional code to happen after the REST API call returns back.
  * Alternatively, you can use the 'async' and 'await' patterns
  * https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Promises
  */

  let fetchArgs = {
          method: httpVerb.toUpperCase(),
          headers: {
              "Accept": "application/json",
              "X-Requested-By": "ThoughtSpot",
              "Content-Type": "application/json"
              },
          credentials: "include"
      }
  // Some type of request might not have a body
  if (apiRequestObj !== null){
      fetchArgs['body'] = JSON.stringify(apiRequestObj);
  }

  // With the async modifier on the function, you add return await to the fetch() call here
  return await tokenizedFetch(
      apiFullEndpoint,
      fetchArgs
  ).then(response =>
  {
      console.log("Fetch response returned with status code " + response.status);
      // Parse 4XX or 500 HTTP status code errors from the API
      if (!response.ok) {
          console.log("HTTP response indicates an error from the API");
          throw new Error("Received HTTP response " + response.status + "with the message " + response.statusText)

          //
          // Alternatively, check for specific error codes you might expect, like a 403
          /*
          if( response.status == 403){
              // retrySSOProcess(); // Example action to take based on status
          }
          else{
              throw new Error("Received HTTP response " + response.status + "with the message " + response.statusText)
          }
          */
      }
      else {
          if( response.status === 200){
              return response.json(); // Returns the JSON of the response
          }
          else if (response.status === 204){
              return true;  // 204 is success without any body
          }

      }
  }).catch(error =>
  {
      console.error("Unable to get the " + endpoint + " response: " + error);
  });
}

Once you have the first wrapper function handling the baseline functionality, you can write functions that wrap calling individual endpoints:

/*
* Wrapper function for calling /metadata/search with any request
*/
async function callMetadataSearchApi(searchRequestObject){
    let endpoint = 'metadata/search';
    let verb = 'POST';

    return await restApiCallV2(endpoint, verb, searchRequestObject).then(
        response => {
            // Additional logging to the console
            console.log("API response:", response);
            console.log(response);
            return response;
        }
    );
}

To use such a function you define your request object, copied from the REST API V2.0 Playground directly. This code can be called anywhere - here we show another function that could be attached to an event listener or called by other code at any point in time after SSO has completed:

// Here we actually CALL the functions to make the request
function makeSearchRequest(){
   // Copy the request directly from the REST API Playground, substituting in any variables you need
   const apiRequestObject = {
       "metadata": [
           {
               "name_pattern": "(Sample)",
               "type": "LIVEBOARD"
           },
           {
               "name_pattern": "(Sample)",
               "type": "ANSWER"
           }
           ],
       'record_offset': 0,
       'record_size': 100000
   }


   let results = callMetadataSearchApi(apiRequestObject);
   console.log("Final results from the callMetadataSearchApi function: ");
   console.log(results);
}

04 - ConclusionπŸ”—

Due to the JSON based nature of the V2.0 REST API, implementing within browser-side JavaScript is more about knowing the intricacies of browser-security models and JavaScript scoping rules than any particular difficulty with the message formats.

There is a TypeScript SDK that can be used when working in Node.js or other environments where TypeScript is available.

It is up to you to determine if the extra rigor and structure of the Typescript REST API SDK is helpful in the projects you build, or if a simple light implementation of a few of the REST API calls within a simple set of functions will suffice.