Files
gerbeur/api/services/comment-like-service.ts
khannurien 7afb5d3f07
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 42s
v3: added comment likes, added votes/likes list view, added db migrations mechanism, updated project dependencies
2026-06-21 14:17:56 +00:00

108 lines
3.3 KiB
TypeScript

import {
APIErrorCode,
APIException,
type User,
} from "../model/interfaces.ts";
import { db, isUserRow, userRowToApi } from "../model/db.ts";
import { notifyCommentOwnerLike } from "./notification-service.ts";
export function castCommentLike(commentId: string, userId: string): number {
let likeCount: number;
try {
db.exec("BEGIN;");
db.prepare(
`INSERT INTO comment_likes (comment_id, user_id, created_at) VALUES (?, ?, ?);`,
).run(commentId, userId, new Date().toISOString());
db.prepare(
`UPDATE comments SET like_count = like_count + 1 WHERE id = ?;`,
).run(commentId);
const row = db.prepare(
`SELECT like_count FROM comments WHERE id = ?;`,
).get(commentId) as { like_count: number } | undefined;
db.exec("COMMIT;");
likeCount = row?.like_count ?? 0;
} catch (err) {
try {
db.exec("ROLLBACK;");
} catch { /* ignore if no active transaction */ }
if (err instanceof Error && err.message.includes("UNIQUE constraint")) {
throw new APIException(
APIErrorCode.VALIDATION_ERROR,
409,
"Already liked",
);
}
throw err;
}
// Notification is best-effort — must not prevent like_ack from being sent
try {
notifyCommentOwnerLike(userId, commentId);
} catch { /* ignore */ }
return likeCount;
}
export function removeCommentLike(commentId: string, userId: string): number {
try {
db.exec("BEGIN;");
const result = db.prepare(
`DELETE FROM comment_likes WHERE comment_id = ? AND user_id = ?;`,
).run(commentId, userId);
if (result.changes === 0) {
db.exec("ROLLBACK;");
throw new APIException(APIErrorCode.NOT_FOUND, 404, "Like not found");
}
db.prepare(
`UPDATE comments SET like_count = like_count - 1 WHERE id = ?;`,
).run(commentId);
const row = db.prepare(
`SELECT like_count FROM comments WHERE id = ?;`,
).get(commentId) as { like_count: number } | undefined;
db.exec("COMMIT;");
return row?.like_count ?? 0;
} catch (err) {
if (err instanceof APIException) throw err;
db.exec("ROLLBACK;");
throw err;
}
}
export function getUserCommentLikes(userId: string): string[] {
const rows = db.prepare(
`SELECT comment_id FROM comment_likes WHERE user_id = ?;`,
).all(userId) as { comment_id: string }[];
return rows.map((r) => r.comment_id);
}
export function getCommentLikers(
commentId: string,
page: number,
limit: number,
): { items: User[]; total: number } {
const offset = (page - 1) * limit;
const totalRow = db.prepare(
`SELECT COUNT(*) as count FROM comment_likes WHERE comment_id = ?;`,
).get(commentId) as { count: number } | undefined;
const rawRows = db.prepare(
`SELECT u.id, u.username, u.password_hash, u.is_admin, u.created_at, u.updated_at,
u.avatar_mime, u.description, u.invited_by, u.email,
i.username as invited_by_username
FROM users u
LEFT JOIN users i ON i.id = u.invited_by
INNER JOIN comment_likes cl ON cl.comment_id = ? AND cl.user_id = u.id
ORDER BY cl.created_at DESC
LIMIT ? OFFSET ?;`,
).all(commentId, limit, offset);
if (!rawRows.every(isUserRow)) {
throw new APIException(
APIErrorCode.SERVER_ERROR,
500,
"Malformed user data",
);
}
return { items: rawRows.map(userRowToApi), total: totalRow?.count ?? 0 };
}