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:
+58
-15
@@ -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}.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+5
-2
@@ -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,
|
||||
|
||||
+5
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+17
-2
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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,
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+1
@@ -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',
|
||||
}
|
||||
|
||||
+1
@@ -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);
|
||||
|
||||
+1
@@ -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');
|
||||
|
||||
+2
@@ -8,4 +8,6 @@ export type CommonSelectedFieldsResult = {
|
||||
select: CommonSelectedFields;
|
||||
relations: CommonSelectedFields;
|
||||
aggregate: Record<string, AggregationField>;
|
||||
relationFieldsCount?: number;
|
||||
hasAtLeastTwoNestedOneToManyRelations?: boolean;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user