Files
twenty/packages/twenty-server/src/engine/api/common/common-query-runners/common-group-by-query-runner.service.ts
T

276 lines
9.3 KiB
TypeScript

import { Injectable } from '@nestjs/common';
import {
CompositeFieldSubFieldName,
PartialFieldMetadataItemOption,
RecordFilterGroupLogicalOperator,
} from 'twenty-shared/types';
import {
assertIsDefinedOrThrow,
combineFilters,
computeRecordGqlOperationFilter,
convertViewFilterValueToString,
getFilterTypeFromFieldType,
isDefined,
turnAnyFieldFilterIntoRecordGqlFilter,
} from 'twenty-shared/utils';
import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { CommonBaseQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-base-query-runner.service';
import { CommonBaseQueryRunnerContext } from 'src/engine/api/common/types/common-base-query-runner-context.type';
import { CommonExtendedQueryRunnerContext } from 'src/engine/api/common/types/common-extended-query-runner-context.type';
import { CommonGroupByOutputItem } from 'src/engine/api/common/types/common-group-by-output-item.type';
import {
CommonExtendedInput,
CommonInput,
CommonQueryNames,
GroupByQueryArgs,
} from 'src/engine/api/common/types/common-query-args.type';
import { formatResultWithGroupByDimensionValues } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/format-result-with-group-by-dimension-values.util';
import { getGroupByExpression } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/get-group-by-expression.util';
import { isGroupByDateField } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/is-group-by-date-field.util';
import { parseGroupByArgs } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/parse-group-by-args.util';
import { removeQuotes } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/remove-quote.util';
import { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { ViewFilterGroupService } from 'src/engine/metadata-modules/view-filter-group/services/view-filter-group.service';
import { ViewFilterService } from 'src/engine/metadata-modules/view-filter/services/view-filter.service';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
import { ViewService } from 'src/engine/metadata-modules/view/services/view.service';
import { formatColumnNamesFromCompositeFieldAndSubfields } from 'src/engine/twenty-orm/utils/format-column-names-from-composite-field-and-subfield.util';
@Injectable()
export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerService<
GroupByQueryArgs,
CommonGroupByOutputItem[]
> {
constructor(
private readonly viewFilterService: ViewFilterService,
private readonly viewFilterGroupService: ViewFilterGroupService,
private readonly viewService: ViewService,
) {
super();
}
protected readonly operationName = CommonQueryNames.GROUP_BY;
async run(
args: CommonExtendedInput<GroupByQueryArgs>,
queryRunnerContext: CommonExtendedQueryRunnerContext,
): Promise<CommonGroupByOutputItem[]> {
const {
repository,
commonQueryParser,
objectMetadataItemWithFieldMaps,
authContext,
} = queryRunnerContext;
const objectMetadataNameSingular =
objectMetadataItemWithFieldMaps.nameSingular;
let queryBuilder = repository.createQueryBuilder(
objectMetadataNameSingular,
);
let appliedFilters = args.filter ?? ({} as ObjectRecordFilter);
if (isDefined(args.viewId)) {
appliedFilters = await this.addFiltersFromView({
args,
authContext,
objectMetadataItemWithFieldMaps,
appliedFilters,
});
}
commonQueryParser.applyFilterToBuilder(
queryBuilder,
objectMetadataNameSingular,
appliedFilters,
);
commonQueryParser.applyDeletedAtToBuilder(queryBuilder, appliedFilters);
ProcessAggregateHelper.addSelectedAggregatedFieldsQueriesToQueryBuilder({
selectedAggregatedFields: args.selectedFieldsResult.aggregate,
queryBuilder,
objectMetadataNameSingular,
});
const groupByFields = parseGroupByArgs(
args,
objectMetadataItemWithFieldMaps,
);
const groupByDefinitions = groupByFields.map((groupByField) => {
const columnNameWithQuotes = `"${
formatColumnNamesFromCompositeFieldAndSubfields(
groupByField.fieldMetadata.name,
groupByField.subFieldName ? [groupByField.subFieldName] : undefined,
)[0]
}"`;
const alias =
removeQuotes(columnNameWithQuotes) +
(isGroupByDateField(groupByField)
? `_${groupByField.dateGranularity}`
: '');
return {
columnNameWithQuotes,
expression: getGroupByExpression({
groupByField,
columnNameWithQuotes,
}),
alias,
dateGranularity: isGroupByDateField(groupByField)
? groupByField.dateGranularity
: undefined,
};
});
groupByDefinitions.forEach((groupByColumn, index) => {
queryBuilder.addSelect(groupByColumn.expression, groupByColumn.alias);
if (index === 0) {
queryBuilder.groupBy(groupByColumn.expression);
} else {
queryBuilder.addGroupBy(groupByColumn.expression);
}
});
commonQueryParser.applyGroupByOrderToBuilder(
queryBuilder,
args.orderBy ?? [],
groupByFields,
);
const result = await queryBuilder.getRawMany();
return formatResultWithGroupByDimensionValues(
result,
groupByDefinitions,
Object.keys(args.selectedFieldsResult.aggregate),
);
}
async processQueryResult(
queryResult: CommonGroupByOutputItem[],
_objectMetadataItemId: string,
_objectMetadataMaps: ObjectMetadataMaps,
_authContext: WorkspaceAuthContext,
): Promise<CommonGroupByOutputItem[]> {
return queryResult;
}
private async addFiltersFromView({
args,
objectMetadataItemWithFieldMaps,
appliedFilters,
authContext,
}: {
args: GroupByQueryArgs;
authContext: WorkspaceAuthContext;
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
appliedFilters: ObjectRecordFilter;
}): Promise<ObjectRecordFilter> {
assertIsDefinedOrThrow(args.viewId);
const workspaceId = authContext.workspace.id;
const viewFilters = await this.viewFilterService.findByViewId(
workspaceId,
args.viewId,
);
const viewFilterGroups = await this.viewFilterGroupService.findByViewId(
workspaceId,
args.viewId,
);
const recordFilters = viewFilters.map((viewFilter) => {
const fieldMetadataItem =
objectMetadataItemWithFieldMaps.fieldsById[viewFilter.fieldMetadataId];
return {
id: viewFilter.id,
fieldMetadataId: viewFilter.fieldMetadataId,
value: convertViewFilterValueToString(viewFilter.value),
type: getFilterTypeFromFieldType(fieldMetadataItem.type),
operand: viewFilter.operand,
recordFilterGroupId: viewFilter.viewFilterGroupId,
positionInRecordFilterGroup: viewFilter.positionInViewFilterGroup,
subFieldName: viewFilter.subFieldName as CompositeFieldSubFieldName,
};
});
const recordFilterGroups = viewFilterGroups.map((viewFilterGroup) => {
return {
id: viewFilterGroup.id,
logicalOperator:
viewFilterGroup.logicalOperator as unknown as RecordFilterGroupLogicalOperator, // TODO - https://github.com/twentyhq/twenty/issues/14746
parentRecordFilterGroupId: viewFilterGroup.parentViewFilterGroupId,
};
});
const fields = Object.values(
objectMetadataItemWithFieldMaps.fieldsById,
).map((field) => ({
id: field.id,
name: field.name,
type: field.type,
label: field.label,
options: field.options as PartialFieldMetadataItemOption[],
}));
const filtersFromView = computeRecordGqlOperationFilter({
recordFilters,
recordFilterGroups: recordFilterGroups,
fields,
filterValueDependencies: {},
});
let view: ViewEntity | null = viewFilters[0]?.view;
if (!view) {
view = await this.viewService.findById(args.viewId, workspaceId);
}
const { recordGqlOperationFilter: anyFieldFilter } =
turnAnyFieldFilterIntoRecordGqlFilter({
fields,
filterValue: view?.anyFieldFilterValue ?? '',
});
appliedFilters = combineFilters([
appliedFilters,
filtersFromView,
anyFieldFilter,
]);
return appliedFilters;
}
async validate(
_args: CommonInput<GroupByQueryArgs>,
_queryRunnerContext: CommonBaseQueryRunnerContext,
): Promise<void> {}
async computeArgs(
args: CommonInput<GroupByQueryArgs>,
queryRunnerContext: CommonBaseQueryRunnerContext,
): Promise<CommonInput<GroupByQueryArgs>> {
const { objectMetadataItemWithFieldMaps } = queryRunnerContext;
return {
...args,
filter: this.queryRunnerArgsFactory.overrideFilterByFieldMetadata(
args.filter,
objectMetadataItemWithFieldMaps,
),
};
}
}