Query complexity validation (#16274)

Validations : 
- relations count (in common api)
- oneToMany relation nested count (in common)
- requested fields count (in gql)
- root resolver count (in gql)
- root resolver duplicates (in gql)
- specific complexity for metadata / nesting count (in gql)
This commit is contained in:
Etienne
2025-12-04 16:33:08 +01:00
committed by GitHub
parent 53d34f4d14
commit d1befa7e35
36 changed files with 1208 additions and 91 deletions
@@ -1,5 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { msg } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { Omit } from 'zod/v4/core/util.cjs';
@@ -22,6 +23,7 @@ import {
CommonQueryNames,
} from 'src/engine/api/common/types/common-query-args.type';
import { CommonQueryResult } from 'src/engine/api/common/types/common-query-result.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 { OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS } from 'src/engine/api/graphql/graphql-query-runner/constants/objects-with-settings-permissions-requirements';
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
@@ -124,13 +126,17 @@ export abstract class CommonBaseQueryRunnerService<
flatFieldMetadataMaps,
);
const processedArgs = await this.processArgs(
args,
queryRunnerContext,
this.operationName,
commonQueryParser,
const selectedFieldsResult = commonQueryParser.parseSelectedFields(
args.selectedFields,
);
this.validateQueryComplexity(selectedFieldsResult, args);
const processedArgs = {
...(await this.processArgs(args, queryRunnerContext, this.operationName)),
selectedFieldsResult,
} as CommonExtendedInput<Args>;
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
authContext,
async () =>
@@ -166,16 +172,22 @@ export abstract class CommonBaseQueryRunnerService<
authContext: WorkspaceAuthContext,
): Promise<Output>;
protected computeQueryComplexity(
selectedFieldsResult: CommonSelectedFieldsResult,
_args: CommonInput<Args>,
): number {
const simpleFieldsComplexity = 1;
const selectedFieldsComplexity =
simpleFieldsComplexity + (selectedFieldsResult.relationFieldsCount ?? 0);
return selectedFieldsComplexity;
}
private async processArgs(
args: CommonInput<Args>,
queryRunnerContext: CommonBaseQueryRunnerContext,
operationName: CommonQueryNames,
commonQueryParser: GraphqlQueryParser,
): Promise<CommonExtendedInput<Args>> {
const selectedFieldsResult = commonQueryParser.parseSelectedFields(
args.selectedFields,
);
): Promise<CommonInput<Args>> {
const { authContext, flatObjectMetadata } = queryRunnerContext;
const computedArgs = await this.computeArgs(args, queryRunnerContext);
@@ -188,10 +200,7 @@ export abstract class CommonBaseQueryRunnerService<
computedArgs as WorkspacePreQueryHookPayload<CommonQueryNames>,
)) as CommonInput<Args>;
return {
...hookedArgs,
selectedFieldsResult,
};
return hookedArgs;
}
private async executeQueryAndEnrichResults(
@@ -388,4 +397,38 @@ export abstract class CommonBaseQueryRunnerService<
throw error;
}
}
private validateQueryComplexity(
selectedFieldsResult: CommonSelectedFieldsResult,
args: CommonInput<Args>,
) {
const maximumComplexity = this.twentyConfigService.get(
'COMMON_QUERY_COMPLEXITY_LIMIT',
);
if (selectedFieldsResult.hasAtLeastTwoNestedOneToManyRelations) {
throw new CommonQueryRunnerException(
`Query complexity is too high. One-to-Many relation cannot be nested in another One-to-Many relation.`,
CommonQueryRunnerExceptionCode.TOO_COMPLEX_QUERY,
{
userFriendlyMessage: msg`Query complexity is too high. One-to-Many relation cannot be nested in another One-to-Many relation.`,
},
);
}
const queryComplexity = this.computeQueryComplexity(
selectedFieldsResult,
args,
);
if (queryComplexity > maximumComplexity) {
throw new CommonQueryRunnerException(
`Query complexity is too high. Please, reduce the amount of relation fields requested. Query complexity: ${queryComplexity}. Maximum complexity: ${maximumComplexity}.`,
CommonQueryRunnerExceptionCode.TOO_COMPLEX_QUERY,
{
userFriendlyMessage: msg`Query complexity is too high. Please, reduce the amount of relation fields requested. Query complexity: ${queryComplexity}. Maximum complexity: ${maximumComplexity}.`,
},
);
}
}
}
@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { QUERY_MAX_RECORDS } from 'twenty-shared/constants';
import { QUERY_MAX_RECORDS_FROM_RELATION } from 'twenty-shared/constants';
import { ObjectRecord } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { FindOptionsRelations, ObjectLiteral } from 'typeorm';
@@ -84,7 +84,7 @@ export class CommonDeleteManyQueryRunnerService extends CommonBaseQueryRunnerSer
string,
FindOptionsRelations<ObjectLiteral>
>,
limit: QUERY_MAX_RECORDS,
limit: QUERY_MAX_RECORDS_FROM_RELATION,
authContext,
workspaceDataSource,
rolePermissionConfig,
@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { QUERY_MAX_RECORDS } from 'twenty-shared/constants';
import { QUERY_MAX_RECORDS_FROM_RELATION } from 'twenty-shared/constants';
import { ObjectRecord } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { FindOptionsRelations, ObjectLiteral } from 'typeorm';
@@ -85,7 +85,7 @@ export class CommonDestroyManyQueryRunnerService extends CommonBaseQueryRunnerSe
string,
FindOptionsRelations<ObjectLiteral>
>,
limit: QUERY_MAX_RECORDS,
limit: QUERY_MAX_RECORDS_FROM_RELATION,
authContext,
workspaceDataSource,
rolePermissionConfig,
@@ -1,7 +1,10 @@
import { Injectable } from '@nestjs/common';
import isEmpty from 'lodash.isempty';
import { QUERY_MAX_RECORDS } from 'twenty-shared/constants';
import {
QUERY_MAX_RECORDS,
QUERY_MAX_RECORDS_FROM_RELATION,
} from 'twenty-shared/constants';
import { ObjectRecord, OrderByDirection } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { FindOptionsRelations, In, ObjectLiteral } from 'typeorm';
@@ -150,7 +153,7 @@ export class CommonFindDuplicatesQueryRunnerService extends CommonBaseQueryRunne
string,
FindOptionsRelations<ObjectLiteral>
>,
limit: QUERY_MAX_RECORDS,
limit: QUERY_MAX_RECORDS_FROM_RELATION,
authContext,
workspaceDataSource,
rolePermissionConfig,
@@ -1,7 +1,10 @@
import { Injectable } from '@nestjs/common';
import { isDefined } from 'class-validator';
import { QUERY_MAX_RECORDS } from 'twenty-shared/constants';
import {
QUERY_MAX_RECORDS,
QUERY_MAX_RECORDS_FROM_RELATION,
} from 'twenty-shared/constants';
import { ObjectRecord, OrderByDirection } from 'twenty-shared/types';
import { FindOptionsRelations, ObjectLiteral } from 'typeorm';
@@ -168,7 +171,7 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi
FindOptionsRelations<ObjectLiteral>
>,
aggregate: args.selectedFieldsResult.aggregate,
limit: QUERY_MAX_RECORDS,
limit: QUERY_MAX_RECORDS_FROM_RELATION,
authContext,
workspaceDataSource,
rolePermissionConfig,
@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { QUERY_MAX_RECORDS } from 'twenty-shared/constants';
import { QUERY_MAX_RECORDS_FROM_RELATION } from 'twenty-shared/constants';
import { ObjectRecord } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { FindOptionsRelations, ObjectLiteral } from 'typeorm';
@@ -96,7 +96,7 @@ export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerServic
string,
FindOptionsRelations<ObjectLiteral>
>,
limit: QUERY_MAX_RECORDS,
limit: QUERY_MAX_RECORDS_FROM_RELATION,
authContext,
workspaceDataSource,
rolePermissionConfig,
@@ -34,7 +34,7 @@ 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 { CommonSelectedFieldsResult } from 'src/engine/api/common/types/common-selected-fields-result.type';
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.type';
import { GroupByField } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/types/group-by-field.types';
@@ -322,7 +322,7 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic
}: {
queryBuilder: WorkspaceSelectQueryBuilder<ObjectLiteral>;
groupByDefinitions: GroupByDefinition[];
selectedFieldsResult: GraphqlQuerySelectedFieldsResult;
selectedFieldsResult: CommonSelectedFieldsResult;
groupLimit?: number;
}): Promise<CommonGroupByOutputItem[]> {
const effectiveGroupLimit = getGroupLimit(groupLimit);
@@ -400,4 +400,19 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic
),
};
}
protected override computeQueryComplexity(
selectedFieldsResult: CommonSelectedFieldsResult,
args: CommonInput<GroupByQueryArgs>,
): number {
const groupByQueryComplexity = 1;
const simpleFieldsComplexity = 1;
const selectedFieldsComplexity =
simpleFieldsComplexity + (selectedFieldsResult.relationFieldsCount ?? 0);
return (args.includeRecords ?? false)
? groupByQueryComplexity +
selectedFieldsComplexity * getGroupLimit(args.limit)
: groupByQueryComplexity;
}
}
@@ -2,7 +2,7 @@ import { Injectable, Logger } from '@nestjs/common';
import {
MUTATION_MAX_MERGE_RECORDS,
QUERY_MAX_RECORDS,
QUERY_MAX_RECORDS_FROM_RELATION,
} from 'twenty-shared/constants';
import {
FieldMetadataRelationSettings,
@@ -157,7 +157,7 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ
string,
FindOptionsRelations<ObjectLiteral>
>,
limit: QUERY_MAX_RECORDS,
limit: QUERY_MAX_RECORDS_FROM_RELATION,
authContext: context.authContext,
workspaceDataSource: context.workspaceDataSource,
rolePermissionConfig: context.rolePermissionConfig,
@@ -444,7 +444,7 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ
string,
FindOptionsRelations<ObjectLiteral>
>,
limit: QUERY_MAX_RECORDS,
limit: QUERY_MAX_RECORDS_FROM_RELATION,
authContext,
workspaceDataSource,
rolePermissionConfig,
@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { QUERY_MAX_RECORDS } from 'twenty-shared/constants';
import { QUERY_MAX_RECORDS_FROM_RELATION } from 'twenty-shared/constants';
import { ObjectRecord } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { FindOptionsRelations, ObjectLiteral } from 'typeorm';
@@ -85,7 +85,7 @@ export class CommonRestoreManyQueryRunnerService extends CommonBaseQueryRunnerSe
string,
FindOptionsRelations<ObjectLiteral>
>,
limit: QUERY_MAX_RECORDS,
limit: QUERY_MAX_RECORDS_FROM_RELATION,
authContext,
workspaceDataSource,
rolePermissionConfig,
@@ -1,7 +1,7 @@
import { Injectable } from '@nestjs/common';
import { isDefined } from 'class-validator';
import { QUERY_MAX_RECORDS } from 'twenty-shared/constants';
import { QUERY_MAX_RECORDS_FROM_RELATION } from 'twenty-shared/constants';
import { ObjectRecord } from 'twenty-shared/types';
import { FindOptionsRelations, ObjectLiteral } from 'typeorm';
@@ -85,7 +85,7 @@ export class CommonUpdateManyQueryRunnerService extends CommonBaseQueryRunnerSer
string,
FindOptionsRelations<ObjectLiteral>
>,
limit: QUERY_MAX_RECORDS,
limit: QUERY_MAX_RECORDS_FROM_RELATION,
authContext,
workspaceDataSource,
rolePermissionConfig,
@@ -16,4 +16,5 @@ export enum CommonQueryRunnerExceptionCode {
TOO_MANY_RECORDS_TO_UPDATE = 'TOO_MANY_RECORDS_TO_UPDATE',
BAD_REQUEST = 'BAD_REQUEST',
INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR',
TOO_COMPLEX_QUERY = 'TOO_COMPLEX_QUERY',
}
@@ -26,6 +26,7 @@ export const commonQueryRunnerToGraphqlApiExceptionHandler = (
case CommonQueryRunnerExceptionCode.INVALID_CURSOR:
case CommonQueryRunnerExceptionCode.TOO_MANY_RECORDS_TO_UPDATE:
case CommonQueryRunnerExceptionCode.BAD_REQUEST:
case CommonQueryRunnerExceptionCode.TOO_COMPLEX_QUERY:
throw new UserInputError(error);
case CommonQueryRunnerExceptionCode.INVALID_AUTH_CONTEXT:
throw new AuthenticationError(error);
@@ -25,6 +25,7 @@ export const commonQueryRunnerToRestApiExceptionHandler = (
case CommonQueryRunnerExceptionCode.INVALID_CURSOR:
case CommonQueryRunnerExceptionCode.TOO_MANY_RECORDS_TO_UPDATE:
case CommonQueryRunnerExceptionCode.BAD_REQUEST:
case CommonQueryRunnerExceptionCode.TOO_COMPLEX_QUERY:
throw new BadRequestException(error.message);
case CommonQueryRunnerExceptionCode.RECORD_NOT_FOUND:
throw new NotFoundException('Record not found');
@@ -8,4 +8,6 @@ export type CommonSelectedFieldsResult = {
select: CommonSelectedFields;
relations: CommonSelectedFields;
aggregate: Record<string, AggregationField>;
relationFieldsCount?: number;
hasAtLeastTwoNestedOneToManyRelations?: boolean;
};
@@ -33,9 +33,9 @@ import {
import { CoreEngineModule } from 'src/engine/core-modules/core-engine.module';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import { useSentryTracing } from 'src/engine/core-modules/exception-handler/hooks/use-sentry-tracing';
import { useComputeComplexity } from 'src/engine/core-modules/graphql/hooks/use-compute-complexity.hook';
import { useDisableIntrospectionAndSuggestionsForUnauthenticatedUsers } from 'src/engine/core-modules/graphql/hooks/use-disable-introspection-and-suggestions-for-unauthenticated-users.hook';
import { useGraphQLErrorHandlerHook } from 'src/engine/core-modules/graphql/hooks/use-graphql-error-handler.hook';
import { useValidateGraphqlQueryComplexity } from 'src/engine/core-modules/graphql/hooks/use-validate-graphql-query-complexity.hook';
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
@@ -76,9 +76,14 @@ export class GraphQLConfigService
useDisableIntrospectionAndSuggestionsForUnauthenticatedUsers(
this.twentyConfigService.get('NODE_ENV') === NodeEnvironment.PRODUCTION,
),
useComputeComplexity(
this.twentyConfigService.get('GRAPHQL_MAX_COMPLEXITY'),
),
useValidateGraphqlQueryComplexity({
maximumAllowedFields:
this.twentyConfigService.get('GRAPHQL_MAX_FIELDS'),
maximumAllowedRootResolvers: this.twentyConfigService.get(
'GRAPHQL_MAX_ROOT_RESOLVERS',
),
checkDuplicateRootResolvers: true,
}),
];
if (Sentry.isInitialized()) {
@@ -1,3 +1,4 @@
import { RelationType, type FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import {
@@ -22,16 +23,26 @@ export class GraphqlQuerySelectedFieldsRelationParser {
}
parseRelationField(
fieldMetadata: FlatFieldMetadata,
fieldMetadata:
| FlatFieldMetadata<FieldMetadataType.RELATION>
| FlatFieldMetadata<FieldMetadataType.MORPH_RELATION>,
fieldKey: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
fieldValue: any,
accumulator: GraphqlQuerySelectedFieldsResult,
isFromOneToManyRelation?: boolean,
): void {
if (!fieldValue || typeof fieldValue !== 'object') {
return;
}
const isOneToManyRelation =
fieldMetadata.settings?.relationType === RelationType.ONE_TO_MANY;
if (isFromOneToManyRelation && isOneToManyRelation) {
accumulator.hasAtLeastTwoNestedOneToManyRelations = true;
}
accumulator.relations[fieldKey] = true;
if (!isDefined(fieldMetadata.relationTargetObjectMetadataId)) {
@@ -52,6 +63,7 @@ export class GraphqlQuerySelectedFieldsRelationParser {
const relationAccumulator = fieldParser.parse(
fieldValue,
targetObjectMetadata,
isFromOneToManyRelation || isOneToManyRelation,
);
accumulator.select[fieldKey] = {
@@ -60,5 +72,12 @@ export class GraphqlQuerySelectedFieldsRelationParser {
};
accumulator.relations[fieldKey] = relationAccumulator.relations;
accumulator.aggregate[fieldKey] = relationAccumulator.aggregate;
accumulator.relationFieldsCount =
accumulator.relationFieldsCount +
relationAccumulator.relationFieldsCount +
1;
accumulator.hasAtLeastTwoNestedOneToManyRelations =
accumulator.hasAtLeastTwoNestedOneToManyRelations ||
relationAccumulator.hasAtLeastTwoNestedOneToManyRelations;
}
}
@@ -21,6 +21,8 @@ export type GraphqlQuerySelectedFieldsResult = {
relations: Record<string, any>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
aggregate: Record<string, any>;
relationFieldsCount: number;
hasAtLeastTwoNestedOneToManyRelations: boolean;
};
export class GraphqlQuerySelectedFieldsParser {
@@ -47,11 +49,14 @@ export class GraphqlQuerySelectedFieldsParser {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
graphqlSelectedFields: Partial<Record<string, any>>,
flatObjectMetadata: FlatObjectMetadata,
isFromOneToManyRelation?: boolean,
): GraphqlQuerySelectedFieldsResult {
const accumulator: GraphqlQuerySelectedFieldsResult = {
select: {},
relations: {},
aggregate: {},
relationFieldsCount: 0,
hasAtLeastTwoNestedOneToManyRelations: false,
};
if (this.isRootConnection(graphqlSelectedFields)) {
@@ -59,6 +64,7 @@ export class GraphqlQuerySelectedFieldsParser {
graphqlSelectedFields,
flatObjectMetadata,
accumulator,
isFromOneToManyRelation,
);
return accumulator;
@@ -75,6 +81,7 @@ export class GraphqlQuerySelectedFieldsParser {
graphqlSelectedFields,
flatObjectMetadata,
accumulator,
isFromOneToManyRelation,
);
return accumulator;
@@ -85,6 +92,7 @@ export class GraphqlQuerySelectedFieldsParser {
graphqlSelectedFields: Partial<Record<string, any>>,
flatObjectMetadata: FlatObjectMetadata,
accumulator: GraphqlQuerySelectedFieldsResult,
isFromOneToManyRelation?: boolean,
): void {
for (const fieldMetadataId of flatObjectMetadata.fieldMetadataIds) {
const fieldMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({
@@ -116,6 +124,7 @@ export class GraphqlQuerySelectedFieldsParser {
fieldMetadata.name,
graphqlSelectedFieldValue,
accumulator,
isFromOneToManyRelation,
);
continue;
@@ -160,6 +169,7 @@ export class GraphqlQuerySelectedFieldsParser {
fieldMetadata.name,
graphqlSelectedFieldValue,
accumulator,
isFromOneToManyRelation,
);
continue;
@@ -197,6 +207,7 @@ export class GraphqlQuerySelectedFieldsParser {
graphqlSelectedFields: Partial<Record<string, any>>,
flatObjectMetadata: FlatObjectMetadata,
accumulator: GraphqlQuerySelectedFieldsResult,
isFromOneToManyRelation?: boolean,
): void {
this.aggregateParser.parse(
graphqlSelectedFields,
@@ -207,7 +218,12 @@ export class GraphqlQuerySelectedFieldsParser {
const node = graphqlSelectedFields.edges.node;
this.parseRecordFields(node, flatObjectMetadata, accumulator);
this.parseRecordFields(
node,
flatObjectMetadata,
accumulator,
isFromOneToManyRelation,
);
}
private isRootConnection(
@@ -4,7 +4,7 @@ import { isNonEmptyString } from '@sniptt/guards';
import isEmpty from 'lodash.isempty';
import { ObjectRecord } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type ObjectLiteral } from 'typeorm';
import { FindOptionsRelations, type ObjectLiteral } from 'typeorm';
import { ObjectRecordOrderBy } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
@@ -12,7 +12,7 @@ import { getObjectAlias } from 'src/engine/api/common/common-query-runners/utils
import { CommonResultGettersService } from 'src/engine/api/common/common-result-getters/common-result-getters.service';
import { CommonExtendedQueryRunnerContext } from 'src/engine/api/common/types/common-extended-query-runner-context.type';
import { type CommonGroupByOutputItem } from 'src/engine/api/common/types/common-group-by-output-item.type';
import { type GraphqlQuerySelectedFieldsResult } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser';
import { CommonSelectedFieldsResult } from 'src/engine/api/common/types/common-selected-fields-result.type';
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
import { type GroupByDefinition } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/types/group-by-definition.type';
import { formatResultWithGroupByDimensionValues } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/format-result-with-group-by-dimension-values.util';
@@ -50,7 +50,7 @@ export class GroupByWithRecordsService {
queryBuilderWithGroupBy: WorkspaceSelectQueryBuilder<ObjectLiteral>;
queryBuilderWithFiltersAndWithoutGroupBy: WorkspaceSelectQueryBuilder<ObjectLiteral>;
groupByDefinitions: GroupByDefinition[];
selectedFieldsResult: GraphqlQuerySelectedFieldsResult;
selectedFieldsResult: CommonSelectedFieldsResult;
queryRunnerContext: CommonExtendedQueryRunnerContext;
orderByForRecords: ObjectRecordOrderBy;
groupLimit?: number;
@@ -110,7 +110,10 @@ export class GroupByWithRecordsService {
parentObjectMetadataItem: flatObjectMetadata,
parentObjectRecords: allRecords,
parentObjectRecordsAggregatedValues: {},
relations: selectedFieldsResult.relations,
relations: selectedFieldsResult.relations as Record<
string,
FindOptionsRelations<ObjectLiteral>
>,
aggregate: selectedFieldsResult.aggregate,
limit: RELATIONS_PER_RECORD_LIMIT,
authContext,
@@ -7,9 +7,9 @@ import { useCachedMetadata } from 'src/engine/api/graphql/graphql-config/hooks/u
import { MetadataGraphQLApiModule } from 'src/engine/api/graphql/metadata-graphql-api.module';
import { type CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service';
import { type ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import { useComputeComplexity } from 'src/engine/core-modules/graphql/hooks/use-compute-complexity.hook';
import { useDisableIntrospectionAndSuggestionsForUnauthenticatedUsers } from 'src/engine/core-modules/graphql/hooks/use-disable-introspection-and-suggestions-for-unauthenticated-users.hook';
import { useGraphQLErrorHandlerHook } from 'src/engine/core-modules/graphql/hooks/use-graphql-error-handler.hook';
import { useValidateGraphqlQueryComplexity } from 'src/engine/core-modules/graphql/hooks/use-validate-graphql-query-complexity.hook';
import { type I18nService } from 'src/engine/core-modules/i18n/i18n.service';
import { type MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
import { type TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
@@ -46,7 +46,12 @@ export const metadataModuleFactory = async (
useDisableIntrospectionAndSuggestionsForUnauthenticatedUsers(
twentyConfigService.get('NODE_ENV') === NodeEnvironment.PRODUCTION,
),
useComputeComplexity(twentyConfigService.get('GRAPHQL_MAX_COMPLEXITY')),
useValidateGraphqlQueryComplexity({
maximumAllowedFields: twentyConfigService.get('GRAPHQL_MAX_FIELDS'),
maximumAllowedRootResolvers: 10,
maximumAllowedNestedFields: 7,
checkDuplicateRootResolvers: true,
}),
],
path: '/metadata',
context: () => ({
@@ -1,11 +1,14 @@
import { Injectable } from '@nestjs/common';
import { RestApiBaseHandler } from 'src/engine/api/rest/core/handlers/rest-api-base.handler';
import { DEFAULT_NUMBER_OF_GROUPS_LIMIT } from 'twenty-shared/constants';
import { CommonGroupByQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-group-by-query-runner.service';
import { RestApiBaseHandler } from 'src/engine/api/rest/core/handlers/rest-api-base.handler';
import { parseAggregateFieldsRestRequest } from 'src/engine/api/rest/input-request-parsers/aggregate-fields-parser-utils/parse-aggregate-fields-rest-request.util';
import { parseFilterRestRequest } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-rest-request.util';
import { parseGroupByRestRequest } from 'src/engine/api/rest/input-request-parsers/group-by-parser-utils/parse-group-by-rest-request.util';
import { parseIncludeRecordsSampleRestRequest } from 'src/engine/api/rest/input-request-parsers/group-by-with-records/parse-include-records-sample-rest-request.util';
import { parseLimitRestRequest } from 'src/engine/api/rest/input-request-parsers/limit-parser-utils/parse-limit-rest-request.util';
import { parseOrderByForRecordsWithGroupByRestRequest } from 'src/engine/api/rest/input-request-parsers/order-by-with-group-by-parser-utils/parse-order-by-for-records-rest-request.util';
import { parseOrderByWithGroupByRestRequest } from 'src/engine/api/rest/input-request-parsers/order-by-with-group-by-parser-utils/parse-order-by-with-group-by-rest-request.util';
import { parseViewIdRestRequest } from 'src/engine/api/rest/input-request-parsers/view-id-parser-utils/parse-view-id-rest-request.util';
@@ -35,6 +38,7 @@ export class RestApiGroupByHandler extends RestApiBaseHandler {
objectIdByNameSingular,
includeRecords,
orderByForRecords,
limit,
} = await this.parseRequestArgs(request);
return await this.commonGroupByQueryRunnerService.execute(
@@ -44,6 +48,7 @@ export class RestApiGroupByHandler extends RestApiBaseHandler {
viewId,
groupBy,
selectedFields,
limit,
includeRecords,
orderByForRecords,
},
@@ -77,6 +82,10 @@ export class RestApiGroupByHandler extends RestApiBaseHandler {
const groupBy = parseGroupByRestRequest(request);
const includeRecords = parseIncludeRecordsSampleRestRequest(request);
const aggregateFields = parseAggregateFieldsRestRequest(request);
const limit = parseLimitRestRequest(
request,
DEFAULT_NUMBER_OF_GROUPS_LIMIT,
);
let selectedFields = { ...aggregateFields, groupByDimensionValues: true };
if (includeRecords) {
@@ -104,6 +113,7 @@ export class RestApiGroupByHandler extends RestApiBaseHandler {
groupBy,
selectedFields,
includeRecords,
limit,
};
}
}