// (C) 2007-2019 GoodData Corporation import { ExecuteAFM as AFM, Execution } from "@gooddata/typings"; import { addDays, addMonths, addQuarters, addWeeks, addYears, startOfDay, startOfMonth, startOfQuarter, startOfWeek, startOfYear, } from "date-fns"; import { cloneDeep } from "lodash"; import { IAttribute } from "../../../model/Attribute"; import { IAttributeDisplayForm } from "../../../model/AttributeDisplayForm"; import { IDateAttribute } from "../../../model/DateDataSet"; import { IElement } from "../../../model/Element"; import { getAttributeByUri, getDateAttributes, getDateDataSets, IMockProject, isDateDisplayForm, } from "../../../model/MockProject"; import { elementUri } from "../../../route/routes"; import { ElementMap, generateDateAttributeElementValues } from "../../../schema/builders/ElementsBuilder"; import { DateRange } from "../../../utils/model/DateRange"; import { getDisplayFormByQualifier } from "./afm"; export interface IDataResultRandomGeneratorParams { column: number; row: number; localIdentifier: AFM.Identifier; } type AttributeResolver = (displayFormQualifier: AFM.ObjQualifier) => IAttribute; type AttributeElementsPredicate = (element: IElement) => boolean; type AttributeElementsReducer = (allElements: ElementMap) => ElementMap; interface IAttributeElementsReducerDef { displayForm: AFM.ObjQualifier; inverted: boolean; attributeElements: AFM.AttributeElements; } function getAttributeElementsReducerDef( filter: AFM.CompatibilityFilter, ): IAttributeElementsReducerDef | undefined { if (AFM.isPositiveAttributeFilter(filter)) { return { displayForm: filter.positiveAttributeFilter.displayForm, inverted: false, attributeElements: filter.positiveAttributeFilter.in, }; } else if (AFM.isNegativeAttributeFilter(filter)) { return { displayForm: filter.negativeAttributeFilter.displayForm, inverted: true, attributeElements: filter.negativeAttributeFilter.notIn, }; } return; } function AttributeElementsPredicate(attributeElements: AFM.AttributeElements): AttributeElementsPredicate { if (AFM.isAttributeElementsArray(attributeElements)) { return element => { return attributeElements.indexOf(element.element.uri) > -1; }; } else if (AFM.isAttributeElementsByRef(attributeElements)) { return element => { return attributeElements.uris.indexOf(element.element.uri) > -1; }; } else if (AFM.isAttributeElementsByValue(attributeElements)) { return element => { return attributeElements.values.indexOf(element.element.title) > -1; }; } return _ => false; } /** * Given AFM filter, creates reducer for ElementMap which will filter out elements of the filtered attribute. * If given AFM filter is not an attribute element filter, then the resulting reducer will be noop. * * @param filter AFM filter, any type of filter * @param attributeResolver function which resolved attribute display form to attribute * @constructor */ function AttributeElementsReducer( filter: AFM.CompatibilityFilter, attributeResolver: AttributeResolver, ): AttributeElementsReducer { const def = getAttributeElementsReducerDef(filter); if (def === undefined) { // doesn't look like attr element filter -> no element filtering return elements => elements; } const attr = attributeResolver(def.displayForm); const predicate = AttributeElementsPredicate(def.attributeElements); return elementsMap => { const elements = elementsMap.get(attr.meta.identifier).filter(element => { return !def.inverted ? predicate(element) : !predicate(element); }); return elementsMap.set(attr.meta.identifier, elements); }; } /** * Applies attribute filters. * * Note: exposed for testability * * @param project mock project with element infos * @param filters filters to apply * */ export function getFilteredAttributeElements( project: IMockProject, filters: AFM.CompatibilityFilter[], ): ElementMap { const attributeResolver = (displayFormQualifier: AFM.ObjQualifier): IAttribute => { const displayForm = getDisplayFormByQualifier(project, displayFormQualifier); return getAttributeByUri(project, displayForm.content.formOf); }; return filters .map(filter => AttributeElementsReducer(filter, attributeResolver)) .reduce( (filteredElements, reducer) => reducer(filteredElements), cloneDeep(project.elements), ); } function addToDate(date: Date, granularity: string, value: number) { switch (granularity) { case "GDC.time.date": return addDays(date, value); case "GDC.time.week_us": return addWeeks(date, value); case "GDC.time.month": return addMonths(date, value); case "GDC.time.quarter": return addQuarters(date, value); case "GDC.time.year": return addYears(date, value); } } function getStartOfGranularity(date: Date, granularity: string) { switch (granularity) { case "GDC.time.date": return startOfDay(date); case "GDC.time.week_us": return startOfWeek(date); case "GDC.time.month": return startOfMonth(date); case "GDC.time.quarter": return startOfQuarter(date); case "GDC.time.year": return startOfYear(date); } } function isCurrentUnit(from: number, to: number): boolean { return from === 0 && to === 0; } function dateRangeFromRelativeFilter(filter: AFM.IRelativeDateFilter, currentDate: Date): DateRange { const { from, to, granularity } = filter.relativeDateFilter; if (isCurrentUnit(from, to)) { const startDate = getStartOfGranularity(currentDate, granularity); const finalDate = currentDate; return DateRange.fromDates(startDate, finalDate); } const startDate = addToDate(currentDate, granularity, from); const finalDate = addToDate(currentDate, granularity, to); return DateRange.fromDates(startDate, finalDate); } function getAttributeHeaderItemsByDateDisplayForm( project: IMockProject, filters: AFM.CompatibilityFilter[], displayForm: IAttributeDisplayForm, date: Date, ): Execution.IResultAttributeHeaderItem[] { // Get date attribute by display form const dateAttribute = getDateAttributes(project).find( dateAttribute => dateAttribute.displayForms[0].meta.identifier === displayForm.meta.identifier, ); return getAttributeHeaderItemsByDateAttribute(project, filters, dateAttribute, date); } function getAttributeHeaderItemsByDateAttribute( project: IMockProject, filters: AFM.CompatibilityFilter[], dateAttribute: IDateAttribute, date: Date, ): Execution.IResultAttributeHeaderItem[] { // Find date dataset by attribute id const dateDataSet = getDateDataSets(project).find(dateDataSet => { return dateDataSet.availableDateAttributes.some(attribute => { return attribute.meta.identifier === dateAttribute.meta.identifier; }); }); const dateDataSetDateRange = project.dateRanges.get(dateDataSet.meta.identifier); const filteredDateRange = filters.reduce((tmpRange, filter) => { if (AFM.isAbsoluteDateFilter(filter)) { const { from, to } = filter.absoluteDateFilter; return tmpRange.intersect(new DateRange(from, to)); } if (AFM.isRelativeDateFilter(filter)) { const dateRange = dateRangeFromRelativeFilter(filter, date); return tmpRange.intersect(dateRange); } return tmpRange; }, dateDataSetDateRange); const dateAttributeElementValues = generateDateAttributeElementValues( dateAttribute.type, filteredDateRange, ); const dateAttributeElements = dateAttributeElementValues.map( (value, i): IElement => ({ element: { title: value, uri: elementUri(project.project.meta.identifier, dateAttribute.meta.identifier, `${i + 1}`), }, }), ); return dateAttributeElements.map(elementToAttributeHeaderItem); } function getAttributeHeaderItemsByAttributeDisplayForm( project: IMockProject, filters: AFM.CompatibilityFilter[], displayForm: IAttributeDisplayForm, ): Execution.IResultAttributeHeaderItem[] { const attributesMap = getFilteredAttributeElements(project, filters); const attribute = getAttributeByUri(project, displayForm.content.formOf); const attributeElements = attributesMap.get(attribute.meta.identifier); return attributeElements.map(elementToAttributeHeaderItem); } export function getAttributeHeaderItemsByDisplayForm( project: IMockProject, filters: AFM.CompatibilityFilter[], displayForm: IAttributeDisplayForm, date: Date, ): Execution.IResultHeaderItem[] { if (isDateDisplayForm(project, displayForm)) { return getAttributeHeaderItemsByDateDisplayForm(project, filters, displayForm, date); } return getAttributeHeaderItemsByAttributeDisplayForm(project, filters, displayForm); } function elementToAttributeHeaderItem({ element }: IElement): Execution.IResultAttributeHeaderItem { return { attributeHeaderItem: { uri: element.uri, name: element.title, }, }; }