twenty / packages /twenty-server /src /modules /dashboard /chart-data /services /chart-data-query.service.ts
Jules
Initial clean CRM deployment with prebuilt assets
d9494a5
Raw
History Blame Contribute Delete
9.79 kB
import { Injectable } from '@nestjs/common';
import { CalendarStartDay } from 'twenty-shared/constants';
import {
AggregateOperations,
type ChartFilter,
ObjectRecordGroupByDateGranularity,
OrderByWithGroupBy,
} from 'twenty-shared/types';
import {
isDefined,
isFieldMetadataArrayKind,
isFieldMetadataDateKind,
} from 'twenty-shared/utils';
import { ObjectRecordGroupBy } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { CommonGroupByQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-group-by-query-runner.service';
import { isUserAuthContext } from 'src/engine/core-modules/auth/guards/is-user-auth-context.guard';
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { GraphOrderBy } from 'src/engine/metadata-modules/page-layout-widget/enums/graph-order-by.enum';
import { GRAPH_DEFAULT_DATE_GRANULARITY } from 'src/modules/dashboard/chart-data/constants/graph-default-date-granularity.constant';
import { GRAPH_DEFAULT_ORDER_BY } from 'src/modules/dashboard/chart-data/constants/graph-default-order-by.constant';
import {
ChartDataException,
ChartDataExceptionCode,
generateChartDataExceptionMessage,
} from 'src/modules/dashboard/chart-data/exceptions/chart-data.exception';
import { GroupByRawResult } from 'src/modules/dashboard/chart-data/types/group-by-raw-result.type';
import { buildAggregateFieldKey } from 'src/modules/dashboard/chart-data/utils/build-aggregate-field-key.util';
import {
buildGroupByFieldObject,
type GroupByFieldObject,
} from 'src/modules/dashboard/chart-data/utils/build-group-by-field-object.util';
import { convertChartFilterToGqlOperationFilter } from 'src/modules/dashboard/chart-data/utils/convert-chart-filter-to-gql-operation-filter.util';
import { getFieldMetadata } from 'src/modules/dashboard/chart-data/utils/get-field-metadata.util';
import { getGroupByOrderBy } from 'src/modules/dashboard/chart-data/utils/get-group-by-order-by.util';
import { isRelationNestedFieldDateKind } from 'src/modules/dashboard/chart-data/utils/is-relation-nested-field-date-kind.util';
import { transformAggregateValue } from 'src/modules/dashboard/chart-data/utils/transform-aggregate-value.util';
type ExecuteGroupByQueryParams = {
flatObjectMetadata: FlatObjectMetadata;
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
objectIdByNameSingular: Record<string, string>;
authContext: WorkspaceAuthContext;
groupByFieldMetadataId: string;
groupBySubFieldName?: string | null;
aggregateFieldMetadataId: string;
aggregateOperation: AggregateOperations;
filter?: ChartFilter;
dateGranularity?: ObjectRecordGroupByDateGranularity;
userTimezone: string;
firstDayOfTheWeek: CalendarStartDay;
limit: number;
secondaryGroupByFieldMetadataId?: string;
secondaryGroupBySubFieldName?: string | null;
secondaryDateGranularity?: ObjectRecordGroupByDateGranularity;
primaryAxisOrderBy?: GraphOrderBy;
secondaryAxisOrderBy?: GraphOrderBy;
splitMultiValueFields?: boolean;
};
@Injectable()
export class ChartDataQueryService {
constructor(
private readonly commonGroupByQueryRunnerService: CommonGroupByQueryRunnerService,
) {}
async executeGroupByQuery({
flatObjectMetadata,
flatFieldMetadataMaps,
flatObjectMetadataMaps,
objectIdByNameSingular,
authContext,
groupByFieldMetadataId,
groupBySubFieldName,
aggregateFieldMetadataId,
aggregateOperation,
filter,
dateGranularity,
userTimezone,
firstDayOfTheWeek,
limit,
primaryAxisOrderBy,
secondaryGroupByFieldMetadataId,
secondaryGroupBySubFieldName,
secondaryDateGranularity,
secondaryAxisOrderBy,
splitMultiValueFields,
}: ExecuteGroupByQueryParams): Promise<GroupByRawResult[]> {
const currentWorkspaceMemberId = isUserAuthContext(authContext)
? authContext.workspaceMemberId
: undefined;
const gqlOperationFilter = convertChartFilterToGqlOperationFilter({
filter,
flatFieldMetadataMaps,
userTimezone,
currentWorkspaceMemberId,
});
const primaryGroupByField = getFieldMetadata(
groupByFieldMetadataId,
flatFieldMetadataMaps,
);
const aggregateField = getFieldMetadata(
aggregateFieldMetadataId,
flatFieldMetadataMaps,
);
const isPrimaryFieldDate = isFieldMetadataDateKind(
primaryGroupByField.type,
);
const isPrimaryNestedDate = isRelationNestedFieldDateKind({
relationFieldMetadata: primaryGroupByField,
relationNestedFieldName: groupBySubFieldName ?? undefined,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
});
const shouldApplyPrimaryDateGranularity =
isPrimaryFieldDate || isPrimaryNestedDate;
const shouldSplitMultiValueFields = splitMultiValueFields ?? true;
const shouldUnnestPrimary =
shouldSplitMultiValueFields &&
isFieldMetadataArrayKind(primaryGroupByField.type);
const groupBy: GroupByFieldObject[] = [];
groupBy.push(
buildGroupByFieldObject({
fieldMetadata: primaryGroupByField,
subFieldName: groupBySubFieldName,
dateGranularity: shouldApplyPrimaryDateGranularity
? (dateGranularity ?? GRAPH_DEFAULT_DATE_GRANULARITY)
: undefined,
firstDayOfTheWeek,
isNestedDateField: isPrimaryNestedDate,
timeZone: userTimezone,
shouldUnnest: shouldUnnestPrimary,
}),
);
const orderBy: OrderByWithGroupBy = [];
const primaryOrderBy = getGroupByOrderBy({
graphOrderBy: primaryAxisOrderBy ?? GRAPH_DEFAULT_ORDER_BY,
groupByFieldMetadata: primaryGroupByField,
groupBySubFieldName,
aggregateOperation,
aggregateFieldMetadata: aggregateField,
dateGranularity: shouldApplyPrimaryDateGranularity
? (dateGranularity ?? GRAPH_DEFAULT_DATE_GRANULARITY)
: undefined,
});
if (isDefined(primaryOrderBy)) {
orderBy.push(primaryOrderBy);
}
if (isDefined(secondaryGroupByFieldMetadataId)) {
const secondaryGroupByField = getFieldMetadata(
secondaryGroupByFieldMetadataId,
flatFieldMetadataMaps,
);
const isSecondaryFieldDate = isFieldMetadataDateKind(
secondaryGroupByField.type,
);
const isSecondaryNestedDate = isRelationNestedFieldDateKind({
relationFieldMetadata: secondaryGroupByField,
relationNestedFieldName: secondaryGroupBySubFieldName ?? undefined,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
});
const shouldApplySecondaryDateGranularity =
isSecondaryFieldDate || isSecondaryNestedDate;
const shouldUnnestSecondary =
shouldSplitMultiValueFields &&
isFieldMetadataArrayKind(secondaryGroupByField.type);
if (shouldUnnestPrimary && shouldUnnestSecondary) {
throw new ChartDataException(
generateChartDataExceptionMessage(
ChartDataExceptionCode.INVALID_WIDGET_CONFIGURATION,
'Split multiple values can only be enabled when one grouped field is multi-value.',
),
ChartDataExceptionCode.INVALID_WIDGET_CONFIGURATION,
);
}
groupBy.push(
buildGroupByFieldObject({
fieldMetadata: secondaryGroupByField,
subFieldName: secondaryGroupBySubFieldName,
dateGranularity: shouldApplySecondaryDateGranularity
? (secondaryDateGranularity ?? GRAPH_DEFAULT_DATE_GRANULARITY)
: undefined,
firstDayOfTheWeek,
isNestedDateField: isSecondaryNestedDate,
timeZone: userTimezone,
shouldUnnest: shouldUnnestSecondary,
}),
);
if (isDefined(secondaryAxisOrderBy)) {
const secondaryOrderByItem = getGroupByOrderBy({
graphOrderBy: secondaryAxisOrderBy,
groupByFieldMetadata: secondaryGroupByField,
groupBySubFieldName: secondaryGroupBySubFieldName,
aggregateOperation,
aggregateFieldMetadata: aggregateField,
dateGranularity: shouldApplySecondaryDateGranularity
? (secondaryDateGranularity ?? GRAPH_DEFAULT_DATE_GRANULARITY)
: undefined,
});
if (isDefined(secondaryOrderByItem)) {
orderBy.push(secondaryOrderByItem);
}
}
}
const aggregateFieldKey = buildAggregateFieldKey({
aggregateOperation,
aggregateFieldMetadata: aggregateField,
});
const selectedFields = {
[aggregateFieldKey]: true,
groupByDimensionValues: true,
};
const { results } = await this.commonGroupByQueryRunnerService.execute(
{
filter: gqlOperationFilter,
orderBy: orderBy.length > 0 ? orderBy : undefined,
groupBy: groupBy as ObjectRecordGroupBy,
selectedFields,
limit,
},
{
authContext,
flatObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
objectIdByNameSingular,
},
);
return results.map((result) => ({
groupByDimensionValues: result.groupByDimensionValues ?? [],
aggregateValue: transformAggregateValue({
rawValue: result[aggregateFieldKey],
aggregateFieldType: aggregateField.type,
aggregateOperation,
}),
}));
}
}