UNPKG

@gooddata/api-client-bear

Version:
528 lines 22.2 kB
// (C) 2007-2022 GoodData Corporation import SparkMD5 from "spark-md5"; import { invariant } from "ts-invariant"; import cloneDeep from "lodash/cloneDeep.js"; import compact from "lodash/compact.js"; import filter from "lodash/filter.js"; import first from "lodash/first.js"; import find from "lodash/find.js"; import map from "lodash/map.js"; import merge from "lodash/merge.js"; import every from "lodash/every.js"; import isEmpty from "lodash/isEmpty.js"; import negate from "lodash/negate.js"; import partial from "lodash/partial.js"; import flatten from "lodash/flatten.js"; import set from "lodash/set.js"; import { getAttributesDisplayForms, isVisualizationObjectAttribute, isVisualizationObjectAttributeFilter, isVisualizationObjectMeasure, } from "@gooddata/api-model-bear"; import { Rules } from "../utils/rules.js"; import { sortDefinitions } from "../utils/definitions.js"; import { getMissingUrisInAttributesMap } from "../utils/attributesMapLoader.js"; const notEmpty = negate(isEmpty); function findHeaderForMappingFn(mapping, header) { return ((mapping.element === header.id || mapping.element === header.uri) && header.measureIndex === undefined); } function wrapMeasureIndexesFromMappings(metricMappings, headers) { if (metricMappings) { metricMappings.forEach((mapping) => { const header = find(headers, partial(findHeaderForMappingFn, mapping)); if (header) { header.measureIndex = mapping.measureIndex; header.isPoP = mapping.isPoP; } }); } return headers; } const emptyResult = { extendedTabularDataResult: { values: [], warnings: [], }, }; const MAX_TITLE_LENGTH = 1000; function getMetricTitle(suffix, title) { const maxLength = MAX_TITLE_LENGTH - suffix.length; if (title && title.length > maxLength) { if (title[title.length - 1] === ")") { return `${title.substring(0, maxLength - 2)}…)${suffix}`; } return `${title.substring(0, maxLength - 1)}${suffix}`; } return `${title}${suffix}`; } const getBaseMetricTitle = partial(getMetricTitle, ""); const CONTRIBUTION_METRIC_FORMAT = "#,##0.00%"; function getPoPDefinition(measure) { return measure?.definition?.popMeasureDefinition ?? {}; } function getAggregation(measure) { return (getDefinition(measure)?.aggregation ?? "").toLowerCase(); } function isEmptyFilter(metricFilter) { if (metricFilter?.positiveAttributeFilter) { return isEmpty(metricFilter.positiveAttributeFilter?.in); } if (metricFilter?.negativeAttributeFilter) { return isEmpty(metricFilter.negativeAttributeFilter?.notIn); } if (metricFilter?.absoluteDateFilter) { return (metricFilter?.absoluteDateFilter?.from === undefined && metricFilter?.absoluteDateFilter?.to === undefined); } return (metricFilter?.relativeDateFilter?.from === undefined && metricFilter?.relativeDateFilter?.to === undefined); } function allFiltersEmpty(item) { return every(map(getMeasureFilters(item), (f) => isEmptyFilter(f))); } function isDerived(measure) { const aggregation = getAggregation(measure); return aggregation !== "" || !allFiltersEmpty(measure); } function getAttrTypeFromMap(dfUri, attributesMap) { return attributesMap?.[dfUri]?.attribute?.content?.type; } function getAttrUriFromMap(dfUri, attributesMap) { return attributesMap?.[dfUri]?.attribute?.meta?.uri; } function isAttrFilterNegative(attributeFilter) { return attributeFilter?.negativeAttributeFilter !== undefined; } function getAttrFilterElements(attributeFilter) { const isNegative = isAttrFilterNegative(attributeFilter); const elements = isNegative ? attributeFilter?.negativeAttributeFilter?.notIn : attributeFilter?.positiveAttributeFilter?.in; return elements ?? []; } function getAttrFilterExpression(measureFilter, attributesMap) { const isNegative = !!measureFilter?.negativeAttributeFilter; const detailPath = isNegative ? "negativeAttributeFilter" : "positiveAttributeFilter"; const attributeUri = getAttrUriFromMap(measureFilter?.[detailPath]?.displayForm?.uri, attributesMap); const elements = getAttrFilterElements(measureFilter); if (isEmpty(elements)) { return null; } const elementsForQuery = map(elements, (e) => `[${e}]`); const negative = isNegative ? "NOT " : ""; return `[${attributeUri}] ${negative}IN (${elementsForQuery.join(",")})`; } function getDateFilterExpression() { // measure date filter was never supported return ""; } function getFilterExpression(attributesMap, measureFilter) { if (isVisualizationObjectAttributeFilter(measureFilter)) { return getAttrFilterExpression(measureFilter, attributesMap); } return getDateFilterExpression(); } function getGeneratedMetricExpression(item, attributesMap) { const aggregation = getAggregation(item).toUpperCase(); const objectUri = getDefinition(item)?.item?.uri; const where = filter(map(getMeasureFilters(item), partial(getFilterExpression, attributesMap)), (e) => !!e); return [ "SELECT", aggregation ? `${aggregation}([${objectUri}])` : `[${objectUri}]`, notEmpty(...where) && `WHERE ${where.join(" AND ")}`, ] .filter(Boolean) .join(" "); } function getPercentMetricExpression(category, attributesMap, measure) { let metricExpressionWithoutFilters = `SELECT [${getDefinition(measure)?.item?.uri}]`; if (isDerived(measure)) { metricExpressionWithoutFilters = getGeneratedMetricExpression(set(cloneDeep(measure), ["definition", "measureDefinition", "filters"], []), attributesMap); } const attributeUri = getAttrUriFromMap(category?.displayForm?.uri, attributesMap); const whereFilters = filter(map(getMeasureFilters(measure), partial(getFilterExpression, attributesMap)), (e) => !!e); const byAllExpression = attributeUri ? ` BY ALL [${attributeUri}]` : ""; const whereExpression = notEmpty(...whereFilters) ? ` WHERE ${whereFilters.join(" AND ")}` : ""; return `SELECT (${metricExpressionWithoutFilters}${whereExpression}) / (${metricExpressionWithoutFilters}${byAllExpression}${whereExpression})`; } function getPoPExpression(attributeUri, metricExpression) { return `SELECT ${metricExpression} FOR PREVIOUS ([${attributeUri}])`; } function getGeneratedMetricHash(title, format, expression) { return SparkMD5.hash(`${expression}#${title}#${format}`); } function getMeasureType(measure) { const aggregation = getAggregation(measure); if (aggregation === "") { return "metric"; } else if (aggregation === "count") { return "attribute"; } return "fact"; } function getGeneratedMetricIdentifier(item, aggregation, expressionCreator, hasher, attributesMap) { const [, , , prjId, , id] = (getDefinition(item)?.item?.uri ?? "").split("/"); const identifier = `${prjId}_${id}`; const hash = hasher(expressionCreator(item, attributesMap)); const hasNoFilters = isEmpty(getMeasureFilters(item)); const type = getMeasureType(item); const prefix = hasNoFilters || allFiltersEmpty(item) ? "" : "_filtered"; return `${type}_${identifier}.generated.${hash}${prefix}_${aggregation}`; } function isDateAttribute(attribute, attributesMap = {}) { return getAttrTypeFromMap(attribute?.displayForm?.uri, attributesMap) !== undefined; } function getMeasureSorting(measure, mdObj) { const sorting = mdObj?.properties?.sortItems ?? []; const matchedSorting = sorting.find((sortItem) => { const measureSortItem = sortItem?.measureSortItem; if (measureSortItem) { // only one item now, we support only 2D data const identifier = measureSortItem.locators?.[0]?.measureLocatorItem?.measureIdentifier; return identifier === measure?.localIdentifier; } return false; }); return matchedSorting?.measureSortItem?.direction ?? null; } function getCategorySorting(category, mdObj) { const sorting = mdObj?.properties?.sortItems ?? []; const matchedSorting = sorting.find((sortItem) => { const attributeSortItem = sortItem?.attributeSortItem; if (attributeSortItem) { const identifier = attributeSortItem?.attributeIdentifier; return identifier === category?.localIdentifier; } return false; }); return matchedSorting?.attributeSortItem?.direction ?? null; } const createPureMetric = (measure, mdObj, measureIndex) => ({ element: measure?.definition?.measureDefinition?.item?.uri, sort: getMeasureSorting(measure, mdObj), meta: { measureIndex }, }); function createDerivedMetric(measure, mdObj, measureIndex, attributesMap) { const { format } = measure; const sort = getMeasureSorting(measure, mdObj); const title = getBaseMetricTitle(measure.title); const hasher = partial(getGeneratedMetricHash, title, format); const aggregation = getAggregation(measure); const element = getGeneratedMetricIdentifier(measure, aggregation.length ? aggregation : "base", getGeneratedMetricExpression, hasher, attributesMap); const definition = { metricDefinition: { identifier: element, expression: getGeneratedMetricExpression(measure, attributesMap), title, format, }, }; return { element, definition, sort, meta: { measureIndex, }, }; } function createContributionMetric(measure, mdObj, measureIndex, attributesMap) { const attribute = first(getAttributes(mdObj)); const getMetricExpression = partial(getPercentMetricExpression, attribute, attributesMap); const title = getBaseMetricTitle(measure?.title); const hasher = partial(getGeneratedMetricHash, title, CONTRIBUTION_METRIC_FORMAT); const identifier = getGeneratedMetricIdentifier(measure, "percent", getMetricExpression, hasher, attributesMap); return { element: identifier, definition: { metricDefinition: { identifier, expression: getMetricExpression(measure), title, format: CONTRIBUTION_METRIC_FORMAT, }, }, sort: getMeasureSorting(measure, mdObj), meta: { measureIndex, }, }; } function getOriginalMeasureForPoP(popMeasure, mdObj) { return getMeasures(mdObj).find((measure) => measure?.localIdentifier === getPoPDefinition(popMeasure)?.measureIdentifier); } function createPoPMetric(popMeasure, mdObj, measureIndex, attributesMap) { const title = getBaseMetricTitle(popMeasure?.title); const format = popMeasure?.format; const hasher = partial(getGeneratedMetricHash, title, format); const attributeUri = popMeasure?.definition?.popMeasureDefinition?.popAttribute?.uri; const originalMeasure = getOriginalMeasureForPoP(popMeasure, mdObj); const originalMeasureExpression = `[${getDefinition(originalMeasure)?.item?.uri}]`; let metricExpression = getPoPExpression(attributeUri, originalMeasureExpression); if (isDerived(originalMeasure)) { const generated = createDerivedMetric(originalMeasure, mdObj, measureIndex, attributesMap); const generatedMeasureExpression = `(${generated.definition.metricDefinition.expression})`; metricExpression = getPoPExpression(attributeUri, generatedMeasureExpression); } const identifier = getGeneratedMetricIdentifier(originalMeasure, "pop", () => metricExpression, hasher, attributesMap); return { element: identifier, definition: { metricDefinition: { identifier, expression: metricExpression, title, format, }, }, sort: getMeasureSorting(popMeasure, mdObj), meta: { measureIndex, isPoP: true, }, }; } function createContributionPoPMetric(popMeasure, mdObj, measureIndex, attributesMap) { const attributeUri = popMeasure?.definition?.popMeasureDefinition?.popAttribute?.uri; const originalMeasure = getOriginalMeasureForPoP(popMeasure, mdObj); const generated = createContributionMetric(originalMeasure, mdObj, measureIndex, attributesMap); const title = getBaseMetricTitle(popMeasure?.title); const format = CONTRIBUTION_METRIC_FORMAT; const hasher = partial(getGeneratedMetricHash, title, format); const generatedMeasureExpression = `(${generated.definition.metricDefinition.expression})`; const metricExpression = getPoPExpression(attributeUri, generatedMeasureExpression); const identifier = getGeneratedMetricIdentifier(originalMeasure, "pop", () => metricExpression, hasher, attributesMap); return { element: identifier, definition: { metricDefinition: { identifier, expression: metricExpression, title, format, }, }, sort: getMeasureSorting(), meta: { measureIndex, isPoP: true, }, }; } function categoryToElement(attributesMap, mdObj, category) { const element = getAttrUriFromMap(category?.displayForm?.uri, attributesMap); return { element, sort: getCategorySorting(category, mdObj), }; } function isPoP({ definition }) { return definition?.popMeasureDefinition !== undefined; } function isContribution({ definition }) { return definition?.measureDefinition?.computeRatio; } function isPoPContribution(popMeasure, mdObj) { if (isPoP(popMeasure)) { const originalMeasure = getOriginalMeasureForPoP(popMeasure, mdObj); return isContribution(originalMeasure); } return false; } function isCalculatedMeasure({ definition }) { return definition?.measureDefinition?.aggregation === undefined; } const rules = new Rules(); rules.addRule([isPoPContribution], createContributionPoPMetric); rules.addRule([isPoP], createPoPMetric); rules.addRule([isContribution], createContributionMetric); rules.addRule([isDerived], createDerivedMetric); rules.addRule([isCalculatedMeasure], createPureMetric); function getMetricFactory(measure, mdObj) { const factory = rules.match(measure, mdObj); invariant(factory, `Unknown metric factory for: ${measure}`); return factory; } function getExecutionDefinitionsAndColumns(mdObj, options, attributesMap) { const measures = getMeasures(mdObj); let attributes = getAttributes(mdObj); const metrics = flatten(map(measures, (measure, index) => getMetricFactory(measure, mdObj)(measure, mdObj, index, attributesMap))); if (options.removeDateItems) { attributes = filter(attributes, (attribute) => !isDateAttribute(attribute, attributesMap)); } attributes = map(attributes, partial(categoryToElement, attributesMap, mdObj)); const columns = compact(map([...attributes, ...metrics], "element")); return { columns, definitions: sortDefinitions(compact(map(metrics, "definition"))), }; } function getBuckets(mdObj) { return mdObj?.buckets ?? []; } function getAttributesInBucket(bucket) { return bucket.items.reduce((list, bucketItem) => { if (isVisualizationObjectAttribute(bucketItem)) { list.push(bucketItem.visualizationAttribute); } return list; }, []); } function getAttributes(mdObject) { const buckets = getBuckets(mdObject); return buckets.reduce((categoriesList, bucket) => { categoriesList.push(...getAttributesInBucket(bucket)); return categoriesList; }, []); } function getDefinition(measure) { return measure?.definition?.measureDefinition ?? {}; } function getMeasuresInBucket(bucket) { return bucket.items.reduce((list, bucketItem) => { if (isVisualizationObjectMeasure(bucketItem)) { list.push(bucketItem.measure); } return list; }, []); } function getMeasures(mdObject) { const buckets = getBuckets(mdObject); return buckets.reduce((categoriesList, bucket) => { categoriesList.push(...getMeasuresInBucket(bucket)); return categoriesList; }, []); } function getMeasureFilters(measure) { return getDefinition(measure)?.filters ?? []; } /** * Module for execution on experimental execution resource * * @deprecated The module is in maintenance mode only (just the the compilation issues are being fixed when * referenced utilities and interfaces are being changed) and is not being extended when AFM executor * have new functionality added. */ export class ExperimentalExecutionsModule { xhr; loadAttributesMap; // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types constructor(xhr, loadAttributesMap) { this.xhr = xhr; this.loadAttributesMap = loadAttributesMap; } /** * For the given projectId it returns table structure with the given * elements in column headers. * * @param projectId - GD project identifier * @param columns - An array of attribute or metric identifiers. * @param executionConfiguration - Execution configuration - can contain for example * property "where" containing query-like filters * property "orderBy" contains array of sorted properties to order in form * `[{column: 'identifier', direction: 'asc|desc'}]` * @param settings - Supports additional settings accepted by the underlying * xhr.ajax() calls * * @returns Structure with `headers` and `rawData` keys filled with values from execution. */ getData(projectId, columns, executionConfiguration = {}, settings = {}) { if (process.env.NODE_ENV !== "test") { console.warn("ExperimentalExecutionsModule is deprecated and is no longer being maintained. " + "Please migrate to the ExecuteAfmModule."); } const executedReport = { isLoaded: false, }; // Create request and result structures const request = { execution: { columns }, }; // enrich configuration with supported properties such as // where clause with query-like filters ["where", "orderBy", "definitions"].forEach((property) => { if (executionConfiguration[property]) { request.execution[property] = executionConfiguration[property]; } }); // Execute request return this.xhr .post(`/gdc/internal/projects/${projectId}/experimental/executions`, { ...settings, body: JSON.stringify(request), }) .then((r) => r.getData()) .then((response) => { executedReport.headers = wrapMeasureIndexesFromMappings(executionConfiguration?.metricMappings, response?.executionResult?.headers ?? []); // Start polling on url returned in the executionResult for tabularData return this.loadExtendedDataResults(response.executionResult.extendedTabularDataResult, settings); }) .then((r) => { const { result, status } = r; return { ...executedReport, rawData: result?.extendedTabularDataResult?.values ?? [], warnings: result?.extendedTabularDataResult?.warnings ?? [], isLoaded: true, isEmpty: status === 204, }; }); } mdToExecutionDefinitionsAndColumns(projectId, mdObj, options = {}) { const allDfUris = getAttributesDisplayForms(mdObj); const attributesMapPromise = this.getAttributesMap(options, allDfUris, projectId); return attributesMapPromise.then((attributesMap) => { return getExecutionDefinitionsAndColumns(mdObj, options, attributesMap); }); } getAttributesMap(options = {}, displayFormUris, projectId) { const attributesMap = options.attributesMap ?? {}; const missingUris = getMissingUrisInAttributesMap(displayFormUris, attributesMap); return this.loadAttributesMap(projectId, missingUris).then((result) => { return { ...attributesMap, ...result, }; }); } loadExtendedDataResults(uri, settings, prevResult = emptyResult) { return new Promise((resolve, reject) => { this.xhr .ajax(uri, settings) .then((r) => { const { response } = r; if (response.status === 204) { return { status: response.status, result: "", }; } return { status: response.status, result: r.getData(), }; }) .then(({ status, result }) => { const values = [ ...(prevResult?.extendedTabularDataResult?.values ?? []), ...(result?.extendedTabularDataResult?.values ?? []), ]; const warnings = [ ...(prevResult?.extendedTabularDataResult?.warnings ?? []), ...(result?.extendedTabularDataResult?.warnings ?? []), ]; const updatedResult = merge({}, prevResult, { extendedTabularDataResult: { values, warnings, }, }); const nextUri = result?.extendedTabularDataResult?.paging?.next; if (nextUri) { resolve(this.loadExtendedDataResults(nextUri, settings, updatedResult)); } else { resolve({ status, result: updatedResult }); } }, reject); }); } } //# sourceMappingURL=experimental-executions.js.map