Skip to content

Commit

Permalink
feat: add validation exception (#43)
Browse files Browse the repository at this point in the history
  • Loading branch information
lamngockhuong authored Jun 22, 2024
1 parent 0074efa commit 5d7c1e3
Show file tree
Hide file tree
Showing 19 changed files with 107 additions and 47 deletions.
2 changes: 0 additions & 2 deletions jest.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,7 @@
"^@/generated(.*)$": "<rootDir>/generated$1",
"^@/guards(.*)$": "<rootDir>/guards$1",
"^@/interceptors(.*)$": "<rootDir>/api$1",
"^@/interfaces(.*)$": "<rootDir>/interfaces$1",
"^@/shared(.*)$": "<rootDir>/shared$1",
"^@/types(.*)$": "<rootDir>/types$1",
"^@/utils(.*)$": "<rootDir>/utils$1",
"^@/validators(.*)$": "<rootDir>/validators$1"
},
Expand Down
8 changes: 3 additions & 5 deletions src/api/user/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { UserEntity } from './entities/user.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { plainToInstance } from 'class-transformer';
import { UserDto } from './dto/user.dto';
import { I18nContext } from 'nestjs-i18n';
import { I18nTranslations } from '@/generated/i18n.generated';
import { ErrorCode } from '@/constants/error-code.constant';
import { ValidationException } from '@/exceptions/validation.exception';

@Injectable()
export class UserService {
Expand All @@ -16,8 +16,6 @@ export class UserService {
private readonly userRepository: Repository<UserEntity>,
) {}
async create(dto: CreateUserDto): Promise<UserDto> {
const i18n = I18nContext.current<I18nTranslations>();

const { username, email, password } = dto;

// check uniqueness of username/email
Expand All @@ -33,7 +31,7 @@ export class UserService {
});

if (user) {
throw new Error(i18n.t('validation.user.errors.userAlreadyExists'));
throw new ValidationException(ErrorCode.E001);
}

const newUser = new UserEntity({
Expand Down
3 changes: 3 additions & 0 deletions src/common/dto/error.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ export class ErrorDto {
@ApiProperty()
error: string;

@ApiPropertyOptional()
errorCode?: string;

@ApiProperty()
message: string;

Expand Down
12 changes: 12 additions & 0 deletions src/constants/error-code.constant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export enum ErrorCode {
// Common Validation
V000 = 'common.validation.error',

// Validation
V001 = 'user.validation.is_empty',
V002 = 'user.validation.is_invalid',

// Error
E001 = 'user.error.username_or_email_exists',
E002 = 'user.error.not_found',
}
12 changes: 12 additions & 0 deletions src/exceptions/validation.exception.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ErrorCode } from '@/constants/error-code.constant';
import { BadRequestException } from '@nestjs/common';

/**
* ValidationException used to throw validation errors with a custom error code and message.
* ErrorCode default is V000 (Common Validation)
*/
export class ValidationException extends BadRequestException {
constructor(error: ErrorCode = ErrorCode.V000, message?: string) {
super({ errorCode: error, message });
}
}
27 changes: 27 additions & 0 deletions src/filters/global-exception.filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import { QueryFailedError } from 'typeorm';
import { ConfigService } from '@nestjs/config';
import { AllConfigType } from '@/config/config.type';
import { I18nTranslations } from '@/generated/i18n.generated';
import { ValidationException } from '@/exceptions/validation.exception';
import { ErrorCode } from '@/constants/error-code.constant';

@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
Expand All @@ -37,6 +39,8 @@ export class GlobalExceptionFilter implements ExceptionFilter {

if (exception instanceof UnprocessableEntityException) {
error = this.handleUnprocessableEntityException(exception);
} else if (exception instanceof ValidationException) {
error = this.handleValidationException(exception);
} else if (exception instanceof HttpException) {
error = this.handleHttpException(exception);
} else if (exception instanceof QueryFailedError) {
Expand Down Expand Up @@ -79,6 +83,29 @@ export class GlobalExceptionFilter implements ExceptionFilter {
return errorRes;
}

private handleValidationException(exception: ValidationException): ErrorDto {
const r = exception.getResponse() as {
errorCode: ErrorCode;
message: string;
};
const statusCode = exception.getStatus();

const errorRes = {
timestamp: new Date().toISOString(),
statusCode,
error: STATUS_CODES[statusCode],
errorCode:
Object.keys(ErrorCode)[Object.values(ErrorCode).indexOf(r.errorCode)],
message:
r.message ||
this.i18n.t(r.errorCode as unknown as keyof I18nTranslations),
};

this.logger.debug(this.logMsg(errorRes, exception));

return errorRes;
}

/**
* Handles HttpException
* @param exception HttpException
Expand Down
21 changes: 14 additions & 7 deletions src/generated/i18n.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,24 @@
import { Path } from "nestjs-i18n";
/* prettier-ignore */
export type I18nTranslations = {
"error": {
"common": {
"validation": {
"error": string;
};
};
"user": {
"unique": {
"username": string;
"email": string;
};
};
"validation": {
"user": {
"errors": {
"userAlreadyExists": string;
};
"validation": {
"is_empty": string;
};
"error": {
"username_or_email_exists": string;
"not_found": string;
"invalid_password": string;
"invalid_token": string;
};
};
};
Expand Down
6 changes: 5 additions & 1 deletion src/i18n/en/common.json
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
{}
{
"validation": {
"error": "Common validation"
}
}
6 changes: 0 additions & 6 deletions src/i18n/en/error.json

This file was deleted.

15 changes: 15 additions & 0 deletions src/i18n/en/user.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"unique": {
"username": "User already exists",
"email": "User already exists"
},
"validation": {
"is_empty": "Field is required"
},
"error": {
"username_or_email_exists": "Username or email already exists",
"not_found": "User not found",
"invalid_password": "Invalid password",
"invalid_token": "Invalid token"
}
}
7 changes: 0 additions & 7 deletions src/i18n/en/validation.json

This file was deleted.

6 changes: 5 additions & 1 deletion src/i18n/jp/common.json
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
{}
{
"validation": {
"error": "Common validation"
}
}
3 changes: 3 additions & 0 deletions src/i18n/jp/error.json → src/i18n/jp/user.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,8 @@
"unique": {
"username": "このユーザーは既に存在します",
"email": "このユーザーは既に存在します"
},
"validation": {
"is_empty": "は必須です"
}
}
7 changes: 0 additions & 7 deletions src/i18n/jp/validation.json

This file was deleted.

6 changes: 5 additions & 1 deletion src/i18n/vi/common.json
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
{}
{
"validation": {
"error": "Common validation"
}
}
3 changes: 3 additions & 0 deletions src/i18n/vi/error.json → src/i18n/vi/user.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,8 @@
"unique": {
"username": "Người dùng đã tồn tại",
"email": "Người dùng đã tồn tại"
},
"validation": {
"is_empty": "Trường này không được để trống"
}
}
7 changes: 0 additions & 7 deletions src/i18n/vi/validation.json

This file was deleted.

1 change: 0 additions & 1 deletion src/utils/modules-set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ function generateModulesSet() {
__dirname,
'../../src/generated/i18n.generated.ts',
),
throwOnMissingKey: isDevelopment, // throw an error if a key is missing
logging: isDevelopment, // log info on missing keys
};
},
Expand Down
2 changes: 0 additions & 2 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,7 @@
"@/generated/*": ["src/generated/*"],
"@/guards/*": ["src/guards/*"],
"@/interceptors/*": ["src/interceptors/*"],
"@/interfaces/*": ["src/interfaces/*"],
"@/shared/*": ["src/shared/*"],
"@/types/*": ["src/types/*"],
"@/utils/*": ["src/utils/*"],
"@/validators/*": ["src/validators/*"]
}
Expand Down

0 comments on commit 5d7c1e3

Please sign in to comment.