/** /docs-meta — endpoint catalogue (ApiEndpointDoc[]) generated from the live route registry for the web API docs page. */ import type { FastifyInstance } from "fastify"; import type { ApiEndpointDoc } from "@dci/core"; import { getEnv } from "../../env.js"; import { publicGet, routeRegistry, type RegisteredRoute } from "../../lib/route.js"; import { TTL } from "../../lib/http.js"; const SAMPLE: Record = { idOrSlug: "equinix-dc2", slug: "equinix", slugOrIso2: "us", key: "facilities", id: "evt_example", kind: "facilities", lat: "39.04", lng: "-77.49", radius_km: "25", country: "US", q: "hyperscale texas", zoom: "4", window: "7d", slugs: "equinix,digital-realty", format: "csv", entity: "facilities", status: "operational", per_page: "10", page: "1", layer: "facilities", year: "2020", types: "facilities,ixps", metric: "facilities_total" }; function sampleFor(p: RegisteredRoute["params"][number]): string { if (p.example) return p.example; if (SAMPLE[p.name]) return SAMPLE[p.name]!; if (p.type === "integer" || p.type === "number") return "1"; if (p.type === "boolean") return "true"; return "value"; } export function endpointDoc(r: RegisteredRoute, siteUrl: string): ApiEndpointDoc { let path = r.path; for (const p of r.params.filter((x) => x.in === "path")) path = path.replace(`:${p.name}`, encodeURIComponent(sampleFor(p))); const qs = r.params.filter((x) => x.in === "query").slice(0, 2).map((p) => `${p.name}=${encodeURIComponent(sampleFor(p))}`).join("&"); const url = `${siteUrl}${path}${qs && r.method === "GET" ? `?${qs}` : ""}`; const method: ApiEndpointDoc["method"] = r.method === "PATCH" ? "POST" : r.method; const bodyObj = r.method === "POST" ? Object.fromEntries(r.params.filter((x) => x.in === "query").slice(0, 2).map((p) => [p.name, sampleFor(p)])) : null; const body = bodyObj ? JSON.stringify(bodyObj) : null; const curl = method === "GET" ? `curl -s "${url}"` : method === "DELETE" ? `curl -s -X DELETE "${url}"` : `curl -s -X POST "${url}" -H 'content-type: application/json' -d '${body}'`; const js = method === "GET" ? `const res = await fetch("${url}");\nconst { data, meta, sources } = await res.json();` : `const res = await fetch("${url}", { method: "${method}"${body ? `, headers: { "content-type": "application/json" }, body: JSON.stringify(${body})` : ""}, credentials: "include" });\nconst { data } = await res.json();`; const python = method === "GET" ? `import requests\nr = requests.get("${url}")\npayload = r.json()\ndata, meta, sources = payload["data"], payload.get("meta"), payload.get("sources")` : `import requests\nr = requests.${method.toLowerCase()}("${url}"${body ? `, json=${body}` : ""})\ndata = r.json()["data"]`; return { method, path: r.path, summary: r.summary, group: r.group, params: r.params.map((p) => ({ name: p.name, in: p.in, type: p.type, description: p.description, ...(p.example ? { example: p.example } : {}) })), example: { curl, js, python }, responseSchema: r.responseType + (r.responseDescription ? ` — ${r.responseDescription}` : "") }; } export async function docsMetaRoutes(app: FastifyInstance): Promise { publicGet(app, { url: "/docs-meta", ttl: TTL.sitemap, tags: ["docs"], summary: "Endpoint catalogue (ApiEndpointDoc[]) generated from the live route table: method, path, params, curl / JS / Python examples, response type", response: { type: "ApiEndpointDoc[]", example: [{ method: "GET", path: "/api/v1/datacenters", summary: "List facilities", group: "facilities", params: [{ name: "country", in: "query", type: "string", description: "ISO-3166 alpha-2" }], example: { curl: "curl -s https://www.datacenterindex.io/api/v1/datacenters?country=US", js: "await fetch(…)", python: "requests.get(…)" }, responseSchema: "FacilitySummary[]" }] } }, async () => { const siteUrl = getEnv().siteUrl.replace(/\/$/, ""); const data = routeRegistry.map((r) => endpointDoc(r, siteUrl)).sort((a, b) => a.group.localeCompare(b.group) || a.path.localeCompare(b.path) || a.method.localeCompare(b.method)); return { data, meta: { total: data.length, methodology: "Generated from the Fastify route table at boot; every 200 response is the envelope { data, meta, sources } unless the summary says otherwise (downloads stream raw rows)." } }; }); }