Files
twenty/packages/twenty-server/src/engine/api/graphql/graphql-config/graphql-config.service.ts
T
Etienne d1befa7e35 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)
2025-12-04 16:33:08 +01:00

220 lines
7.4 KiB
TypeScript

import { Injectable, UnauthorizedException } from '@nestjs/common';
import { ContextIdFactory, ModuleRef } from '@nestjs/core';
import { type GqlOptionsFactory } from '@nestjs/graphql';
import {
type YogaDriverConfig,
type YogaDriverServerContext,
} from '@graphql-yoga/nestjs';
import * as Sentry from '@sentry/node';
import { GraphQLError, GraphQLSchema } from 'graphql';
import GraphQLJSON from 'graphql-type-json';
import {
type GraphQLSchemaWithContext,
type YogaInitialContext,
} from 'graphql-yoga';
import { JsonWebTokenError, TokenExpiredError } from 'jsonwebtoken';
import { NodeEnvironment } from 'src/engine/core-modules/twenty-config/interfaces/node-environment.interface';
import { WorkspaceSchemaFactory } from 'src/engine/api/graphql/workspace-schema.factory';
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import {
ApiConfig,
Billing,
Captcha,
ClientAIModelConfig,
NativeModelCapabilities,
PublicFeatureFlag,
PublicFeatureFlagMetadata,
Sentry as SentryConfig,
Support,
} from 'src/engine/core-modules/client-config/client-config.entity';
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 { 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';
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataloaderService } from 'src/engine/dataloaders/dataloader.service';
import { handleExceptionAndConvertToGraphQLError } from 'src/engine/utils/global-exception-handler.util';
import { renderApolloPlayground } from 'src/engine/utils/render-apollo-playground.util';
export interface GraphQLContext extends YogaDriverServerContext<'express'> {
user?: UserEntity;
workspace?: WorkspaceEntity;
}
@Injectable()
export class GraphQLConfigService
implements GqlOptionsFactory<YogaDriverConfig<'express'>>
{
constructor(
private readonly exceptionHandlerService: ExceptionHandlerService,
private readonly twentyConfigService: TwentyConfigService,
private readonly moduleRef: ModuleRef,
private readonly metricsService: MetricsService,
private readonly dataloaderService: DataloaderService,
private readonly i18nService: I18nService,
) {}
createGqlOptions(): YogaDriverConfig {
const isDebugMode =
this.twentyConfigService.get('NODE_ENV') === NodeEnvironment.DEVELOPMENT;
const plugins = [
useGraphQLErrorHandlerHook({
metricsService: this.metricsService,
exceptionHandlerService: this.exceptionHandlerService,
i18nService: this.i18nService,
twentyConfigService: this.twentyConfigService,
}),
useDisableIntrospectionAndSuggestionsForUnauthenticatedUsers(
this.twentyConfigService.get('NODE_ENV') === NodeEnvironment.PRODUCTION,
),
useValidateGraphqlQueryComplexity({
maximumAllowedFields:
this.twentyConfigService.get('GRAPHQL_MAX_FIELDS'),
maximumAllowedRootResolvers: this.twentyConfigService.get(
'GRAPHQL_MAX_ROOT_RESOLVERS',
),
checkDuplicateRootResolvers: true,
}),
];
if (Sentry.isInitialized()) {
plugins.push(useSentryTracing());
}
const config: YogaDriverConfig = {
autoSchemaFile: true,
include: [CoreEngineModule],
buildSchemaOptions: {
orphanedTypes: [
ApiConfig,
Billing,
Captcha,
ClientAIModelConfig,
NativeModelCapabilities,
PublicFeatureFlag,
PublicFeatureFlagMetadata,
SentryConfig,
Support,
],
},
conditionalSchema: async (context) => {
let user: UserEntity | null | undefined;
let workspace: WorkspaceEntity | undefined;
try {
const {
user,
workspace,
apiKey,
workspaceMemberId,
userWorkspaceId,
} = context.req;
if (!workspace) {
return new GraphQLSchema({});
}
return await this.createSchema(context, {
user,
workspace,
apiKey,
workspaceMemberId,
userWorkspaceId,
});
} catch (error) {
if (error instanceof UnauthorizedException) {
throw new GraphQLError('Unauthenticated', {
extensions: {
code: 'UNAUTHENTICATED',
},
});
}
if (error instanceof JsonWebTokenError) {
//mockedUserJWT
throw new GraphQLError('Unauthenticated', {
extensions: {
code: 'UNAUTHENTICATED',
},
});
}
if (error instanceof TokenExpiredError) {
throw new GraphQLError('Unauthenticated', {
extensions: {
code: 'UNAUTHENTICATED',
},
});
}
throw handleExceptionAndConvertToGraphQLError(
error,
this.exceptionHandlerService,
user
? {
id: user.id,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
}
: undefined,
workspace
? {
id: workspace.id,
displayName: workspace.displayName,
activationStatus: workspace.activationStatus,
}
: undefined,
);
}
},
resolvers: { JSON: GraphQLJSON },
plugins: plugins,
context: () => ({
loaders: this.dataloaderService.createLoaders(),
}),
};
if (isDebugMode) {
config.renderGraphiQL = () => {
return renderApolloPlayground();
};
}
return config;
}
async createSchema(
context: YogaDriverServerContext<'express'> & YogaInitialContext,
data: AuthContext,
): Promise<GraphQLSchemaWithContext<YogaDriverServerContext<'express'>>> {
// Create a new contextId for each request
const contextId = ContextIdFactory.create();
if (this.moduleRef.registerRequestByContextId) {
// Register the request in the contextId
this.moduleRef.registerRequestByContextId(context.req, contextId);
}
// Resolve the WorkspaceSchemaFactory for the contextId
const workspaceFactory = await this.moduleRef.resolve(
WorkspaceSchemaFactory,
contextId,
{
strict: false,
},
);
return await workspaceFactory.createGraphQLSchema(data);
}
}