Common api - Group by query (#15108)
closes https://github.com/twentyhq/core-team-issues/issues/1626
This commit is contained in:
+37
-19
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+22
-14
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+18
-14
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+327
@@ -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
@@ -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;
|
||||
|
||||
+4
-4
@@ -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>;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user