147 lines
4.2 KiB
TypeScript
147 lines
4.2 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 { notifyMentions } from "./notification-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;
|
|
notifyMentions(userId, body, "comment", id, dumpRow?.title ?? "", dumpId);
|
|
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,
|
|
);
|
|
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) };
|
|
}
|