UNPKG

@thoughtspot/visual-embed-sdk

Version:
411 lines 14.5 kB
"use strict"; 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