fix(api): add error handling to 29 CQRS handlers in admin, inquiries, leads, reviews
Add standardized try-catch error handling pattern to all command and query handlers in the four priority modules: - admin (15 handlers): commands + queries, added LoggerService injection - inquiries (4 handlers): commands + queries - leads (5 handlers): commands + queries - reviews (5 handlers): commands + queries Each handler now: - Wraps execute() in try-catch - Re-throws DomainException subclasses (NotFoundException, etc.) - Logs infrastructure errors via LoggerService - Throws InternalServerErrorException for unexpected failures Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports -- NestJS DI requires value imports for emitDecoratorMetadata
|
||||
import { CommandHandler, EventBus, ICommandHandler } from '@nestjs/cqrs';
|
||||
import { createId } from '@paralleldrive/cuid2';
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports -- NestJS DI requires value imports for emitDecoratorMetadata
|
||||
import { ConflictException, ValidationException, LoggerService } from '@modules/shared';
|
||||
import { ConflictException, DomainException, ValidationException, LoggerService } from '@modules/shared';
|
||||
import { ReviewEntity } from '../../../domain/entities/review.entity';
|
||||
import { REVIEW_REPOSITORY, type IReviewRepository } from '../../../domain/repositories/review.repository';
|
||||
import { Rating } from '../../../domain/value-objects/rating.vo';
|
||||
@@ -26,54 +26,64 @@ export class CreateReviewHandler implements ICommandHandler<CreateReviewCommand>
|
||||
) {}
|
||||
|
||||
async execute(command: CreateReviewCommand): Promise<CreateReviewResult> {
|
||||
// Validate rating value object
|
||||
const ratingResult = Rating.create(command.rating);
|
||||
if (ratingResult.isErr) {
|
||||
throw new ValidationException(ratingResult.unwrapErr());
|
||||
try {
|
||||
// Validate rating value object
|
||||
const ratingResult = Rating.create(command.rating);
|
||||
if (ratingResult.isErr) {
|
||||
throw new ValidationException(ratingResult.unwrapErr());
|
||||
}
|
||||
const rating = ratingResult.unwrap();
|
||||
|
||||
// Prevent self-review
|
||||
if (command.userId === command.targetId && command.targetType === 'user') {
|
||||
throw new ValidationException('Không thể tự đánh giá bản thân');
|
||||
}
|
||||
|
||||
// One review per user per target
|
||||
const existing = await this.reviewRepo.findByUserAndTarget(
|
||||
command.userId,
|
||||
command.targetType,
|
||||
command.targetId,
|
||||
);
|
||||
if (existing) {
|
||||
throw new ConflictException('Bạn đã đánh giá mục tiêu này rồi');
|
||||
}
|
||||
|
||||
const id = createId();
|
||||
const review = ReviewEntity.createNew(
|
||||
id,
|
||||
command.userId,
|
||||
command.targetType,
|
||||
command.targetId,
|
||||
rating,
|
||||
command.comment,
|
||||
);
|
||||
|
||||
await this.reviewRepo.save(review);
|
||||
|
||||
// Publish domain events
|
||||
const events = review.clearDomainEvents();
|
||||
for (const event of events) {
|
||||
this.eventBus.publish(event);
|
||||
}
|
||||
|
||||
this.logger.log(`Review ${id} created by user ${command.userId} for ${command.targetType}:${command.targetId}`, 'CreateReviewHandler');
|
||||
|
||||
return {
|
||||
id,
|
||||
rating: rating.value,
|
||||
targetType: command.targetType,
|
||||
targetId: command.targetId,
|
||||
createdAt: review.createdAt.toISOString(),
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof DomainException) throw error;
|
||||
this.logger.error(
|
||||
`Failed to create review: ${error instanceof Error ? error.message : String(error)}`,
|
||||
error instanceof Error ? error.stack : undefined,
|
||||
'CreateReviewHandler',
|
||||
);
|
||||
throw new InternalServerErrorException('Lỗi khi tạo đánh giá');
|
||||
}
|
||||
const rating = ratingResult.unwrap();
|
||||
|
||||
// Prevent self-review
|
||||
if (command.userId === command.targetId && command.targetType === 'user') {
|
||||
throw new ValidationException('Không thể tự đánh giá bản thân');
|
||||
}
|
||||
|
||||
// One review per user per target
|
||||
const existing = await this.reviewRepo.findByUserAndTarget(
|
||||
command.userId,
|
||||
command.targetType,
|
||||
command.targetId,
|
||||
);
|
||||
if (existing) {
|
||||
throw new ConflictException('Bạn đã đánh giá mục tiêu này rồi');
|
||||
}
|
||||
|
||||
const id = createId();
|
||||
const review = ReviewEntity.createNew(
|
||||
id,
|
||||
command.userId,
|
||||
command.targetType,
|
||||
command.targetId,
|
||||
rating,
|
||||
command.comment,
|
||||
);
|
||||
|
||||
await this.reviewRepo.save(review);
|
||||
|
||||
// Publish domain events
|
||||
const events = review.clearDomainEvents();
|
||||
for (const event of events) {
|
||||
this.eventBus.publish(event);
|
||||
}
|
||||
|
||||
this.logger.log(`Review ${id} created by user ${command.userId} for ${command.targetType}:${command.targetId}`, 'CreateReviewHandler');
|
||||
|
||||
return {
|
||||
id,
|
||||
rating: rating.value,
|
||||
targetType: command.targetType,
|
||||
targetId: command.targetId,
|
||||
createdAt: review.createdAt.toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports -- NestJS DI requires value imports for emitDecoratorMetadata
|
||||
import { CommandHandler, EventBus, ICommandHandler } from '@nestjs/cqrs';
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports -- NestJS DI requires value imports for emitDecoratorMetadata
|
||||
import { ForbiddenException, NotFoundException, LoggerService } from '@modules/shared';
|
||||
import { DomainException, ForbiddenException, NotFoundException, LoggerService } from '@modules/shared';
|
||||
import { REVIEW_REPOSITORY, type IReviewRepository } from '../../../domain/repositories/review.repository';
|
||||
import { DeleteReviewCommand } from './delete-review.command';
|
||||
|
||||
@@ -15,24 +15,34 @@ export class DeleteReviewHandler implements ICommandHandler<DeleteReviewCommand>
|
||||
) {}
|
||||
|
||||
async execute(command: DeleteReviewCommand): Promise<void> {
|
||||
const review = await this.reviewRepo.findById(command.reviewId);
|
||||
if (!review) {
|
||||
throw new NotFoundException('Review', command.reviewId);
|
||||
try {
|
||||
const review = await this.reviewRepo.findById(command.reviewId);
|
||||
if (!review) {
|
||||
throw new NotFoundException('Review', command.reviewId);
|
||||
}
|
||||
|
||||
if (review.userId !== command.userId) {
|
||||
throw new ForbiddenException('Bạn chỉ có thể xóa đánh giá của chính mình');
|
||||
}
|
||||
|
||||
review.markDeleted();
|
||||
await this.reviewRepo.delete(command.reviewId);
|
||||
|
||||
// Publish domain events
|
||||
const events = review.clearDomainEvents();
|
||||
for (const event of events) {
|
||||
this.eventBus.publish(event);
|
||||
}
|
||||
|
||||
this.logger.log(`Review ${command.reviewId} deleted by user ${command.userId}`, 'DeleteReviewHandler');
|
||||
} catch (error) {
|
||||
if (error instanceof DomainException) throw error;
|
||||
this.logger.error(
|
||||
`Failed to delete review: ${error instanceof Error ? error.message : String(error)}`,
|
||||
error instanceof Error ? error.stack : undefined,
|
||||
'DeleteReviewHandler',
|
||||
);
|
||||
throw new InternalServerErrorException('Lỗi khi xóa đánh giá');
|
||||
}
|
||||
|
||||
if (review.userId !== command.userId) {
|
||||
throw new ForbiddenException('Bạn chỉ có thể xóa đánh giá của chính mình');
|
||||
}
|
||||
|
||||
review.markDeleted();
|
||||
await this.reviewRepo.delete(command.reviewId);
|
||||
|
||||
// Publish domain events
|
||||
const events = review.clearDomainEvents();
|
||||
for (const event of events) {
|
||||
this.eventBus.publish(event);
|
||||
}
|
||||
|
||||
this.logger.log(`Review ${command.reviewId} deleted by user ${command.userId}`, 'DeleteReviewHandler');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||
import { type IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
||||
import { DomainException, type LoggerService } from '@modules/shared';
|
||||
import { type ReviewStatsData } from '../../../domain/repositories/review-read.dto';
|
||||
import { REVIEW_REPOSITORY, type IReviewRepository } from '../../../domain/repositories/review.repository';
|
||||
import { GetAverageRatingQuery } from './get-average-rating.query';
|
||||
@@ -8,9 +9,20 @@ import { GetAverageRatingQuery } from './get-average-rating.query';
|
||||
export class GetAverageRatingHandler implements IQueryHandler<GetAverageRatingQuery> {
|
||||
constructor(
|
||||
@Inject(REVIEW_REPOSITORY) private readonly reviewRepo: IReviewRepository,
|
||||
private readonly logger: LoggerService,
|
||||
) {}
|
||||
|
||||
async execute(query: GetAverageRatingQuery): Promise<ReviewStatsData> {
|
||||
return this.reviewRepo.getStats(query.targetType, query.targetId);
|
||||
try {
|
||||
return await this.reviewRepo.getStats(query.targetType, query.targetId);
|
||||
} catch (error) {
|
||||
if (error instanceof DomainException) throw error;
|
||||
this.logger.error(
|
||||
`Failed to get average rating: ${error instanceof Error ? error.message : String(error)}`,
|
||||
error instanceof Error ? error.stack : undefined,
|
||||
'GetAverageRatingHandler',
|
||||
);
|
||||
throw new InternalServerErrorException('Lỗi khi lấy đánh giá trung bình');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||
import { type IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
||||
import { DomainException, type LoggerService } from '@modules/shared';
|
||||
import { type ReviewItemData } from '../../../domain/repositories/review-read.dto';
|
||||
import { REVIEW_REPOSITORY, type IReviewRepository, type PaginatedResult } from '../../../domain/repositories/review.repository';
|
||||
import { GetReviewsByTargetQuery } from './get-reviews-by-target.query';
|
||||
@@ -8,14 +9,25 @@ import { GetReviewsByTargetQuery } from './get-reviews-by-target.query';
|
||||
export class GetReviewsByTargetHandler implements IQueryHandler<GetReviewsByTargetQuery> {
|
||||
constructor(
|
||||
@Inject(REVIEW_REPOSITORY) private readonly reviewRepo: IReviewRepository,
|
||||
private readonly logger: LoggerService,
|
||||
) {}
|
||||
|
||||
async execute(query: GetReviewsByTargetQuery): Promise<PaginatedResult<ReviewItemData>> {
|
||||
return this.reviewRepo.findByTarget(
|
||||
query.targetType,
|
||||
query.targetId,
|
||||
query.page,
|
||||
query.limit,
|
||||
);
|
||||
try {
|
||||
return await this.reviewRepo.findByTarget(
|
||||
query.targetType,
|
||||
query.targetId,
|
||||
query.page,
|
||||
query.limit,
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof DomainException) throw error;
|
||||
this.logger.error(
|
||||
`Failed to get reviews by target: ${error instanceof Error ? error.message : String(error)}`,
|
||||
error instanceof Error ? error.stack : undefined,
|
||||
'GetReviewsByTargetHandler',
|
||||
);
|
||||
throw new InternalServerErrorException('Lỗi khi lấy danh sách đánh giá');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||
import { type IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
||||
import { DomainException, type LoggerService } from '@modules/shared';
|
||||
import { type ReviewItemData } from '../../../domain/repositories/review-read.dto';
|
||||
import { REVIEW_REPOSITORY, type IReviewRepository, type PaginatedResult } from '../../../domain/repositories/review.repository';
|
||||
import { GetReviewsByUserQuery } from './get-reviews-by-user.query';
|
||||
@@ -8,9 +9,20 @@ import { GetReviewsByUserQuery } from './get-reviews-by-user.query';
|
||||
export class GetReviewsByUserHandler implements IQueryHandler<GetReviewsByUserQuery> {
|
||||
constructor(
|
||||
@Inject(REVIEW_REPOSITORY) private readonly reviewRepo: IReviewRepository,
|
||||
private readonly logger: LoggerService,
|
||||
) {}
|
||||
|
||||
async execute(query: GetReviewsByUserQuery): Promise<PaginatedResult<ReviewItemData>> {
|
||||
return this.reviewRepo.findByUserId(query.userId, query.page, query.limit);
|
||||
try {
|
||||
return await this.reviewRepo.findByUserId(query.userId, query.page, query.limit);
|
||||
} catch (error) {
|
||||
if (error instanceof DomainException) throw error;
|
||||
this.logger.error(
|
||||
`Failed to get reviews by user: ${error instanceof Error ? error.message : String(error)}`,
|
||||
error instanceof Error ? error.stack : undefined,
|
||||
'GetReviewsByUserHandler',
|
||||
);
|
||||
throw new InternalServerErrorException('Lỗi khi lấy danh sách đánh giá');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user