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:
+9
-4
@@ -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()) {
|
||||
|
||||
+20
-1
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+17
-1
@@ -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(
|
||||
|
||||
+7
-4
@@ -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: () => ({
|
||||
|
||||
Reference in New Issue
Block a user