@thoughtspot/visual-embed-sdk
Version:
ThoughtSpot Embed SDK
411 lines • 14.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.AnswerService = exports.OperationType = void 0;
const tslib_1 = require("tslib");
// import YAML from 'yaml';
const tokenizedFetch_1 = require("../../../tokenizedFetch");
const utils_1 = require("../../../utils");
const graphql_request_1 = require("../graphql-request");
const sourceService_1 = require("../sourceService");
const queries = tslib_1.__importStar(require("./answer-queries"));
// eslint-disable-next-line no-shadow
var OperationType;
(function (OperationType) {
OperationType["GetChartWithData"] = "GetChartWithData";
OperationType["GetTableWithHeadlineData"] = "GetTableWithHeadlineData";
})(OperationType = exports.OperationType || (exports.OperationType = {}));
/**
* AnswerService provides a simple way to work with ThoughtSpot Answers.
*
* This service allows you to interact with ThoughtSpot Answers programmatically,
* making it easy to customize visualizations, filter data, and extract insights
* directly from your application.
*
* You can use this service to:
*
* - Add or remove columns from Answers (`addColumns`, `removeColumns`, `addColumnsByName`)
* - Apply filters to Answers (`addFilter`)
* - Get data from Answers in different formats (JSON, CSV, PNG) (`fetchData`, `fetchCSVBlob`, `fetchPNGBlob`)
* - Get data for specific points in visualizations (`getUnderlyingDataForPoint`)
* - Run custom queries (`executeQuery`)
* - Add visualizations to Liveboards (`addDisplayedVizToLiveboard`)
*
* @example
* ```js
* // Get the answer service
* embed.on(EmbedEvent.Data, async (e) => {
* const service = await embed.getAnswerService();
*
* // Add columns to the answer
* await service.addColumnsByName(["Sales", "Region"]);
*
* // Get the data
* const data = await service.fetchData();
* console.log(data);
* });
* ```
*
* @example
* ```js
* // Get data for a point in a visualization
* embed.on(EmbedEvent.CustomAction, async (e) => {
* const underlying = await e.answerService.getUnderlyingDataForPoint([
* 'Product Name',
* 'Sales Amount'
* ]);
*
* const data = await underlying.fetchData(0, 100);
* console.log(data);
* });
* ```
*
* @version SDK: 1.25.0| ThoughtSpot: 9.10.0.cl
* @group Events
*/
class AnswerService {
/**
* Should not need to be called directly.
* @param session
* @param answer
* @param thoughtSpotHost
* @param selectedPoints
*/
constructor(session, answer, thoughtSpotHost, selectedPoints) {
this.session = session;
this.thoughtSpotHost = thoughtSpotHost;
this.selectedPoints = selectedPoints;
this.tmlOverride = {};
this.session = (0, utils_1.removeTypename)(session);
this.answer = answer;
}
/**
* Get the details about the source used in the answer.
* This can be used to get the list of all columns in the data source for example.
*/
async getSourceDetail() {
const sourceId = (await this.getAnswer()).sources[0].header.guid;
return (0, sourceService_1.getSourceDetail)(this.thoughtSpotHost, sourceId);
}
/**
* Remove columnIds and return updated answer session.
* @param columnIds
* @returns
*/
async removeColumns(columnIds) {
return this.executeQuery(queries.removeColumns, {
logicalColumnIds: columnIds,
});
}
/**
* Add columnIds and return updated answer session.
* @param columnIds
* @returns
*/
async addColumns(columnIds) {
return this.executeQuery(queries.addColumns, {
columns: columnIds.map((colId) => ({ logicalColumnId: colId })),
});
}
/**
* Add columns by names and return updated answer session.
* @param columnNames
* @returns
* @example
* ```js
* embed.on(EmbedEvent.Data, async (e) => {
* const service = await embed.getAnswerService();
* await service.addColumnsByName([
* "col name 1",
* "col name 2"
* ]);
* console.log(await service.fetchData());
* });
*/
async addColumnsByName(columnNames) {
const sourceDetail = await this.getSourceDetail();
const columnGuids = getGuidsFromColumnNames(sourceDetail, columnNames);
return this.addColumns([...columnGuids]);
}
/**
* Add a filter to the answer.
* @param columnName
* @param operator
* @param values
* @returns
*/
async addFilter(columnName, operator, values) {
const sourceDetail = await this.getSourceDetail();
const columnGuids = getGuidsFromColumnNames(sourceDetail, [columnName]);
return this.executeQuery(queries.addFilter, {
params: {
filterContent: [{
filterType: operator,
value: values.map((v) => {
const [type, prefix] = (0, utils_1.getTypeFromValue)(v);
return {
type: type.toUpperCase(),
[`${prefix}Value`]: v,
};
}),
}],
filterGroupId: {
logicalColumnId: columnGuids.values().next().value,
},
},
});
}
async getSQLQuery() {
const { sql } = await this.executeQuery(queries.getSQLQuery, {});
return sql;
}
/**
* Fetch data from the answer.
* @param offset
* @param size
* @returns
*/
async fetchData(offset = 0, size = 1000) {
const { answer } = await this.executeQuery(queries.getAnswerData, {
deadline: 0,
dataPaginationParams: {
isClientPaginated: true,
offset,
size,
},
});
const { columns, data } = answer.visualizations.find((viz) => !!viz.data) || {};
return {
columns,
data,
};
}
/**
* Fetch the data for the answer as a CSV blob. This might be
* quicker for larger data.
* @param userLocale
* @param includeInfo Include the CSV header in the output
* @returns Response
*/
async fetchCSVBlob(userLocale = 'en-us', includeInfo = false) {
const fetchUrl = this.getFetchCSVBlobUrl(userLocale, includeInfo);
return (0, tokenizedFetch_1.tokenizedFetch)(fetchUrl, {
credentials: 'include',
});
}
/**
* Fetch the data for the answer as a PNG blob. This might be
* quicker for larger data.
* @param userLocale
* @param includeInfo
* @param omitBackground Omit the background in the PNG
* @param deviceScaleFactor The scale factor for the PNG
* @return Response
*/
async fetchPNGBlob(userLocale = 'en-us', omitBackground = false, deviceScaleFactor = 2) {
const fetchUrl = this.getFetchPNGBlobUrl(userLocale, omitBackground, deviceScaleFactor);
return (0, tokenizedFetch_1.tokenizedFetch)(fetchUrl, {
credentials: 'include',
});
}
/**
* Just get the internal URL for this answer's data
* as a CSV blob.
* @param userLocale
* @param includeInfo
* @returns
*/
getFetchCSVBlobUrl(userLocale = 'en-us', includeInfo = false) {
return `${this.thoughtSpotHost}/prism/download/answer/csv?sessionId=${this.session.sessionId}&genNo=${this.session.genNo}&userLocale=${userLocale}&exportFileName=data&hideCsvHeader=${!includeInfo}`;
}
/**
* Just get the internal URL for this answer's data
* as a PNG blob.
* @param userLocale
* @param omitBackground
* @param deviceScaleFactor
*/
getFetchPNGBlobUrl(userLocale = 'en-us', omitBackground = false, deviceScaleFactor = 2) {
return `${this.thoughtSpotHost}/prism/download/answer/png?sessionId=${this.session.sessionId}&deviceScaleFactor=${deviceScaleFactor}&omitBackground=${omitBackground}&genNo=${this.session.genNo}&userLocale=${userLocale}&exportFileName=data`;
}
/**
* Get underlying data given a point and the output column names.
* In case of a context menu action, the selectedPoints are
* automatically passed.
* @param outputColumnNames
* @param selectedPoints
* @example
* ```js
* embed.on(EmbedEvent.CustomAction, e => {
* const underlying = await e.answerService.getUnderlyingDataForPoint([
* 'col name 1' // The column should exist in the data source.
* ]);
* const data = await underlying.fetchData(0, 100);
* })
* ```
* @version SDK: 1.25.0| ThoughtSpot: 9.10.0.cl
*/
async getUnderlyingDataForPoint(outputColumnNames, selectedPoints) {
if (!selectedPoints && !this.selectedPoints) {
throw new Error('Needs to be triggered in context of a point');
}
if (!selectedPoints) {
selectedPoints = getSelectedPointsForUnderlyingDataQuery(this.selectedPoints);
}
const sourceDetail = await this.getSourceDetail();
const ouputColumnGuids = getGuidsFromColumnNames(sourceDetail, outputColumnNames);
const unAggAnswer = await (0, graphql_request_1.graphqlQuery)({
query: queries.getUnaggregatedAnswerSession,
variables: {
session: this.session,
columns: selectedPoints,
},
thoughtSpotHost: this.thoughtSpotHost,
});
const unaggAnswerSession = new AnswerService(unAggAnswer.id, unAggAnswer.answer, this.thoughtSpotHost);
const currentColumns = new Set(unAggAnswer.answer.visualizations[0].columns
.map((c) => c.column.referencedColumns[0].guid));
const columnsToAdd = [...ouputColumnGuids].filter((col) => !currentColumns.has(col));
if (columnsToAdd.length) {
await unaggAnswerSession.addColumns(columnsToAdd);
}
const columnsToRemove = [...currentColumns].filter((col) => !ouputColumnGuids.has(col));
if (columnsToRemove.length) {
await unaggAnswerSession.removeColumns(columnsToRemove);
}
return unaggAnswerSession;
}
/**
* Execute a custom graphql query in the context of the answer.
* @param query graphql query
* @param variables graphql variables
* @returns
*/
async executeQuery(query, variables) {
const data = await (0, graphql_request_1.graphqlQuery)({
query,
variables: {
session: this.session,
...variables,
},
thoughtSpotHost: this.thoughtSpotHost,
isCompositeQuery: false,
});
this.session = (0, utils_1.deepMerge)(this.session, (data === null || data === void 0 ? void 0 : data.id) || {});
return data;
}
/**
* Get the internal session details for the answer.
* @returns
*/
getSession() {
return this.session;
}
async getAnswer() {
if (this.answer) {
return this.answer;
}
this.answer = this.executeQuery(queries.getAnswer, {}).then((data) => data === null || data === void 0 ? void 0 : data.answer);
return this.answer;
}
async getTML() {
const { object } = await this.executeQuery(queries.getAnswerTML, {});
const edoc = object[0].edoc;
const YAML = await Promise.resolve().then(() => tslib_1.__importStar(require('yaml')));
const parsedDoc = YAML.parse(edoc);
return {
answer: {
...parsedDoc.answer,
...this.tmlOverride,
},
};
}
async addDisplayedVizToLiveboard(liveboardId) {
const { displayMode, visualizations } = await this.getAnswer();
const viz = getDisplayedViz(visualizations, displayMode);
return this.executeQuery(queries.addVizToLiveboard, {
liveboardId,
vizId: viz.id,
});
}
setTMLOverride(override) {
this.tmlOverride = override;
}
}
exports.AnswerService = AnswerService;
/**
*
* @param sourceDetail
* @param colNames
*/
function getGuidsFromColumnNames(sourceDetail, colNames) {
const cols = sourceDetail.columns.reduce((colSet, col) => {
colSet[col.name.toLowerCase()] = col;
return colSet;
}, {});
return new Set(colNames.map((colName) => {
const col = cols[colName.toLowerCase()];
return col.id;
}));
}
/**
*
* @param selectedPoints
*/
function getSelectedPointsForUnderlyingDataQuery(selectedPoints) {
const underlyingDataPoint = [];
/**
*
* @param colVal
*/
function addPointFromColVal(colVal) {
var _a;
const dataType = colVal.column.dataType;
const id = colVal.column.id;
let dataValue;
if (dataType === 'DATE') {
if (Number.isFinite(colVal.value)) {
dataValue = [{
epochRange: {
startEpoch: colVal.value,
},
}];
// Case for custom calendar.
}
else if ((_a = colVal.value) === null || _a === void 0 ? void 0 : _a.v) {
dataValue = [{
epochRange: {
startEpoch: colVal.value.v.s,
endEpoch: colVal.value.v.e,
},
}];
}
}
else {
dataValue = [{ value: colVal.value }];
}
underlyingDataPoint.push({
columnId: colVal.column.id,
dataValue,
});
}
selectedPoints.forEach((p) => {
p.selectedAttributes.forEach(addPointFromColVal);
});
return underlyingDataPoint;
}
/**
*
* @param visualizations
* @param displayMode
*/
function getDisplayedViz(visualizations, displayMode) {
if (displayMode === 'CHART_MODE') {
return visualizations.find(
// eslint-disable-next-line no-underscore-dangle
(viz) => viz.__typename === 'ChartViz');
}
return visualizations.find(
// eslint-disable-next-line no-underscore-dangle
(viz) => viz.__typename === 'TableViz');
}
//# sourceMappingURL=answerService.js.map