Skip to content

Commit

Permalink
feat: add comment apis (#69)
Browse files Browse the repository at this point in the history
  • Loading branch information
khuongln-1346 authored Nov 18, 2024
1 parent 55a49f9 commit c82f548
Show file tree
Hide file tree
Showing 15 changed files with 224 additions and 17 deletions.
46 changes: 40 additions & 6 deletions apps/admin-api/src/api/article/comment/comment.controller.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,58 @@
import { Controller, Delete, Get, Param, Post } from '@nestjs/common';
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
SerializeOptions,
} from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { CurrentUser } from '@repo/api';
import { ApiAuth } from '@repo/api/decorators/http.decorators';
import { CommentService } from './comment.service';
import { CommentListResDto } from './dto/comment-list.dto';
import { CommentResDto } from './dto/comment.dto';
import { CreateCommentReqDto } from './dto/create-comment.dto';

@ApiTags('Comment')
@Controller('articles/:slug/comments')
export class CommentController {
constructor(private readonly commentService: CommentService) {}

@Post()
async create(@Param('slug') slug: string) {
return await this.commentService.create(slug);
@SerializeOptions({ type: CommentResDto })
@ApiAuth({
summary: 'Add Comments to an Article',
type: CommentResDto,
})
async create(
@Param('slug') slug: string,
@Body('comment') commentData: CreateCommentReqDto,
@CurrentUser('id') userId: number,
): Promise<CommentResDto> {
return await this.commentService.create(slug, commentData, userId);
}

@Get()
async list(@Param('slug') slug: string) {
@SerializeOptions({ type: CommentListResDto })
@ApiAuth({
summary: 'Get Comments from an Article',
type: CommentListResDto,
isAuthOptional: true,
})
async list(@Param('slug') slug: string): Promise<CommentListResDto> {
return await this.commentService.list(slug);
}

@Delete(':id')
async delete(@Param('slug') slug: string, @Param('id') id: string) {
return await this.commentService.delete(slug, id);
@ApiAuth({
summary: 'Delete Comment',
})
async delete(
@CurrentUser('id') userId: number,
@Param('id') commentId: number,
) {
return await this.commentService.delete(commentId, userId);
}
}
9 changes: 9 additions & 0 deletions apps/admin-api/src/api/article/comment/comment.module.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import {
ArticleEntity,
CommentEntity,
UserEntity,
} from '@repo/database-typeorm';
import { CommentController } from './comment.controller';
import { CommentService } from './comment.service';

@Module({
imports: [
TypeOrmModule.forFeature([ArticleEntity, UserEntity, CommentEntity]),
],
controllers: [CommentController],
providers: [CommentService],
})
Expand Down
32 changes: 31 additions & 1 deletion apps/admin-api/src/api/article/comment/comment.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,42 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import {
ArticleEntity,
CommentEntity,
UserEntity,
} from '@repo/database-typeorm';
import { Repository } from 'typeorm';
import { CommentService } from './comment.service';

describe('CommentService', () => {
let service: CommentService;
let articleRepositoryValue: Partial<
Record<keyof Repository<ArticleEntity>, jest.Mock>
>;
let userRepositoryValue: Partial<
Record<keyof Repository<UserEntity>, jest.Mock>
>;
let commentRepositoryValue: Partial<
Record<keyof Repository<CommentEntity>, jest.Mock>
>;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [CommentService],
providers: [
CommentService,
{
provide: getRepositoryToken(ArticleEntity),
useValue: articleRepositoryValue,
},
{
provide: getRepositoryToken(UserEntity),
useValue: userRepositoryValue,
},
{
provide: getRepositoryToken(CommentEntity),
useValue: commentRepositoryValue,
},
],
}).compile();

service = module.get<CommentService>(CommentService);
Expand Down
88 changes: 82 additions & 6 deletions apps/admin-api/src/api/article/comment/comment.service.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,92 @@
import { ProfileDto } from '@/api/profile/dto/profile.dto';
import { ErrorCode } from '@/constants/error-code.constant';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { ValidationException } from '@repo/api';
import {
ArticleEntity,
CommentEntity,
UserEntity,
} from '@repo/database-typeorm';
import { Repository } from 'typeorm';
import { CommentListResDto } from './dto/comment-list.dto';
import { CommentResDto } from './dto/comment.dto';
import { CreateCommentReqDto } from './dto/create-comment.dto';

@Injectable()
export class CommentService {
async create(_slug: string) {
throw new Error('Method not implemented.');
constructor(
@InjectRepository(ArticleEntity)
private readonly articleRepository: Repository<ArticleEntity>,
@InjectRepository(UserEntity)
private readonly userRepository: Repository<UserEntity>,
@InjectRepository(CommentEntity)
private readonly commentRepository: Repository<CommentEntity>,
) {}

async create(
slug: string,
commentData: CreateCommentReqDto,
userId: number,
): Promise<CommentResDto> {
const article = await this.articleRepository.findOneBy({ slug: slug });

if (!article) {
throw new ValidationException(ErrorCode.E201);
}

const user = await this.userRepository.findOneByOrFail({ id: userId });
const comment = new CommentEntity({
body: commentData.body,
articleId: article.id,
authorId: userId,
});

const savedComment = await this.commentRepository.save(comment);

return {
comment: {
...savedComment,
author: user.toDto(ProfileDto),
},
};
}

async list(_slug: string) {
throw new Error('Method not implemented.');
async list(slug: string): Promise<CommentListResDto> {
const article = await this.articleRepository.findOneBy({ slug: slug });

if (!article) {
throw new ValidationException(ErrorCode.E201);
}

const comments = await this.commentRepository.find({
where: { articleId: article.id },
relations: ['author'],
});

return {
comments: comments.map((comment) => {
return {
...comment,
author: comment.author.toDto(ProfileDto),
};
}),
};
}

async delete(_slug: string, _id: string) {
throw new Error('Method not implemented.');
async delete(commentId: number, userId: number) {
const comment = await this.commentRepository.findOneBy({
id: commentId,
});

if (!comment) {
throw new ValidationException(ErrorCode.E301);
}

if (comment.authorId !== userId) {
throw new ValidationException(ErrorCode.E302);
}

await this.commentRepository.remove(comment);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { ClassField } from '@repo/api';
import { CommentDto } from './comment.dto';

export class CommentListResDto {
@ClassField(() => CommentDto, { isArray: true, expose: true })
comments: CommentDto[];
}
24 changes: 24 additions & 0 deletions apps/admin-api/src/api/article/comment/dto/comment.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ProfileDto } from '@/api/profile/dto/profile.dto';
import { ClassField, DateField, NumberField, StringField } from '@repo/api';

export class CommentDto {
@NumberField({ expose: true })
id: number;

@StringField({ expose: true })
body: string;

@DateField({ expose: true })
createdAt: Date;

@DateField({ expose: true })
updatedAt: Date;

@ClassField(() => ProfileDto, { expose: true })
author: ProfileDto;
}

export class CommentResDto {
@ClassField(() => CommentDto, { expose: true })
comment: CommentDto;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { StringField } from '@repo/api';

export class CreateCommentReqDto {
@StringField({ minLength: 1, maxLength: 1000 })
readonly body: string;
}
2 changes: 2 additions & 0 deletions apps/admin-api/src/constants/error-code.constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@ export enum ErrorCode {
E103 = 'app.profile.already_followed',
E104 = 'app.profile.not_followed',
E201 = 'app.article.not_found',
E301 = 'app.comment.not_found',
E302 = 'app.comment.not_authorized_to_delete',
}
4 changes: 4 additions & 0 deletions apps/admin-api/src/generated/i18n.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ export type I18nTranslations = {
"article": {
"not_found": string;
};
"comment": {
"not_found": string;
"not_authorized_to_delete": string;
};
};
};
/* prettier-ignore */
Expand Down
4 changes: 4 additions & 0 deletions apps/admin-api/src/i18n/en/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,9 @@
},
"article": {
"not_found": "Article not found"
},
"comment": {
"not_found": "comment not found",
"not_authorized_to_delete": "You are not authorized to delete this comment"
}
}
4 changes: 4 additions & 0 deletions apps/admin-api/src/i18n/vi/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,9 @@
},
"article": {
"not_found": "Bài viết không tìm thấy"
},
"comment": {
"not_found": "Bình luận không tìm thấy",
"not_authorized_to_delete": "Bạn không được phép xóa bình luận này"
}
}
8 changes: 6 additions & 2 deletions packages/database-typeorm/src/entities/comment.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
Entity,
JoinColumn,
ManyToOne,
OneToOne,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
Expand All @@ -14,6 +13,11 @@ import { UserEntity } from './user.entity';

@Entity('comment')
export class CommentEntity extends AbstractEntity {
constructor(data?: Partial<CommentEntity>) {
super();
Object.assign(this, data);
}

@PrimaryGeneratedColumn({ primaryKeyConstraintName: 'PK_comment_id' })
id!: number;

Expand All @@ -34,7 +38,7 @@ export class CommentEntity extends AbstractEntity {
@Column({ name: 'author_id' })
authorId!: number;

@OneToOne(() => UserEntity)
@ManyToOne(() => UserEntity, (user) => user.comments)
@JoinColumn({
name: 'author_id',
referencedColumnName: 'id',
Expand Down
4 changes: 4 additions & 0 deletions packages/database-typeorm/src/entities/user.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from 'typeorm';
import { AbstractEntity } from './abstract.entity';
import { ArticleEntity } from './article.entity';
import { CommentEntity } from './comment.entity';
import { UserFollowsEntity } from './user-follows.entity';

@Entity('user')
Expand Down Expand Up @@ -48,6 +49,9 @@ export class UserEntity extends AbstractEntity {
@OneToMany(() => ArticleEntity, (article) => article.author)
articles: Relation<ArticleEntity[]>;

@OneToMany(() => CommentEntity, (comment) => comment.author)
comments: Relation<CommentEntity[]>;

@ManyToMany(() => ArticleEntity, (article) => article.favoritedBy)
@JoinTable({
name: 'user_favorites',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export class CreateUserFavoritesTable1730196400597
`);
await queryRunner.query(`
ALTER TABLE "user_favorites"
ADD CONSTRAINT "FK_user_favorites_article" FOREIGN KEY ("article_id") REFERENCES "article"("id") ON DELETE CASCADE ON UPDATE CASCADE
ADD CONSTRAINT "FK_user_favorites_article" FOREIGN KEY ("article_id") REFERENCES "article"("id") ON DELETE NO ACTION ON UPDATE NO ACTION
`);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ export class CreateCommentTable1730198033817 implements MigrationInterface {
"author_id" integer NOT NULL,
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
CONSTRAINT "REL_3ce66469b26697baa097f8da92" UNIQUE ("author_id"),
CONSTRAINT "PK_comment_id" PRIMARY KEY ("id")
)
`);
Expand Down

0 comments on commit c82f548

Please sign in to comment.