UNPKG

@gooddata/api-client-bear

Version:
289 lines • 12.2 kB
// (C) 2007-2020 GoodData Corporation import { invariant } from "ts-invariant"; import qs from "qs"; import range from "lodash/range.js"; import { ApiResponseError } from "../xhr.js"; import { convertExecutionToJson } from "./execute-afm.convert.js"; import { stringify } from "../utils/queryString.js"; export const DEFAULT_LIMIT = 1000; /** * This interface represents error caused during second part of api execution (data fetching) * and contains information about first execution part if that part was successful. * * @internal * @internal */ export class ApiExecutionResponseError extends ApiResponseError { executionResponse; constructor(error, executionResponse) { super(error.message, error.response, error.responseBody); this.executionResponse = executionResponse; } } export class ExecuteAfmModule { xhr; constructor(xhr) { this.xhr = xhr; } /** * Execute AFM and fetch all data results * * @param projectId - GD project identifier * @param execution - what to execute * * @returns Structure with `executionResponse` and `executionResult` */ executeAfm(projectId, execution) { validateNumOfDimensions(execution.execution.resultSpec?.dimensions?.length ?? 0); return this.getExecutionResponse(projectId, execution).then((executionResponse) => { return this.getExecutionResult(executionResponse.links.executionResult) .then((executionResult) => { return { executionResponse, executionResult }; }) .catch((error) => { throw new ApiExecutionResponseError(error, executionResponse); }); }); } /** * Execute AFM and return execution's response; the response describes dimensionality of the results and * includes link to poll for the results. * * @param projectId - GD project identifier * @param execution - what to get the response for * * @returns Promise with `executionResponse` */ getExecutionResponse(projectId, execution) { validateNumOfDimensions(execution.execution.resultSpec?.dimensions?.length ?? 0); return this.xhr .post(`/gdc/app/projects/${projectId}/executeAfm`, { body: convertExecutionToJson(execution) }) .then((apiResponse) => apiResponse.getData()) .then(unwrapExecutionResponse); } /** * Execute saved visualization and get all data. * * NOTE: all functionality related to executeVisualization is experimental and subject to possible breaking changes * in the future; location and shape of this interface WILL change when the functionality is made GA. * * @param projectId - GD project identifier * @param visExecution - execution payload * * @internal * @internal */ _executeVisualization(projectId, visExecution) { // We have ONE-3961 as followup to take this out of experimental mode return this._getVisExecutionResponse(projectId, visExecution).then((executionResponse) => { return this.getExecutionResult(executionResponse.links.executionResult).then((executionResult) => { return { executionResponse, executionResult }; }); }); } /** * * Execute visualization and return the response; the response describes dimensionality of the results and * includes link to poll for the results. * * NOTE: all functionality related to executeVisualization is experimental and subject to possible breaking changes * in the future; location and shape of this interface WILL change when the functionality is made GA. * * @param projectId - GD project identifier * @param visExecution - execution payload * * @internal * @internal */ _getVisExecutionResponse(projectId, visExecution) { // We have ONE-3961 as followup to take this out of experimental mode const body = createExecuteVisualizationBody(visExecution); return this.xhr .post(`/gdc/app/projects/${projectId}/executeVisualization`, { body }) .then((apiResponse) => apiResponse.getData()) .then(unwrapExecutionResponse); } // // working with results // /** * Get one page of Result from Execution (with requested limit and offset) * * @param executionResultUri - URI of the execution result to work with * @param limit - limit for each dimension * @param offset - offset for each dimension * * @returns Promise with `executionResult` or `null` (null means empty response - HTTP 204) */ getPartialExecutionResult(executionResultUri, limit, offset) { const executionResultUriQueryPart = getExecutionResultUriQueryPart(executionResultUri); const numOfDimensions = Number(qs.parse(executionResultUriQueryPart).dimensions); validateNumOfDimensions(numOfDimensions); return this.getPage(executionResultUri, limit, offset); } /** * Get whole ExecutionResult * * @param executionResultUri - URI of the execution result to work with * * @returns Promise with `executionResult` or `null` (null means empty response - HTTP 204) */ getExecutionResult(executionResultUri) { const executionResultUriQueryPart = getExecutionResultUriQueryPart(executionResultUri); const numOfDimensions = Number(qs.parse(executionResultUriQueryPart).dimensions); validateNumOfDimensions(numOfDimensions); const limit = Array(numOfDimensions).fill(DEFAULT_LIMIT); const offset = Array(numOfDimensions).fill(0); return this.getAllPages(executionResultUri, limit, offset); } getPage(executionResultUri, limit, offset) { return this.fetchExecutionResult(executionResultUri, limit, offset).then((executionResultWrapper) => { return executionResultWrapper ? unwrapExecutionResult(executionResultWrapper) : null; }); } getAllPages(executionResultUri, limit, offset, prevExecutionResult) { return this.fetchExecutionResult(executionResultUri, limit, offset).then((executionResultWrapper) => { if (!executionResultWrapper) { return null; } const executionResult = unwrapExecutionResult(executionResultWrapper); const newExecutionResult = prevExecutionResult ? mergePage(prevExecutionResult, executionResult) : executionResult; const { offset, total } = executionResult.paging; const nextOffset = getNextOffset(limit, offset, total); const nextLimit = getNextLimit(limit, nextOffset, total); return nextPageExists(nextOffset, total) ? this.getAllPages(executionResultUri, nextLimit, nextOffset, newExecutionResult) : newExecutionResult; }); } fetchExecutionResult(executionResultUri, limit, offset) { const uri = replaceLimitAndOffsetInUri(executionResultUri, limit, offset); return this.xhr .get(uri) .then((apiResponse) => (apiResponse.response.status === 204 ? null : apiResponse.getData())); } } function getExecutionResultUriQueryPart(executionResultUri) { return executionResultUri.split(/\?(.+)/)[1]; } function unwrapExecutionResponse(executionResponseWrapper) { return executionResponseWrapper.executionResponse; } function unwrapExecutionResult(executionResultWrapper) { return executionResultWrapper.executionResult; } function validateNumOfDimensions(numOfDimensions) { invariant(numOfDimensions === 1 || numOfDimensions === 2, `${numOfDimensions} dimensions are not allowed. Only 1 or 2 dimensions are supported.`); } function createExecuteVisualizationBody(visExecution) { const { reference, resultSpec, filters } = visExecution.visualizationExecution; const resultSpecProp = resultSpec ? { resultSpec } : undefined; const filtersProp = filters ? { filters } : undefined; return JSON.stringify({ visualizationExecution: { reference, ...resultSpecProp, ...filtersProp, }, }); } export function replaceLimitAndOffsetInUri(oldUri, limit, offset) { const [uriPart, queryPart] = oldUri.split(/\?(.+)/); const query = { ...qs.parse(queryPart), limit: limit.join(","), offset: offset.join(","), }; return uriPart + stringify(query, { addQueryPrefix: true }); } export function getNextOffset(limit, offset, total) { const numOfDimensions = total.length; const defaultNextRowsOffset = offset[0] + limit[0]; if (numOfDimensions === 1) { return [defaultNextRowsOffset]; } const defaultNextColumnsOffset = offset[1] + limit[1]; const nextColumnsExist = offset[1] + limit[1] < total[1]; const nextRowsOffset = nextColumnsExist ? offset[0] // stay in the same rows : defaultNextRowsOffset; // go to the next rows const nextColumnsOffset = nextColumnsExist ? defaultNextColumnsOffset // next columns for the same rows : 0; // start in the beginning of the next rows return [nextRowsOffset, nextColumnsOffset]; } export function getNextLimit(limit, nextOffset, total) { const numOfDimensions = total.length; validateNumOfDimensions(numOfDimensions); const getSingleNextLimit = (limit, nextOffset, total) => nextOffset + limit > total ? total - nextOffset : limit; // prevent set up lower limit than possible for 2nd dimension in the beginning of the next rows if (numOfDimensions === 2 && nextOffset[1] === 0 && // beginning of the next rows limit[0] < total[1] // limit from 1st dimension should be used in 2nd dimension ) { return [getSingleNextLimit(limit[0], nextOffset[0], total[0]), limit[0]]; } return range(numOfDimensions).map((i) => getSingleNextLimit(limit[i], nextOffset[i], total[i])); } export function nextPageExists(nextOffset, total) { // expression "return nextLimit[0] > 0" also returns correct result return nextOffset[0] < total[0]; } function mergeHeaderItemsForEachAttribute(dimension, headerItems, result) { if (headerItems && result.headerItems) { for (let attrIdx = 0; attrIdx < headerItems[dimension].length; attrIdx += 1) { result.headerItems[dimension][attrIdx].push(...headerItems[dimension][attrIdx]); } } } // works only for one or two dimensions export function mergePage(prevExecutionResult, executionResult) { const result = prevExecutionResult; const { headerItems, data, paging } = executionResult; const mergeHeaderItems = (dimension) => { // for 1 dimension we already have the headers from first page const otherDimension = dimension === 0 ? 1 : 0; const isEdge = paging.offset[otherDimension] === 0; if (isEdge) { mergeHeaderItemsForEachAttribute(dimension, headerItems, result); } }; // merge data const rowOffset = paging.offset[0]; if (result.data[rowOffset]) { // appending columns to existing rows for (let i = 0; i < data.length; i += 1) { const columns = data[i]; const resultData = result.data[i + rowOffset]; resultData.push(...columns); } } else { // appending new rows const resultData = result.data; const currentPageData = data; resultData.push(...currentPageData); } // merge headerItems if (paging.offset.length > 1) { mergeHeaderItems(0); mergeHeaderItems(1); } else { mergeHeaderItemsForEachAttribute(0, headerItems, result); } // update page count if (paging.offset.length === 1) { result.paging.count = [result?.headerItems?.[0]?.[0]?.length ?? 0]; } if (paging.offset.length === 2) { result.paging.count = [ result?.headerItems?.[0]?.[0]?.length ?? 0, result?.headerItems?.[1]?.[0]?.length ?? 0, ]; } return result; } //# sourceMappingURL=execute-afm.js.map