Repository navigation
Expand file tree
/
Copy pathadmin-api-table.ts
More file actions
86 lines (77 loc) · 3.77 KB
/
Copy pathadmin-api-table.ts
File metadata and controls
86 lines (77 loc) · 3.77 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { buildAdminOpenApiDocument } from "@/lib/api-auth/openapi-admin";
/**
* Generates the "Administrator API" resource table in docs/api.md FROM the
* admin OpenAPI document (review #198). The hand-written table drifted (it
* listed `PATCH` on `/api-keys/[id]`, which the route never had); now the
* table is derived, and `tests/unit/docs-admin-api-table.test.ts` fails when
* the committed markdown disagrees with the spec.
*
* pnpm docs:admin-table # rewrite the block between the markers
*
* One row per tag (in the spec's tag order): every path under that tag with
* its methods in `GET/POST/PUT/PATCH/DELETE` order, and the tag's
* `x-permissions` summary. `{id}` is printed as `[id]` to match the rest of
* the doc.
*/
export const TABLE_START = "<!-- admin-api-table:start (generated by pnpm docs:admin-table) -->";
export const TABLE_END = "<!-- admin-api-table:end -->";
const METHOD_ORDER = ["get", "post", "put", "patch", "delete"] as const;
interface AdminDoc {
tags: Array<{ name: string; "x-permissions"?: string }>;
paths: Record<string, Record<string, { tags?: string[] }>>;
}
export function renderAdminApiTable(doc: Record<string, unknown>): string {
const { tags, paths } = doc as unknown as AdminDoc;
const byTag = new Map<string, string[]>();
for (const tag of tags) byTag.set(tag.name, []);
for (const [apiPath, item] of Object.entries(paths)) {
const methods = METHOD_ORDER.filter((m) => item[m]);
if (methods.length === 0) continue;
const tag = item[methods[0]!]?.tags?.[0];
if (!tag || !byTag.has(tag)) throw new Error(`${apiPath}: first operation has no known tag`);
const printed = apiPath.replace(/\{(\w+)\}/g, "[$1]");
byTag.get(tag)!.push(`\`${methods.map((m) => m.toUpperCase()).join("/")} ${printed}\``);
}
const rows = tags.map((tag) => {
const cells = byTag.get(tag.name)!;
if (cells.length === 0) throw new Error(`tag ${tag.name} has no operations`);
return `| ${tag.name} | ${cells.join("; ")} | ${tag["x-permissions"] ?? "—"} |`;
});
return ["| Resource | Methods & paths | Permissions |", "| --- | --- | --- |", ...rows].join(
"\n",
);
}
/** Replaces the block between the markers (both must be present, once). */
export function replaceAdminApiTable(markdown: string, table: string): string {
const start = markdown.indexOf(TABLE_START);
const end = markdown.indexOf(TABLE_END);
if (start < 0 || end < 0 || end < start) {
throw new Error("docs/api.md: admin-api-table markers missing or out of order");
}
return `${markdown.slice(0, start + TABLE_START.length)}\n${table}\n${markdown.slice(end)}`;
}
/** The block as it currently sits between the markers, or null when absent. */
export function extractAdminApiTable(markdown: string): string | null {
const start = markdown.indexOf(TABLE_START);
const end = markdown.indexOf(TABLE_END);
if (start < 0 || end < 0 || end < start) return null;
return markdown.slice(start + TABLE_START.length, end).trim();
}
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export const API_DOC_FILE = path.join(__dirname, "..", "docs", "api.md");
async function main() {
const markdown = await fs.readFile(API_DOC_FILE, "utf8");
// The server URL does not affect the table; any origin works.
const table = renderAdminApiTable(buildAdminOpenApiDocument("https://app.devresponse.com"));
await fs.writeFile(API_DOC_FILE, replaceAdminApiTable(markdown, table), "utf8");
console.log("[docs] rewrote the administrator API table in docs/api.md");
}
if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
main().catch((error) => {
console.error("[docs] FAILED", error);
process.exit(1);
});
}