Files
gerbeur/api/services/comment-service.ts
khannurien ed7695663e
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 44s
v3: correctly using SITE_NAME across the app, added notifications on comments
2026-04-08 20:12:30 +00:00

154 lines
4.4 KiB
TypeScript

import {
APIErrorCode,
APIException,
type Comment,
} from "../model/interfaces.ts";
import { type SQLOutputValue } from "node:sqlite";
import { commentRowToApi, db, isCommentRow } from "../model/db.ts";
import {
notifyDumpOwnerNewComment,
notifyMentions,
} from "./notification-service.ts";
import { linkAttachments } from "./attachment-service.ts";
const SELECT_COLS =
`c.id, c.dump_id, c.user_id, c.parent_id, c.body, c.created_at, c.updated_at, c.deleted,
u.username as author_username, u.avatar_mime as author_avatar_mime`;
function fetchComment(commentId: string): Comment {
const row = db.prepare(
`SELECT ${SELECT_COLS} FROM comments c JOIN users u ON c.user_id = u.id WHERE c.id = ?;`,
).get(commentId);
if (!row || !isCommentRow(row as Record<string, SQLOutputValue>)) {
throw new APIException(APIErrorCode.NOT_FOUND, 404, "Comment not found");
}
if (!isCommentRow(row)) {
throw new APIException(
APIErrorCode.SERVER_ERROR,
500,
"Malformed comment data",
);
}
return commentRowToApi(row);
}
export function getComments(dumpId: string): Comment[] {
const rows = db.prepare(
`SELECT ${SELECT_COLS} FROM comments c JOIN users u ON c.user_id = u.id
WHERE c.dump_id = ? ORDER BY c.created_at ASC;`,
).all(dumpId);
if (!rows.every(isCommentRow)) {
throw new APIException(
APIErrorCode.SERVER_ERROR,
500,
"Malformed comment data",
);
}
return rows.map(commentRowToApi);
}
export function createComment(
dumpId: string,
userId: string,
body: string,
parentId?: string,
): Comment {
const id = crypto.randomUUID();
const createdAt = new Date();
db.prepare(
`INSERT INTO comments (id, dump_id, user_id, parent_id, body, created_at) VALUES (?, ?, ?, ?, ?, ?);`,
).run(
id,
dumpId,
userId,
parentId ?? null,
body.trim(),
createdAt.toISOString(),
);
const comment = fetchComment(id);
const dumpRow = db.prepare(`SELECT title FROM dumps WHERE id = ?;`).get(
dumpId,
) as { title: string } | undefined;
notifyDumpOwnerNewComment(userId, id, dumpId);
notifyMentions(userId, body, "comment", id, dumpRow?.title ?? "", dumpId);
linkAttachments(body, id);
return comment;
}
export function updateComment(
commentId: string,
body: string,
requestingUserId: string,
isAdmin: boolean,
): { comment: Comment; dumpId: string; isPrivate: boolean } {
const row = db.prepare(
`SELECT c.dump_id, d.is_private FROM comments c JOIN dumps d ON c.dump_id = d.id WHERE c.id = ?;`,
).get(commentId) as { dump_id: string; is_private: number } | undefined;
if (!row) {
throw new APIException(APIErrorCode.NOT_FOUND, 404, "Comment not found");
}
const existing = fetchComment(commentId);
if (existing.deleted) {
throw new APIException(
APIErrorCode.VALIDATION_ERROR,
400,
"Cannot edit a deleted comment",
);
}
if (existing.userId !== requestingUserId && !isAdmin) {
throw new APIException(
APIErrorCode.UNAUTHORIZED,
401,
"Not authorized to edit this comment",
);
}
const now = new Date().toISOString();
db.prepare(`UPDATE comments SET body = ?, updated_at = ? WHERE id = ?;`).run(
body.trim(),
now,
commentId,
);
const dumpRow = db.prepare(`SELECT title FROM dumps WHERE id = ?;`).get(
row.dump_id,
) as { title: string } | undefined;
notifyMentions(
requestingUserId,
body,
"comment",
commentId,
dumpRow?.title ?? "",
row.dump_id,
);
linkAttachments(body, commentId);
return {
comment: fetchComment(commentId),
dumpId: row.dump_id,
isPrivate: Boolean(row.is_private),
};
}
export function deleteComment(
commentId: string,
requestingUserId: string,
isAdmin: boolean,
): { dumpId: string; isPrivate: boolean } {
const row = db.prepare(
`SELECT c.dump_id, d.is_private FROM comments c JOIN dumps d ON c.dump_id = d.id WHERE c.id = ?;`,
).get(commentId) as { dump_id: string; is_private: number } | undefined;
if (!row) {
throw new APIException(APIErrorCode.NOT_FOUND, 404, "Comment not found");
}
const comment = fetchComment(commentId);
if (comment.userId !== requestingUserId && !isAdmin) {
throw new APIException(
APIErrorCode.UNAUTHORIZED,
401,
"Not authorized to delete this comment",
);
}
db.prepare(`UPDATE comments SET deleted = 1, body = '' WHERE id = ?;`).run(
commentId,
);
return { dumpId: row.dump_id, isPrivate: Boolean(row.is_private) };
}