Common api - Group by query (#15108)

closes https://github.com/twentyhq/core-team-issues/issues/1626
This commit is contained in:
Etienne
2025-10-20 17:51:01 +02:00
committed by GitHub
parent 5186e73ce1
commit dea08a3a41
49 changed files with 1223 additions and 254 deletions
@@ -20,7 +20,9 @@ import {
CommonQueryNames,
CreateManyQueryArgs,
} from 'src/engine/api/common/types/common-query-args.type';
import { CommonSelectedFieldsResult } from 'src/engine/api/common/types/common-selected-fields-result.type';
import { isWorkspaceAuthContext } from 'src/engine/api/common/utils/is-workspace-auth-context.util';
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return';
import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select';
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
@@ -37,7 +39,7 @@ import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-perm
export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerService {
async run({
args,
authContext: toValidateAuthContext,
authContext,
objectMetadataMaps,
objectMetadataItemWithFieldMaps,
}: {
@@ -46,8 +48,6 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
objectMetadataMaps: ObjectMetadataMaps;
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
}): Promise<ObjectRecord[]> {
const authContext = toValidateAuthContext;
if (!isWorkspaceAuthContext(authContext)) {
throw new CommonQueryRunnerException(
'Invalid auth context',
@@ -63,6 +63,18 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
}
});
//TODO : Refacto-common - QueryParser should be common branded service
const commonQueryParser = new GraphqlQueryParser(
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
);
const selectedFieldsResult = commonQueryParser.parseSelectedFields(
objectMetadataItemWithFieldMaps,
args.selectedFields,
objectMetadataMaps,
);
const { workspaceDataSource, repository, rolePermissionConfig } =
await this.prepareQueryRunnerContext({
authContext,
@@ -80,24 +92,25 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
args: processedArgs,
selectedFieldsResult,
});
const upsertedRecords = await this.fetchUpsertedRecords({
args: processedArgs,
objectRecords,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
repository,
selectedFieldsResult,
});
await this.processNestedRelationsIfNeeded({
args: processedArgs,
records: upsertedRecords,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
rolePermissionConfig,
authContext,
workspaceDataSource,
selectedFieldsResult,
});
return upsertedRecords;
@@ -136,16 +149,18 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
args,
selectedFieldsResult,
}: {
repository: WorkspaceRepository<ObjectLiteral>;
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
objectMetadataMaps: ObjectMetadataMaps;
args: CreateManyQueryArgs;
selectedFieldsResult: CommonSelectedFieldsResult;
}): Promise<InsertResult> {
if (!args.upsert) {
const selectedColumns = buildColumnsToReturn({
select: args.selectedFieldsResult.select,
relations: args.selectedFieldsResult.relations,
select: selectedFieldsResult.select,
relations: selectedFieldsResult.relations,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
});
@@ -158,6 +173,7 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
args,
selectedFieldsResult,
});
}
@@ -166,11 +182,13 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
args,
selectedFieldsResult,
}: {
repository: WorkspaceRepository<ObjectLiteral>;
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
objectMetadataMaps: ObjectMetadataMaps;
args: CreateManyQueryArgs;
selectedFieldsResult: CommonSelectedFieldsResult;
}): Promise<InsertResult> {
const conflictingFields = getConflictingFields(
objectMetadataItemWithFieldMaps,
@@ -195,8 +213,8 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
};
const columnsToReturn = buildColumnsToReturn({
select: args.selectedFieldsResult.select,
relations: args.selectedFieldsResult.relations,
select: selectedFieldsResult.select,
relations: selectedFieldsResult.relations,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
});
@@ -325,25 +343,25 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
}
private async fetchUpsertedRecords({
args,
objectRecords,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
repository,
selectedFieldsResult,
}: {
args: CreateManyQueryArgs;
objectRecords: InsertResult;
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
objectMetadataMaps: ObjectMetadataMaps;
repository: WorkspaceRepository<ObjectLiteral>;
selectedFieldsResult: CommonSelectedFieldsResult;
}): Promise<ObjectRecord[]> {
const queryBuilder = repository.createQueryBuilder(
objectMetadataItemWithFieldMaps.nameSingular,
);
const columnsToSelect = buildColumnsToSelect({
select: args.selectedFieldsResult.select,
relations: args.selectedFieldsResult.relations,
select: selectedFieldsResult.select,
relations: selectedFieldsResult.relations,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
});
@@ -363,23 +381,23 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
}
private async processNestedRelationsIfNeeded({
args,
records,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
rolePermissionConfig,
authContext,
workspaceDataSource,
selectedFieldsResult,
}: {
args: CreateManyQueryArgs;
records: ObjectRecord[];
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
objectMetadataMaps: ObjectMetadataMaps;
rolePermissionConfig?: RolePermissionConfig;
authContext: AuthContext;
workspaceDataSource: WorkspaceDataSource;
rolePermissionConfig?: RolePermissionConfig;
selectedFieldsResult: CommonSelectedFieldsResult;
}): Promise<void> {
if (!args.selectedFieldsResult.relations) {
if (!selectedFieldsResult.relations) {
return;
}
@@ -388,7 +406,7 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
parentObjectRecords: records,
//TODO : Refacto-common - Typing to fix when switching processNestedRelationsHelper to Common
relations: args.selectedFieldsResult.relations as Record<
relations: selectedFieldsResult.relations as Record<
string,
FindOptionsRelations<ObjectLiteral>
>,
@@ -396,7 +414,7 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
authContext,
workspaceDataSource,
rolePermissionConfig,
selectedFields: args.selectedFieldsResult.select,
selectedFields: selectedFieldsResult.select,
});
}
@@ -21,6 +21,7 @@ import {
CommonQueryNames,
FindManyQueryArgs,
} from 'src/engine/api/common/types/common-query-args.type';
import { CommonSelectedFieldsResult } from 'src/engine/api/common/types/common-selected-fields-result.type';
import { getPageInfo } from 'src/engine/api/common/utils/get-page-info.util';
import { isWorkspaceAuthContext } from 'src/engine/api/common/utils/is-workspace-auth-context.util';
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
@@ -36,7 +37,7 @@ import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-met
export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerService {
async run({
args,
authContext: toValidateAuthContext,
authContext,
objectMetadataMaps,
objectMetadataItemWithFieldMaps,
}: {
@@ -49,9 +50,9 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi
aggregatedValues: Record<string, number>;
totalCount: number;
pageInfo: CommonPageInfo;
selectedFieldsResult: CommonSelectedFieldsResult;
}> {
this.validate(args);
const authContext = toValidateAuthContext;
if (!isWorkspaceAuthContext(authContext)) {
throw new CommonQueryRunnerException(
@@ -66,6 +67,17 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi
objectMetadataItemWithFieldMaps,
});
const commonQueryParser = new GraphqlQueryParser(
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
);
const selectedFieldsResult = commonQueryParser.parseSelectedFields(
objectMetadataItemWithFieldMaps,
args.selectedFields,
objectMetadataMaps,
);
const processedArgs = await this.processQueryArgs({
authContext,
objectMetadataItemWithFieldMaps,
@@ -80,11 +92,6 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi
let appliedFilters = processedArgs.filter ?? ({} as ObjectRecordFilter);
const commonQueryParser = new GraphqlQueryParser(
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
);
commonQueryParser.applyFilterToBuilder(
aggregateQueryBuilder,
objectMetadataItemWithFieldMaps.nameSingular,
@@ -136,7 +143,7 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi
commonQueryParser.applyDeletedAtToBuilder(queryBuilder, appliedFilters);
ProcessAggregateHelper.addSelectedAggregatedFieldsQueriesToQueryBuilder({
selectedAggregatedFields: processedArgs.selectedFieldsResult.aggregate,
selectedAggregatedFields: selectedFieldsResult.aggregate,
queryBuilder: aggregateQueryBuilder,
objectMetadataNameSingular: objectMetadataItemWithFieldMaps.nameSingular,
});
@@ -145,8 +152,8 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi
processedArgs.first ?? processedArgs.last ?? QUERY_MAX_RECORDS;
const columnsToSelect = buildColumnsToSelect({
select: processedArgs.selectedFieldsResult.select,
relations: processedArgs.selectedFieldsResult.relations,
select: selectedFieldsResult.select,
relations: selectedFieldsResult.relations,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
});
@@ -176,23 +183,23 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi
const parentObjectRecordsAggregatedValues =
await aggregateQueryBuilder.getRawOne();
if (processedArgs.selectedFieldsResult.relations) {
if (selectedFieldsResult.relations) {
await this.processNestedRelationsHelper.processNestedRelations({
objectMetadataMaps,
parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
parentObjectRecords: objectRecords,
parentObjectRecordsAggregatedValues,
//TODO : Refacto-common - Typing to fix when switching processNestedRelationsHelper to Common
relations: processedArgs.selectedFieldsResult.relations as Record<
relations: selectedFieldsResult.relations as Record<
string,
FindOptionsRelations<ObjectLiteral>
>,
aggregate: processedArgs.selectedFieldsResult.aggregate,
aggregate: selectedFieldsResult.aggregate,
limit: QUERY_MAX_RECORDS,
authContext,
workspaceDataSource,
rolePermissionConfig,
selectedFields: processedArgs.selectedFieldsResult.select,
selectedFields: selectedFieldsResult.select,
});
}
@@ -209,6 +216,7 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi
aggregatedValues: parentObjectRecordsAggregatedValues,
totalCount: parentObjectRecordsAggregatedValues?.totalCount,
pageInfo,
selectedFieldsResult,
};
}
@@ -28,7 +28,7 @@ import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-met
export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerService {
async run({
args,
authContext: toValidateAuthContext,
authContext,
objectMetadataMaps,
objectMetadataItemWithFieldMaps,
}: {
@@ -37,8 +37,6 @@ export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerServic
objectMetadataMaps: ObjectMetadataMaps;
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
}): Promise<ObjectRecord> {
const authContext = toValidateAuthContext;
if (!isWorkspaceAuthContext(authContext)) {
throw new CommonQueryRunnerException(
'Invalid auth context',
@@ -52,6 +50,18 @@ export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerServic
objectMetadataItemWithFieldMaps,
});
//TODO : Refacto-common - QueryParser should be common branded service
const commonQueryParser = new GraphqlQueryParser(
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
);
const selectedFieldsResult = commonQueryParser.parseSelectedFields(
objectMetadataItemWithFieldMaps,
args.selectedFields,
objectMetadataMaps,
);
const processedArgs = await this.processQueryArgs({
authContext,
objectMetadataItemWithFieldMaps,
@@ -72,12 +82,6 @@ export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerServic
objectMetadataItemWithFieldMaps.nameSingular,
);
//TODO : Refacto-common - QueryParser should be common branded service
const commonQueryParser = new GraphqlQueryParser(
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
);
commonQueryParser.applyFilterToBuilder(
queryBuilder,
objectMetadataItemWithFieldMaps.nameSingular,
@@ -90,8 +94,8 @@ export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerServic
);
const columnsToSelect = buildColumnsToSelect({
select: args.selectedFieldsResult.select,
relations: args.selectedFieldsResult.relations,
select: selectedFieldsResult.select,
relations: selectedFieldsResult.relations,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
});
@@ -111,13 +115,13 @@ export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerServic
const objectRecords = [objectRecord] as ObjectRecord[];
if (isDefined(args.selectedFieldsResult.relations)) {
if (isDefined(selectedFieldsResult.relations)) {
await this.processNestedRelationsHelper.processNestedRelations({
objectMetadataMaps,
parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
parentObjectRecords: objectRecords,
//TODO : Refacto-common - To fix when switching processNestedRelationsHelper to Common
relations: args.selectedFieldsResult.relations as Record<
relations: selectedFieldsResult.relations as Record<
string,
FindOptionsRelations<ObjectLiteral>
>,
@@ -125,7 +129,7 @@ export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerServic
authContext,
workspaceDataSource,
rolePermissionConfig,
selectedFields: args.selectedFieldsResult.select,
selectedFields: selectedFieldsResult.select,
});
}
@@ -0,0 +1,327 @@
import { Injectable } from '@nestjs/common';
import {
CompositeFieldSubFieldName,
PartialFieldMetadataItemOption,
RecordFilterGroupLogicalOperator,
} from 'twenty-shared/types';
import {
assertIsDefinedOrThrow,
combineFilters,
computeRecordGqlOperationFilter,
convertViewFilterValueToString,
getFilterTypeFromFieldType,
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 {
CommonQueryRunnerException,
CommonQueryRunnerExceptionCode,
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
import { CommonGroupByOutputItem } from 'src/engine/api/common/types/common-group-by-output-item.type';
import {
CommonQueryNames,
GroupByQueryArgs,
} from 'src/engine/api/common/types/common-query-args.type';
import { isWorkspaceAuthContext } from 'src/engine/api/common/utils/is-workspace-auth-context.util';
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
import { computeIsNumericReturningAggregate } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/compute-is-numeric-returning-aggregate.util';
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 { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
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 {
constructor(
private readonly viewFilterService: ViewFilterService,
private readonly viewFilterGroupService: ViewFilterGroupService,
private readonly viewService: ViewService,
) {
super();
}
async run({
args,
authContext,
objectMetadataMaps,
objectMetadataItemWithFieldMaps,
}: {
args: GroupByQueryArgs;
authContext: AuthContext;
objectMetadataMaps: ObjectMetadataMaps;
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
}): Promise<CommonGroupByOutputItem[]> {
if (!isWorkspaceAuthContext(authContext)) {
throw new CommonQueryRunnerException(
'Invalid auth context',
CommonQueryRunnerExceptionCode.INVALID_AUTH_CONTEXT,
);
}
const { repository } = await this.prepareQueryRunnerContext({
authContext,
objectMetadataItemWithFieldMaps,
});
//TODO : Refacto-common - QueryParser should be common branded service
const commonQueryParser = new GraphqlQueryParser(
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
);
const selectedFieldsResult = commonQueryParser.parseSelectedFields(
objectMetadataItemWithFieldMaps,
args.selectedFields,
objectMetadataMaps,
);
const processedArgs = await this.processQueryArgs({
authContext,
objectMetadataItemWithFieldMaps,
args,
});
const objectMetadataNameSingular =
objectMetadataItemWithFieldMaps.nameSingular;
let queryBuilder = repository.createQueryBuilder(
objectMetadataNameSingular,
);
let appliedFilters = args.filter ?? ({} as ObjectRecordFilter);
if (args.viewId) {
appliedFilters = await this.addFiltersFromView({
args: processedArgs,
authContext,
objectMetadataItemWithFieldMaps,
appliedFilters,
});
}
commonQueryParser.applyFilterToBuilder(
queryBuilder,
objectMetadataNameSingular,
appliedFilters,
);
commonQueryParser.applyDeletedAtToBuilder(queryBuilder, appliedFilters);
ProcessAggregateHelper.addSelectedAggregatedFieldsQueriesToQueryBuilder({
selectedAggregatedFields: selectedFieldsResult.aggregate,
queryBuilder,
objectMetadataNameSingular,
});
const groupByFields = parseGroupByArgs(
processedArgs,
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);
}
});
if (processedArgs.omitNullValues) {
const aggregateFields = selectedFieldsResult.aggregate ?? {};
Object.values(aggregateFields).forEach((aggregationField) => {
const aggregateExpression =
ProcessAggregateHelper.getAggregateExpression(
aggregationField,
objectMetadataNameSingular,
);
if (aggregateExpression) {
queryBuilder.andHaving(`${aggregateExpression} IS NOT NULL`);
const isNumericReturningAggregate =
computeIsNumericReturningAggregate(
aggregationField.aggregateOperation,
aggregationField.fromFieldType,
);
if (isNumericReturningAggregate) {
queryBuilder.andHaving(`${aggregateExpression} != 0`);
}
}
});
}
commonQueryParser.applyGroupByOrderToBuilder(
queryBuilder,
processedArgs.orderBy ?? [],
groupByFields,
);
const result = await queryBuilder.getRawMany();
return formatResultWithGroupByDimensionValues(
result,
groupByDefinitions,
Object.keys(selectedFieldsResult.aggregate),
);
}
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 processQueryArgs({
authContext,
objectMetadataItemWithFieldMaps,
args,
}: {
authContext: WorkspaceAuthContext;
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
args: GroupByQueryArgs;
}): Promise<GroupByQueryArgs> {
const hookedArgs =
(await this.workspaceQueryHookService.executePreQueryHooks(
authContext,
objectMetadataItemWithFieldMaps.nameSingular,
CommonQueryNames.groupBy,
args,
//TODO : Refacto-common - To fix when updating workspaceQueryHookService, removing gql typing dependency
)) as GroupByQueryArgs;
return {
...hookedArgs,
filter: this.queryRunnerArgsFactory.overrideFilterByFieldMetadata(
hookedArgs.filter,
objectMetadataItemWithFieldMaps,
),
};
}
}
@@ -2,10 +2,12 @@ import { CommonCreateManyQueryRunnerService } from 'src/engine/api/common/common
import { CommonCreateOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-create-one-query-runner.service';
import { CommonFindManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-many-query-runner.service';
import { CommonFindOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-one-query-runner.service';
import { CommonGroupByQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-group-by-query-runner.service';
export const CommonQueryRunners = [
CommonFindOneQueryRunnerService,
CommonFindManyQueryRunnerService,
CommonCreateOneQueryRunnerService,
CommonCreateManyQueryRunnerService,
CommonGroupByQueryRunnerService,
];
@@ -14,6 +14,9 @@ import { FileModule } from 'src/engine/core-modules/file/file.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
import { ViewFilterGroupModule } from 'src/engine/metadata-modules/view-filter-group/view-filter-group.module';
import { ViewFilterModule } from 'src/engine/metadata-modules/view-filter/view-filter.module';
import { ViewModule } from 'src/engine/metadata-modules/view/view.module';
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
@Module({
@@ -26,6 +29,9 @@ import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/wor
ApiKeyModule,
WorkspacePermissionsCacheModule,
FileModule,
ViewModule,
ViewFilterModule,
ViewFilterGroupModule,
],
providers: [
ProcessNestedRelationsHelper,
@@ -0,0 +1,9 @@
type AggregateValues = {
[key: string]: string;
};
type GroupByDimensionValues = {
groupByDimensionValues: string[];
};
export type CommonGroupByOutputItem = AggregateValues & GroupByDimensionValues;
@@ -1,25 +1,30 @@
import { type ObjectRecord } from 'twenty-shared/types';
import {
type ObjectRecord,
type OrderByWithGroupBy,
} from 'twenty-shared/types';
import {
type ObjectRecordFilter,
type ObjectRecordGroupBy,
type ObjectRecordOrderBy,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { type CommonSelectedFieldsResult } from 'src/engine/api/common/types/common-selected-fields-result.type';
import { type CommonSelectedFields } from 'src/engine/api/common/types/common-selected-fields-result.type';
export enum CommonQueryNames {
findOne = 'findOne',
findMany = 'findMany',
createMany = 'createMany',
groupBy = 'groupBy',
}
export interface FindOneQueryArgs {
selectedFieldsResult: CommonSelectedFieldsResult;
selectedFields: CommonSelectedFields;
filter?: ObjectRecordFilter;
}
export interface FindManyQueryArgs {
selectedFieldsResult: CommonSelectedFieldsResult;
selectedFields: CommonSelectedFields;
filter?: ObjectRecordFilter;
orderBy?: ObjectRecordOrderBy;
first?: number;
@@ -29,13 +34,26 @@ export interface FindManyQueryArgs {
}
export interface CreateManyQueryArgs {
selectedFieldsResult: CommonSelectedFieldsResult;
selectedFields: CommonSelectedFields;
data: Partial<ObjectRecord>[];
upsert?: boolean;
}
export interface CreateOneQueryArgs {
selectedFieldsResult: CommonSelectedFieldsResult;
selectedFields: CommonSelectedFields;
data: Partial<ObjectRecord>;
upsert?: boolean;
}
export interface GroupByQueryArgs {
selectedFields: CommonSelectedFields;
filter?: ObjectRecordFilter;
orderBy?: OrderByWithGroupBy;
groupBy: ObjectRecordGroupBy;
omitNullValues?: boolean;
viewId?: string;
}
export type CommonQueryArgs =
| FindOneQueryArgs
| FindManyQueryArgs
| GroupByQueryArgs;
@@ -1,12 +1,12 @@
import { type AggregationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util';
interface SelectedFields {
[key: string]: boolean | SelectedFields;
export interface CommonSelectedFields {
[key: string]: boolean | CommonSelectedFields;
}
export type CommonSelectedFieldsResult = {
select: SelectedFields;
relations: SelectedFields;
select: CommonSelectedFields;
relations: CommonSelectedFields;
//TODO = Refacto-common - to update when rest api will handle aggregates
aggregate: Record<string, AggregationField>;
};