v3: added site-wide categories, added admin category management, various visual fixes
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 2m52s

This commit is contained in:
khannurien
2026-06-28 16:23:44 +00:00
parent c8c7b05c25
commit fae25f3e6c
44 changed files with 2144 additions and 399 deletions

View File

@@ -33,6 +33,7 @@ import {
} from "../config.ts";
import { linkAttachments } from "./attachment-service.ts";
import { relinkBacklinks } from "./backlink-service.ts";
import { attachCategoryIds, setDumpCategories } from "./category-service.ts";
function isAllowedMime(mime: string): boolean {
return DUMP_ALLOWED_MIME_PREFIXES.some((p) => mime.startsWith(p)) ||
@@ -78,6 +79,9 @@ export async function createUrlDump(
isPrivate ? 1 : 0,
);
const categoryIds = [...new Set(request.categoryIds ?? [])];
setDumpCategories(dumpId, categoryIds);
const dump: Dump = {
id: dumpId,
kind: "url",
@@ -91,6 +95,7 @@ export async function createUrlDump(
voteCount: 0,
commentCount: 0,
isPrivate,
categoryIds,
};
if (!isPrivate) {
broadcastNewDump(dump);
@@ -110,6 +115,7 @@ export async function createFileDump(
userId: string,
isPrivate = false,
title?: string,
categoryIds: string[] = [],
): Promise<Dump> {
if (!isAllowedMime(file.type)) {
throw new APIException(
@@ -130,6 +136,7 @@ export async function createFileDump(
const createdAt = new Date();
const finalTitle = title?.trim() || file.name;
const slug = makeSlug(finalTitle, dumpId);
const dedupedCategoryIds = [...new Set(categoryIds)];
await Deno.mkdir(DUMPS_DIR, { recursive: true });
const data = new Uint8Array(await file.arrayBuffer());
@@ -153,6 +160,7 @@ export async function createFileDump(
file.size,
isPrivate ? 1 : 0,
);
setDumpCategories(dumpId, dedupedCategoryIds);
} catch (err) {
// Roll back the file if DB insert fails
await Deno.remove(`${DUMPS_DIR}/${dumpId}`).catch(() => {});
@@ -173,6 +181,7 @@ export async function createFileDump(
voteCount: 0,
commentCount: 0,
isPrivate,
categoryIds: dedupedCategoryIds,
};
if (!isPrivate) {
broadcastNewDump(dump);
@@ -205,6 +214,7 @@ export function getDump(dumpId: string, requestingUserId?: string): Dump {
if (dump.isPrivate && dump.userId !== requestingUserId) {
throw new APIException(APIErrorCode.NOT_FOUND, 404, "Dump not found");
}
attachCategoryIds([dump]);
return dump;
}
@@ -229,10 +239,9 @@ export function searchDumps(
const totalRow = db.prepare(
`SELECT COUNT(*) as count FROM dumps WHERE (is_private = 0 OR user_id = ?) AND ${searchClause};`,
).get(requestingUserId, ...searchParams) as { count: number } | undefined;
return {
items: rows.filter(isDumpRow).map(dumpRowToApi),
total: totalRow?.count ?? 0,
};
const items = rows.filter(isDumpRow).map(dumpRowToApi);
attachCategoryIds(items);
return { items, total: totalRow?.count ?? 0 };
} else {
const rows = db.prepare(
`SELECT ${SELECT_COLS} FROM dumps WHERE is_private = 0 AND ${searchClause} ORDER BY created_at DESC LIMIT ? OFFSET ?;`,
@@ -240,10 +249,9 @@ export function searchDumps(
const totalRow = db.prepare(
`SELECT COUNT(*) as count FROM dumps WHERE is_private = 0 AND ${searchClause};`,
).get(...searchParams) as { count: number } | undefined;
return {
items: rows.filter(isDumpRow).map(dumpRowToApi),
total: totalRow?.count ?? 0,
};
const items = rows.filter(isDumpRow).map(dumpRowToApi);
attachCategoryIds(items);
return { items, total: totalRow?.count ?? 0 };
}
}
@@ -277,7 +285,9 @@ export function listDumps(
);
}
return { items: rows.map(dumpRowToApi), total: totalRow?.count ?? 0 };
const items = rows.map(dumpRowToApi);
attachCategoryIds(items);
return { items, total: totalRow?.count ?? 0 };
}
export async function updateDump(
@@ -314,6 +324,13 @@ export async function updateDump(
now.toISOString(),
dumpId,
);
if (request.categoryIds !== undefined) {
const ids = [...new Set(request.categoryIds)];
setDumpCategories(dumpId, ids);
updatedDump.categoryIds = ids;
} else {
attachCategoryIds([updatedDump]);
}
if (updatedDump.isPrivate && !dump.isPrivate) broadcastDumpDeleted(dumpId);
else if (!updatedDump.isPrivate) broadcastDumpUpdated(updatedDump);
if (updatedDump.comment) {
@@ -382,6 +399,14 @@ export async function updateDump(
throw new APIException(APIErrorCode.NOT_FOUND, 404, "Dump not found");
}
if (request.categoryIds !== undefined) {
const ids = [...new Set(request.categoryIds)];
setDumpCategories(dumpId, ids);
updatedDump.categoryIds = ids;
} else {
attachCategoryIds([updatedDump]);
}
if (updatedDump.isPrivate && !dump.isPrivate) broadcastDumpDeleted(dumpId);
else if (!updatedDump.isPrivate) broadcastDumpUpdated(updatedDump);
if (updatedDump.comment) {
@@ -403,6 +428,7 @@ export async function replaceFileDump(
file: File,
comment: string | undefined,
title?: string,
categoryIds?: string[],
): Promise<Dump> {
if (!isAllowedMime(file.type)) {
throw new APIException(
@@ -455,6 +481,14 @@ export async function replaceFileDump(
throw err;
}
if (categoryIds !== undefined) {
const ids = [...new Set(categoryIds)];
setDumpCategories(dumpId, ids);
dump.categoryIds = ids;
} else {
attachCategoryIds([dump]);
}
if (comment) {
notifyMentions(dump.userId, comment, "dump", dumpId, finalTitle);
linkAttachments(comment, dumpId);
@@ -493,7 +527,9 @@ export function getDumpsByUser(
"Malformed dump data",
);
}
return { items: rows.map(dumpRowToApi), total: totalRow?.count ?? 0 };
const items = rows.map(dumpRowToApi);
attachCategoryIds(items);
return { items, total: totalRow?.count ?? 0 };
}
export function getVotedDumpsByUser(
@@ -544,7 +580,9 @@ export function getVotedDumpsByUser(
"Malformed dump data",
);
}
return { items: rows.map(dumpRowToApi), total: totalRow?.count ?? 0 };
const items = rows.map(dumpRowToApi);
attachCategoryIds(items);
return { items, total: totalRow?.count ?? 0 };
}
export async function refreshDumpMetadata(dumpId: string): Promise<Dump> {