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
32 changes: 32 additions & 0 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Continuous Deployment

on:
push:
branches: [main]

jobs:
deploy:
runs-on: ubuntu-latest

steps:
- name: Deploy to Server via SSH
uses: appleboy/ssh-action@v1.0.0
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_KEY }}
port: 22
script: |
cd forum-api

echo "Pull latest code..."
git pull origin main

echo "Install dependencies..."
npm install

echo "Run migration..."
npm run migrate:prod

echo "Restart app..."
pm2 restart forum-api || pm2 start npm --name "forum-api" -- start
58 changes: 58 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: Continuous Integration

on:
pull_request:
branches:
- main

jobs:
test:
runs-on: ubuntu-latest

services:
postgres:
image: postgres:15
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: forumapi_test
ports:
- 5432:5432
options: >-
--health-cmd="pg_isready -U postgres"
--health-interval=10s
--health-timeout=5s
--health-retries=5

env:
PGHOST: localhost
PGUSER: postgres
PGPASSWORD: password
PGDATABASE: forumapi_test
PGPORT: 5432

steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 18

- name: Install dependencies
run: npm install

- name: Wait for PostgreSQL
run: |
for i in {1..10}; do
pg_isready -h localhost -p 5432 && break
echo "Waiting for postgres..."
sleep 2
done

- name: Run migration
run: npm run migrate:test

- name: Run test
run: npm test
7 changes: 7 additions & 0 deletions config/database/process.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const config = {
user: process.env.PGUSER,
password: process.env.PGPASSWORD,
host: process.env.PGHOST,
port: process.env.PGPORT,
database: process.env.PGDATABASE,
};
54 changes: 54 additions & 0 deletions migrations/1776882637934_create-table-user-comment-likes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
export const up = (pgm) => {
pgm.createTable('user_comment_likes', {
id: {
type: 'VARCHAR(50)',
primaryKey: true,
},
// eslint-disable-next-line camelcase
user_id: {
type: 'VARCHAR(50)',
notNull: true,
},
// eslint-disable-next-line camelcase
comment_id: {
type: 'VARCHAR(50)',
notNull: true,
},
});

pgm.addConstraint(
'user_comment_likes',
'fk_user_comment_likes.user_id_users.id',
{
foreignKeys: {
columns: 'user_id',
references: 'users(id)',
onDelete: 'CASCADE',
},
}
);

pgm.addConstraint(
'user_comment_likes',
'fk_user_comment_likes.comment_id_comments.id',
{
foreignKeys: {
columns: 'comment_id',
references: 'comments(id)',
onDelete: 'CASCADE',
},
}
);

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

export const down = (pgm) => {
pgm.dropTable('user_comment_likes');
};
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
"test": "vitest --run",
"test:watch": "vitest --watch",
"test:coverage": "vitest --coverage",
"migrate": "node-pg-migrate",
"migrate:test": "node-pg-migrate --envPath .test.env",
"migrate": "node-pg-migrate up",
"migrate:test": "node-pg-migrate up",
"lint": "eslint ./"
},
"keywords": [],
Expand Down
4 changes: 2 additions & 2 deletions src/Applications/use_case/GetThreadDetailUseCase.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ class GetThreadDetailUseCase {

const thread = await this._threadRepository.getThreadById(threadId);

const comments =
(await this._commentRepository.getCommentsByThreadId(threadId)) || [];
const comments = thread.comments || [];

const commentIds = comments.map((c) => c.id);

Expand Down Expand Up @@ -55,6 +54,7 @@ class GetThreadDetailUseCase {
date: this._normalizeDate(comment.date),
content: comment.content,
isDelete: comment.is_delete,
likeCount: comment.likeCount,
replies: mappedReplies,
});
});
Expand Down
36 changes: 36 additions & 0 deletions src/Applications/use_case/ToggleLikeCommentUseCase.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
class ToggleLikeCommentUseCase {
constructor({
userCommentLikeRepository,
commentRepository,
threadRepository,
}) {
this._userCommentLikeRepository = userCommentLikeRepository;
this._commentRepository = commentRepository;
this._threadRepository = threadRepository;
}

async execute({ userId, threadId, commentId }) {
await this._threadRepository.verifyThreadExists(threadId);

await this._commentRepository.verifyCommentExists(commentId);

const isLiked = await this._userCommentLikeRepository.verifyLike(
userId,
commentId
);

if (isLiked) {
await this._userCommentLikeRepository.deleteLike(
userId,
commentId
);
} else {
await this._userCommentLikeRepository.addLike(
userId,
commentId
);
}
}
}

export default ToggleLikeCommentUseCase;
47 changes: 22 additions & 25 deletions src/Applications/use_case/_test/GetThreadDetailUseCase.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,30 +14,29 @@ describe('GetThreadDetailUseCase', () => {
body: 'body',
date: new Date().toISOString(),
username: 'john',
comments: [
{
id: 'comment-1',
content: 'comment 1',
date: new Date().toISOString(),
username: 'john',
// eslint-disable-next-line camelcase
is_delete: false,
likeCount: 1,
},
{
id: 'comment-2',
content: 'comment 2',
date: new Date().toISOString(),
username: 'doe',
// eslint-disable-next-line camelcase
is_delete: true,
likeCount: 0,
},
],
}),
};

const mockCommentRepo = {
getCommentsByThreadId: vi.fn().mockResolvedValue([
{
id: 'comment-1',
content: 'comment 1',
date: new Date().toISOString(),
username: 'john',
// eslint-disable-next-line camelcase
is_delete: false,
},
{
id: 'comment-2',
content: 'comment 2',
date: new Date().toISOString(),
username: 'doe',
// eslint-disable-next-line camelcase
is_delete: true,
},
]),
};

const mockReplyRepo = {
getRepliesByCommentIds: vi.fn().mockResolvedValue([
{
Expand All @@ -63,7 +62,6 @@ describe('GetThreadDetailUseCase', () => {

const useCase = new GetThreadDetailUseCase({
threadRepository: mockThreadRepo,
commentRepository: mockCommentRepo,
replyRepository: mockReplyRepo,
});

Expand All @@ -79,9 +77,6 @@ describe('GetThreadDetailUseCase', () => {
expect(mockThreadRepo.getThreadById)
.toHaveBeenCalledWith(threadId);

expect(mockCommentRepo.getCommentsByThreadId)
.toHaveBeenCalledWith(threadId);

expect(mockReplyRepo.getRepliesByCommentIds)
.toHaveBeenCalledWith(['comment-1', 'comment-2']);

Expand All @@ -98,6 +93,7 @@ describe('GetThreadDetailUseCase', () => {
username: 'john',
date: normalized.comments[0].date,
content: 'comment 1',
likeCount: 1,
replies: [
{
id: 'reply-1',
Expand All @@ -118,6 +114,7 @@ describe('GetThreadDetailUseCase', () => {
username: 'doe',
date: normalized.comments[1].date,
content: '**komentar telah dihapus**',
likeCount: 0,
replies: [],
},
],
Expand Down
9 changes: 1 addition & 8 deletions src/Commons/config.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
/* istanbul ignore file */
import dotenv from 'dotenv';
import path from 'path';

if (process.env.NODE_ENV === 'test') {
dotenv.config({
path: path.resolve(process.cwd(), '.test.env'),
});
} else {
dotenv.config();
}
dotenv.config();

const config = {
app: {
Expand Down
8 changes: 5 additions & 3 deletions src/Domains/comments/entities/DetailComment.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,18 @@ class DetailComment {
constructor(payload) {
this._verify(payload);

const { id, username, date, content, isDelete, replies } = payload;
const { id, username, date, content, isDelete, likeCount, replies } = payload;

this.id = id;
this.username = username;
this.date = date;
this.content = isDelete ? '**komentar telah dihapus**' : content;
this.likeCount = likeCount;
this.replies = replies;
}

_verify({ id, username, date, content, isDelete, replies }) {
if (id === undefined || username === undefined || date === undefined || content === undefined || isDelete === undefined || replies === undefined) {
_verify({ id, username, date, content, isDelete, likeCount, replies }) {
if (id === undefined || username === undefined || date === undefined || content === undefined || isDelete === undefined || likeCount === undefined || replies === undefined) {
throw new Error('DETAIL_COMMENT.NOT_CONTAIN_NEEDED_PROPERTY');
}

Expand All @@ -21,6 +22,7 @@ class DetailComment {
typeof date !== 'string' ||
typeof content !== 'string' ||
typeof isDelete !== 'boolean' ||
typeof likeCount !== 'number' ||
!Array.isArray(replies)) {
throw new Error('DETAIL_COMMENT.NOT_MEET_DATA_TYPE_SPECIFICATION');
}
Expand Down
Loading
Loading