Browser Javascript

Browser Javascript

Table of Contents

REST API v2.0 uses JSON for the request and the response format, so it is easy to implement any v2.0 call in JavaScript.

Every REST API v2.0 endpoint uses either an HTTP GET or POST request, with or without a JSON request, so this simple async wrapper function can be used generically to build out any specific endpoint request. Before making an API request to a REST API v2.0 endpoint, obtain a bearer token or set session cookies.

The Visual Embed SDK includes a tokenizedFetch function which will automatically add the bearer token that the SDK has retrieved already. Add tokenizedFetch to the list of imports from the Visual Embed SDK, and then the following code will work for any authorization method:

/* Make sure to add tokenizedFetch to your imports from the Visual Embed SDK */

/*
* Generic function to make a call to the V2.0 REST API
*
*/
let tsHost = 'https://{yourdomain}.thoughtspot.cloud';
async function restApiCallV2(endpoint, httpVerb, apiRequestObject){
    const tsApiVersion = '2.0';
    const baseUrl = `${tsHost}/api/rest/${tsApiVersion}/`;  // Forward ticks allow variables in strings
    const apiFullEndpoint = baseUrl + endpoint;

    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() or tokenizedFetch() 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 {
            if( response.status === 200){
                return response.json(); // Returns the JSON of the response
            }
            else if (response.status === 204){
                return true;  // 204 is success without a response body
            }
        }
    }).catch(error =>
    {
        console.error("Unable to get the " + endpoint + " response: " + error);
    });
}

You can use the restApiCallV2 function directly in a code block, or wrap it in another function.

Here’s a direct example:

let tmlExportEndpoint = 'metadata/tml/export';
let apiRequestForTML = {
    "metadata" : [{
        "type": "LIVEBOARD",
        "identifier": liveboardId
    }],
    "export_associated": false,
    "export_fqn": true
}

// Place call to export the TML for the Liveboard, to get the details of the Viz
return restApiCallV2(tmlExportEndpoint, 'POST', apiRequestForTML).then(
//tmlExportRestApiCallV2(tmlRequestOptions).then(
    response => {
        // console.log(response);
        let tmlObject = JSON.parse(response[0].edoc);
        // console.log(tmlObject);
        return tmlObject;
    }
).then(...)

Here are some example functions wrapping specific endpoints:

/*
* Wrapper function for calling /metadata/search with any request
* Can be used to generate UI components like menus or drop-downs of the content a user has access to
*/
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;
        }
    );
}

// Copy the request directly from the REST API Playground, substituting in any variables you need
const apiRequestObject = {
    "metadata": [
        {
            "name_pattern": "%QA%",
            "type": "LIVEBOARD"
        },
        {
            "name_pattern": "%QA%",
            "type": "ANSWER"
        }
        ],
    'record_offset': 0,
    'record_size': 100000
}


let results = await callMetadataSearchApi(apiRequestObject);
console.log("Final results from the callMetadataSearchApi function: ");
console.log(results);
}
async function callSearchDataApi(tmlSearchString, datasourceId, recordOffset, recordSize){
    console.log("Using following Search String for Search Data API: ", tmlSearchString);
    let searchDataEndpoint = 'searchdata';
    let apiRequestForSearchData = {
          "query_string": tmlSearchString
        , "logical_table_identifier": datasourceId
        , data_format: "COMPACT"
        , record_offset: recordOffset
        , record_size: recordSize
    }

    return restApiCallV2(searchDataEndpoint, 'POST', apiRequestForSearchData);
}

let vizTmlSearchString = '[Product] [Region]';
let dsId = '80c9b38f-1b2a-4ff4-a759-378259130f58';

let recordSize = 10000;
let offset = 0;

// The function above is async, so you can assign this variable and the next steps won't occur until Promise is fulfilled
let searchResult = await callSearchDataApi(vizTmlSearchString, dsId, offset, recordSize)
console.log("Search Data response:");
console.log(searchResult);

Pagination and offsets🔗

The data APIs have limits to how much data can be returned in a single call. These APIs have record_offset and record_size arguments that can be used in multiple calls to paginate through and retrieve all of the data.

There must be a sort clause in the search or saved viz to guarantee that you are getting the full set of unique results, because each API call results in an indepedent SQL query to the data warehouse, and databases typically do not maintain any sort order unless there are specified sort clauses.

The following function implements an algorithm for paging through all results and storing the results into a single allResults array that can then be processed for later:

 async function getAllSearch(){
    let allResults = [];
    let resultCount = 0;
    let recordSize = 300; // Set this to 10000 in all production cases, it is set LOW to see the iteration working
    let offset = 0;
    let searchResult = await callSearchDataApi(vizTmlSearchString, tsAppState.currentDatasources[0], offset, recordSize);
    console.log("Got the searchResult: ", searchResult);
    allResults.push(searchResult);
    resultCount = searchResult['contents'][0]['returned_data_row_count'];
    console.log("This many records returned " + resultCount);
    while (resultCount == recordSize) {
        console.log('Need another batch');
        offset += recordSize;
        searchResult = await callSearchDataApi(vizTmlSearchString, tsAppState.currentDatasources[0], offset, recordSize);
        allResults.push(searchResult);
        resultCount = searchResult['contents'][0]['returned_data_row_count'];
    }

    console.log(allResults);
}
// Call the async function from directly above to do the full search
getAllSearch();