// (C) 2007-2020 GoodData Corporation import { ExecuteAFM as AFM, Execution } from "@gooddata/typings"; import { cloneDeep, isEqual, isEmpty } from "lodash"; import { ISchemaAfmExecution, isExecutionResponseWrapper, ISchemaAfmExecutionResponse, } from "../../../schema/model/SchemaAfmExecution"; import { sanitize } from "../executeAfm"; import IHeader = Execution.IHeader; const RELATIVIZED_ALIAS = "relativized-alias"; function relativizeTotals( totals: AFM.ITotalItem[], replacedKeys: Map, ): Partial { if (totals === undefined) { return {}; } return { totals: totals.map((totalItem: AFM.ITotalItem) => { const { measureIdentifier, attributeIdentifier } = totalItem; return { ...totalItem, measureIdentifier: replacedKeys.get(measureIdentifier), attributeIdentifier: replacedKeys.get(attributeIdentifier), }; }), }; } function relativizeAlias(enabled: boolean) { if (!enabled) { return {}; } return { alias: RELATIVIZED_ALIAS, }; } export function isILocalIdentifierQualifier( objQualifier: AFM.Qualifier, ): objQualifier is AFM.ILocalIdentifierQualifier { return ( !isEmpty(objQualifier) && (objQualifier as AFM.ILocalIdentifierQualifier).localIdentifier !== undefined ); } function isFilterReferencingLocalAfmMeasure(filter: AFM.CompatibilityFilter) { return ( (AFM.isMeasureValueFilter(filter) && isILocalIdentifierQualifier(filter.measureValueFilter.measure)) || (AFM.isRankingFilter(filter) && filter.rankingFilter.measures.some(isILocalIdentifierQualifier)) ); } function relativizeIdentifiers( execution: AFM.IExecution, replacedKeys: Map, relativizeAliases: boolean, stripLocalMeasureValueAndRankingFilters = false, ): AFM.IExecution { const cloned = sanitize(cloneDeep(execution)); if (cloned.execution.afm.measures) { cloned.execution.afm.measures = cloned.execution.afm.measures.map((measure, index) => { const originalKey = measure.localIdentifier; const newKey = `m${index + 1}`; // index measures from 1 (m1, m2, m3) to match our unwritten conventions replacedKeys.set(originalKey, newKey); return { ...measure, localIdentifier: newKey, ...relativizeAlias(relativizeAliases), }; }); cloned.execution.afm.measures.forEach(measure => { if (AFM.isPreviousPeriodMeasureDefinition(measure.definition)) { const definition = measure.definition as AFM.IPreviousPeriodMeasureDefinition; const ref = definition.previousPeriodMeasure.measureIdentifier; definition.previousPeriodMeasure.measureIdentifier = replacedKeys.get(ref); } else if (AFM.isPopMeasureDefinition(measure.definition)) { const definition = measure.definition as AFM.IPopMeasureDefinition; const ref = definition.popMeasure.measureIdentifier; definition.popMeasure.measureIdentifier = replacedKeys.get(ref); } else if (AFM.isArithmeticMeasureDefinition(measure.definition)) { const definition = measure.definition as AFM.IArithmeticMeasureDefinition; const refs = definition.arithmeticMeasure.measureIdentifiers; definition.arithmeticMeasure.measureIdentifiers = refs.map(ref => replacedKeys.get(ref)); } }); } if (cloned.execution.afm.attributes) { cloned.execution.afm.attributes = cloned.execution.afm.attributes.map((attribute, index) => { const originalKey = attribute.localIdentifier; const newKey = `a${index + 1}`; // index attributes from 1 (a1, a2, a3) to match our unwritten conventions replacedKeys.set(originalKey, newKey); return { ...attribute, localIdentifier: newKey, ...relativizeAlias(relativizeAliases), }; }); } if (stripLocalMeasureValueAndRankingFilters && cloned.execution.afm.filters) { cloned.execution.afm.filters = cloned.execution.afm.filters.filter( filter => !isFilterReferencingLocalAfmMeasure(filter), ); } if (!cloned.execution.resultSpec) { return cloned; } if (cloned.execution.resultSpec.dimensions) { cloned.execution.resultSpec.dimensions = cloned.execution.resultSpec.dimensions.map(dimension => { return { ...dimension, itemIdentifiers: dimension.itemIdentifiers.map(itemIdentifier => { if (replacedKeys.has(itemIdentifier)) { return replacedKeys.get(itemIdentifier); } return itemIdentifier; }), ...relativizeTotals(dimension.totals, replacedKeys), }; }); } if (cloned.execution.resultSpec.sorts) { cloned.execution.resultSpec.sorts = cloned.execution.resultSpec.sorts.map(sort => { if (AFM.isAttributeSortItem(sort)) { return { attributeSortItem: { direction: sort.attributeSortItem.direction, attributeIdentifier: replacedKeys.get(sort.attributeSortItem.attributeIdentifier), }, }; } if (AFM.isMeasureSortItem(sort)) { return { measureSortItem: { direction: sort.measureSortItem.direction, locators: sort.measureSortItem.locators.map((locatorItem: AFM.LocatorItem) => { if (AFM.isMeasureLocatorItem(locatorItem)) { return { measureLocatorItem: { measureIdentifier: replacedKeys.get( locatorItem.measureLocatorItem.measureIdentifier, ), }, }; } return { attributeLocatorItem: { ...locatorItem.attributeLocatorItem, attributeIdentifier: locatorItem.attributeLocatorItem.attributeIdentifier, }, }; }), }, }; } }); } return cloned; } function relativizeResponseIdentifiers( executionResponse: ISchemaAfmExecutionResponse, replacedKeys: Map, ): ISchemaAfmExecutionResponse { if (isExecutionResponseWrapper(executionResponse)) { const cloned = cloneDeep(executionResponse); cloned.executionResponse.dimensions.forEach((dimension: Execution.IResultDimension) => { dimension.headers.forEach((header: IHeader) => { if (Execution.isMeasureGroupHeader(header)) { const { measureGroupHeader: { items }, } = header; items.forEach((measureItem: Execution.IMeasureHeaderItem) => { const { measureHeaderItem } = measureItem; measureHeaderItem.localIdentifier = replacedKeys.get( measureHeaderItem.localIdentifier, ); }); } if (Execution.isAttributeHeader(header)) { header.attributeHeader.localIdentifier = replacedKeys.get( header.attributeHeader.localIdentifier, ); } }); }); return cloned; } return executionResponse; } /** * Compares two execution objects ignoring differences in identifiers * * This is necessary due to the fact that our client apps (namely AD) creates * unique local identifiers every single session. */ export function isRelativelyEqual(execution: AFM.IExecution, other: AFM.IExecution): boolean { return isEqual( relativizeIdentifiers(execution, new Map(), true, true), relativizeIdentifiers(other, new Map(), true, true), ); } export function relativizeMockedExecutionLocalIdentifiers( mockedExecution: ISchemaAfmExecution, ): ISchemaAfmExecution { const { execution, executionResponse, executionResult } = mockedExecution; const replacedKeys = new Map(); return { ...mockedExecution, execution: relativizeIdentifiers(execution, replacedKeys, false), executionResponse: relativizeResponseIdentifiers(executionResponse, replacedKeys), executionResult, }; }