// (C) 2019-2021 GoodData Corporation import { Application } from "express"; import { isEmpty } from "lodash"; import { IMockProject, IProjectGroup } from "../../model/MockProject"; import { OK, NOT_FOUND } from "http-status-codes"; import { AFM, Execution } from "@gooddata/typings"; import { ISchemaAfmExecution } from "../../schema/model/SchemaAfmExecution"; import { IMetadataObject } from "../../model/MetadataObject"; import { IObjectMeta } from "../../model/ObjectMeta"; import { IAttribute as SchemaAttribute } from "../../model/Attribute"; import uiHtmlContent from "./mockBuilderUi"; import { relativizeMockedExecutionLocalIdentifiers } from "../webapp/executeAfm/execution"; import IHeader = Execution.IHeader; import IAttributeHeader = Execution.IAttributeHeader; import IMeasureGroupHeader = Execution.IMeasureGroupHeader; import IMeasureHeaderItem = Execution.IMeasureHeaderItem; import IResultDimension = Execution.IResultDimension; import isAttributeHeader = Execution.isAttributeHeader; import isMeasureGroupHeader = Execution.isMeasureGroupHeader; import IResultHeaderItem = Execution.IResultHeaderItem; import IExecutionResultWrapper = Execution.IExecutionResultWrapper; import IExecutionResponseWrapper = Execution.IExecutionResponseWrapper; import IExecution = AFM.IExecution; import IRelativeDateFilter = AFM.IRelativeDateFilter; import IAbsoluteDateFilter = AFM.IAbsoluteDateFilter; import IPositiveAttributeFilter = AFM.IPositiveAttributeFilter; import INegativeAttributeFilter = AFM.INegativeAttributeFilter; import isPositiveAttributeFilter = AFM.isPositiveAttributeFilter; import isNegativeAttributeFilter = AFM.isNegativeAttributeFilter; import IAttribute = AFM.IAttribute; import IMeasure = AFM.IMeasure; import MeasureDefinition = AFM.MeasureDefinition; import ISimpleMeasureDefinition = AFM.ISimpleMeasureDefinition; import ISimpleMeasure = AFM.ISimpleMeasure; import IPopMeasureDefinition = AFM.IPopMeasureDefinition; import IPreviousPeriodMeasureDefinition = AFM.IPreviousPeriodMeasureDefinition; import IPreviousPeriodDateDataSet = AFM.IPreviousPeriodDateDataSet; import isPopMeasureDefinition = AFM.isPopMeasureDefinition; import isPreviousPeriodMeasureDefinition = AFM.isPreviousPeriodMeasureDefinition; import isSimpleMeasureDefinition = AFM.isSimpleMeasureDefinition; import CompatibilityFilter = AFM.CompatibilityFilter; import IAfm = AFM.IAfm; import FilterItem = AFM.FilterItem; import ObjQualifier = AFM.ObjQualifier; import IObjUriQualifier = AFM.IObjUriQualifier; export interface IMockBuilderPayload { execution: AFM.IExecution; executionResponse: Execution.IExecutionResponseWrapper; executionResult: Execution.IExecutionResultWrapper; } class SchemaDictionary { private readonly projectId: string; private readonly schemaDataSet: IProjectGroup; private readonly schemaAttributesWithElements: SchemaAttribute[]; private readonly schemaAttributeElementsMapping = {}; // schema attr uri to attr elements private attributeMapping = {}; // staging attr uri to schema attr uri private metadataObjectCache = {}; // staging metadata obj uri to schema metadata obj uri private schemaAttributeIndex = 0; private schemaMetricIndex = 0; private schemaDateDataSetIndex = 0; constructor(project: IMockProject, projectGroupIndex: number) { const schemaDataSet = project.groups[projectGroupIndex]; this.projectId = project.project.meta.identifier; this.schemaDataSet = schemaDataSet; this.schemaAttributesWithElements = schemaDataSet.attributes.filter((attribute: SchemaAttribute) => { const { meta } = attribute; const attributeElements = project.elements.get(meta.identifier); this.schemaAttributeElementsMapping[meta.uri] = { elements: attributeElements, index: 0, }; return attributeElements !== undefined && attributeElements.length > 0; }); } public getAttributeDisplayFormObjQualifier(displayFormObjIdentifier: ObjQualifier): IObjUriQualifier { const attribute = this.getNextSchemaAttribute( this.extractUri(displayFormObjIdentifier), ) as SchemaAttribute; return this.asUriObjQualifier(attribute.content.displayForms[0]); } public getAttribute(displayFormObjIdentifier: ObjQualifier, attributeUri?: string): SchemaAttribute { const attribute = this.getNextSchemaAttribute( this.extractUri(displayFormObjIdentifier), ) as SchemaAttribute; this.attributeMapping[attributeUri] = attribute.meta.uri; return attribute; } public getAttributeElementUri(elementUri: string, schemaAttributeUri?: string): string { const cachedElementUri = this.metadataObjectCache[elementUri]; if (cachedElementUri !== undefined) { return cachedElementUri; } const attributeUri = elementUri.substr(0, elementUri.indexOf("/elements?id=")); const elementMappingUri = schemaAttributeUri === undefined ? this.attributeMapping[attributeUri] : schemaAttributeUri; const attributeElements = this.schemaAttributeElementsMapping[elementMappingUri]; const nextElement = attributeElements.elements[attributeElements.index++]; if (nextElement === undefined) { throw new Error(`There are no more elements in ${elementMappingUri}!`); } const nextElementUri = nextElement.element.uri; this.metadataObjectCache[elementUri] = nextElementUri; return nextElementUri; } public getMeasureObjQualifier(objIdentifier: ObjQualifier): IObjUriQualifier { const metric = this.getNextSchemaMetric(this.extractUri(objIdentifier)); return this.asUriObjQualifier(metric); } public getMeasureMetaFor(measureUri: string): IObjectMeta { const metric = this.getNextSchemaMetric(measureUri); return metric.meta; } public getDateDataSetObjQualifier(objIdentifier: ObjQualifier): IObjUriQualifier { const dateDataSet = this.getNextSchemaDateDataSet(this.extractUri(objIdentifier)); return this.asUriObjQualifier(dateDataSet); } public getProjectId(): string { return this.projectId; } private getNextSchemaAttribute(attributeUri: string): IMetadataObject { return this.getMetadataObject( attributeUri, () => this.schemaAttributesWithElements[this.schemaAttributeIndex++], ); } private getNextSchemaMetric(measureUri: string): IMetadataObject { return this.getMetadataObject(measureUri, () => this.schemaDataSet.metrics[this.schemaMetricIndex++]); } private getNextSchemaDateDataSet(dateDataSetUri: string): IMetadataObject { return this.getMetadataObject( dateDataSetUri, () => this.schemaDataSet.dateDataSets[this.schemaDateDataSetIndex++], ); } private extractUri(objIdentifier: ObjQualifier): string { const objUriQualifier = objIdentifier as IObjUriQualifier; return objUriQualifier.uri; } private asUriObjQualifier(metadataObject: IMetadataObject): IObjUriQualifier { return { uri: metadataObject.meta.uri, }; } private getMetadataObject(objUri: string, objectFactory: () => IMetadataObject): IMetadataObject { const cachedObject = this.metadataObjectCache[objUri]; if (cachedObject !== undefined) { return cachedObject; } const metadataObject = objectFactory(); if (metadataObject === undefined) { throw new Error(`There are no more objects in schema to replace ${objUri}!`); } this.metadataObjectCache[objUri] = metadataObject; return metadataObject; } } class MockBuilder { private schema: SchemaDictionary; constructor(project: IMockProject, projectGroupIndex: number) { this.schema = new SchemaDictionary(project, projectGroupIndex); } public buildExecutionMockWithReplacedUris(payload: IMockBuilderPayload): ISchemaAfmExecution { const mockedExecution = { execution: this.mapExecution(payload.execution), executionResponse: this.mapExecutionResponse(payload.executionResponse), executionResult: this.mapExecutionResult(payload.executionResult), }; return relativizeMockedExecutionLocalIdentifiers(mockedExecution); } private mapExecution(wrappedExecution: IExecution): IExecution { const { execution, execution: { afm, afm: { measures, attributes, filters }, }, } = wrappedExecution; return { execution: { ...execution, afm: { ...afm, ...this.mapAfmMeasures(measures), ...this.mapAfmAttributes(attributes), ...this.mapAfmFilters(filters), }, }, }; } private mapAfmMeasures(measures: IMeasure[]): Partial { if (measures === undefined) { return {}; } return { measures: measures.map((measure: IMeasure) => this.mapMeasure(measure)), }; } private mapMeasure(measure: IMeasure): IMeasure { const { definition } = measure; return { ...measure, definition: { ...definition, ...this.mapMeasureDefinition(definition), }, }; } private mapMeasureDefinition(definition: MeasureDefinition): MeasureDefinition { if (isSimpleMeasureDefinition(definition)) { return this.mapSimpleMeasureDefinition(definition); } if (isPopMeasureDefinition(definition)) { return this.mapPopMeasureDefinition(definition); } if (isPreviousPeriodMeasureDefinition(definition)) { return this.mapPreviousPeriodMeasureDefinition(definition); } return definition; } private mapSimpleMeasureDefinition(definition: ISimpleMeasureDefinition): ISimpleMeasureDefinition { const { measure: simpleMeasure, measure: { filters, item }, } = definition; return { measure: { ...simpleMeasure, ...this.mapMeasureFilters(filters), item: this.schema.getMeasureObjQualifier(item), }, }; } private mapMeasureFilters(filters: FilterItem[]): Partial { if (filters === undefined) { return {}; } return { filters: this.mapFilters(filters) as FilterItem[], }; } private mapPopMeasureDefinition(definition: IPopMeasureDefinition): IPopMeasureDefinition { const { popMeasure } = definition; return { popMeasure: { ...popMeasure, popAttribute: this.schema.getDateDataSetObjQualifier(popMeasure.popAttribute), }, }; } private mapPreviousPeriodMeasureDefinition( definition: IPreviousPeriodMeasureDefinition, ): IPreviousPeriodMeasureDefinition { const { previousPeriodMeasure, previousPeriodMeasure: { dateDataSets }, } = definition; return { previousPeriodMeasure: { ...previousPeriodMeasure, dateDataSets: dateDataSets.map((dateDataSet: IPreviousPeriodDateDataSet) => { return { ...dateDataSet, dataSet: this.schema.getDateDataSetObjQualifier(dateDataSet.dataSet), }; }), }, }; } private mapAfmAttributes(attributes: IAttribute[]): Partial { if (attributes === undefined) { return {}; } return { attributes: attributes.map((attribute: IAttribute) => this.mapAttribute(attribute)), }; } private mapAttribute(attribute: IAttribute): IAttribute { return { ...attribute, displayForm: this.schema.getAttributeDisplayFormObjQualifier(attribute.displayForm), }; } private mapAfmFilters(filters: CompatibilityFilter[]): Partial { if (filters === undefined) { return {}; } return { filters: this.mapFilters(filters), }; } private mapFilters(filters: AFM.CompatibilityFilter[]): AFM.CompatibilityFilter[] { return filters.map((filter: AFM.CompatibilityFilter) => { if (isPositiveAttributeFilter(filter)) { return this.mapPositiveAttributeFilter(filter); } if (isNegativeAttributeFilter(filter)) { return this.mapNegativeAttributeFilter(filter); } if (this.isAbsoluteDateFilter(filter)) { return this.mapAbsoluteDateFilter(filter); } if (this.isRelativeDateFilter(filter)) { return this.mapRelativeDateFilter(filter); } return filter; }); } private mapPositiveAttributeFilter(filter: IPositiveAttributeFilter): IPositiveAttributeFilter { const { positiveAttributeFilter, positiveAttributeFilter: { displayForm }, } = filter; const attribute = this.schema.getAttribute(displayForm); return { ...filter, positiveAttributeFilter: { ...positiveAttributeFilter, displayForm: { uri: attribute.content.displayForms[0].meta.uri, }, in: this.mapAttributeElements(positiveAttributeFilter.in, attribute.meta.uri), }, }; } private mapAttributeElements(valueUris: string[], attributeUri: string): string[] { return valueUris.map((valueUri: string) => this.schema.getAttributeElementUri(valueUri, attributeUri), ); } private mapNegativeAttributeFilter(filter: INegativeAttributeFilter): INegativeAttributeFilter { const { negativeAttributeFilter, negativeAttributeFilter: { displayForm, notIn }, } = filter; const attribute = this.schema.getAttribute(displayForm); return { ...filter, negativeAttributeFilter: { ...negativeAttributeFilter, displayForm: { uri: attribute.content.displayForms[0].meta.uri, }, notIn: this.mapAttributeElements(notIn, attribute.meta.uri), }, }; } private isAbsoluteDateFilter(filter: AFM.CompatibilityFilter): filter is AFM.IAbsoluteDateFilter { return !isEmpty(filter) && (filter as AFM.IAbsoluteDateFilter).absoluteDateFilter !== undefined; } private mapAbsoluteDateFilter(filter: IAbsoluteDateFilter): IAbsoluteDateFilter { const { absoluteDateFilter } = filter; return { ...filter, absoluteDateFilter: { ...absoluteDateFilter, dataSet: this.schema.getDateDataSetObjQualifier(absoluteDateFilter.dataSet), }, }; } private isRelativeDateFilter(filter: AFM.CompatibilityFilter): filter is AFM.IRelativeDateFilter { return !isEmpty(filter) && (filter as AFM.IRelativeDateFilter).relativeDateFilter !== undefined; } private mapRelativeDateFilter(filter: IRelativeDateFilter): IRelativeDateFilter { const { relativeDateFilter } = filter; return { ...filter, relativeDateFilter: { ...relativeDateFilter, dataSet: this.schema.getDateDataSetObjQualifier(relativeDateFilter.dataSet), }, }; } private mapExecutionResponse( wrappedExecutionResponse: IExecutionResponseWrapper, ): IExecutionResponseWrapper { const { executionResponse, executionResponse: { dimensions, links }, } = wrappedExecutionResponse; return { executionResponse: { ...executionResponse, dimensions: dimensions.map((dimension: IResultDimension) => this.mapDimension(dimension)), links: { ...links, executionResult: this.replaceExecutionResultProjectId(links.executionResult), }, }, }; } private mapDimension(dimension: IResultDimension): IResultDimension { return { ...dimension, headers: dimension.headers.map((header: Execution.IHeader) => this.mapResponseHeader(header)), }; } private mapResponseHeader(header: IHeader): IHeader { if (isAttributeHeader(header)) { return this.mapAttributeHeader(header); } if (isMeasureGroupHeader(header)) { return this.mapMeasureGroupHeader(header); } return header; } private mapAttributeHeader(header: IAttributeHeader): IAttributeHeader { const { attributeHeader: headerContent } = header; const attributeUriObjQualifier = { uri: headerContent.uri }; const attribute = this.schema.getAttribute(attributeUriObjQualifier, headerContent.formOf.uri); const attributeMeta = attribute.meta; const firstDisplayFormMeta = attribute.content.displayForms[0].meta; return { attributeHeader: { ...headerContent, uri: firstDisplayFormMeta.uri, identifier: firstDisplayFormMeta.identifier, formOf: { ...headerContent.formOf, uri: attributeMeta.uri, identifier: attributeMeta.identifier, }, }, }; } private mapMeasureGroupHeader(header: IMeasureGroupHeader): IMeasureGroupHeader { const { measureGroupHeader: { items }, } = header; return { measureGroupHeader: { items: items.map((measureHeaderItem: IMeasureHeaderItem) => { const { measureHeaderItem: headerItemContent } = measureHeaderItem; const metricMeta = this.schema.getMeasureMetaFor(headerItemContent.uri); return { measureHeaderItem: { ...headerItemContent, uri: metricMeta.uri, identifier: metricMeta.identifier, }, }; }), }, }; } private replaceExecutionResultProjectId(resultLink: string): string { const match = resultLink.match(/\/gdc\/app\/projects\/(.*)\/executionResults\//); const oldProjectId = match !== null && match.length > 0 ? match[match.length - 1] : null; return resultLink.replace(oldProjectId, this.schema.getProjectId()); } private mapExecutionResult(wrappedExecutionResult: IExecutionResultWrapper): IExecutionResultWrapper { const { executionResult, executionResult: { headerItems }, } = wrappedExecutionResult; return { executionResult: { ...executionResult, headerItems: headerItems.map((dimension: IResultHeaderItem[][]) => this.mapResultDimension(dimension), ), }, }; } private mapResultDimension(dimension: IResultHeaderItem[][]): IResultHeaderItem[][] { return dimension.map((headers: IResultHeaderItem[]) => this.mapResultDimensionHeaders(headers)); } private mapResultDimensionHeaders(headers: IResultHeaderItem[]): IResultHeaderItem[] { return headers.map((header: IResultHeaderItem) => this.mapResultHeader(header)); } private mapResultHeader(header: IResultHeaderItem): IResultHeaderItem { if (Execution.isAttributeHeaderItem(header)) { const { attributeHeaderItem } = header; return { ...header, attributeHeaderItem: { ...attributeHeaderItem, uri: this.schema.getAttributeElementUri(attributeHeaderItem.uri), }, }; } return header; } } function prettyPrint(jsValue: any) { return JSON.stringify(jsValue, null, 2); } function prettyPrintAsMock(jsObject: ISchemaAfmExecution) { return prettyPrint(jsObject) .replace(/"(.*)":/g, "$1:") // remove quotes from JSON keys .replace(/"/g, "'"); // replace string double quotes with apostrophes) } export const mockBuilder = { register(app: Application, project: IMockProject) { app.get("/mock-builder", (_, response) => { response .status(OK) .set("Content-Type", "text/html") .send(uiHtmlContent); }); app.post("/mock-builder/:projectId/:groupIndex", (request, response) => { if (request.params.projectId === project.project.meta.identifier) { const payload: IMockBuilderPayload = request.body; const mockBuilder = new MockBuilder(project, (request.params .groupIndex as unknown) as number); const executionMock = mockBuilder.buildExecutionMockWithReplacedUris(payload); const stringMock = prettyPrintAsMock(executionMock); response .status(OK) .set("Content-Type", "text/plain") .send(stringMock); } else { response.sendStatus(NOT_FOUND); } }); return app; }, };