[DASHBOARDS] Move all the graph computing logic to the backend (#17189)
- Create resolvers for each type of charts which needs data transformation after the group by operation: Bar Chart, Line Chart and Pie Chart - Move all the utils to the backend and refactored some into services This allows all the computation to be done in the backend, improving performances in the frontend.
This commit is contained in:
+240
@@ -0,0 +1,240 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { CalendarStartDay } from 'twenty-shared/constants';
|
||||
import { FirstDayOfTheWeek } from 'twenty-shared/types';
|
||||
import {
|
||||
convertCalendarStartDayNonIsoNumberToFirstDayOfTheWeek,
|
||||
isDefined,
|
||||
} from 'twenty-shared/utils';
|
||||
|
||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { PieChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/pie-chart-configuration.dto';
|
||||
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
|
||||
import { EXTRA_ITEM_TO_DETECT_TOO_MANY_GROUPS } from 'src/modules/dashboard/chart-data/constants/extra-item-to-detect-too-many-groups.constant';
|
||||
import { PIE_CHART_MAXIMUM_NUMBER_OF_SLICES } from 'src/modules/dashboard/chart-data/constants/pie-chart-maximum-number-of-slices.constant';
|
||||
import { PieChartDataOutputDTO } from 'src/modules/dashboard/chart-data/dtos/outputs/pie-chart-data-output.dto';
|
||||
import {
|
||||
ChartDataException,
|
||||
ChartDataExceptionCode,
|
||||
generateChartDataExceptionMessage,
|
||||
} from 'src/modules/dashboard/chart-data/exceptions/chart-data.exception';
|
||||
import { ChartDataQueryService } from 'src/modules/dashboard/chart-data/services/chart-data-query.service';
|
||||
import { getFieldMetadata } from 'src/modules/dashboard/chart-data/utils/get-field-metadata.util';
|
||||
import { getSelectOptions } from 'src/modules/dashboard/chart-data/utils/get-select-options.util';
|
||||
import { processOneDimensionalResults } from 'src/modules/dashboard/chart-data/utils/process-one-dimensional-results.util';
|
||||
import { sortChartDataIfNeeded } from 'src/modules/dashboard/chart-data/utils/sort-chart-data-if-needed.util';
|
||||
|
||||
type GetPieChartDataParams = {
|
||||
workspaceId: string;
|
||||
objectMetadataId: string;
|
||||
configuration: PieChartConfigurationDTO;
|
||||
authContext: AuthContext;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class PieChartDataService {
|
||||
constructor(
|
||||
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
private readonly chartDataQueryService: ChartDataQueryService,
|
||||
) {}
|
||||
|
||||
async getPieChartData({
|
||||
workspaceId,
|
||||
objectMetadataId,
|
||||
configuration,
|
||||
authContext,
|
||||
}: GetPieChartDataParams): Promise<PieChartDataOutputDTO> {
|
||||
try {
|
||||
if (
|
||||
configuration.configurationType !== WidgetConfigurationType.PIE_CHART
|
||||
) {
|
||||
throw new ChartDataException(
|
||||
generateChartDataExceptionMessage(
|
||||
ChartDataExceptionCode.INVALID_WIDGET_CONFIGURATION,
|
||||
`Expected PIE_CHART, got ${configuration.configurationType}`,
|
||||
),
|
||||
ChartDataExceptionCode.INVALID_WIDGET_CONFIGURATION,
|
||||
);
|
||||
}
|
||||
|
||||
const { flatObjectMetadataMaps, flatFieldMetadataMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatObjectMetadataMaps', 'flatFieldMetadataMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
if (!isDefined(objectMetadataId)) {
|
||||
throw new ChartDataException(
|
||||
generateChartDataExceptionMessage(
|
||||
ChartDataExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
||||
'Widget has no objectMetadataId',
|
||||
),
|
||||
ChartDataExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const flatObjectMetadata = flatObjectMetadataMaps.byId[objectMetadataId];
|
||||
|
||||
if (!isDefined(flatObjectMetadata)) {
|
||||
throw new ChartDataException(
|
||||
generateChartDataExceptionMessage(
|
||||
ChartDataExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
||||
objectMetadataId,
|
||||
),
|
||||
ChartDataExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const groupByField = getFieldMetadata(
|
||||
configuration.groupByFieldMetadataId,
|
||||
flatFieldMetadataMaps.byId,
|
||||
);
|
||||
|
||||
const limit =
|
||||
PIE_CHART_MAXIMUM_NUMBER_OF_SLICES +
|
||||
EXTRA_ITEM_TO_DETECT_TOO_MANY_GROUPS;
|
||||
|
||||
const objectIdByNameSingular: Record<string, string> = {};
|
||||
|
||||
for (const objectId in flatObjectMetadataMaps.byId) {
|
||||
const objMetadata = flatObjectMetadataMaps.byId[objectId];
|
||||
|
||||
if (isDefined(objMetadata)) {
|
||||
objectIdByNameSingular[objMetadata.nameSingular] = objectId;
|
||||
}
|
||||
}
|
||||
|
||||
const rawResults = await this.chartDataQueryService.executeGroupByQuery({
|
||||
flatObjectMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
objectIdByNameSingular,
|
||||
authContext,
|
||||
groupByFieldMetadataId: configuration.groupByFieldMetadataId,
|
||||
groupBySubFieldName: configuration.groupBySubFieldName,
|
||||
aggregateFieldMetadataId: configuration.aggregateFieldMetadataId,
|
||||
aggregateOperation: configuration.aggregateOperation,
|
||||
filter: configuration.filter,
|
||||
dateGranularity: configuration.dateGranularity,
|
||||
userTimezone: configuration.timezone ?? 'UTC',
|
||||
firstDayOfTheWeek:
|
||||
(configuration.firstDayOfTheWeek as CalendarStartDay | undefined) ??
|
||||
CalendarStartDay.MONDAY,
|
||||
limit,
|
||||
primaryAxisOrderBy: configuration.orderBy,
|
||||
});
|
||||
|
||||
return this.transformToPieChartData({
|
||||
rawResults,
|
||||
groupByField,
|
||||
configuration,
|
||||
userTimezone: configuration.timezone ?? 'UTC',
|
||||
firstDayOfTheWeek:
|
||||
(configuration.firstDayOfTheWeek as CalendarStartDay | undefined) ??
|
||||
CalendarStartDay.MONDAY,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof ChartDataException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new ChartDataException(
|
||||
generateChartDataExceptionMessage(
|
||||
ChartDataExceptionCode.QUERY_EXECUTION_FAILED,
|
||||
`Pie chart data retrieval failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
),
|
||||
ChartDataExceptionCode.QUERY_EXECUTION_FAILED,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private transformToPieChartData({
|
||||
rawResults,
|
||||
groupByField,
|
||||
configuration,
|
||||
userTimezone,
|
||||
firstDayOfTheWeek,
|
||||
}: {
|
||||
rawResults: Array<{
|
||||
groupByDimensionValues: unknown[];
|
||||
aggregateValue: number;
|
||||
}>;
|
||||
groupByField: FlatFieldMetadata;
|
||||
configuration: PieChartConfigurationDTO;
|
||||
userTimezone: string;
|
||||
firstDayOfTheWeek: CalendarStartDay;
|
||||
}): PieChartDataOutputDTO {
|
||||
const filteredResults = configuration.hideEmptyCategory
|
||||
? rawResults.filter(
|
||||
(result) =>
|
||||
isDefined(result.groupByDimensionValues?.[0]) &&
|
||||
result.aggregateValue !== 0,
|
||||
)
|
||||
: rawResults;
|
||||
|
||||
const selectOptions = getSelectOptions(groupByField);
|
||||
|
||||
const convertedFirstDayOfTheWeek =
|
||||
convertCalendarStartDayNonIsoNumberToFirstDayOfTheWeek(
|
||||
firstDayOfTheWeek,
|
||||
FirstDayOfTheWeek.SUNDAY,
|
||||
);
|
||||
|
||||
const limitedResults = filteredResults.slice(
|
||||
0,
|
||||
PIE_CHART_MAXIMUM_NUMBER_OF_SLICES,
|
||||
);
|
||||
|
||||
const {
|
||||
processedDataPoints: rawProcessedDataPoints,
|
||||
formattedToRawLookup,
|
||||
} = processOneDimensionalResults({
|
||||
rawResults: limitedResults,
|
||||
primaryAxisGroupByField: groupByField,
|
||||
dateGranularity: configuration.dateGranularity,
|
||||
subFieldName: configuration.groupBySubFieldName,
|
||||
userTimezone,
|
||||
firstDayOfTheWeek: convertedFirstDayOfTheWeek,
|
||||
});
|
||||
|
||||
const processedDataPoints = rawProcessedDataPoints.map((point) => {
|
||||
const rawValueString = isDefined(point.rawValue)
|
||||
? String(point.rawValue)
|
||||
: null;
|
||||
|
||||
return {
|
||||
id: point.formattedValue,
|
||||
value: point.aggregateValue,
|
||||
rawValue: rawValueString,
|
||||
};
|
||||
});
|
||||
|
||||
const sortedData = sortChartDataIfNeeded({
|
||||
data: processedDataPoints,
|
||||
orderBy: configuration.orderBy,
|
||||
manualSortOrder: configuration.manualSortOrder,
|
||||
formattedToRawLookup,
|
||||
getFieldValue: (item) => item.id,
|
||||
getNumericValue: (item) => item.value,
|
||||
selectFieldOptions: selectOptions,
|
||||
fieldType: groupByField.type,
|
||||
dateGranularity: configuration.dateGranularity,
|
||||
});
|
||||
|
||||
const data = sortedData.map(({ rawValue: _rawValue, ...item }) => item);
|
||||
|
||||
return {
|
||||
data,
|
||||
showLegend: configuration.displayLegend ?? true,
|
||||
showDataLabels: configuration.displayDataLabel ?? false,
|
||||
showCenterMetric: configuration.showCenterMetric ?? true,
|
||||
hasTooManyGroups:
|
||||
filteredResults.length > PIE_CHART_MAXIMUM_NUMBER_OF_SLICES,
|
||||
formattedToRawLookup: Object.fromEntries(formattedToRawLookup),
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user