[groupBy] Load record within groups (without relations) (#15307)
First step of https://github.com/twentyhq/core-team-issues/issues/1726. I will handle relations in another PR. Another ticket is planned to allow for sorting among the records. When querying records we have set the number of maximum groups to 50, and of maximum records par group to 10. <img width="1283" height="798" alt="image" src="https://github.com/user-attachments/assets/7ffc9805-d715-4017-8030-4f3521e6f741" />
This commit is contained in:
+100
-25
@@ -11,9 +11,9 @@ import {
|
||||
computeRecordGqlOperationFilter,
|
||||
convertViewFilterValueToString,
|
||||
getFilterTypeFromFieldType,
|
||||
isDefined,
|
||||
turnAnyFieldFilterIntoRecordGqlFilter,
|
||||
} from 'twenty-shared/utils';
|
||||
import { ObjectLiteral } from 'typeorm';
|
||||
|
||||
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';
|
||||
@@ -28,11 +28,15 @@ import {
|
||||
CommonQueryNames,
|
||||
GroupByQueryArgs,
|
||||
} from 'src/engine/api/common/types/common-query-args.type';
|
||||
import { GraphqlQuerySelectedFieldsResult } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser';
|
||||
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
|
||||
import { GroupByDefinition } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/types/group-by-definition.types';
|
||||
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 { GroupByWithRecordsService } from 'src/engine/api/graphql/graphql-query-runner/group-by/services/group-by-with-records.service';
|
||||
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';
|
||||
@@ -40,6 +44,7 @@ import { ViewFilterGroupService } from 'src/engine/metadata-modules/view-filter-
|
||||
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 { WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder';
|
||||
import { formatColumnNamesFromCompositeFieldAndSubfields } from 'src/engine/twenty-orm/utils/format-column-names-from-composite-field-and-subfield.util';
|
||||
|
||||
@Injectable()
|
||||
@@ -51,6 +56,7 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic
|
||||
private readonly viewFilterService: ViewFilterService,
|
||||
private readonly viewFilterGroupService: ViewFilterGroupService,
|
||||
private readonly viewService: ViewService,
|
||||
private readonly groupByWithRecordsService: GroupByWithRecordsService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -66,6 +72,7 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic
|
||||
commonQueryParser,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
authContext,
|
||||
objectMetadataMaps,
|
||||
} = queryRunnerContext;
|
||||
|
||||
const objectMetadataNameSingular =
|
||||
@@ -77,22 +84,14 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic
|
||||
|
||||
let appliedFilters = args.filter ?? ({} as ObjectRecordFilter);
|
||||
|
||||
if (isDefined(args.viewId)) {
|
||||
appliedFilters = await this.addFiltersFromView({
|
||||
args,
|
||||
authContext,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
appliedFilters,
|
||||
});
|
||||
}
|
||||
|
||||
commonQueryParser.applyFilterToBuilder(
|
||||
queryBuilder,
|
||||
objectMetadataNameSingular,
|
||||
await this.addFiltersToQueryBuilder({
|
||||
args,
|
||||
appliedFilters,
|
||||
);
|
||||
|
||||
commonQueryParser.applyDeletedAtToBuilder(queryBuilder, appliedFilters);
|
||||
queryBuilder,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
workspaceId: authContext.workspace.id,
|
||||
commonQueryParser,
|
||||
});
|
||||
|
||||
ProcessAggregateHelper.addSelectedAggregatedFieldsQueriesToQueryBuilder({
|
||||
selectedAggregatedFields: args.selectedFieldsResult.aggregate,
|
||||
@@ -147,13 +146,37 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic
|
||||
groupByFields,
|
||||
);
|
||||
|
||||
const result = await queryBuilder.getRawMany();
|
||||
const shouldIncludeRecords = args.includeRecords ?? false;
|
||||
|
||||
return formatResultWithGroupByDimensionValues(
|
||||
result,
|
||||
if (shouldIncludeRecords) {
|
||||
const queryBuilderWithFiltersAndWithoutGroupBy =
|
||||
repository.createQueryBuilder(objectMetadataNameSingular);
|
||||
|
||||
await this.addFiltersToQueryBuilder({
|
||||
args,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
workspaceId: authContext.workspace.id,
|
||||
commonQueryParser,
|
||||
appliedFilters,
|
||||
queryBuilder: queryBuilderWithFiltersAndWithoutGroupBy,
|
||||
});
|
||||
|
||||
return this.groupByWithRecordsService.resolveWithRecords({
|
||||
queryBuilderWithFiltersAndWithoutGroupBy,
|
||||
queryBuilderWithGroupBy: queryBuilder,
|
||||
groupByDefinitions,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps,
|
||||
repository,
|
||||
selectedFieldsResult: args.selectedFieldsResult,
|
||||
});
|
||||
}
|
||||
|
||||
return this.resolveWithoutRecords({
|
||||
queryBuilder,
|
||||
groupByDefinitions,
|
||||
Object.keys(args.selectedFieldsResult.aggregate),
|
||||
);
|
||||
selectedFieldsResult: args.selectedFieldsResult,
|
||||
});
|
||||
}
|
||||
|
||||
async processQueryResult(
|
||||
@@ -169,17 +192,15 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic
|
||||
args,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
appliedFilters,
|
||||
authContext,
|
||||
workspaceId,
|
||||
}: {
|
||||
args: GroupByQueryArgs;
|
||||
authContext: WorkspaceAuthContext;
|
||||
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
|
||||
appliedFilters: ObjectRecordFilter;
|
||||
workspaceId: string;
|
||||
}): Promise<ObjectRecordFilter> {
|
||||
assertIsDefinedOrThrow(args.viewId);
|
||||
|
||||
const workspaceId = authContext.workspace.id;
|
||||
|
||||
const viewFilters = await this.viewFilterService.findByViewId(
|
||||
workspaceId,
|
||||
args.viewId,
|
||||
@@ -253,6 +274,60 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic
|
||||
return appliedFilters;
|
||||
}
|
||||
|
||||
private async addFiltersToQueryBuilder({
|
||||
args,
|
||||
appliedFilters,
|
||||
queryBuilder,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
workspaceId,
|
||||
commonQueryParser,
|
||||
}: {
|
||||
args: GroupByQueryArgs;
|
||||
appliedFilters: ObjectRecordFilter;
|
||||
queryBuilder: WorkspaceSelectQueryBuilder<ObjectLiteral>;
|
||||
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
|
||||
workspaceId: string;
|
||||
commonQueryParser: GraphqlQueryParser;
|
||||
}): Promise<void> {
|
||||
const objectMetadataNameSingular =
|
||||
objectMetadataItemWithFieldMaps.nameSingular;
|
||||
|
||||
if (args.viewId) {
|
||||
appliedFilters = await this.addFiltersFromView({
|
||||
args,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
appliedFilters,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
commonQueryParser.applyFilterToBuilder(
|
||||
queryBuilder,
|
||||
objectMetadataNameSingular,
|
||||
appliedFilters,
|
||||
);
|
||||
|
||||
commonQueryParser.applyDeletedAtToBuilder(queryBuilder, appliedFilters);
|
||||
}
|
||||
|
||||
private async resolveWithoutRecords({
|
||||
queryBuilder,
|
||||
groupByDefinitions,
|
||||
selectedFieldsResult,
|
||||
}: {
|
||||
queryBuilder: WorkspaceSelectQueryBuilder<ObjectLiteral>;
|
||||
groupByDefinitions: GroupByDefinition[];
|
||||
selectedFieldsResult: GraphqlQuerySelectedFieldsResult;
|
||||
}): Promise<CommonGroupByOutputItem[]> {
|
||||
const result = await queryBuilder.getRawMany();
|
||||
|
||||
return formatResultWithGroupByDimensionValues({
|
||||
groupsResult: result,
|
||||
groupByDefinitions,
|
||||
aggregateFieldNames: Object.keys(selectedFieldsResult.aggregate),
|
||||
});
|
||||
}
|
||||
|
||||
async validate(
|
||||
_args: CommonInput<GroupByQueryArgs>,
|
||||
_queryRunnerContext: CommonBaseQueryRunnerContext,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { CommonArgsHandlers } from 'src/engine/api/common/common-args-handlers/common-query-selected-fields/common-arg-handlers';
|
||||
import { CommonQueryRunners } from 'src/engine/api/common/common-query-runners/common-query-runners';
|
||||
import { CommonResultGettersService } from 'src/engine/api/common/common-result-getters/common-result-getters.service';
|
||||
import { GroupByWithRecordsService } from 'src/engine/api/graphql/graphql-query-runner/group-by/services/group-by-with-records.service';
|
||||
import { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper';
|
||||
import { ProcessNestedRelationsV2Helper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper';
|
||||
import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper';
|
||||
@@ -40,6 +41,7 @@ import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/wor
|
||||
ProcessAggregateHelper,
|
||||
...CommonQueryRunners,
|
||||
CommonResultGettersService,
|
||||
GroupByWithRecordsService,
|
||||
],
|
||||
exports: [...CommonQueryRunners],
|
||||
})
|
||||
|
||||
+9
-1
@@ -1,3 +1,5 @@
|
||||
import { type ObjectRecord } from 'twenty-shared/types';
|
||||
|
||||
type AggregateValues = {
|
||||
[key: string]: string;
|
||||
};
|
||||
@@ -6,4 +8,10 @@ type GroupByDimensionValues = {
|
||||
groupByDimensionValues: string[];
|
||||
};
|
||||
|
||||
export type CommonGroupByOutputItem = AggregateValues & GroupByDimensionValues;
|
||||
type Records = {
|
||||
records?: ObjectRecord[];
|
||||
};
|
||||
|
||||
export type CommonGroupByOutputItem = GroupByDimensionValues &
|
||||
AggregateValues &
|
||||
Records;
|
||||
|
||||
@@ -66,6 +66,8 @@ export interface GroupByQueryArgs {
|
||||
orderBy?: OrderByWithGroupBy;
|
||||
groupBy: ObjectRecordGroupBy;
|
||||
viewId?: string;
|
||||
includeRecords?: boolean;
|
||||
selectedFields: CommonSelectedFields;
|
||||
}
|
||||
export interface DestroyOneQueryArgs {
|
||||
id: string;
|
||||
|
||||
Reference in New Issue
Block a user