chore: add comment reply for review and blog comment

This commit is contained in:
mahyargdz
2025-08-31 09:55:33 +03:30
parent 9c409bef27
commit 8e9b939a0b
20 changed files with 624 additions and 222 deletions
@@ -0,0 +1,17 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsNotEmpty, IsOptional, IsString, IsUUID } from "class-validator";
import { BlogMessage } from "../../../common/enums/message.enum";
export class CreateCommentReplyDto {
@IsNotEmpty({ message: BlogMessage.CONTENT_REQUIRED })
@IsString({ message: BlogMessage.CONTENT_STRING })
@ApiProperty({ description: "the content of reply", example: "This is a reply" })
content: string;
@IsOptional()
@IsNotEmpty({ message: BlogMessage.PARENT_REPLY_ID_REQUIRED })
@IsUUID("4", { message: BlogMessage.PARENT_REPLY_ID_SHOULD_BE_A_UUID })
@ApiProperty({ description: "the parent reply id", example: "d290f1ee-6c54-4b01-90e6-d701748f0851" })
parentReplyId?: string;
}
+16 -5
View File
@@ -4,12 +4,14 @@ import { ApiOperation } from "@nestjs/swagger";
import { BlogCommentQueryDto } from "./DTO/blog-comment-query.dto";
import { CreateBlogDto } from "./DTO/create-blog.dto";
import { CreateBlogCategoryDto } from "./DTO/create-category.dto";
import { CreateCommentReplyDto } from "./DTO/create-comment-reply.dto";
import { CreateCommentDto } from "./DTO/create-comment.dto";
import { BlogSlugDto } from "./DTO/get-blog-by-slug.dto";
import { BlogListSearchQueryDto, BlogSearchQueryDto } from "./DTO/search-blog-query.dto";
import { UpdateBlogDto } from "./DTO/update-blog.dto";
import { UpdateBlogCategoryDto } from "./DTO/update-category.dto";
import { UpdateCommentStatusDto } from "./DTO/update-comment-status.dto";
import { BlogCommentService } from "./providers/blog-comment.service";
import { BlogsService } from "./providers/blogs.service";
import { AdminRoute } from "../../common/decorators/admin.decorator";
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
@@ -23,7 +25,10 @@ import { PermissionEnum } from "../users/enums/permission.enum";
@Controller("blogs")
@AuthGuards()
export class BlogsController {
constructor(private readonly blogsService: BlogsService) {}
constructor(
private readonly blogsService: BlogsService,
private readonly blogCommentService: BlogCommentService,
) {}
@ApiOperation({ summary: "Get all public and active blogs with category id" })
@SkipAuth()
@@ -120,7 +125,13 @@ export class BlogsController {
@ApiOperation({ summary: "Create a new comment" })
@Post(":id/comments")
createComment(@Param() paramDto: ParamDto, @Body() createCommentDto: CreateCommentDto, @UserDec("id") userId: string) {
return this.blogsService.createComment(paramDto.id, createCommentDto, userId);
return this.blogCommentService.createComment(paramDto.id, createCommentDto, userId);
}
@ApiOperation({ summary: "Add a reply to a comment" })
@Post("comments/:id/replies")
addCommentReply(@Param() paramDto: ParamDto, @Body() createCommentReplyDto: CreateCommentReplyDto, @UserDec("id") userId: string) {
return this.blogCommentService.addCommentReply(paramDto.id, createCommentReplyDto, userId);
}
@ApiOperation({ summary: "Get all comments for a blog (admin route)" })
@@ -128,7 +139,7 @@ export class BlogsController {
@PermissionsDec(PermissionEnum.BLOGS)
@Get(":id/comments")
getBlogCommentsForAdmin(@Param() paramDto: ParamDto) {
return this.blogsService.getBlogCommentsForAdmin(paramDto.id);
return this.blogCommentService.getBlogCommentsForAdmin(paramDto.id);
}
@ApiOperation({ summary: "Get all comments for a blog (admin route)" })
@@ -136,7 +147,7 @@ export class BlogsController {
@PermissionsDec(PermissionEnum.BLOGS)
@Get("comments")
getAllCommentForAdmin(@Query() queryDto: BlogCommentQueryDto) {
return this.blogsService.getAllCommentForAdmin(queryDto);
return this.blogCommentService.getAllCommentForAdmin(queryDto);
}
@ApiOperation({ summary: "Update the status of a comment (admin route)" })
@@ -144,7 +155,7 @@ export class BlogsController {
@PermissionsDec(PermissionEnum.BLOGS)
@Patch("comments/:id/status")
updateCommentStatus(@Param() paramDto: ParamDto, @Body() updateCommentStatusDto: UpdateCommentStatusDto) {
return this.blogsService.updateCommentStatus(paramDto.id, updateCommentStatusDto);
return this.blogCommentService.updateCommentStatus(paramDto.id, updateCommentStatusDto);
}
//-------------- categories --------------
+14 -3
View File
@@ -3,18 +3,29 @@ import { TypeOrmModule } from "@nestjs/typeorm";
import { BlogsController } from "./blogs.controller";
import { BlogCategory } from "./entities/blog-category.entity";
import { BlogCommentReply } from "./entities/blog-comment-reply.entity";
import { BlogComment } from "./entities/blog-comment.entity";
import { Blog } from "./entities/blog.entity";
import { BlogCommentService } from "./providers/blog-comment.service";
import { BlogsService } from "./providers/blogs.service";
import { BlogCategoriesRepository } from "./repositories/blog-categories.repository";
import { BlogCommentReplyRepository } from "./repositories/blog-comment-reply.repository";
import { BlogCommentsRepository } from "./repositories/blog-comments.repository";
import { BlogsRepository } from "./repositories/blogs.repository";
import { NotificationModule } from "../notifications/notifications.module";
import { UsersModule } from "../users/users.module";
@Module({
imports: [TypeOrmModule.forFeature([Blog, BlogComment, BlogCategory]), UsersModule, NotificationModule],
imports: [TypeOrmModule.forFeature([Blog, BlogComment, BlogCommentReply, BlogCategory]), UsersModule, NotificationModule],
controllers: [BlogsController],
providers: [BlogsService, BlogsRepository, BlogCommentsRepository, BlogCategoriesRepository],
exports: [BlogsService],
providers: [
BlogsService,
BlogCommentService,
BlogsRepository,
BlogCommentsRepository,
BlogCommentReplyRepository,
BlogCategoriesRepository,
],
exports: [BlogsService, BlogCommentService],
})
export class BlogsModule {}
@@ -0,0 +1,26 @@
import { Column, Entity, ManyToOne, OneToMany } from "typeorm";
import { BlogComment } from "./blog-comment.entity";
import { BaseEntity } from "../../../common/entities/base.entity";
import { User } from "../../users/entities/user.entity";
@Entity()
export class BlogCommentReply extends BaseEntity {
@Column({ type: "text", nullable: false })
content: string;
@Column({ type: "boolean", default: false })
isOfficialReply: boolean;
@ManyToOne(() => BlogComment, (comment) => comment.replies, { onDelete: "CASCADE" })
comment: BlogComment;
@ManyToOne(() => User, { onDelete: "SET NULL" })
author: User;
@ManyToOne(() => BlogCommentReply, (reply) => reply.replies, { nullable: true })
parentReply: BlogCommentReply | null;
@OneToMany(() => BlogCommentReply, (reply) => reply.parentReply)
replies: BlogCommentReply[];
}
@@ -1,5 +1,6 @@
import { Column, Entity, ManyToOne } from "typeorm";
import { Column, Entity, ManyToOne, OneToMany } from "typeorm";
import { BlogCommentReply } from "./blog-comment-reply.entity";
import { Blog } from "./blog.entity";
import { BaseEntity } from "../../../common/entities/base.entity";
import { User } from "../../users/entities/user.entity";
@@ -21,4 +22,7 @@ export class BlogComment extends BaseEntity {
@ManyToOne(() => Blog, (blog) => blog.comments, { onDelete: "CASCADE" })
blog: Blog;
@OneToMany(() => BlogCommentReply, (reply) => reply.comment)
replies: BlogCommentReply[];
}
@@ -0,0 +1,137 @@
import { BadRequestException, Injectable } from "@nestjs/common";
import { DataSource, IsNull } from "typeorm";
import { BlogMessage, CommonMessage } from "../../../common/enums/message.enum";
import { NotificationQueue } from "../../notifications/queue/notification.queue";
import { AdminsService } from "../../users/providers/admins.service";
import { UsersService } from "../../users/providers/users.service";
import { BlogCommentQueryDto } from "../DTO/blog-comment-query.dto";
import { CreateCommentReplyDto } from "../DTO/create-comment-reply.dto";
import { CreateCommentDto } from "../DTO/create-comment.dto";
import { UpdateCommentStatusDto } from "../DTO/update-comment-status.dto";
import { BlogCommentReply } from "../entities/blog-comment-reply.entity";
import { BlogComment } from "../entities/blog-comment.entity";
import { CommentStatus } from "../enums/comment-status.enum";
import { BlogCommentReplyRepository } from "../repositories/blog-comment-reply.repository";
import { BlogCommentsRepository } from "../repositories/blog-comments.repository";
import { BlogsRepository } from "../repositories/blogs.repository";
@Injectable()
export class BlogCommentService {
constructor(
private blogsRepository: BlogsRepository,
private blogCommentReplyRepository: BlogCommentReplyRepository,
private blogCommentsRepository: BlogCommentsRepository,
private usersService: UsersService,
private dataSource: DataSource,
private adminsService: AdminsService,
private notificationQueue: NotificationQueue,
) {}
//########################################################
async addCommentReply(commentId: string, createReplyDto: CreateCommentReplyDto, authorId: string) {
const comment = await this.blogCommentsRepository.findOne({ where: { id: commentId } });
let parentReply: BlogCommentReply | null = null;
if (!comment) throw new BadRequestException(BlogMessage.COMMENT_NOT_FOUND);
if (createReplyDto.parentReplyId) {
parentReply = await this.blogCommentReplyRepository.findOne({ where: { id: createReplyDto.parentReplyId } });
if (!parentReply) throw new BadRequestException(BlogMessage.PARENT_REPLY_NOT_EXIST);
}
const isSuperAdmin = await this.usersService.isSuperAdmin(authorId);
const reply = this.blogCommentReplyRepository.create({
content: createReplyDto.content,
isOfficialReply: isSuperAdmin || false,
author: { id: authorId },
comment: { id: commentId } as BlogComment,
parentReply,
});
await this.blogCommentReplyRepository.save(reply);
return {
message: BlogMessage.REPLY_ADDED,
reply,
};
}
//*********************************** */
async createComment(blogId: string, createCommentDto: CreateCommentDto, userId: string) {
const queryRunner = this.dataSource.createQueryRunner();
try {
await queryRunner.connect();
await queryRunner.startTransaction();
const blog = await queryRunner.manager.findOne(this.blogsRepository.target, { where: { id: blogId, deletedAt: IsNull() } });
if (!blog) throw new BadRequestException(BlogMessage.BLOG_NOT_FOUND);
const user = await this.usersService.findOneByIdWithQueryRunner(userId, queryRunner);
const comment = queryRunner.manager.create(this.blogCommentsRepository.target, {
...createCommentDto,
blog,
user,
status: CommentStatus.APPROVED,
});
await queryRunner.manager.save(this.blogCommentsRepository.target, comment);
const superAdmin = await this.adminsService.getSuperAdmins(queryRunner);
for (const admin of superAdmin) {
await this.notificationQueue.addNewBlogCommentNotification(admin.id, {
userPhone: admin.phone,
userEmail: admin.email,
blogTitle: blog.title,
fullName: `${user.firstName} ${user.lastName}`,
});
}
await queryRunner.commitTransaction();
return {
message: CommonMessage.CREATED,
comment,
};
} catch (error) {
await queryRunner.rollbackTransaction();
throw error;
} finally {
await queryRunner.release();
}
}
//*********************************** */
async updateCommentStatus(commentId: string, updateCommentStatusDto: UpdateCommentStatusDto) {
const comment = await this.blogCommentsRepository.findOneById(commentId);
if (!comment) throw new BadRequestException(BlogMessage.COMMENT_NOT_FOUND);
comment.status = updateCommentStatusDto.status;
await this.blogCommentsRepository.save(comment);
return {
message: CommonMessage.UPDATE_SUCCESS,
comment,
};
}
//*********************************** */
async getBlogCommentsForAdmin(blogId: string) {
const comments = await this.blogCommentsRepository.findBlogCommentsForAdmin(blogId);
return { comments };
}
//*********************************** */
async getAllCommentForAdmin(queryDto: BlogCommentQueryDto) {
const [comments, count] = await this.blogCommentsRepository.getAllCommentsForAdmin(queryDto);
return { comments, count, paginate: true };
}
}
+1 -84
View File
@@ -1,20 +1,14 @@
import { BadRequestException, Injectable } from "@nestjs/common";
import { DataSource, IsNull, Not } from "typeorm";
import { Not } from "typeorm";
import { AdminMessage, BlogMessage, CommonMessage } from "../../../common/enums/message.enum";
import { CategoryListSearchQueryDto } from "../../danak-services/DTO/category-search-query.dto";
import { NotificationQueue } from "../../notifications/queue/notification.queue";
import { AdminsService } from "../../users/providers/admins.service";
import { UsersService } from "../../users/providers/users.service";
import { BlogCommentQueryDto } from "../DTO/blog-comment-query.dto";
import { CreateBlogDto } from "../DTO/create-blog.dto";
import { CreateBlogCategoryDto } from "../DTO/create-category.dto";
import { CreateCommentDto } from "../DTO/create-comment.dto";
import { BlogListSearchQueryDto, BlogSearchQueryDto } from "../DTO/search-blog-query.dto";
import { UpdateBlogDto } from "../DTO/update-blog.dto";
import { UpdateBlogCategoryDto } from "../DTO/update-category.dto";
import { UpdateCommentStatusDto } from "../DTO/update-comment-status.dto";
import { CommentStatus } from "../enums/comment-status.enum";
import { BlogCategoriesRepository } from "../repositories/blog-categories.repository";
import { BlogCommentsRepository } from "../repositories/blog-comments.repository";
import { BlogsRepository } from "../repositories/blogs.repository";
@@ -25,9 +19,6 @@ export class BlogsService {
private readonly blogCommentsRepository: BlogCommentsRepository,
private readonly blogCategoriesRepository: BlogCategoriesRepository,
private readonly usersService: UsersService,
private readonly adminsService: AdminsService,
private readonly dataSource: DataSource,
private readonly notificationQueue: NotificationQueue,
) {}
//*********************************** */
@@ -235,80 +226,6 @@ export class BlogsService {
const [blogs, count] = await this.blogsRepository.getBlogListForAdmin(queryDto);
return { blogs, count, paginate: true };
}
//*********************************** */
async createComment(blogId: string, createCommentDto: CreateCommentDto, userId: string) {
const queryRunner = this.dataSource.createQueryRunner();
try {
await queryRunner.connect();
await queryRunner.startTransaction();
const blog = await queryRunner.manager.findOne(this.blogsRepository.target, { where: { id: blogId, deletedAt: IsNull() } });
if (!blog) throw new BadRequestException(BlogMessage.BLOG_NOT_FOUND);
const user = await this.usersService.findOneByIdWithQueryRunner(userId, queryRunner);
const comment = queryRunner.manager.create(this.blogCommentsRepository.target, {
...createCommentDto,
blog,
user,
status: CommentStatus.PENDING,
});
await queryRunner.manager.save(this.blogCommentsRepository.target, comment);
const superAdmin = await this.adminsService.getSuperAdmins(queryRunner);
for (const admin of superAdmin) {
await this.notificationQueue.addNewBlogCommentNotification(admin.id, {
userPhone: admin.phone,
userEmail: admin.email,
blogTitle: blog.title,
fullName: `${user.firstName} ${user.lastName}`,
});
}
await queryRunner.commitTransaction();
return {
message: CommonMessage.CREATED,
comment,
};
} catch (error) {
await queryRunner.rollbackTransaction();
throw error;
} finally {
await queryRunner.release();
}
}
//*********************************** */
async updateCommentStatus(commentId: string, updateCommentStatusDto: UpdateCommentStatusDto) {
const comment = await this.blogCommentsRepository.findOneById(commentId);
if (!comment) throw new BadRequestException(BlogMessage.COMMENT_NOT_FOUND);
comment.status = updateCommentStatusDto.status;
await this.blogCommentsRepository.save(comment);
return {
message: CommonMessage.UPDATE_SUCCESS,
comment,
};
}
//*********************************** */
async getBlogCommentsForAdmin(blogId: string) {
const comments = await this.blogCommentsRepository.findBlogCommentsForAdmin(blogId);
return { comments };
}
//*********************************** */
async getAllCommentForAdmin(queryDto: BlogCommentQueryDto) {
const [comments, count] = await this.blogCommentsRepository.getAllCommentsForAdmin(queryDto);
return { comments, count, paginate: true };
}
//*********************************** */
private generateSlug(slug: string): string {
@@ -0,0 +1,31 @@
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { BlogCommentReply } from "../entities/blog-comment-reply.entity";
@Injectable()
export class BlogCommentReplyRepository extends Repository<BlogCommentReply> {
constructor(@InjectRepository(BlogCommentReply) repository: Repository<BlogCommentReply>) {
super(repository.target, repository.manager, repository.queryRunner);
}
//+********************
async findByCommentId(commentId: string): Promise<BlogCommentReply[]> {
return this.createQueryBuilder("reply")
.leftJoinAndSelect("reply.author", "author")
.leftJoinAndSelect("reply.parentReply", "parentReply")
.leftJoinAndSelect("reply.replies", "replies")
.where("reply.comment.id = :commentId", { commentId })
.andWhere("reply.parentReply IS NULL")
.orderBy("reply.createdAt", "ASC")
.getMany();
}
//+********************
async findRepliesByParentId(parentId: string): Promise<BlogCommentReply[]> {
return this.createQueryBuilder("reply")
.leftJoinAndSelect("reply.author", "author")
.where("reply.parentReply.id = :parentId", { parentId })
.orderBy("reply.createdAt", "ASC")
.getMany();
}
}
@@ -25,6 +25,7 @@ export class BlogCommentsRepository extends Repository<BlogComment> {
order: { createdAt: "DESC" },
relations: {
user: true,
replies: true,
},
select: {
id: true,
@@ -33,6 +34,7 @@ export class BlogCommentsRepository extends Repository<BlogComment> {
createdAt: true,
status: true,
user: { id: true, firstName: true, lastName: true },
replies: { id: true, content: true, createdAt: true, author: { id: true, firstName: true, lastName: true } },
},
});
}
@@ -44,7 +46,11 @@ export class BlogCommentsRepository extends Repository<BlogComment> {
.where("blog.deletedAt IS NULL")
.addSelect(["blog.id", "blog.title"])
.leftJoin("blogComment.user", "user")
.addSelect(["user.id", "user.firstName", "user.lastName"]);
.addSelect(["user.id", "user.firstName", "user.lastName"])
.leftJoin("blogComment.replies", "replies")
.addSelect(["replies.id", "replies.content", "replies.createdAt"])
.leftJoin("replies.author", "repliesAuthor")
.addSelect(["repliesAuthor.id", "repliesAuthor.firstName", "repliesAuthor.lastName"]);
if (queryDto.q) {
query.andWhere("blogComment.content ILIKE :q", { q: `%${queryDto.q}%` });
@@ -58,13 +64,14 @@ export class BlogCommentsRepository extends Repository<BlogComment> {
return query.getManyAndCount();
}
//*********************************** */
async findBlogCommentsByBlogId(blogId: string) {
return this.find({
where: { blog: { id: blogId, deletedAt: IsNull() }, status: CommentStatus.APPROVED },
order: { createdAt: "DESC" },
relations: {
user: true,
replies: true,
},
select: {
id: true,
@@ -73,6 +80,7 @@ export class BlogCommentsRepository extends Repository<BlogComment> {
createdAt: true,
status: true,
user: { id: true, firstName: true, lastName: true, profilePic: true },
replies: { id: true, content: true, createdAt: true, author: { id: true, firstName: true, lastName: true } },
},
});
}