From 3060eb4e1e3389c6e56abc024e7d222e1a863bda Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Sat, 27 Jul 2024 12:27:04 +0200 Subject: [PATCH] Handle query runner errors (#6424) - Throw service error from query runner - Catch in resolver factories - Map to graphql errors --------- Co-authored-by: Charles Bochet --- .../graphql-config/graphql-config.service.ts | 22 +-- .../utils/assert-is-valid-uuid.util.ts | 10 +- .../utils/compute-pg-graphql-error.util.ts | 30 ++-- ...nner-graphql-api-exception-handler.util.ts | 36 +++++ .../workspace-query-runner.exception.ts | 19 +++ .../workspace-query-runner.service.ts | 41 +++-- .../factories/create-many-resolver.factory.ts | 25 +-- .../factories/create-one-resolver.factory.ts | 25 +-- .../factories/delete-many-resolver.factory.ts | 25 +-- .../factories/delete-one-resolver.factory.ts | 25 +-- ...te-quick-action-on-one-resolver.factory.ts | 35 ++-- .../find-duplicates-resolver.factory.ts | 25 +-- .../factories/find-many-resolver.factory.ts | 25 +-- .../factories/find-one-resolver.factory.ts | 25 +-- .../factories/update-many-resolver.factory.ts | 25 +-- .../factories/update-one-resolver.factory.ts | 25 +-- .../hooks/use-exception-handler.hook.ts | 150 ------------------ 17 files changed, 276 insertions(+), 292 deletions(-) create mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util.ts create mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception.ts delete mode 100644 packages/twenty-server/src/engine/integrations/exception-handler/hooks/use-exception-handler.hook.ts diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-config/graphql-config.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-config/graphql-config.service.ts index 266dbf2ce48c..ae1aa2e78e7d 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-config/graphql-config.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-config/graphql-config.service.ts @@ -6,25 +6,25 @@ import { YogaDriverConfig, YogaDriverServerContext, } from '@graphql-yoga/nestjs'; -import { GraphQLSchema, GraphQLError } from 'graphql'; +import * as Sentry from '@sentry/node'; +import { GraphQLError, GraphQLSchema } from 'graphql'; import GraphQLJSON from 'graphql-type-json'; -import { JsonWebTokenError, TokenExpiredError } from 'jsonwebtoken'; import { GraphQLSchemaWithContext, YogaInitialContext } from 'graphql-yoga'; -import * as Sentry from '@sentry/node'; +import { JsonWebTokenError, TokenExpiredError } from 'jsonwebtoken'; +import { useThrottler } from 'src/engine/api/graphql/graphql-config/hooks/use-throttler'; +import { WorkspaceSchemaFactory } from 'src/engine/api/graphql/workspace-schema.factory'; import { TokenService } from 'src/engine/core-modules/auth/services/token.service'; +import { JwtData } from 'src/engine/core-modules/auth/types/jwt-data.type'; import { CoreEngineModule } from 'src/engine/core-modules/core-engine.module'; +import { useGraphQLErrorHandlerHook } from 'src/engine/core-modules/graphql/hooks/use-graphql-error-handler.hook'; +import { User } from 'src/engine/core-modules/user/user.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; -import { WorkspaceSchemaFactory } from 'src/engine/api/graphql/workspace-schema.factory'; +import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; import { ExceptionHandlerService } from 'src/engine/integrations/exception-handler/exception-handler.service'; +import { useSentryTracing } from 'src/engine/integrations/exception-handler/hooks/use-sentry-tracing'; import { handleExceptionAndConvertToGraphQLError } from 'src/engine/utils/global-exception-handler.util'; import { renderApolloPlayground } from 'src/engine/utils/render-apollo-playground.util'; -import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; -import { useExceptionHandler } from 'src/engine/integrations/exception-handler/hooks/use-exception-handler.hook'; -import { User } from 'src/engine/core-modules/user/user.entity'; -import { useThrottler } from 'src/engine/api/graphql/graphql-config/hooks/use-throttler'; -import { JwtData } from 'src/engine/core-modules/auth/types/jwt-data.type'; -import { useSentryTracing } from 'src/engine/integrations/exception-handler/hooks/use-sentry-tracing'; export interface GraphQLContext extends YogaDriverServerContext<'express'> { user?: User; @@ -52,7 +52,7 @@ export class GraphQLConfigService return context.req.user?.id ?? context.req.ip ?? 'anonymous'; }, }), - useExceptionHandler({ + useGraphQLErrorHandlerHook({ exceptionHandlerService: this.exceptionHandlerService, }), ]; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util.ts index 61dbac25513f..27c4b6c14214 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util.ts @@ -1,4 +1,7 @@ -import { BadRequestException } from '@nestjs/common'; +import { + WorkspaceQueryRunnerException, + WorkspaceQueryRunnerExceptionCode, +} from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception'; export const assertIsValidUuid = (value: string) => { const isValid = @@ -7,6 +10,9 @@ export const assertIsValidUuid = (value: string) => { ); if (!isValid) { - throw new BadRequestException(`Value "${value}" is not a valid UUID`); + throw new WorkspaceQueryRunnerException( + `Value "${value}" is not a valid UUID`, + WorkspaceQueryRunnerExceptionCode.INVALID_QUERY_INPUT, + ); } }; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/compute-pg-graphql-error.util.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/compute-pg-graphql-error.util.ts index b3f0e23f26c0..ebb8ae5e34ce 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/compute-pg-graphql-error.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/compute-pg-graphql-error.util.ts @@ -1,8 +1,7 @@ import { - BadRequestException, - HttpException, - InternalServerErrorException, -} from '@nestjs/common'; + WorkspaceQueryRunnerException, + WorkspaceQueryRunnerExceptionCode, +} from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception'; export type PgGraphQLConfig = { atMost: number; @@ -13,7 +12,7 @@ interface PgGraphQLErrorMapping { command: string, objectName: string, pgGraphqlConfig: PgGraphQLConfig, - ) => HttpException; + ) => WorkspaceQueryRunnerException; } const pgGraphQLCommandMapping = { @@ -24,18 +23,28 @@ const pgGraphQLCommandMapping = { const pgGraphQLErrorMapping: PgGraphQLErrorMapping = { 'delete impacts too many records': (_, objectName, pgGraphqlConfig) => - new BadRequestException( + new WorkspaceQueryRunnerException( `Cannot delete ${objectName} because it impacts too many records (more than ${pgGraphqlConfig?.atMost}).`, + WorkspaceQueryRunnerExceptionCode.TOO_MANY_ROWS_AFFECTED, ), 'update impacts too many records': (_, objectName, pgGraphqlConfig) => - new BadRequestException( + new WorkspaceQueryRunnerException( `Cannot update ${objectName} because it impacts too many records (more than ${pgGraphqlConfig?.atMost}).`, + WorkspaceQueryRunnerExceptionCode.TOO_MANY_ROWS_AFFECTED, ), 'duplicate key value violates unique constraint': (command, objectName, _) => - new BadRequestException( + new WorkspaceQueryRunnerException( `Cannot ${ pgGraphQLCommandMapping[command] ?? command } ${objectName} because it violates a uniqueness constraint.`, + WorkspaceQueryRunnerExceptionCode.QUERY_VIOLATES_UNIQUE_CONSTRAINT, + ), + 'violates foreign key constraint': (command, objectName, _) => + new WorkspaceQueryRunnerException( + `Cannot ${ + pgGraphQLCommandMapping[command] ?? command + } ${objectName} because it violates a foreign key constraint.`, + WorkspaceQueryRunnerExceptionCode.QUERY_VIOLATES_FOREIGN_KEY_CONSTRAINT, ), }; @@ -49,7 +58,7 @@ export const computePgGraphQLError = ( const errorMessage = error?.message; const mappedErrorKey = Object.keys(pgGraphQLErrorMapping).find( - (key) => errorMessage?.startsWith(key), + (key) => errorMessage?.includes(key), ); const mappedError = mappedErrorKey @@ -60,7 +69,8 @@ export const computePgGraphQLError = ( return mappedError(command, objectName, pgGraphqlConfig); } - return new InternalServerErrorException( + return new WorkspaceQueryRunnerException( `GraphQL errors on ${command}${objectName}: ${JSON.stringify(error)}`, + WorkspaceQueryRunnerExceptionCode.INTERNAL_SERVER_ERROR, ); }; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util.ts new file mode 100644 index 000000000000..2a52c5273055 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util.ts @@ -0,0 +1,36 @@ +import { + WorkspaceQueryRunnerException, + WorkspaceQueryRunnerExceptionCode, +} from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception'; +import { + ForbiddenError, + InternalServerError, + NotFoundError, + TimeoutError, + UserInputError, +} from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; + +export const workspaceQueryRunnerGraphqlApiExceptionHandler = ( + error: Error, +) => { + if (error instanceof WorkspaceQueryRunnerException) { + switch (error.code) { + case WorkspaceQueryRunnerExceptionCode.DATA_NOT_FOUND: + throw new NotFoundError(error.message); + case WorkspaceQueryRunnerExceptionCode.INVALID_QUERY_INPUT: + throw new UserInputError(error.message); + case WorkspaceQueryRunnerExceptionCode.QUERY_VIOLATES_UNIQUE_CONSTRAINT: + case WorkspaceQueryRunnerExceptionCode.QUERY_VIOLATES_FOREIGN_KEY_CONSTRAINT: + case WorkspaceQueryRunnerExceptionCode.TOO_MANY_ROWS_AFFECTED: + case WorkspaceQueryRunnerExceptionCode.NO_ROWS_AFFECTED: + throw new ForbiddenError(error.message); + case WorkspaceQueryRunnerExceptionCode.QUERY_TIMEOUT: + throw new TimeoutError(error.message); + case WorkspaceQueryRunnerExceptionCode.INTERNAL_SERVER_ERROR: + default: + throw new InternalServerError(error.message); + } + } + + throw error; +}; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception.ts new file mode 100644 index 000000000000..e750eec1f4a8 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception.ts @@ -0,0 +1,19 @@ +import { CustomException } from 'src/utils/custom-exception'; + +export class WorkspaceQueryRunnerException extends CustomException { + code: WorkspaceQueryRunnerExceptionCode; + constructor(message: string, code: WorkspaceQueryRunnerExceptionCode) { + super(message, code); + } +} + +export enum WorkspaceQueryRunnerExceptionCode { + INVALID_QUERY_INPUT = 'INVALID_FIELD_INPUT', + DATA_NOT_FOUND = 'DATA_NOT_FOUND', + QUERY_TIMEOUT = 'QUERY_TIMEOUT', + QUERY_VIOLATES_UNIQUE_CONSTRAINT = 'QUERY_VIOLATES_UNIQUE_CONSTRAINT', + QUERY_VIOLATES_FOREIGN_KEY_CONSTRAINT = 'QUERY_VIOLATES_FOREIGN_KEY_CONSTRAINT', + TOO_MANY_ROWS_AFFECTED = 'TOO_MANY_ROWS_AFFECTED', + NO_ROWS_AFFECTED = 'NO_ROWS_AFFECTED', + INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR', +} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service.ts index 94010c533c2e..6bb6156fd819 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service.ts @@ -1,9 +1,4 @@ -import { - BadRequestException, - Injectable, - Logger, - RequestTimeoutException, -} from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { EventEmitter2 } from '@nestjs/event-emitter'; import isEmpty from 'lodash.isempty'; @@ -40,8 +35,11 @@ import { import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; import { parseResult } from 'src/engine/api/graphql/workspace-query-runner/utils/parse-result.util'; import { WorkspaceQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service'; +import { + WorkspaceQueryRunnerException, + WorkspaceQueryRunnerExceptionCode, +} from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception'; import { DuplicateService } from 'src/engine/core-modules/duplicate/duplicate.service'; -import { NotFoundError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; import { ObjectRecordCreateEvent } from 'src/engine/integrations/event-emitter/types/object-record-create.event'; import { ObjectRecordDeleteEvent } from 'src/engine/integrations/event-emitter/types/object-record-delete.event'; @@ -138,7 +136,10 @@ export class WorkspaceQueryRunnerService { options: WorkspaceQueryRunnerOptions, ): Promise { if (!args.filter || Object.keys(args.filter).length === 0) { - throw new BadRequestException('Missing filter argument'); + throw new WorkspaceQueryRunnerException( + 'Missing filter argument', + WorkspaceQueryRunnerExceptionCode.INVALID_QUERY_INPUT, + ); } const { workspaceId, userId, objectMetadataItem } = options; @@ -176,14 +177,16 @@ export class WorkspaceQueryRunnerService { options: WorkspaceQueryRunnerOptions, ): Promise | undefined> { if (!args.data && !args.ids) { - throw new BadRequestException( + throw new WorkspaceQueryRunnerException( 'You have to provide either "data" or "id" argument', + WorkspaceQueryRunnerExceptionCode.INVALID_QUERY_INPUT, ); } if (!args.ids && isEmpty(args.data)) { - throw new BadRequestException( + throw new WorkspaceQueryRunnerException( 'The "data" condition can not be empty when ID input not provided', + WorkspaceQueryRunnerExceptionCode.INVALID_QUERY_INPUT, ); } @@ -205,7 +208,10 @@ export class WorkspaceQueryRunnerService { ); if (!existingRecords || existingRecords.length === 0) { - throw new NotFoundError(`Object with id ${args.ids} not found`); + throw new WorkspaceQueryRunnerException( + `Object with id ${args.ids} not found`, + WorkspaceQueryRunnerExceptionCode.DATA_NOT_FOUND, + ); } } @@ -386,7 +392,10 @@ export class WorkspaceQueryRunnerService { }); if (!existingRecord) { - throw new NotFoundError(`Object with id ${args.id} not found`); + throw new WorkspaceQueryRunnerException( + `Object with id ${args.id} not found`, + WorkspaceQueryRunnerExceptionCode.DATA_NOT_FOUND, + ); } const query = await this.workspaceQueryBuilderFactory.updateOne( @@ -681,8 +690,9 @@ export class WorkspaceQueryRunnerService { ); } catch (error) { if (isQueryTimeoutError(error)) { - throw new RequestTimeoutException( + throw new WorkspaceQueryRunnerException( 'The SQL request took too long to process, resulting in a query read timeout. To resolve this issue, consider modifying your query by reducing the depth of relationships or limiting the number of records being fetched.', + WorkspaceQueryRunnerExceptionCode.QUERY_TIMEOUT, ); } @@ -733,7 +743,10 @@ export class WorkspaceQueryRunnerService { ['update', 'deleteFrom'].includes(command) && !result.affectedCount ) { - throw new BadRequestException('No rows were affected.'); + throw new WorkspaceQueryRunnerException( + 'No rows were affected.', + WorkspaceQueryRunnerExceptionCode.NO_ROWS_AFFECTED, + ); } if (errors && errors.length > 0) { diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-many-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-many-resolver.factory.ts index dc1942eccc19..d74d4c6b3917 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-many-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-many-resolver.factory.ts @@ -1,12 +1,13 @@ import { Injectable } from '@nestjs/common'; +import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; import { CreateManyResolverArgs, Resolver, } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface'; -import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; +import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util'; import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service'; @Injectable() @@ -24,15 +25,19 @@ export class CreateManyResolverFactory ): Resolver { const internalContext = context; - return (_source, args, context, info) => { - return this.workspaceQueryRunnerService.createMany(args, { - objectMetadataItem: internalContext.objectMetadataItem, - workspaceId: internalContext.workspaceId, - userId: internalContext.userId, - info, - fieldMetadataCollection: internalContext.fieldMetadataCollection, - objectMetadataCollection: internalContext.objectMetadataCollection, - }); + return async (_source, args, context, info) => { + try { + return await this.workspaceQueryRunnerService.createMany(args, { + objectMetadataItem: internalContext.objectMetadataItem, + workspaceId: internalContext.workspaceId, + userId: internalContext.userId, + info, + fieldMetadataCollection: internalContext.fieldMetadataCollection, + objectMetadataCollection: internalContext.objectMetadataCollection, + }); + } catch (error) { + workspaceQueryRunnerGraphqlApiExceptionHandler(error); + } }; } } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-one-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-one-resolver.factory.ts index 01583f0de9bd..a86787e3943e 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-one-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-one-resolver.factory.ts @@ -1,12 +1,13 @@ import { Injectable } from '@nestjs/common'; +import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; import { CreateOneResolverArgs, Resolver, } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface'; -import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; +import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util'; import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service'; @Injectable() @@ -24,15 +25,19 @@ export class CreateOneResolverFactory ): Resolver { const internalContext = context; - return (_source, args, context, info) => { - return this.workspaceQueryRunnerService.createOne(args, { - objectMetadataItem: internalContext.objectMetadataItem, - workspaceId: internalContext.workspaceId, - userId: internalContext.userId, - info, - fieldMetadataCollection: internalContext.fieldMetadataCollection, - objectMetadataCollection: internalContext.objectMetadataCollection, - }); + return async (_source, args, context, info) => { + try { + return await this.workspaceQueryRunnerService.createOne(args, { + objectMetadataItem: internalContext.objectMetadataItem, + workspaceId: internalContext.workspaceId, + userId: internalContext.userId, + info, + fieldMetadataCollection: internalContext.fieldMetadataCollection, + objectMetadataCollection: internalContext.objectMetadataCollection, + }); + } catch (error) { + workspaceQueryRunnerGraphqlApiExceptionHandler(error); + } }; } } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-many-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-many-resolver.factory.ts index e74685bb12d7..f5d5a0b9f660 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-many-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-many-resolver.factory.ts @@ -1,12 +1,13 @@ import { Injectable } from '@nestjs/common'; +import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; import { DeleteManyResolverArgs, Resolver, } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface'; -import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; +import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util'; import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service'; @Injectable() @@ -24,15 +25,19 @@ export class DeleteManyResolverFactory ): Resolver { const internalContext = context; - return (_source, args, context, info) => { - return this.workspaceQueryRunnerService.deleteMany(args, { - objectMetadataItem: internalContext.objectMetadataItem, - workspaceId: internalContext.workspaceId, - userId: internalContext.userId, - info, - fieldMetadataCollection: internalContext.fieldMetadataCollection, - objectMetadataCollection: internalContext.objectMetadataCollection, - }); + return async (_source, args, context, info) => { + try { + return await this.workspaceQueryRunnerService.deleteMany(args, { + objectMetadataItem: internalContext.objectMetadataItem, + workspaceId: internalContext.workspaceId, + userId: internalContext.userId, + info, + fieldMetadataCollection: internalContext.fieldMetadataCollection, + objectMetadataCollection: internalContext.objectMetadataCollection, + }); + } catch (error) { + workspaceQueryRunnerGraphqlApiExceptionHandler(error); + } }; } } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-one-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-one-resolver.factory.ts index f02676182915..0dc7e20f3594 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-one-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-one-resolver.factory.ts @@ -1,12 +1,13 @@ import { Injectable } from '@nestjs/common'; +import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; import { DeleteOneResolverArgs, Resolver, } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface'; -import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; +import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util'; import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service'; @Injectable() @@ -24,15 +25,19 @@ export class DeleteOneResolverFactory ): Resolver { const internalContext = context; - return (_source, args, context, info) => { - return this.workspaceQueryRunnerService.deleteOne(args, { - objectMetadataItem: internalContext.objectMetadataItem, - workspaceId: internalContext.workspaceId, - userId: internalContext.userId, - info, - fieldMetadataCollection: internalContext.fieldMetadataCollection, - objectMetadataCollection: internalContext.objectMetadataCollection, - }); + return async (_source, args, context, info) => { + try { + return await this.workspaceQueryRunnerService.deleteOne(args, { + objectMetadataItem: internalContext.objectMetadataItem, + workspaceId: internalContext.workspaceId, + userId: internalContext.userId, + info, + fieldMetadataCollection: internalContext.fieldMetadataCollection, + objectMetadataCollection: internalContext.objectMetadataCollection, + }); + } catch (error) { + workspaceQueryRunnerGraphqlApiExceptionHandler(error); + } }; } } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/execute-quick-action-on-one-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/execute-quick-action-on-one-resolver.factory.ts index 3968ed760b3a..1b80ec4e8bde 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/execute-quick-action-on-one-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/execute-quick-action-on-one-resolver.factory.ts @@ -1,16 +1,17 @@ import { Injectable } from '@nestjs/common'; +import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; +import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; +import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; import { - Resolver, - FindOneResolverArgs, - ExecuteQuickActionOnOneResolverArgs, DeleteOneResolverArgs, + ExecuteQuickActionOnOneResolverArgs, + FindOneResolverArgs, + Resolver, } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; -import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface'; -import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; -import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; +import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util'; import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service'; import { QuickActionsService } from 'src/engine/core-modules/quick-actions/quick-actions.service'; @@ -30,15 +31,19 @@ export class ExecuteQuickActionOnOneResolverFactory ): Resolver { const internalContext = context; - return (_source, args, context, info) => { - return this.executeQuickActionOnOne(args, { - objectMetadataItem: internalContext.objectMetadataItem, - userId: internalContext.userId, - workspaceId: internalContext.workspaceId, - info, - fieldMetadataCollection: internalContext.fieldMetadataCollection, - objectMetadataCollection: internalContext.objectMetadataCollection, - }); + return async (_source, args, context, info) => { + try { + return await this.executeQuickActionOnOne(args, { + objectMetadataItem: internalContext.objectMetadataItem, + userId: internalContext.userId, + workspaceId: internalContext.workspaceId, + info, + fieldMetadataCollection: internalContext.fieldMetadataCollection, + objectMetadataCollection: internalContext.objectMetadataCollection, + }); + } catch (error) { + workspaceQueryRunnerGraphqlApiExceptionHandler(error); + } }; } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-duplicates-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-duplicates-resolver.factory.ts index ee288c708efa..e976267995ad 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-duplicates-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-duplicates-resolver.factory.ts @@ -1,12 +1,13 @@ import { Injectable } from '@nestjs/common'; +import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; import { FindDuplicatesResolverArgs, Resolver, } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface'; -import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; +import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util'; import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service'; @Injectable() @@ -24,15 +25,19 @@ export class FindDuplicatesResolverFactory ): Resolver { const internalContext = context; - return (_source, args, context, info) => { - return this.workspaceQueryRunnerService.findDuplicates(args, { - objectMetadataItem: internalContext.objectMetadataItem, - workspaceId: internalContext.workspaceId, - userId: internalContext.userId, - info, - fieldMetadataCollection: internalContext.fieldMetadataCollection, - objectMetadataCollection: internalContext.objectMetadataCollection, - }); + return async (_source, args, context, info) => { + try { + return await this.workspaceQueryRunnerService.findDuplicates(args, { + objectMetadataItem: internalContext.objectMetadataItem, + workspaceId: internalContext.workspaceId, + userId: internalContext.userId, + info, + fieldMetadataCollection: internalContext.fieldMetadataCollection, + objectMetadataCollection: internalContext.objectMetadataCollection, + }); + } catch (error) { + workspaceQueryRunnerGraphqlApiExceptionHandler(error); + } }; } } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-many-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-many-resolver.factory.ts index deead28be2e0..d6322aa3b473 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-many-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-many-resolver.factory.ts @@ -1,12 +1,13 @@ import { Injectable } from '@nestjs/common'; +import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; import { FindManyResolverArgs, Resolver, } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface'; -import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; +import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util'; import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service'; @Injectable() @@ -24,15 +25,19 @@ export class FindManyResolverFactory ): Resolver { const internalContext = context; - return (_source, args, context, info) => { - return this.workspaceQueryRunnerService.findMany(args, { - objectMetadataItem: internalContext.objectMetadataItem, - workspaceId: internalContext.workspaceId, - userId: internalContext.userId, - info, - fieldMetadataCollection: internalContext.fieldMetadataCollection, - objectMetadataCollection: internalContext.objectMetadataCollection, - }); + return async (_source, args, context, info) => { + try { + return await this.workspaceQueryRunnerService.findMany(args, { + objectMetadataItem: internalContext.objectMetadataItem, + workspaceId: internalContext.workspaceId, + userId: internalContext.userId, + info, + fieldMetadataCollection: internalContext.fieldMetadataCollection, + objectMetadataCollection: internalContext.objectMetadataCollection, + }); + } catch (error) { + workspaceQueryRunnerGraphqlApiExceptionHandler(error); + } }; } } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-one-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-one-resolver.factory.ts index 1e8ce6d52da1..fc1fd3d8c3e2 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-one-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-one-resolver.factory.ts @@ -1,12 +1,13 @@ import { Injectable } from '@nestjs/common'; +import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; import { FindOneResolverArgs, Resolver, } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface'; -import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; +import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util'; import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service'; @Injectable() @@ -24,15 +25,19 @@ export class FindOneResolverFactory ): Resolver { const internalContext = context; - return (_source, args, context, info) => { - return this.workspaceQueryRunnerService.findOne(args, { - objectMetadataItem: internalContext.objectMetadataItem, - workspaceId: internalContext.workspaceId, - userId: internalContext.userId, - info, - fieldMetadataCollection: internalContext.fieldMetadataCollection, - objectMetadataCollection: internalContext.objectMetadataCollection, - }); + return async (_source, args, context, info) => { + try { + return await this.workspaceQueryRunnerService.findOne(args, { + objectMetadataItem: internalContext.objectMetadataItem, + workspaceId: internalContext.workspaceId, + userId: internalContext.userId, + info, + fieldMetadataCollection: internalContext.fieldMetadataCollection, + objectMetadataCollection: internalContext.objectMetadataCollection, + }); + } catch (error) { + workspaceQueryRunnerGraphqlApiExceptionHandler(error); + } }; } } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory.ts index 470f4e793531..1f0be848ed47 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory.ts @@ -1,12 +1,13 @@ import { Injectable } from '@nestjs/common'; +import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; import { Resolver, UpdateManyResolverArgs, } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface'; -import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; +import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util'; import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service'; @Injectable() @@ -24,15 +25,19 @@ export class UpdateManyResolverFactory ): Resolver { const internalContext = context; - return (_source, args, context, info) => { - return this.workspaceQueryRunnerService.updateMany(args, { - objectMetadataItem: internalContext.objectMetadataItem, - workspaceId: internalContext.workspaceId, - userId: internalContext.userId, - info, - fieldMetadataCollection: internalContext.fieldMetadataCollection, - objectMetadataCollection: internalContext.objectMetadataCollection, - }); + return async (_source, args, context, info) => { + try { + return await this.workspaceQueryRunnerService.updateMany(args, { + objectMetadataItem: internalContext.objectMetadataItem, + workspaceId: internalContext.workspaceId, + userId: internalContext.userId, + info, + fieldMetadataCollection: internalContext.fieldMetadataCollection, + objectMetadataCollection: internalContext.objectMetadataCollection, + }); + } catch (error) { + workspaceQueryRunnerGraphqlApiExceptionHandler(error); + } }; } } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-one-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-one-resolver.factory.ts index 0851c54c8007..92b2a81d5fa6 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-one-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-one-resolver.factory.ts @@ -1,12 +1,13 @@ import { Injectable } from '@nestjs/common'; +import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; import { Resolver, UpdateOneResolverArgs, } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface'; -import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; +import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util'; import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service'; @Injectable() @@ -24,15 +25,19 @@ export class UpdateOneResolverFactory ): Resolver { const internalContext = context; - return (_source, args, context, info) => { - return this.workspaceQueryRunnerService.updateOne(args, { - objectMetadataItem: internalContext.objectMetadataItem, - workspaceId: internalContext.workspaceId, - userId: internalContext.userId, - info, - fieldMetadataCollection: internalContext.fieldMetadataCollection, - objectMetadataCollection: internalContext.objectMetadataCollection, - }); + return async (_source, args, context, info) => { + try { + return await this.workspaceQueryRunnerService.updateOne(args, { + objectMetadataItem: internalContext.objectMetadataItem, + workspaceId: internalContext.workspaceId, + userId: internalContext.userId, + info, + fieldMetadataCollection: internalContext.fieldMetadataCollection, + objectMetadataCollection: internalContext.objectMetadataCollection, + }); + } catch (error) { + workspaceQueryRunnerGraphqlApiExceptionHandler(error); + } }; } } diff --git a/packages/twenty-server/src/engine/integrations/exception-handler/hooks/use-exception-handler.hook.ts b/packages/twenty-server/src/engine/integrations/exception-handler/hooks/use-exception-handler.hook.ts deleted file mode 100644 index bb4522796c34..000000000000 --- a/packages/twenty-server/src/engine/integrations/exception-handler/hooks/use-exception-handler.hook.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { - OnExecuteDoneHookResultOnNextHook, - Plugin, - getDocumentString, - handleStreamOrSingleExecutionResult, -} from '@envelop/core'; -import { GraphQLError, Kind, OperationDefinitionNode, print } from 'graphql'; - -import { GraphQLContext } from 'src/engine/api/graphql/graphql-config/interfaces/graphql-context.interface'; - -import { ExceptionHandlerService } from 'src/engine/integrations/exception-handler/exception-handler.service'; -import { - convertExceptionToGraphQLError, - shouldFilterException, -} from 'src/engine/utils/global-exception-handler.util'; - -export type ExceptionHandlerPluginOptions = { - /** - * The exception handler service to use. - */ - exceptionHandlerService: ExceptionHandlerService; - /** - * The key of the event id in the error's extension. `null` to disable. - * @default exceptionEventId - */ - eventIdKey?: string | null; -}; - -// This hook is deprecated. -// We should either handle exception in the context of graphql, controller or command -// @deprecated -export const useExceptionHandler = ( - options: ExceptionHandlerPluginOptions, -): Plugin => { - const eventIdKey = options.eventIdKey === null ? null : 'exceptionEventId'; - - function addEventId( - err: GraphQLError, - eventId: string | undefined | null, - ): GraphQLError { - if (eventIdKey !== null && eventId) { - err.extensions[eventIdKey] = eventId; - } - - return err; - } - - return { - async onExecute({ args }) { - const exceptionHandlerService = options.exceptionHandlerService; - const rootOperation = args.document.definitions.find( - (o) => o.kind === Kind.OPERATION_DEFINITION, - ) as OperationDefinitionNode; - const operationType = rootOperation.operation; - const user = args.contextValue.req.user; - const document = getDocumentString(args.document, print); - const opName = - args.operationName || - rootOperation.name?.value || - 'Anonymous Operation'; - - return { - onExecuteDone(payload) { - const handleResult: OnExecuteDoneHookResultOnNextHook = ({ - result, - setResult, - }) => { - if (result.errors && result.errors.length > 0) { - const exceptions = result.errors.reduce<{ - filtered: any[]; - unfiltered: any[]; - }>( - (acc, err) => { - // Filter out exceptions that we don't want to be captured by exception handler - if (shouldFilterException(err?.originalError ?? err)) { - acc.filtered.push(err); - } else { - acc.unfiltered.push(err); - } - - return acc; - }, - { - filtered: [], - unfiltered: [], - }, - ); - - if (exceptions.unfiltered.length > 0) { - const eventIds = exceptionHandlerService.captureExceptions( - exceptions.unfiltered, - { - operation: { - name: opName, - type: operationType, - }, - document, - user, - }, - ); - - exceptions.unfiltered.map((err, i) => - addEventId(err, eventIds?.[i]), - ); - } - - const concatenatedErrors = [ - ...exceptions.filtered, - ...exceptions.unfiltered, - ]; - const errors = concatenatedErrors.map((err) => { - if (!err.originalError) { - return err; - } - - return convertExceptionToGraphQLError(err.originalError); - }); - - setResult({ - ...result, - errors, - }); - } - }; - - return handleStreamOrSingleExecutionResult(payload, handleResult); - }, - }; - }, - onValidate: ({ context, validateFn, params: { documentAST, schema } }) => { - const errors = validateFn(schema, documentAST); - - if (Array.isArray(errors) && errors.length > 0) { - const headers = context.req.headers; - const currentSchemaVersion = context.req.cacheVersion; - - const requestSchemaVersion = headers['x-schema-version']; - - if ( - requestSchemaVersion && - requestSchemaVersion !== currentSchemaVersion - ) { - throw new GraphQLError( - `Schema version mismatch, please refresh the page.`, - ); - } - } - }, - }; -};