// (C) 2007-2020 GoodData Corporation import { ExecuteAFM as AFM, Execution } from "@gooddata/typings"; import { IMockProject } from "../../../model/MockProject"; import { rotatedCartesianProduct } from "../../../utils/cartesianProduct/cartesianProduct"; import { getNormalizedExecution, isErrorResponse, IErrorResponse } from "../executeAfm"; import { getDisplayFormByQualifier } from "./afm"; import { generateDimensions } from "./dimensions"; import { getAttributeHeaderItemsByDisplayForm, IDataResultRandomGeneratorParams } from "./headerItems"; import invariant = require("invariant"); import IExecution = AFM.IExecution; import IAfm = AFM.IAfm; import IResultSpec = AFM.IResultSpec; import IMeasureGroupHeader = Execution.IMeasureGroupHeader; import IMeasureHeaderItem = Execution.IMeasureHeaderItem; import IResultDimension = Execution.IResultDimension; import IResultHeaderItem = Execution.IResultHeaderItem; import isAttributeHeader = Execution.isAttributeHeader; import isMeasureGroupHeader = Execution.isMeasureGroupHeader; import isMeasureHeaderItem = Execution.isMeasureHeaderItem; import { FunSort } from "./sorting"; import MeasureValueFilters from "./measureValueFilters"; import RankingFilters from "./rankingFilters"; export type RandomMetricValueGenerator = (params: IDataResultRandomGeneratorParams) => string; export type RandomData = any[][] | any[]; /** * Result headers for all dimensions */ export type Headers = DimensionHeaders[]; /** * Result headers for particular dimension. */ export type DimensionHeaders = IResultHeaderItem[][]; export interface IRandomDataResult { /** * Result dimensions - as returned by executeAfm */ dims: IResultDimension[]; /** * Headers - as needed by execution result */ headerItems: Headers; /** * Expanded measure header items matching expanded result header measure items in respective dimension. */ measureHeaderItems: IMeasureHeaderItem[][]; /** * El data */ data: RandomData; /** * Empty data indicator */ isEmpty: boolean; } /** * Data result builder is capable of taking any AFM & result spec and generate the headers and data sheet * accordingly. This builder can be used for all purposes (simple tabular, stacking, pivot table etc) */ export class DataResultBuilder { public static forProject(project: IMockProject): DataResultBuilder { return new DataResultBuilder(project); } private readonly project: IMockProject; private execution: IExecution; private afm?: IAfm; private resultSpec?: IResultSpec; private generator?: RandomMetricValueGenerator; private date: Date = new Date(); private constructor(project: IMockProject) { this.project = project; } public forAfmAndResultSpec(execution: AFM.IExecution): DataResultBuilder { const normalizedExecution = getNormalizedExecution(execution); const afm = normalizedExecution.execution.afm; const resultSpec = normalizedExecution.execution.resultSpec; this.afm = afm; this.resultSpec = resultSpec; this.execution = { execution: { afm, resultSpec, }, }; return this; } public withGenerator(generator: RandomMetricValueGenerator): DataResultBuilder { this.generator = generator; return this; } public atDate(date: Date): DataResultBuilder { this.date = date; return this; } public build(): IRandomDataResult | IErrorResponse { this.checkInputs(); const dims = generateDimensions(this.project, this.afm, this.resultSpec); const measureHeaderItems: IMeasureHeaderItem[][] = [ this.getExpandedMeasuresFromRows(dims), this.getExpandedMeasuresFromCols(dims), ]; const resultHeaderItems: IResultHeaderItem[][][] = this.generateHeaders(dims); const resultData: any[][] = this.generateData(resultHeaderItems, measureHeaderItems); const dataResult = { headerItems: resultHeaderItems, data: resultData }; const measureValueFilteredDataResult = MeasureValueFilters.filterDataResult( this.execution, dataResult, ); const rankedFilteredDataResult = RankingFilters.filterDataResult( this.execution, measureValueFilteredDataResult, ); if (isErrorResponse(rankedFilteredDataResult)) { return rankedFilteredDataResult; } const { data, headerItems } = rankedFilteredDataResult; const result = { dims, headerItems, measureHeaderItems, data, isEmpty: !data || !data.length || (data.length > 0 && data[0].length === 0), }; // sort in place this.sort(result); return result; } private checkInputs() { invariant(this.afm !== undefined, "AFM must be defined"); invariant(this.resultSpec !== undefined, "ResultSpec must be defined"); invariant(this.generator !== undefined, "Random value generator must be defined"); } /** * Generates result headers for each dimension (total of 2). * * 1. Each dimension contains a list of header items; * 2. For each header item in the list: * - if attribute expand to attribute elements * - if measure group expand to all measures in the group * 3. This produces a array X = [Y1, Y2, ... Yn] where each Yn is array and N = number of headers in the dim * 4. Do a cartesian between elementf of X * 5. This produces an array A = [B1, B2, ... Bm] where each Bm is a resulting combination produced by cartesian. * Here M = * (Yn.len) and cardinality of each Bm is N * 6. Finally do rotation on A, leading to R = [H1, H2, ... Hn] where N is number of headers in the dim and * cardinality of each Hn is M * * The resulting list R is the format ready to be returned as a result. * * @param dims result dimensions */ private generateHeaders(dims: Execution.IResultDimension[]): Headers { return dims.map(dim => { const uniqueHeaderItems = dim.headers.map(header => { if (isAttributeHeader(header)) { const df = getDisplayFormByQualifier(this.project, { identifier: header.attributeHeader.identifier, }); return getAttributeHeaderItemsByDisplayForm( this.project, this.afm.filters, df, this.date, ); } else if (isMeasureGroupHeader(header)) { return header.measureGroupHeader.items.map((item, idx) => { return { measureHeaderItem: { name: item.measureHeaderItem.name, order: idx, }, }; }); } }); return rotatedCartesianProduct(uniqueHeaderItems); }); } /** * Generates an expanded array of measure header items for particular dimension. The cardinality of the * resulting array is same as cardinality of header items in the dimension that contains measureGroup. * * The algorithm here is similar to generateHeaders, the only difference in the return value: * * - Measure group is NOT expanded into IResultMeasureHeaderItem but into IMeasureHeaderItem - this structure * contains all necessary detail to generate values for the measure (as opposed to result item which contains * name and order) * * - The result is just the array of IMeasureHeaderItem = one particular subarray * * @param dim result dimensions */ private getExpandedMeasureItems(dim: Execution.IResultDimension): IMeasureHeaderItem[] { const expandedHeaderItems = rotatedCartesianProduct( dim.headers.map(header => { if (isAttributeHeader(header)) { const df = getDisplayFormByQualifier(this.project, { identifier: header.attributeHeader.identifier, }); return getAttributeHeaderItemsByDisplayForm( this.project, this.afm.filters, df, this.date, ); } else if (isMeasureGroupHeader(header)) { return header.measureGroupHeader.items; } }), ); return expandedHeaderItems.find(headers => { return headers.length > 0 && isMeasureHeaderItem(headers[0]); }) as IMeasureHeaderItem[]; } private getExpandedMeasuresFromRows( dims: Execution.IResultDimension[], ): IMeasureHeaderItem[] | undefined { const measureGroup = dims[0].headers.find(header => isMeasureGroupHeader(header), ) as IMeasureGroupHeader; if (measureGroup === undefined) { return; } return this.getExpandedMeasureItems(dims[0]); } private getExpandedMeasuresFromCols( dims: Execution.IResultDimension[], ): IMeasureHeaderItem[] | undefined { if (dims.length < 2) { return []; } const measureGroup = dims[1].headers.find(header => isMeasureGroupHeader(header), ) as IMeasureGroupHeader; if (measureGroup === undefined) { return; } return this.getExpandedMeasureItems(dims[1]); } private generateRowData(rowMeasures: IMeasureHeaderItem[], columnHeaders: DimensionHeaders): RandomData { /* * Measures are in the row dimension - one measure per row. Iterate rows (= measures) and generate * random measure value into each column. */ return rowMeasures.map((measure, row) => { if (columnHeaders === undefined) { /* * One-dimensional result requested - return array with measure values */ return this.generator({ row, column: 0, localIdentifier: measure.measureHeaderItem.localIdentifier, }); } else if (columnHeaders.length === 0) { /* * Two-dimension result requested - but second dimension is empty */ return [ this.generator({ row, column: 0, localIdentifier: measure.measureHeaderItem.localIdentifier, }), ]; } return columnHeaders[0].map((_, column) => { return this.generator({ row, column, localIdentifier: measure.measureHeaderItem.localIdentifier, }); }); }); } private generateColumnData( columnMeasures: IMeasureHeaderItem[], rowHeaders: DimensionHeaders, ): RandomData { if (rowHeaders.length === 0) { /* * Row dimension is empty - generate a single row with all measure values */ return [ columnMeasures.map((measure, column) => { return this.generator({ row: 0, column, localIdentifier: measure.measureHeaderItem.localIdentifier, }); }), ]; } /* * Measures are in the column dimension - one measure per column. Iterate rows (= attributes) and * generate random value for the measure in that column. */ return rowHeaders[0].map((_, row) => { return columnMeasures.map((measure, column) => { return this.generator({ row, column, localIdentifier: measure.measureHeaderItem.localIdentifier, }); }); }); } private generateData(headers: Headers, measureHeaders: IMeasureHeaderItem[][]): RandomData { const [rowMeasures, columnMeasures] = measureHeaders; if (rowMeasures !== undefined) { // ... then generate row data => values for one measure are in all cols in the row return this.generateRowData(rowMeasures, headers[1]); } if (columnMeasures !== undefined) { // ... then generate column data => values for measure are in particular col(s) in all rows return this.generateColumnData(columnMeasures, headers[0]); } // having no measures is valid too, in that case return empty data return []; } private sort(result: IRandomDataResult) { new FunSort(result).run(this.resultSpec.sorts); } }