v3: the backend is documented for deno doc, one way in every file, and the docs build and publish themselves — comments were in three shapes: file headers as // lines, as an untagged /** */ (which deno doc drops, only an @module block counts) or missing; 43 exports described by a // deno doc can't see, two JSDoc blocks sitting on the wrong declaration and one duplicated; endpoints commented in 5 of 18 route files, three ways, on router.get() calls no doc tool reads. The rule, applied to every non-test module of api/, migrations included: each opens with a /** … @module */ block; every export has JSDoc, and so does every field of an exported interface; a comment describing a declaration is /** */, // only notes code; functions open with a verb, everything else with a noun phrase; parameters are named in the prose, {@link} only for symbols in scope; whatever throws to its caller says so with @throws (for an APIException, the statuses and when); a router's JSDoc lists its endpoints, method and path, who may call it, what it does. deno doc --lint goes from 528 errors to none in api/ (shared/ keeps its 385; it is next). The lint also wanted explicit types on config.ts's settings and a few return types, JsonLdResult exported, and the routers typed Router. deno task doc builds the HTML reference into docs/ from every module of api/ and shared/ but the tests, deno task doc:lint lints it; the index page is the module doc of a root mod.ts (deno.json's exports), which introduces the backend, shared/ and the frontend to come and links every module, and mod.test.ts fails when one is missing. The docs workflow builds it on every push to main that touches them and publishes it to the gh-pages branch, like publish-assistant's. Two defects found along the way are fixed: a private playlist's cover was served to anyone who had its id or slug, and now needs its owner's media token, as private dump files do (playlistImageUrl adds it for private playlists only, so public cover URLs stay the same for everyone); og.ts and static.ts each filled index.html's placeholders, and now share lib/index-html.ts, so the OG middleware picks up a rebuilt index.html instead of keeping the first it read. The README states the documentation rule. 205 backend and 628 frontend tests pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,15 @@
|
||||
/**
|
||||
* Guards for the category request bodies.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type {
|
||||
CreateCategoryRequest,
|
||||
UpdateCategoryRequest,
|
||||
} from "../../shared/category.ts";
|
||||
|
||||
/** Checks whether `obj` is a valid {@link CreateCategoryRequest}. */
|
||||
export function isCreateCategoryRequest(
|
||||
obj: unknown,
|
||||
): obj is CreateCategoryRequest {
|
||||
@@ -14,6 +21,7 @@ export function isCreateCategoryRequest(
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Checks whether `obj` is a valid {@link UpdateCategoryRequest}. */
|
||||
export function isUpdateCategoryRequest(
|
||||
obj: unknown,
|
||||
): obj is UpdateCategoryRequest {
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
/**
|
||||
* Guards for the comment request bodies.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type {
|
||||
CreateCommentRequest,
|
||||
UpdateCommentRequest,
|
||||
} from "../../shared/comment.ts";
|
||||
import { VALIDATION } from "../../shared/validation.ts";
|
||||
|
||||
/**
|
||||
* Checks whether `obj` is a valid {@link CreateCommentRequest}: a body that
|
||||
* isn't blank and fits `VALIDATION.COMMENT_BODY_MAX`.
|
||||
*/
|
||||
export function isCreateCommentRequest(
|
||||
obj: unknown,
|
||||
): obj is CreateCommentRequest {
|
||||
@@ -16,6 +26,10 @@ export function isCreateCommentRequest(
|
||||
o.parentId === null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether `obj` is a valid {@link UpdateCommentRequest}: a body that
|
||||
* isn't blank and fits `VALIDATION.COMMENT_BODY_MAX`.
|
||||
*/
|
||||
export function isUpdateCommentRequest(
|
||||
obj: unknown,
|
||||
): obj is UpdateCommentRequest {
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* Guards for the dump request bodies. File dumps arrive as multipart forms
|
||||
* and are checked in their route.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type {
|
||||
CreateUrlDumpRequest,
|
||||
UpdateDumpRequest,
|
||||
@@ -8,6 +15,10 @@ function isStringArray(value: unknown): value is string[] {
|
||||
return Array.isArray(value) && value.every((v) => typeof v === "string");
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether `obj` is a valid {@link CreateUrlDumpRequest}, its title and
|
||||
* comment within the `VALIDATION` limits.
|
||||
*/
|
||||
export function isCreateUrlDumpRequest(
|
||||
obj: unknown,
|
||||
): obj is CreateUrlDumpRequest {
|
||||
@@ -35,6 +46,11 @@ export function isCreateUrlDumpRequest(
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether `obj` is a valid {@link UpdateDumpRequest}: a title, when
|
||||
* given, that isn't blank, and title and comment within the `VALIDATION`
|
||||
* limits.
|
||||
*/
|
||||
export function isUpdateDumpRequest(obj: unknown): obj is UpdateDumpRequest {
|
||||
if (!obj || typeof obj !== "object") return false;
|
||||
const o = obj as Record<string, unknown>;
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/**
|
||||
* Guards for the playlist request bodies.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type {
|
||||
CreatePlaylistRequest,
|
||||
ReorderPlaylistRequest,
|
||||
@@ -5,6 +11,10 @@ import type {
|
||||
} from "../../shared/playlist.ts";
|
||||
import { VALIDATION } from "../../shared/validation.ts";
|
||||
|
||||
/**
|
||||
* Checks whether `obj` is a valid {@link CreatePlaylistRequest}: a non-empty
|
||||
* title, and title and description within the `VALIDATION` limits.
|
||||
*/
|
||||
export function isCreatePlaylistRequest(
|
||||
obj: unknown,
|
||||
): obj is CreatePlaylistRequest {
|
||||
@@ -29,6 +39,11 @@ export function isCreatePlaylistRequest(
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether `obj` is a valid {@link UpdatePlaylistRequest}: a title, when
|
||||
* given, that isn't empty, and title and description within the `VALIDATION`
|
||||
* limits.
|
||||
*/
|
||||
export function isUpdatePlaylistRequest(
|
||||
obj: unknown,
|
||||
): obj is UpdatePlaylistRequest {
|
||||
@@ -53,6 +68,7 @@ export function isUpdatePlaylistRequest(
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Checks whether `obj` is a valid {@link ReorderPlaylistRequest}. */
|
||||
export function isReorderPlaylistRequest(
|
||||
obj: unknown,
|
||||
): obj is ReorderPlaylistRequest {
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/**
|
||||
* Guards for the user and authentication request bodies.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import {
|
||||
isRole,
|
||||
type LoginUserRequest,
|
||||
@@ -6,26 +12,35 @@ import {
|
||||
} from "../../shared/user.ts";
|
||||
import { VALIDATION } from "../../shared/validation.ts";
|
||||
|
||||
/** Checks whether `obj` is a valid {@link LoginUserRequest}. */
|
||||
export function isLoginUserRequest(obj: unknown): obj is LoginUserRequest {
|
||||
return !!obj && typeof obj === "object" &&
|
||||
"username" in obj && typeof obj.username === "string" &&
|
||||
"password" in obj && typeof obj.password === "string";
|
||||
}
|
||||
|
||||
/** `obj[key]` when `obj` is an object that has it, undefined otherwise. */
|
||||
/** Returns `obj[key]` when `obj` is an object that has it, else undefined. */
|
||||
export function fieldOf(obj: unknown, key: string): unknown {
|
||||
return obj !== null && typeof obj === "object" && key in obj
|
||||
? (obj as Record<string, unknown>)[key]
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether `obj` is a valid {@link RegisterUserRequest} (see
|
||||
* {@link validateRegisterUserRequest}).
|
||||
*/
|
||||
export function isRegisterUserRequest(
|
||||
obj: unknown,
|
||||
): obj is RegisterUserRequest {
|
||||
return validateRegisterUserRequest(obj) === null;
|
||||
}
|
||||
|
||||
/** Returns a human-readable error string, or null if the request is valid. */
|
||||
/**
|
||||
* Checks a registration body — its shape, the email's form, and the username
|
||||
* and password against the `VALIDATION` limits — and returns what is wrong
|
||||
* with it, readable by the user, or null when it is valid.
|
||||
*/
|
||||
export function validateRegisterUserRequest(obj: unknown): string | null {
|
||||
if (
|
||||
!obj || typeof obj !== "object" ||
|
||||
@@ -55,6 +70,10 @@ export function validateRegisterUserRequest(obj: unknown): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether `obj` is a valid {@link UpdateUserRequest}: each field it
|
||||
* carries well-formed, and within the `VALIDATION` limits.
|
||||
*/
|
||||
export function isUpdateUserRequest(obj: unknown): obj is UpdateUserRequest {
|
||||
if (!obj || typeof obj !== "object") return false;
|
||||
const o = obj as Record<string, unknown>;
|
||||
|
||||
Reference in New Issue
Block a user