From 23452905f33e6d3c278769e4b738f27dbd895967 Mon Sep 17 00:00:00 2001 From: Park Se Hwan <64393421+SH9480P@users.noreply.github.com> Date: Sat, 3 Feb 2024 18:43:28 +0900 Subject: [PATCH] feat(be): implement GraphQL Exceptions (#1267) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: create transport exceptions for GraphQL Request * feat: add exception conversion onto the transport level * feat: add optional exception message setting argument * refactor: add default message to InternalServerGraphQLError * refactor: replace error throwing logic in controllers by using convert2~ methods * refactor: change exception name * feat: add apollo server error formatter HTTPException의 상태 코드를 GQL 에러 코드로 변환시켜줌 * fix: make resolvers throw HTTP Exceptions * fix: delete status property * refactor: refactor apollo-error-formatter * fix: change Gql Error to HTTP Error * refactor: delete unused class method * refactor: delete unused exceptions - Custom GraphQL Errors 삭제 - 파일 이름 변경 (graphql-error.exception -> graphql-error-code) --------- Co-authored-by: Jaehyeon Kim <65964601+Jaehyeon1020@users.noreply.github.com> --- backend/apps/admin/src/admin.module.ts | 4 +- .../admin/src/contest/contest.resolver.ts | 38 ++++++------ .../apps/admin/src/group/group.resolver.ts | 22 +++---- .../admin/src/problem/problem.resolver.ts | 51 ++++++++------- .../announcement/announcement.controller.ts | 3 +- .../apps/client/src/auth/auth.controller.ts | 8 +-- .../client/src/contest/contest.controller.ts | 3 +- .../apps/client/src/group/group.controller.ts | 15 +++-- .../client/src/problem/problem.controller.ts | 6 +- .../src/submission/submission.controller.ts | 8 +-- .../apps/client/src/user/user.controller.ts | 62 ++++++++++--------- .../exception/src/apollo-error-formatter.ts | 22 +++++++ .../libs/exception/src/business.exception.ts | 43 +++++++++++-- .../libs/exception/src/graphql-error-code.ts | 30 +++++++++ backend/libs/exception/src/index.ts | 2 + 15 files changed, 201 insertions(+), 116 deletions(-) create mode 100644 backend/libs/exception/src/apollo-error-formatter.ts create mode 100644 backend/libs/exception/src/graphql-error-code.ts diff --git a/backend/apps/admin/src/admin.module.ts b/backend/apps/admin/src/admin.module.ts index b1412d00f2..22323a1abf 100644 --- a/backend/apps/admin/src/admin.module.ts +++ b/backend/apps/admin/src/admin.module.ts @@ -12,6 +12,7 @@ import { GroupLeaderGuard } from '@libs/auth' import { CacheConfigService } from '@libs/cache' +import { apolloErrorFormatter } from '@libs/exception' import { pinoLoggerModuleOption } from '@libs/logger' import { PrismaModule } from '@libs/prisma' import { AdminController } from './admin.controller' @@ -34,7 +35,8 @@ import { UserModule } from './user/user.module' driver: ApolloDriver, autoSchemaFile: 'schema.gql', sortSchema: true, - introspection: true + introspection: true, + formatError: apolloErrorFormatter }), CacheModule.registerAsync({ isGlobal: true, diff --git a/backend/apps/admin/src/contest/contest.resolver.ts b/backend/apps/admin/src/contest/contest.resolver.ts index 836b289ebb..4c34194709 100644 --- a/backend/apps/admin/src/contest/contest.resolver.ts +++ b/backend/apps/admin/src/contest/contest.resolver.ts @@ -1,10 +1,7 @@ import { - BadRequestException, InternalServerErrorException, Logger, - NotFoundException, - ParseBoolPipe, - UnprocessableEntityException + ParseBoolPipe } from '@nestjs/common' import { Args, Context, Int, Mutation, Query, Resolver } from '@nestjs/graphql' import { AuthenticatedRequest, UseRolesGuard } from '@libs/auth' @@ -61,10 +58,11 @@ export class ContestResolver { input ) } catch (error) { - if (error instanceof UnprocessableDataException) { - throw new UnprocessableEntityException(error.message) - } else if (error instanceof EntityNotExistException) { - throw new NotFoundException(error.message) + if ( + error instanceof UnprocessableDataException || + error instanceof EntityNotExistException + ) { + throw error.convert2HTTPException() } this.logger.error(error) throw new InternalServerErrorException() @@ -79,10 +77,11 @@ export class ContestResolver { try { return await this.contestService.updateContest(groupId, input) } catch (error) { - if (error instanceof EntityNotExistException) { - throw new NotFoundException(error.message) - } else if (error instanceof UnprocessableDataException) { - throw new UnprocessableEntityException(error.message) + if ( + error instanceof EntityNotExistException || + error instanceof UnprocessableDataException + ) { + throw error.convert2HTTPException() } this.logger.error(error) throw new InternalServerErrorException() @@ -98,7 +97,7 @@ export class ContestResolver { return await this.contestService.deleteContest(groupId, contestId) } catch (error) { if (error instanceof EntityNotExistException) { - throw new NotFoundException(error.message) + throw error.convert2HTTPException() } this.logger.error(error) throw new InternalServerErrorException() @@ -122,10 +121,11 @@ export class ContestResolver { contestId ) } catch (error) { - if (error instanceof EntityNotExistException) { - throw new NotFoundException(error.message) - } else if (error instanceof ConflictFoundException) { - throw new BadRequestException(error.message) + if ( + error instanceof EntityNotExistException || + error instanceof ConflictFoundException + ) { + throw error.convert2HTTPException() } this.logger.error(error) throw new InternalServerErrorException() @@ -145,7 +145,7 @@ export class ContestResolver { ) } catch (error) { if (error instanceof EntityNotExistException) { - throw new NotFoundException(error.message) + throw error.convert2HTTPException() } this.logger.error(error) throw new InternalServerErrorException() @@ -166,7 +166,7 @@ export class ContestResolver { ) } catch (error) { if (error instanceof EntityNotExistException) { - throw new NotFoundException(error.message) + throw error.convert2HTTPException() } this.logger.error(error) throw new InternalServerErrorException() diff --git a/backend/apps/admin/src/group/group.resolver.ts b/backend/apps/admin/src/group/group.resolver.ts index 76f981cc4e..31eeb01d74 100644 --- a/backend/apps/admin/src/group/group.resolver.ts +++ b/backend/apps/admin/src/group/group.resolver.ts @@ -1,9 +1,4 @@ -import { - ConflictException, - ForbiddenException, - InternalServerErrorException, - Logger -} from '@nestjs/common' +import { InternalServerErrorException, Logger } from '@nestjs/common' import { Args, Int, Query, Mutation, Resolver, Context } from '@nestjs/graphql' import { Group } from '@generated' import { Role } from '@prisma/client' @@ -34,7 +29,7 @@ export class GroupResolver { return await this.groupService.createGroup(input, req.user.id) } catch (error) { if (error instanceof DuplicateFoundException) { - throw new ConflictException(error.message) + throw error.convert2HTTPException() } this.logger.error(error) throw new InternalServerErrorException() @@ -66,10 +61,11 @@ export class GroupResolver { try { return await this.groupService.updateGroup(id, input) } catch (error) { - if (error instanceof DuplicateFoundException) { - throw new ConflictException(error.message) - } else if (error instanceof ForbiddenAccessException) { - throw new ForbiddenException(error.message) + if ( + error instanceof DuplicateFoundException || + error instanceof ForbiddenAccessException + ) { + throw error.convert2HTTPException() } this.logger.error(error) throw new InternalServerErrorException() @@ -85,7 +81,7 @@ export class GroupResolver { return await this.groupService.deleteGroup(id, req.user) } catch (error) { if (error instanceof ForbiddenAccessException) { - throw new ForbiddenException(error.message) + throw error.convert2HTTPException() } this.logger.error(error) throw new InternalServerErrorException() @@ -100,7 +96,7 @@ export class GroupResolver { return await this.groupService.issueInvitation(id) } catch (error) { if (error instanceof ConflictFoundException) { - throw new ConflictException(error.message) + throw error.convert2HTTPException() } this.logger.error(error) throw new InternalServerErrorException() diff --git a/backend/apps/admin/src/problem/problem.resolver.ts b/backend/apps/admin/src/problem/problem.resolver.ts index fbd708d92b..53886ddd9d 100644 --- a/backend/apps/admin/src/problem/problem.resolver.ts +++ b/backend/apps/admin/src/problem/problem.resolver.ts @@ -1,6 +1,4 @@ import { - ConflictException, - ForbiddenException, InternalServerErrorException, Logger, NotFoundException, @@ -54,7 +52,7 @@ export class ProblemResolver { ) } catch (error) { if (error instanceof UnprocessableDataException) { - throw new UnprocessableEntityException(error.message) + throw error.convert2HTTPException() } else if ( error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2003' @@ -86,7 +84,7 @@ export class ProblemResolver { ) } catch (error) { if (error instanceof UnprocessableDataException) { - throw new UnprocessableEntityException(error.message) + throw error.convert2HTTPException() } this.logger.error(error) throw new InternalServerErrorException() @@ -146,16 +144,17 @@ export class ProblemResolver { try { return await this.problemService.updateProblem(input, groupId) } catch (error) { - if (error instanceof UnprocessableDataException) { - throw new UnprocessableEntityException(error.message) + if ( + error instanceof UnprocessableDataException || + error instanceof ConflictFoundException + ) { + throw error.convert2HTTPException() } else if (error instanceof Prisma.PrismaClientKnownRequestError) { if (error.name == 'NotFoundError') { throw new NotFoundException(error.message) } else if (error.code === 'P2003') { throw new UnprocessableEntityException(error.message) } - } else if (error instanceof ConflictFoundException) { - throw new ConflictException(error.message) } this.logger.error(error) throw new InternalServerErrorException() @@ -199,10 +198,11 @@ export class ProblemResolver { try { return this.problemService.getWorkbookProblems(groupId, workbookId) } catch (error) { - if (error instanceof UnprocessableDataException) { - throw new UnprocessableEntityException(error.message) - } else if (error instanceof ForbiddenAccessException) { - throw new ForbiddenException(error.message) + if ( + error instanceof UnprocessableDataException || + error instanceof ForbiddenAccessException + ) { + throw error.convert2HTTPException() } else if (error.code == 'P2025') { throw new EntityNotExistException(error.message) } @@ -230,10 +230,11 @@ export class ProblemResolver { orders ) } catch (error) { - if (error instanceof UnprocessableDataException) { - throw new UnprocessableEntityException(error.message) - } else if (error instanceof ForbiddenAccessException) { - throw new ForbiddenException(error.message) + if ( + error instanceof UnprocessableDataException || + error instanceof ForbiddenAccessException + ) { + throw error.convert2HTTPException() } else if (error.code == 'P2025') { throw new EntityNotExistException(error.message) } @@ -255,10 +256,11 @@ export class ProblemResolver { try { return this.problemService.getContestProblems(groupId, contestId) } catch (error) { - if (error instanceof UnprocessableDataException) { - throw new UnprocessableEntityException(error.message) - } else if (error instanceof ForbiddenAccessException) { - throw new ForbiddenException(error.message) + if ( + error instanceof UnprocessableDataException || + error instanceof ForbiddenAccessException + ) { + throw error.convert2HTTPException() } else if (error.code == 'P2025') { throw new EntityNotExistException(error.message) } @@ -285,10 +287,11 @@ export class ProblemResolver { orders ) } catch (error) { - if (error instanceof UnprocessableDataException) { - throw new UnprocessableEntityException(error.message) - } else if (error instanceof ForbiddenAccessException) { - throw new ForbiddenException(error.message) + if ( + error instanceof UnprocessableDataException || + error instanceof ForbiddenAccessException + ) { + throw error.convert2HTTPException() } else if (error.code == 'P2025') { throw new EntityNotExistException(error.message) } diff --git a/backend/apps/client/src/announcement/announcement.controller.ts b/backend/apps/client/src/announcement/announcement.controller.ts index 32fa0de154..cddaac8550 100644 --- a/backend/apps/client/src/announcement/announcement.controller.ts +++ b/backend/apps/client/src/announcement/announcement.controller.ts @@ -2,7 +2,6 @@ import { Controller, Get, Logger, - NotFoundException, InternalServerErrorException, Query, BadRequestException @@ -39,7 +38,7 @@ export class AnnouncementController { } } catch (error) { if (error instanceof EntityNotExistException) { - throw new NotFoundException(error.message) + throw error.convert2HTTPException() } this.logger.error(error) throw new InternalServerErrorException() diff --git a/backend/apps/client/src/auth/auth.controller.ts b/backend/apps/client/src/auth/auth.controller.ts index 3d0be20d18..5132dd4ef7 100644 --- a/backend/apps/client/src/auth/auth.controller.ts +++ b/backend/apps/client/src/auth/auth.controller.ts @@ -52,7 +52,7 @@ export class AuthController { this.setJwtResponse(res, jwtTokens) } catch (error) { if (error instanceof UnidentifiedException) { - throw new UnauthorizedException(error.message) + throw error.convert2HTTPException() } this.logger.error(error) throw new InternalServerErrorException('Login failed') @@ -87,7 +87,7 @@ export class AuthController { this.setJwtResponse(res, newJwtTokens) } catch (error) { if (error instanceof InvalidJwtTokenException) { - throw new UnauthorizedException(error.message) + throw error.convert2HTTPException() } this.logger.error(error) throw new InternalServerErrorException('Failed to reissue tokens') @@ -114,7 +114,7 @@ export class AuthController { return await this.authService.githubLogin(res, githubUser) } catch (error) { if (error instanceof UnidentifiedException) { - throw new UnauthorizedException(error.message) + throw error.convert2HTTPException() } this.logger.error(error) throw new InternalServerErrorException('Login failed') @@ -142,7 +142,7 @@ export class AuthController { return await this.authService.kakaoLogin(res, kakaoUser) } catch (error) { if (error instanceof UnidentifiedException) { - throw new UnauthorizedException(error.message) + throw error.convert2HTTPException() } this.logger.error(error) throw new InternalServerErrorException('Login failed') diff --git a/backend/apps/client/src/contest/contest.controller.ts b/backend/apps/client/src/contest/contest.controller.ts index 0ad242428f..df15a84779 100644 --- a/backend/apps/client/src/contest/contest.controller.ts +++ b/backend/apps/client/src/contest/contest.controller.ts @@ -8,7 +8,6 @@ import { Get, Query, Logger, - ConflictException, DefaultValuePipe } from '@nestjs/common' import { Prisma } from '@prisma/client' @@ -119,7 +118,7 @@ export class ContestController { ) { throw new NotFoundException(error.message) } else if (error instanceof ConflictFoundException) { - throw new ConflictException(error.message) + throw error.convert2HTTPException() } this.logger.error(error) throw new InternalServerErrorException(error.message) diff --git a/backend/apps/client/src/group/group.controller.ts b/backend/apps/client/src/group/group.controller.ts index 36405daf77..8a6815d413 100644 --- a/backend/apps/client/src/group/group.controller.ts +++ b/backend/apps/client/src/group/group.controller.ts @@ -1,9 +1,7 @@ import { - ConflictException, Controller, DefaultValuePipe, Delete, - ForbiddenException, Get, InternalServerErrorException, Logger, @@ -95,7 +93,7 @@ export class GroupController { ) { throw new NotFoundException('Invalid invitation') } else if (error instanceof EntityNotExistException) { - throw new NotFoundException(error.message) + throw error.convert2HTTPException() } this.logger.error(error) throw new InternalServerErrorException() @@ -120,10 +118,11 @@ export class GroupController { error.name === 'NotFoundError' ) { throw new NotFoundException(error.message) - } else if (error instanceof ForbiddenAccessException) { - throw new ForbiddenException(error.message) - } else if (error instanceof ConflictFoundException) { - throw new ConflictException(error.message) + } else if ( + error instanceof ForbiddenAccessException || + error instanceof ConflictFoundException + ) { + throw error.convert2HTTPException() } this.logger.error(error) throw new InternalServerErrorException() @@ -140,7 +139,7 @@ export class GroupController { return await this.groupService.leaveGroup(req.user.id, groupId) } catch (error) { if (error instanceof ConflictFoundException) { - throw new ConflictException(error.message) + throw error.convert2HTTPException() } this.logger.error(error) diff --git a/backend/apps/client/src/problem/problem.controller.ts b/backend/apps/client/src/problem/problem.controller.ts index f55579dcf8..20cbf11db6 100644 --- a/backend/apps/client/src/problem/problem.controller.ts +++ b/backend/apps/client/src/problem/problem.controller.ts @@ -1,8 +1,6 @@ import { - BadRequestException, Controller, DefaultValuePipe, - ForbiddenException, Get, InternalServerErrorException, Logger, @@ -84,7 +82,7 @@ export class ProblemController { ) { throw new NotFoundException(error.message) } else if (error instanceof ForbiddenAccessException) { - throw new ForbiddenException(error.message) + throw error.convert2HTTPException() } this.logger.error(error) throw new InternalServerErrorException() @@ -121,7 +119,7 @@ export class ProblemController { ) { throw new NotFoundException(error.message) } else if (error instanceof ForbiddenAccessException) { - throw new BadRequestException(error.message) + throw error.convert2HTTPException() } this.logger.error(error) throw new InternalServerErrorException() diff --git a/backend/apps/client/src/submission/submission.controller.ts b/backend/apps/client/src/submission/submission.controller.ts index 562a0ac1de..65e7044e49 100644 --- a/backend/apps/client/src/submission/submission.controller.ts +++ b/backend/apps/client/src/submission/submission.controller.ts @@ -7,8 +7,6 @@ import { Req, NotFoundException, InternalServerErrorException, - ConflictException, - ForbiddenException, Logger, Query, DefaultValuePipe @@ -71,7 +69,7 @@ export class SubmissionController { } } catch (error) { if (error instanceof ConflictFoundException) { - throw new ConflictException(error.message) + throw error.convert2HTTPException() } if ( (error instanceof Prisma.PrismaClientKnownRequestError && @@ -120,7 +118,7 @@ export class SubmissionController { ) { throw new NotFoundException(error.message) } else if (error instanceof ForbiddenAccessException) { - throw new ForbiddenException(error.message) + throw error.convert2HTTPException() } this.logger.error(error) throw new InternalServerErrorException() @@ -159,7 +157,7 @@ export class SubmissionController { ) { throw new NotFoundException(error.message) } else if (error instanceof ForbiddenAccessException) { - throw new ForbiddenException(error.message) + throw error.convert2HTTPException() } this.logger.error(error) throw new InternalServerErrorException() diff --git a/backend/apps/client/src/user/user.controller.ts b/backend/apps/client/src/user/user.controller.ts index 71aa06d12c..0664c3508b 100644 --- a/backend/apps/client/src/user/user.controller.ts +++ b/backend/apps/client/src/user/user.controller.ts @@ -3,7 +3,6 @@ import { Get, InternalServerErrorException, Patch, - UnprocessableEntityException, Post, Req, Res, @@ -11,7 +10,6 @@ import { Controller, NotFoundException, Logger, - ConflictException, Delete, Query } from '@nestjs/common' @@ -51,10 +49,11 @@ export class UserController { try { return await this.userService.updatePassword(newPasswordDto, req) } catch (error) { - if (error instanceof UnidentifiedException) { - throw new UnauthorizedException(error.message) - } else if (error instanceof UnprocessableDataException) { - throw new UnprocessableEntityException(error.message) + if ( + error instanceof UnidentifiedException || + error instanceof UnprocessableDataException + ) { + throw error.convert2HTTPException() } this.logger.error(error) throw new InternalServerErrorException('password reset failed') @@ -67,12 +66,12 @@ export class UserController { try { await this.userService.signUp(req, signUpDto) } catch (error) { - if (error instanceof UnprocessableDataException) { - throw new UnprocessableEntityException(error.message) - } else if (error instanceof DuplicateFoundException) { - throw new ConflictException(error.message) - } else if (error instanceof InvalidJwtTokenException) { - throw new UnauthorizedException(error.message) + if ( + error instanceof UnprocessableDataException || + error instanceof DuplicateFoundException || + error instanceof InvalidJwtTokenException + ) { + throw error.convert2HTTPException() } this.logger.error(error) throw new InternalServerErrorException() @@ -85,12 +84,12 @@ export class UserController { try { return await this.userService.socialSignUp(socialSignUpDto) } catch (error) { - if (error instanceof UnprocessableDataException) { - throw new UnprocessableEntityException(error.message) - } else if (error instanceof DuplicateFoundException) { - throw new ConflictException(error.message) - } else if (error instanceof InvalidJwtTokenException) { - throw new UnauthorizedException(error.message) + if ( + error instanceof UnprocessableDataException || + error instanceof DuplicateFoundException || + error instanceof InvalidJwtTokenException + ) { + throw error.convert2HTTPException() } this.logger.error(error) throw new InternalServerErrorException() @@ -110,12 +109,14 @@ export class UserController { } catch (error) { if ( error instanceof UnidentifiedException || - (error instanceof Prisma.PrismaClientKnownRequestError && - error.name === 'RecordNotFound') + error instanceof ConflictFoundException + ) { + throw error.convert2HTTPException() + } else if ( + error instanceof Prisma.PrismaClientKnownRequestError && + error.name === 'RecordNotFound' ) { throw new UnauthorizedException(error.message) - } else if (error instanceof ConflictFoundException) { - throw new ConflictException(error.message) } this.logger.error(error) throw new InternalServerErrorException() @@ -146,10 +147,11 @@ export class UserController { try { return await this.userService.updateUserEmail(req, updateUserEmail) } catch (error) { - if (error instanceof UnprocessableDataException) { - throw new UnprocessableEntityException(error.message) - } else if (error instanceof InvalidJwtTokenException) { - throw new UnauthorizedException(error.message) + if ( + error instanceof UnprocessableDataException || + error instanceof InvalidJwtTokenException + ) { + throw error.convert2HTTPException() } else if ( error instanceof Prisma.PrismaClientKnownRequestError && error.name === 'NotFoundError' @@ -190,7 +192,7 @@ export class UserController { return await this.userService.checkDuplicatedUsername(usernameDto) } catch (error) { if (error instanceof DuplicateFoundException) { - throw new ConflictException(error.message) + throw error.convert2HTTPException() } this.logger.error(error) throw new InternalServerErrorException() @@ -215,7 +217,7 @@ export class EmailAuthenticationController { return await this.userService.sendPinForPasswordReset(userEmailDto) } catch (error) { if (error instanceof UnidentifiedException) { - throw new UnauthorizedException(error.message) + throw error.convert2HTTPException() } this.logger.error(error) throw new InternalServerErrorException() @@ -228,7 +230,7 @@ export class EmailAuthenticationController { return await this.userService.sendPinForRegisterNewEmail(userEmailDto) } catch (error) { if (error instanceof DuplicateFoundException) { - throw new ConflictException(error.message) + throw error.convert2HTTPException() } this.logger.error(error) throw new InternalServerErrorException(error.message) @@ -247,7 +249,7 @@ export class EmailAuthenticationController { this.setJwtInHeader(res, jwt) } catch (error) { if (error instanceof UnidentifiedException) { - throw new UnauthorizedException(error.message) + throw error.convert2HTTPException() } this.logger.error(error) throw new InternalServerErrorException() diff --git a/backend/libs/exception/src/apollo-error-formatter.ts b/backend/libs/exception/src/apollo-error-formatter.ts new file mode 100644 index 0000000000..2eae2f23c5 --- /dev/null +++ b/backend/libs/exception/src/apollo-error-formatter.ts @@ -0,0 +1,22 @@ +import { HttpStatus } from '@nestjs/common' +import type { GraphQLFormattedError } from 'graphql/error/GraphQLError' +import { GqlErrorCodeMapTable } from './graphql-error-code' + +export const apolloErrorFormatter = ( + formattedError: GraphQLFormattedError +): GraphQLFormattedError => { + if (formattedError.extensions?.originalError) { + const { extensions, ...restFormattedError } = formattedError + const httpStatusCode = + (extensions?.originalError as { statusCode: number })?.statusCode ?? + HttpStatus.INTERNAL_SERVER_ERROR + return { + ...restFormattedError, + extensions: { + code: GqlErrorCodeMapTable[httpStatusCode], + stacktrace: extensions?.stacktrace + } + } + } + return formattedError +} diff --git a/backend/libs/exception/src/business.exception.ts b/backend/libs/exception/src/business.exception.ts index ac8c5c94b1..594132aa22 100644 --- a/backend/libs/exception/src/business.exception.ts +++ b/backend/libs/exception/src/business.exception.ts @@ -1,10 +1,21 @@ -class BusinessException extends Error { +import { + UnauthorizedException, + type HttpException, + NotFoundException, + ConflictException, + UnprocessableEntityException, + ForbiddenException +} from '@nestjs/common/exceptions' + +abstract class BusinessException extends Error { name: string constructor(message: string) { super(message) this.name = this.constructor.name } + + abstract convert2HTTPException(message?: string): HttpException } /** [401] Throw when a user cannot be identified with given credential. */ @@ -12,6 +23,10 @@ export class UnidentifiedException extends BusinessException { constructor(credential) { super(`Incorrect ${credential}`) } + + convert2HTTPException(message?: string) { + return new UnauthorizedException(message ?? this.message) + } } /** [401] Throw when JWT token is invalid. */ @@ -19,6 +34,10 @@ export class InvalidJwtTokenException extends BusinessException { constructor(message) { super(`Invalid token: ${message}`) } + + convert2HTTPException(message?: string) { + return new UnauthorizedException(message ?? this.message) + } } /** [404] Throw when requested entity is not found. */ @@ -26,12 +45,20 @@ export class EntityNotExistException extends BusinessException { constructor(entity) { super(`${entity} does not exist`) } + + convert2HTTPException(message?: string) { + return new NotFoundException(message ?? this.message) + } } /** [409] Throw when the request has a conflict with relevant entities. * e.g., participation is not allowed to ended contest. */ -export class ConflictFoundException extends BusinessException {} +export class ConflictFoundException extends BusinessException { + convert2HTTPException(message?: string) { + return new ConflictException(message ?? this.message) + } +} /** [409] Throw when the request has a conflict with relevant entities. * e.g., group name is already in use @@ -43,7 +70,11 @@ export class DuplicateFoundException extends ConflictFoundException { } /** [422] Throw when data is invalid or cannot be processed. */ -export class UnprocessableDataException extends BusinessException {} +export class UnprocessableDataException extends BusinessException { + convert2HTTPException(message?: string) { + return new UnprocessableEntityException(message ?? this.message) + } +} /** [422] Throw when file data is invalid or cannot be processed. */ export class UnprocessableFileDataException extends UnprocessableDataException { @@ -53,4 +84,8 @@ export class UnprocessableFileDataException extends UnprocessableDataException { } /** [403] Throw when request cannot be carried due to lack of permission. */ -export class ForbiddenAccessException extends BusinessException {} +export class ForbiddenAccessException extends BusinessException { + convert2HTTPException(message?: string) { + return new ForbiddenException(message ?? this.message) + } +} diff --git a/backend/libs/exception/src/graphql-error-code.ts b/backend/libs/exception/src/graphql-error-code.ts new file mode 100644 index 0000000000..27fd7a12d3 --- /dev/null +++ b/backend/libs/exception/src/graphql-error-code.ts @@ -0,0 +1,30 @@ +import { HttpStatus } from '@nestjs/common' +import { ApolloServerErrorCode } from '@apollo/server/errors' + +enum GraphQLErrorCode { + /** The GraphQL operation includes an invalid value for a field argument. */ + badUserInput = ApolloServerErrorCode.BAD_USER_INPUT, + /** An unspecified error occurred. When Apollo Server formats an error in a response, it sets the code extension to this value if no other code is set. */ + internalServerError = ApolloServerErrorCode.INTERNAL_SERVER_ERROR, + /** An error occurred before your server could attempt to parse the given GraphQL operation. */ + badRequest = ApolloServerErrorCode.BAD_REQUEST, + /** User do not have a valid credential */ + unauthenticated = 'UNAUTHENTICATED', + /** Not enough permission to conduct the operation */ + forbidden = 'FORBIDDEN', + /** Resource not found */ + notFound = 'NOT_FOUND', + /** User's request makes some conflicts on the server resources or policies */ + conflict = 'CONFLICT', + /** Unable to process the contained instruction */ + unprocessable = 'UNPROCESSABLE' +} + +export const GqlErrorCodeMapTable: Partial> = { + [HttpStatus.BAD_REQUEST]: GraphQLErrorCode.badRequest, + [HttpStatus.UNAUTHORIZED]: GraphQLErrorCode.unauthenticated, + [HttpStatus.FORBIDDEN]: GraphQLErrorCode.unauthenticated, + [HttpStatus.NOT_FOUND]: GraphQLErrorCode.notFound, + [HttpStatus.CONFLICT]: GraphQLErrorCode.conflict, + [HttpStatus.UNPROCESSABLE_ENTITY]: GraphQLErrorCode.unprocessable +} diff --git a/backend/libs/exception/src/index.ts b/backend/libs/exception/src/index.ts index 02a76fc3e7..93fe5bac7a 100644 --- a/backend/libs/exception/src/index.ts +++ b/backend/libs/exception/src/index.ts @@ -1 +1,3 @@ export * from './business.exception' +export * from './graphql-error-code' +export * from './apollo-error-formatter'