Type src/ against API and database shapes, fixing implicit anys

Adds .d.ts declarations beside the untyped JS modules imported from
TS (api.js, navConfig.js, adminSchema.js, adminNav.js, src/data/*),
with shapes taken from the server routes and migrations. get/post/
patch/del now return unknown unless the caller names the response.

Fixes found along the way:
- website/email/instagram are bare strings from splitLinks, not Link
  objects; OrganizationDetail's website and email pills rendered with
  no href or label and now link correctly.
- The chapter map falls back to FALLBACK_COLOR for a region with no
  colour instead of painting its tiles black.

Adds defineSection() so each page manifest entry's props are checked
against its own Component.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Zaldimmar 2026-09-25 04:13:46 -05:00
parent 5286cae9ca
commit 2428f4412a
39 changed files with 1318 additions and 343 deletions

118
src/lib/adminSchema.d.ts vendored Normal file
View file

@ -0,0 +1,118 @@
/* Types for adminSchema.js: the client half of the descriptor-driven
admin CRUD engine. Rows are whatever columns the server-side
descriptor in server/src/admin-schema.js declares, so they stay a
string-keyed record; the descriptors are what's fixed. */
/** A row from /api/admin/:entity or /:entity/:id. Nested paths
* ('region.scope') are side-table objects under their key, and
* child collections are arrays of rows under theirs. */
export type AdminRow = Record<string, unknown>;
/** One row of an OPTION_QUERIES result. `kind` and `org_id` ride
* along on the lists that filterBy narrows. */
export type AdminOption = {
id: string;
label: string;
kind?: string;
org_id?: string | null;
};
/** GET /api/admin/options, keyed by OPTION_QUERIES name. */
export type AdminOptions = Record<string, AdminOption[]>;
export type FieldWidget =
| "text"
| "textarea"
| "select"
| "checkbox"
| "color"
| "number"
| "date";
/** A bare value, or [value, label]. */
export type SelectOption = string | readonly [string, string];
/** Show a group or collection only when another field has this value. */
export type FieldCondition = { path: string; value: unknown };
export type AdminFieldSpec = {
path: string;
label: string;
widget?: FieldWidget;
required?: boolean;
full?: boolean;
help?: string;
options?: readonly SelectOption[];
optionsFrom?: string;
blankLabel?: string;
filterBy?: (option: AdminOption, row: AdminRow) => boolean;
/* Set by EntityEdit on the id field rather than in a manifest. */
readOnly?: boolean;
prefix?: string;
prefixPending?: boolean;
placeholder?: string;
};
export type FieldGroupSpec = {
legend: string;
note?: string;
when?: FieldCondition;
fields: AdminFieldSpec[];
};
export type CollectionSpec = {
key: string;
label: string;
addLabel?: string;
note?: string;
when?: FieldCondition;
title?: (row: AdminRow, options: AdminOptions | null | undefined) => string;
blank: AdminRow;
fields: AdminFieldSpec[];
children?: CollectionSpec[];
};
export type ListColumn = {
key: string;
label: string;
primary?: boolean;
widget?: "bool";
};
export type ListFilter = {
key: string;
label: string;
options?: readonly SelectOption[];
optionsFrom?: string;
};
export type EntitySpec = {
key: string;
label: string;
singular: string;
idLabel: string;
/** "auto": the table assigns the id, so the form shows it rather than asking. */
idKind?: "auto";
/** Field(s) the slug is composed from. Absent when idKind is "auto". */
slugFrom?: string | string[];
titleFrom?: string;
list: { columns: ListColumn[]; filters: ListFilter[] };
groups: FieldGroupSpec[];
children?: CollectionSpec[];
};
export type AdminEntityKey =
| "organizations"
| "events"
| "people"
| "teams"
| "awards"
| "timeline";
/* Indexed by route param as often as by name, so any other string
reads as possibly missing. */
export declare const ADMIN_ENTITIES: { readonly [K in AdminEntityKey]: EntitySpec } & {
readonly [key: string]: EntitySpec | undefined;
};
export declare function slugify(value: unknown): string;