v3: comments say what the code can't — the documentation pass wanted JSDoc on every export and every field of an exported interface, which filled the code with restatements ("The dump.", "Whether it is loading.", "The props of X.", "Renders the X.") and module docs that told a reader nothing ("The hot and new feeds, and the props every main feed of the index takes."); some were wrong as well as vague. Every comment in api/, shared/ and src/ is rewritten under one rule, now stated in the README's "Comments" section: every module and export still has a JSDoc, and it gives context (who uses it and where), usage (what a caller must know: a precondition, what it leaves to the caller, a side effect) or its place in the chain (where its input comes from, where its output goes); a field, parameter, private function or local gets one only when it adds to its parent's, and context about a whole type goes on the type; a type's doc is phrased from the value's side ("Returned to Y by X when Z", "Passed to Y by X"); full sentences, as short as they can be, no label-colon-list; // goes wherever the code isn't obvious and says why, and the section banners and narration go. Every claim was checked against the code, which turned up some forty comments that were wrong: DumpCard's claimed it was only used by the hot and new feeds, the WebSocket welcome claimed to be a socket's first message (presence comes first), UpdateUserRequest said null removes a description (it keeps it), deletedDumpIds leaves out that it also holds dumps made private, THEME_COLOR claimed to mirror index.html's, and the like. The routers' JSDoc now says where they are mounted and what becomes of a thrown APIException. doc-lint.ts lets a field without a doc through; a module or export without one still fails. No code changes; 204 backend and 628 frontend tests pass, and doc:lint, deno check, deno lint and tsc are clean.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
/**
|
||||
* Categories: managing them, and which dumps belong to which.
|
||||
* Stores the categories and which dumps belong to them. The category routes
|
||||
* manage and list them, the dump service files each dump it writes under its
|
||||
* categories, and every service that lists dumps fills in their `categoryIds`.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
@@ -23,7 +25,10 @@ import { type Page, pageOf } from "../lib/pagination.ts";
|
||||
import { isReservedSlug, isValidSlugFormat } from "../lib/reserved-slugs.ts";
|
||||
import { VALIDATION } from "../../shared/validation.ts";
|
||||
|
||||
/** Throws a 400 unless `slug` is well-formed, short enough, and not reserved. */
|
||||
/**
|
||||
* Throws a 400 unless `slug` is well-formed, short enough and not reserved,
|
||||
* with a message for each case.
|
||||
*/
|
||||
function validateSlug(slug: string): void {
|
||||
if (!isValidSlugFormat(slug)) {
|
||||
throw new APIException(
|
||||
@@ -48,7 +53,6 @@ function validateSlug(slug: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns `name` trimmed, or throws a 400 when it is blank or too long. */
|
||||
function requireName(name: string): string {
|
||||
const trimmed = name.trim();
|
||||
if (!trimmed) {
|
||||
@@ -65,8 +69,9 @@ function requireName(name: string): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Rethrows a SQLite UNIQUE violation as a friendly 409 naming the offending
|
||||
* field, and anything else as is.
|
||||
* Rethrows a UNIQUE violation from SQLite as a 409 naming the slug or name
|
||||
* that is taken, and any other error as is. SQLite's message names the
|
||||
* column, so this matches on it.
|
||||
*/
|
||||
function uniqueConflict(err: unknown, slug: string, name: string): never {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
@@ -88,7 +93,7 @@ function uniqueConflict(err: unknown, slug: string, name: string): never {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns category `id`.
|
||||
* Returns category `id`, for {@link updateCategory}.
|
||||
*
|
||||
* @throws {APIException} 404 when there is no such category.
|
||||
*/
|
||||
@@ -101,7 +106,7 @@ export function getCategoryById(id: string): Category {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the category at `slug`.
|
||||
* Returns the category at `slug`, for `GET /api/categories/:slug`.
|
||||
*
|
||||
* @throws {APIException} 404 when there is no such category.
|
||||
*/
|
||||
@@ -113,7 +118,10 @@ export function getCategoryBySlug(slug: string): Category {
|
||||
return categoryRowToApi(row);
|
||||
}
|
||||
|
||||
/** Returns the id of the category at `slug`, or undefined when there is none. */
|
||||
/**
|
||||
* Returns the id of the category at `slug`, or undefined when there is none.
|
||||
* The follows routes use it for their `?category=` feed filter.
|
||||
*/
|
||||
export function getCategoryIdBySlug(slug: string): string | undefined {
|
||||
const row = db.prepare(`SELECT id FROM categories WHERE slug = ?;`).get(slug);
|
||||
return row && typeof (row as { id?: unknown }).id === "string"
|
||||
@@ -121,7 +129,9 @@ export function getCategoryIdBySlug(slug: string): string | undefined {
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/** Returns every category, by position then slug. */
|
||||
/**
|
||||
* Returns every category, by position then slug, for `GET /api/categories`.
|
||||
*/
|
||||
export function listCategories(): Category[] {
|
||||
const rows = db.prepare(
|
||||
`SELECT * FROM categories ORDER BY position ASC, slug ASC;`,
|
||||
@@ -130,7 +140,8 @@ export function listCategories(): Category[] {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a category.
|
||||
* Creates a category, for `POST /api/categories`. The name is stored trimmed,
|
||||
* and the position defaults to 0.
|
||||
*
|
||||
* @throws {APIException} 400 when the slug or name is invalid, 409 when
|
||||
* either is taken.
|
||||
@@ -166,7 +177,8 @@ export function createCategory(req: CreateCategoryRequest): Category {
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies `req` to category `id`.
|
||||
* Applies `req` to category `id`, for `PUT /api/categories/:id`, and returns
|
||||
* the category as it now is. Fields `req` leaves out keep their value.
|
||||
*
|
||||
* @throws {APIException} 404 when there is no such category, 400 when the new
|
||||
* slug or name is invalid, 409 when either is taken.
|
||||
@@ -211,7 +223,8 @@ export function updateCategory(
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes category `id`; its dumps stay, outside it.
|
||||
* Deletes category `id`, for `DELETE /api/categories/:id`. Its dumps stay, and
|
||||
* lose it from their `categoryIds`.
|
||||
*
|
||||
* @throws {APIException} 404 when there is no such category.
|
||||
*/
|
||||
@@ -223,9 +236,9 @@ export function deleteCategory(id: string): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that every id names an existing category. Callers that write
|
||||
* anything else first (a dump row, an uploaded file) run this before those
|
||||
* writes, so a bad id can't leave them half-done.
|
||||
* Checks that every id in `categoryIds` names a category. The dump service
|
||||
* runs it before it writes a dump row or stores an uploaded file, so a bad id
|
||||
* fails the request before anything is written.
|
||||
*
|
||||
* @throws {APIException} 400 when an id names no category.
|
||||
*/
|
||||
@@ -248,8 +261,9 @@ export function assertCategoriesExist(categoryIds: string[]): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the set of categories a dump belongs to. An empty list clears all
|
||||
* category assignments.
|
||||
* Replaces the categories of dump `dumpId` with `categoryIds`, duplicates
|
||||
* dropped. An empty list takes the dump out of every category. Called inside
|
||||
* the dump service's transaction, it joins that one.
|
||||
*
|
||||
* @throws {APIException} 400 when an id names no category.
|
||||
*/
|
||||
@@ -271,7 +285,10 @@ export function setDumpCategories(
|
||||
});
|
||||
}
|
||||
|
||||
/** Fills `categoryIds` on each dump with a single batched lookup. */
|
||||
/**
|
||||
* Sets `categoryIds` on each of `dumps` in place, in one query. The dump row
|
||||
* doesn't carry them, so every service that returns dumps calls this first.
|
||||
*/
|
||||
export function attachCategoryIds(dumps: Dump[]): void {
|
||||
if (dumps.length === 0) return;
|
||||
const ids = dumps.map((d) => d.id);
|
||||
@@ -292,8 +309,9 @@ export function attachCategoryIds(dumps: Dump[]): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a page of the dumps in the category at `slug`, newest first, with
|
||||
* the requesting user's private ones.
|
||||
* Returns a page of the dumps in the category at `slug`, newest first, for
|
||||
* `GET /api/categories/:slug/dumps`. Private dumps are left out, except
|
||||
* `requestingUserId`'s own.
|
||||
*
|
||||
* @throws {APIException} 404 when there is no such category.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user