// (C) 2007-2020 GoodData Corporation import { VisualizationObject, VisualizationClass } from "@gooddata/typings"; import { first, partial, get, toNumber } from "lodash"; import { IUser } from "../../model/User"; import { ensureItems } from "../helpers/objects"; import { objectUri, profileUri } from "../../route/routes"; import { IProject, getProjectId } from "../../model/Project"; import { webalize } from "../../utils/string"; import { v4 as uuid } from "uuid"; import * as invariant from "invariant"; import { ISchemaVisualizationObject, ISchemaVisualizationAttribute, ISchemaMeasure, ISchemaDerivedMeasure, ISchemaArithmeticMeasure, ISchemaBucketItem, ISchemaBucket, ISchemaFilter, IPreviousPeriodDateDataSet, ISchemaTotal, } from "../model/SchemaVisualizationObject"; import { ISchema } from "../model/Schema"; import { createUsers } from "./UserBuilder"; import { createProject } from "./ProjectBuilder"; import { createVisualizationClasses } from "./VisualizationClassBuilder"; const DATE_FILTER: string = "date"; const ATTRIBUTE_FILTER: string = "attribute"; const MEASURE_VALUE_COMPARISON_FILTER: string = "measureComparison"; const MEASURE_VALUE_RANGE_FILTER: string = "measureRange"; const RANKING_FILTER: string = "ranking"; function createBucket(project: IProject, bucketSchema: ISchemaBucket): VisualizationObject.IBucket { const { localIdentifier, items, totals } = bucketSchema; return { localIdentifier, items: items.map((item: ISchemaBucketItem) => createBucketItem(project, item)), ...createVisualizationTotals(totals), }; } function isVisualizationAttribute( item: ISchemaVisualizationAttribute | ISchemaMeasure, ): item is ISchemaVisualizationAttribute { return (item as ISchemaVisualizationAttribute).displayForm !== undefined; } function toUriQualifier(uri: string): VisualizationObject.IObjUriQualifier { return { uri, }; } function createBucketItem( project: IProject, item: ISchemaBucketItem, ): VisualizationObject.IVisualizationAttribute | VisualizationObject.IMeasure { if (isVisualizationAttribute(item)) { return createVisualizationAttribute(project, item); } return createMeasure(project, item); } function createVisualizationTotals(totals: ISchemaTotal[]) { if (totals === undefined || totals === null) { return {}; } return { totals: totals.map((total: ISchemaTotal) => { const { type, measureIdentifier, attributeIdentifier } = total; return { type, measureIdentifier, attributeIdentifier, }; }), }; } function createVisualizationAttribute( project: IProject, attribute: ISchemaVisualizationAttribute, ): VisualizationObject.IVisualizationAttribute { const localIdentifier = attribute.localIdentifier ? attribute.localIdentifier : uuid(); return { visualizationAttribute: { localIdentifier, displayForm: toUriQualifier(objectUri(getProjectId(project), attribute.displayForm)), }, }; } function getMeasureBase(measure: ISchemaMeasure, definition: VisualizationObject.IMeasureDefinitionType) { const localIdentifier = measure.localIdentifier ? measure.localIdentifier : uuid(); const optionalProps: any = {}; if (measure.title !== undefined) { optionalProps.title = measure.title; } if (measure.alias !== undefined) { optionalProps.alias = measure.alias; } if (measure.format !== undefined) { optionalProps.format = measure.format; } return { measure: { definition, localIdentifier, ...optionalProps, }, }; } function createOrdinaryMeasure(project: IProject, measure: ISchemaMeasure): VisualizationObject.IMeasure { return getMeasureBase(measure, createMeasureDefinition(project, measure)); } function createDerivedMeasure( project: IProject, measure: ISchemaDerivedMeasure, ): VisualizationObject.IMeasure { return getMeasureBase(measure, createDerivedMeasureDefinition(project, measure)); } function createArithmeticMeasure(measure: ISchemaArithmeticMeasure): VisualizationObject.IMeasure { return getMeasureBase(measure, createArithmeticMeasureDefinition(measure)); } function measureIsDerived(measure: ISchemaMeasure): measure is ISchemaDerivedMeasure { return measure.isDerived; } function measureIsArithmetic(measure: ISchemaMeasure): measure is ISchemaArithmeticMeasure { return measure.isArithmetic; } function createMeasure(project: IProject, measure: ISchemaMeasure): VisualizationObject.IMeasure { if (measureIsDerived(measure)) { return createDerivedMeasure(project, measure); } else if (measureIsArithmetic(measure)) { return createArithmeticMeasure(measure); } return createOrdinaryMeasure(project, measure); } function createMeasureDefinition( project: IProject, measure: ISchemaMeasure, ): VisualizationObject.IMeasureDefinition { const { identifier, aggregation, computeRatio } = measure; const definition = { item: toUriQualifier(objectUri(getProjectId(project), identifier)), aggregation, computeRatio, }; const definitionFilters = measure.filters ? { filters: measure.filters.map(partial(createVisualizationFilter, project)), } : {}; return { measureDefinition: { ...definition, ...definitionFilters, }, }; } function createDerivedMeasureDefinition( project: IProject, measure: ISchemaDerivedMeasure, ): VisualizationObject.IMeasureDefinitionType { if (measure.popAttribute !== undefined) { return createPoPMeasureDefinition(project, measure); } else if (measure.previousPeriod !== undefined) { return createPreviousPeriodMeasureDefinition(project, measure); } else { throw Error(`The derived measure type is not supported! ${JSON.stringify(measure)}`); } } function createPoPMeasureDefinition( project: IProject, measure: ISchemaDerivedMeasure, ): VisualizationObject.IPoPMeasureDefinition { const { measureIdentifier, popAttribute } = measure; return { popMeasureDefinition: { measureIdentifier, popAttribute: toUriQualifier(objectUri(getProjectId(project), popAttribute)), }, }; } function createPreviousPeriodMeasureDefinition( project: IProject, measure: ISchemaDerivedMeasure, ): VisualizationObject.IPreviousPeriodMeasureDefinition { const { measureIdentifier, previousPeriod } = measure; return { previousPeriodMeasure: { measureIdentifier, dateDataSets: previousPeriod.dateDataSets.map((dateDataSet: IPreviousPeriodDateDataSet) => ({ dataSet: toUriQualifier(objectUri(getProjectId(project), dateDataSet.dataSet)), periodsAgo: dateDataSet.periodsAgo, })), }, }; } function createArithmeticMeasureDefinition( measure: ISchemaArithmeticMeasure, ): VisualizationObject.IArithmeticMeasureDefinition { const { operator, measureIdentifiers } = measure; return { arithmeticMeasure: { measureIdentifiers, operator, }, }; } function createVisualizationDateFilter( project: IProject, filter: ISchemaFilter, ): VisualizationObject.VisualizationObjectDateFilter { return filter.relative ? { relativeDateFilter: { from: Number(filter.from), to: Number(filter.to), granularity: filter.granularity, dataSet: toUriQualifier(objectUri(getProjectId(project), filter.dataset)), }, } : { absoluteDateFilter: { from: String(filter.from), to: String(filter.to), dataSet: toUriQualifier(objectUri(getProjectId(project), filter.dataset)), }, }; } function createVisualizationAttributeFilter( project: IProject, filter: ISchemaFilter, ): VisualizationObject.VisualizationObjectAttributeFilter { const projectId: string = getProjectId(project); return filter.negative ? { negativeAttributeFilter: { displayForm: toUriQualifier(objectUri(projectId, `${filter.identifier}.df`)), notIn: filter.elements.map(id => objectUri(projectId, `${filter.identifier}/elements?id=${id}`), ), }, } : { positiveAttributeFilter: { displayForm: toUriQualifier(objectUri(projectId, `${filter.identifier}.df`)), in: filter.elements.map(id => objectUri(projectId, `${filter.identifier}/elements?id=${id}`), ), }, }; } function asNumber(value: string | number): number { return toNumber(value); } function createTreatAsNumberProp(treatNullValuesAs: number) { return treatNullValuesAs === undefined ? {} : { treatNullValuesAs }; } function createMeasureValueComparisonFilter(filter: ISchemaFilter): VisualizationObject.IMeasureValueFilter { return { measureValueFilter: { measure: { localIdentifier: filter.identifier, }, condition: { comparison: { operator: filter.operator as VisualizationObject.ComparisonConditionOperator, value: asNumber(filter.value), ...createTreatAsNumberProp(filter.treatNullValuesAs), }, }, }, }; } function createMeasureValueRangeFilter(filter: ISchemaFilter): VisualizationObject.IMeasureValueFilter { return { measureValueFilter: { measure: { localIdentifier: filter.identifier, }, condition: { range: { operator: filter.operator as VisualizationObject.RangeConditionOperator, from: asNumber(filter.from), to: asNumber(filter.to), ...createTreatAsNumberProp(filter.treatNullValuesAs), }, }, }, }; } function createRankingFilter(filter: ISchemaFilter): VisualizationObject.IRankingFilter { const attributesProp = filter.attributes ? { attributes: filter.attributes.map(localIdentifier => ({ localIdentifier })) } : {}; return { rankingFilter: { measures: [{ localIdentifier: filter.identifier }], ...attributesProp, operator: filter.operator as VisualizationObject.RankingFilterOperator, value: asNumber(filter.value), }, }; } function createVisualizationFilter( project: IProject, filter: ISchemaFilter, ): VisualizationObject.VisualizationObjectFilter { switch (filter.type) { case DATE_FILTER: return createVisualizationDateFilter(project, filter); case ATTRIBUTE_FILTER: default: return createVisualizationAttributeFilter(project, filter); } } function createVisualizationExtendedFilter( project: IProject, filter: ISchemaFilter, ): VisualizationObject.VisualizationObjectExtendedFilter { switch (filter.type) { case MEASURE_VALUE_COMPARISON_FILTER: return createMeasureValueComparisonFilter(filter); case MEASURE_VALUE_RANGE_FILTER: return createMeasureValueRangeFilter(filter); case RANKING_FILTER: return createRankingFilter(filter); default: return createVisualizationFilter(project, filter); } } function createVisualizationObject( project: IProject, users: IUser[], visualizationClasses: VisualizationClass.IVisualizationClass[], visualizationObject: ISchemaVisualizationObject, ): VisualizationObject.IVisualizationObject { const author = profileUri(visualizationObject.author || first(users).identifier); const identifier = visualizationObject.identifier || webalize(visualizationObject.title); const visualizationObjectType = visualizationObject.type; // find visualization class which has the same url to be able to link it const visualizationClass = visualizationClasses.find( visualizationClass => visualizationClass.content.url === visualizationObjectType, ); invariant(visualizationClass, `VisualizationClass with url ${visualizationObjectType} not found.`); const properties = get(visualizationObject, "properties", ""); const propertiesProp = properties.length > 0 ? { properties } : {}; const filtersProp = visualizationObject.filters ? { filters: visualizationObject.filters.map(partial(createVisualizationExtendedFilter, project)), } : {}; return { content: { visualizationClass: toUriQualifier(get(visualizationClass, "meta.uri")), buckets: [...ensureItems(visualizationObject.buckets).map(partial(createBucket, project))], ...propertiesProp, ...filtersProp, }, meta: { author, category: "visualizationObject", identifier, title: visualizationObject.title, uri: objectUri(getProjectId(project), identifier), }, }; } export function createVisualizationObjects(schema: ISchema): VisualizationObject.IVisualizationObject[] { const project = createProject(schema); const users = createUsers(schema); const visualizationClasses = createVisualizationClasses(schema); return ensureItems(schema.visualizationObjects).map( partial(createVisualizationObject, project, users, visualizationClasses), ); }