Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions migrations/1773680040687_create-table-comment-likes.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ export const up = (pgm) => {
type: 'VARCHAR(50)',
primaryKey: true,
},
comment_id: {
comment_id: { // eslint-disable-line camelcase
type: 'VARCHAR(50)',
notNull: true,
references: 'comments(id)',
onDelete: 'CASCADE',
},
user_id: {
user_id: { // eslint-disable-line camelcase
type: 'VARCHAR(50)',
notNull: true,
references: 'users(id)',
Expand All @@ -26,7 +26,11 @@ export const up = (pgm) => {
notNull: true,
default: pgm.func('CURRENT_TIMESTAMP'),
},
})
});

pgm.addConstraint('comment_likes', 'unique_comment_id_and_user_id', {
unique: ['comment_id', 'user_id']
});
};

/**
Expand Down
5 changes: 5 additions & 0 deletions src/Applications/use_case/GetThreadUseCase.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ class GetThreadUseCase {
threadRepository,
commentRepository,
replyRepository,
likeRepository,
}) {
this._threadRepository = threadRepository;
this._commentRepository = commentRepository;
this._replyRepository = replyRepository;
this._likeRepository = likeRepository;
}

async execute(useCasePayload) {
Expand All @@ -18,6 +20,8 @@ class GetThreadUseCase {
const commentIds = comments.map((comment) => comment.id);

const replies = commentIds.length ? await this._replyRepository.getRepliesByCommentIds(commentIds) : [];
const commentLikes = commentIds.length ? await this._likeRepository.getLikeCountsByCommentIds(commentIds) : {};

const repliesByCommentId = replies.reduce((acc, reply) => {
acc[reply.commentId] = acc[reply.commentId] || [];
acc[reply.commentId].push({
Expand All @@ -33,6 +37,7 @@ class GetThreadUseCase {
const mappedComments = comments.map((comment) => ({
...comment,
content: comment.isDelete ? '**komentar telah dihapus**' : comment.content,
likeCount: commentLikes[comment.id] || 0,
replies: (repliesByCommentId[comment.id] || []).map((reply) => ({
...reply,
content: reply.isDelete ? '**balasan telah dihapus**' : reply.content,
Expand Down
4 changes: 2 additions & 2 deletions src/Applications/use_case/LikeCommentUseCase.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ class LikeCommentUseCase {

const isLiked = await this._likeRepository.isLikedComment(commentId, userId);
if (isLiked) {
await this._likeRepository.unlikeComment(commentId, userId);
await this._likeRepository.deleteLikedComment(commentId, userId);
} else {
await this._likeRepository.likeComment(commentId, userId);
await this._likeRepository.addLikeComment(commentId, userId);
}
}
}
Expand Down
19 changes: 17 additions & 2 deletions src/Applications/use_case/_test/GetThreadUseCase.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import DetailComment from '../../../Domains/comments/entities/DetailComment.js';
import GetThreadUseCase from '../GetThreadUseCase.js';
import ReplyRepository from '../../../Domains/replies/ReplyRepository.js';
import DetailReply from '../../../Domains/replies/entities/DetailReply.js';
import LikeRepository from '../../../Domains/likes/LikeRepository.js';

describe('GetThreadUseCase', () => {
it('should orchestrating the get detail thread action correctly', async () => {
Expand All @@ -13,6 +14,7 @@ describe('GetThreadUseCase', () => {
const mockCommentRepository = new CommentRepository();
const mockThreadRepository = new ThreadRepository();
const mockReplyRepository = new ReplyRepository();
const mockLikeRepository = new LikeRepository();

mockThreadRepository.verifyThreadExists = vi.fn()
.mockImplementation(() => Promise.resolve());
Expand Down Expand Up @@ -52,10 +54,13 @@ describe('GetThreadUseCase', () => {
isDelete: true,
}),
]));
mockLikeRepository.getLikeCountsByCommentIds = vi.fn()
.mockImplementation(() => Promise.resolve({}));
const getThreadUseCase = new GetThreadUseCase({
threadRepository: mockThreadRepository,
commentRepository: mockCommentRepository,
replyRepository: mockReplyRepository,
likeRepository: mockLikeRepository,
});

const thread = await getThreadUseCase.execute(useCasePayload);
Expand All @@ -64,8 +69,8 @@ describe('GetThreadUseCase', () => {
expect(mockThreadRepository.getThreadById).toHaveBeenCalledWith(useCasePayload.threadId);
expect(mockThreadRepository.verifyThreadExists).toHaveBeenCalledWith(useCasePayload.threadId);
expect(mockCommentRepository.getCommentsByThreadId).toHaveBeenCalledWith(useCasePayload.threadId);
expect(mockLikeRepository.getLikeCountsByCommentIds).toHaveBeenCalledWith(['comment-1', 'comment-2']);

expect(thread.date).toEqual(thread.date);
expect(thread.comments).toHaveLength(2);
expect(thread.comments[0].content).toEqual('**komentar telah dihapus**');
expect(thread.comments[1].content).toEqual('komentar 2');
Expand All @@ -75,6 +80,7 @@ describe('GetThreadUseCase', () => {
const mockThreadRepository = new ThreadRepository();
const mockReplyRepository = new ReplyRepository();
const mockCommentRepository = new CommentRepository();
const mockLikeRepository = new LikeRepository();

mockThreadRepository.verifyThreadExists = vi.fn()
.mockImplementation(() => Promise.resolve());
Expand Down Expand Up @@ -107,11 +113,14 @@ describe('GetThreadUseCase', () => {
isDelete: false,
}),
]));
mockLikeRepository.getLikeCountsByCommentIds = vi.fn()
.mockImplementation(() => Promise.resolve({}));

const getThreadUseCase = new GetThreadUseCase({
commentRepository: mockCommentRepository,
threadRepository: mockThreadRepository,
replyRepository: mockReplyRepository,
likeRepository: mockLikeRepository,
});

const thread = await getThreadUseCase.execute('thread-123');
Expand All @@ -123,6 +132,7 @@ describe('GetThreadUseCase', () => {
const mockThreadRepository = new ThreadRepository();
const mockReplyRepository = new ReplyRepository();
const mockCommentRepository = new CommentRepository();
const mockLikeRepository = new LikeRepository();

mockThreadRepository.verifyThreadExists = vi.fn()
.mockImplementation(() => Promise.resolve());
Expand Down Expand Up @@ -155,11 +165,14 @@ describe('GetThreadUseCase', () => {
isDelete: true,
}),
]));
mockLikeRepository.getLikeCountsByCommentIds = vi.fn()
.mockImplementation(() => Promise.resolve({}));

const getThreadUseCase = new GetThreadUseCase({
commentRepository: mockCommentRepository,
threadRepository: mockThreadRepository,
replyRepository: mockReplyRepository,
likeRepository: mockLikeRepository,
});

const thread = await getThreadUseCase.execute('thread-123');
Expand All @@ -172,6 +185,7 @@ describe('GetThreadUseCase', () => {
const mockThreadRepository = new ThreadRepository();
const mockCommentRepository = new CommentRepository();
const mockReplyRepository = new ReplyRepository();
const mockLikeRepository = new LikeRepository();

mockThreadRepository.verifyThreadExists = vi.fn().mockResolvedValue();
mockThreadRepository.getThreadById = vi.fn().mockResolvedValue({
Expand All @@ -181,14 +195,15 @@ describe('GetThreadUseCase', () => {
date: '2026-03-14T16:10:20.555Z',
username: 'dicoding',
});

mockCommentRepository.getCommentsByThreadId = vi.fn().mockResolvedValue([]);
mockReplyRepository.getRepliesByCommentIds = vi.fn();
mockLikeRepository.getLikeCountsByCommentIds = vi.fn();

const getThreadUseCase = new GetThreadUseCase({
threadRepository: mockThreadRepository,
commentRepository: mockCommentRepository,
replyRepository: mockReplyRepository,
likeRepository: mockLikeRepository,
});

const thread = await getThreadUseCase.execute(useCasePayload);
Expand Down
17 changes: 8 additions & 9 deletions src/Applications/use_case/_test/LikeCommentUseCase.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { describe, expect, vi } from 'vitest';
import ThreadRepository from '../../../Domains/threads/ThreadRepository.js';
import CommentRepository from '../../../Domains/comments/CommentRepository.js';
import LikeRepository from '../../../Domains/likes/LikeRepository.js';
Expand All @@ -21,9 +20,9 @@ describe('LikeCommentUseCase', () => {
.mockImplementation(() => Promise.resolve());
mockLikeRepository.isLikedComment = vi.fn()
.mockImplementation(() => Promise.resolve(false));
mockLikeRepository.likeComment = vi.fn()
mockLikeRepository.addLikeComment = vi.fn()
.mockImplementation(() => Promise.resolve());
mockLikeRepository.unlikeComment = vi.fn()
mockLikeRepository.deleteLikedComment = vi.fn()
.mockImplementation(() => Promise.resolve());

const likeCommentUseCase = new LikeCommentUseCase({
Expand All @@ -37,8 +36,8 @@ describe('LikeCommentUseCase', () => {
expect(mockThreadRepository.verifyThreadExists).toHaveBeenCalledWith(useCasePayload.threadId);
expect(mockCommentRepository.verifyCommentExistsOnThread).toHaveBeenCalledWith(useCasePayload.commentId, useCasePayload.threadId);
expect(mockLikeRepository.isLikedComment).toHaveBeenCalledWith(useCasePayload.commentId, userId);
expect(mockLikeRepository.likeComment).toHaveBeenCalledExactlyOnceWith(useCasePayload.commentId, userId);
expect(mockLikeRepository.unlikeComment).not.toHaveBeenCalled();
expect(mockLikeRepository.addLikeComment).toHaveBeenCalledExactlyOnceWith(useCasePayload.commentId, userId);
expect(mockLikeRepository.deleteLikedComment).not.toHaveBeenCalled();
});

it('should orchestrating unlike comment action properly', async () => {
Expand All @@ -57,9 +56,9 @@ describe('LikeCommentUseCase', () => {
.mockImplementation(() => Promise.resolve());
mockLikeRepository.isLikedComment = vi.fn()
.mockImplementation(() => Promise.resolve(true));
mockLikeRepository.unlikeComment = vi.fn()
mockLikeRepository.deleteLikedComment = vi.fn()
.mockImplementation(() => Promise.resolve());
mockLikeRepository.likeComment = vi.fn()
mockLikeRepository.addLikeComment = vi.fn()
.mockImplementation(() => Promise.resolve());

const likeCommentUseCase = new LikeCommentUseCase({
Expand All @@ -73,7 +72,7 @@ describe('LikeCommentUseCase', () => {
expect(mockThreadRepository.verifyThreadExists).toHaveBeenCalledWith(useCasePayload.threadId);
expect(mockCommentRepository.verifyCommentExistsOnThread).toHaveBeenCalledWith(useCasePayload.commentId, useCasePayload.threadId);
expect(mockLikeRepository.isLikedComment).toHaveBeenCalledWith(useCasePayload.commentId, userId);
expect(mockLikeRepository.unlikeComment).toHaveBeenCalledExactlyOnceWith(useCasePayload.commentId, userId);
expect(mockLikeRepository.likeComment).not.toHaveBeenCalled();
expect(mockLikeRepository.deleteLikedComment).toHaveBeenCalledExactlyOnceWith(useCasePayload.commentId, userId);
expect(mockLikeRepository.addLikeComment).not.toHaveBeenCalled();
});
});
8 changes: 4 additions & 4 deletions src/Domains/likes/LikeRepository.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
class LikeRepository {
async likeComment(commentId, userId) {
async addLikeComment(commentId, userId) { // eslint-disable-line no-unused-vars
throw new Error('LIKE_REPOSITORY.METHOD_NOT_IMPLEMENTED');
}

async unlikeComment(commentId, userId) {
async deleteLikedComment(commentId, userId) { // eslint-disable-line no-unused-vars
throw new Error('LIKE_REPOSITORY.METHOD_NOT_IMPLEMENTED');
}

async isLikedComment(commentId, userId) {
async isLikedComment(commentId, userId) { // eslint-disable-line no-unused-vars
throw new Error('LIKE_REPOSITORY.METHOD_NOT_IMPLEMENTED');
}

async getLikeCountByCommentIds(commentIds) {
async getLikeCountsByCommentIds(commentIds) { // eslint-disable-line no-unused-vars
throw new Error('LIKE_REPOSITORY.METHOD_NOT_IMPLEMENTED');
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/Domains/likes/_test/LikeRepository.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ describe('LikeRepository interface', () => {
it('should throw error when invoke abstract behavior', async () => {
const likeRepository = new LikeRepository();

await expect(likeRepository.likeComment('', '')).rejects.toThrowError('LIKE_REPOSITORY.METHOD_NOT_IMPLEMENTED');
await expect(likeRepository.unlikeComment('', '')).rejects.toThrowError('LIKE_REPOSITORY.METHOD_NOT_IMPLEMENTED');
await expect(likeRepository.addLikeComment('', '')).rejects.toThrowError('LIKE_REPOSITORY.METHOD_NOT_IMPLEMENTED');
await expect(likeRepository.deleteLikedComment('', '')).rejects.toThrowError('LIKE_REPOSITORY.METHOD_NOT_IMPLEMENTED');
await expect(likeRepository.isLikedComment('', '')).rejects.toThrowError('LIKE_REPOSITORY.METHOD_NOT_IMPLEMENTED');
await expect(likeRepository.getLikeCountByCommentIds([])).rejects.toThrowError('LIKE_REPOSITORY.METHOD_NOT_IMPLEMENTED');
await expect(likeRepository.getLikeCountsByCommentIds([])).rejects.toThrowError('LIKE_REPOSITORY.METHOD_NOT_IMPLEMENTED');
});
});
44 changes: 43 additions & 1 deletion src/Infrastructures/container.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ import AddReplyUseCase from '../Applications/use_case/AddReplyUseCase.js';
import ReplyRepository from '../Domains/replies/ReplyRepository.js';
import ReplyRepositoryPostgres from './repository/ReplyRepositoryPostgres.js';
import DeleteReplyUseCase from '../Applications/use_case/DeleteReplyUseCase.js';
import LikeRepository from '../Domains/likes/LikeRepository.js';
import LikeRepositoryPostgres from './repository/LikeRepositoryPostgres.js';
import LikeCommentUseCase from '../Applications/use_case/LikeCommentUseCase.js';

// creating container
const container = createContainer();
Expand Down Expand Up @@ -108,6 +111,20 @@ container.register([
]
}
},
{
key: LikeRepository.name,
Class: LikeRepositoryPostgres,
parameter: {
dependencies: [
{
concrete: pool
},
{
concrete: nanoid
},
]
}
},
{
key: PasswordHash.name,
Class: BcryptPasswordHash,
Expand Down Expand Up @@ -270,7 +287,11 @@ container.register([
{
name: 'replyRepository',
internal: ReplyRepository.name,
}
},
{
name: 'likeRepository',
internal: LikeRepository.name,
},
],
},
},
Expand Down Expand Up @@ -312,6 +333,27 @@ container.register([
],
},
},
{
key: LikeCommentUseCase.name,
Class: LikeCommentUseCase,
parameter: {
injectType: 'destructuring',
dependencies: [
{
name: 'likeRepository',
internal: LikeRepository.name,
},
{
name: 'commentRepository',
internal: CommentRepository.name,
},
{
name: 'threadRepository',
internal: ThreadRepository.name,
},
]
}
},
]);

export default container;
Loading
Loading