Spaces:
Runtime error
Runtime error
| /** | |
| * gen-openapi-module.mjs — build helper that reads docs/openapi.yaml, | |
| * flattens the path/method matrix, and emits | |
| * src/app/docs/lib/openapi.generated.ts so the Api Explorer client can | |
| * iterate endpoints without parsing YAML at runtime. | |
| * | |
| * Runtime guarantees: | |
| * - No `any`. Everything is typed via an explicit `OpenApiEndpoint`. | |
| * - Endpoints are pre-sorted by (path, method) for stable output. | |
| * - Internal management endpoints (those under /api/ but NOT /api/v1) are | |
| * filtered out by default so the Api Explorer focuses on the public | |
| * OpenAI-compatible surface. Override with --include-management. | |
| * | |
| * Wired into `prebuild:docs` so `next build` always sees a fresh module. | |
| */ | |
| import { promises as fs } from "node:fs"; | |
| import path from "node:path"; | |
| import { fileURLToPath } from "node:url"; | |
| import * as yaml from "js-yaml"; | |
| const __filename = fileURLToPath(import.meta.url); | |
| const __dirname = path.dirname(__filename); | |
| const ROOT = path.resolve(__dirname, "..", ".."); | |
| const OPENAPI_PATH = path.join(ROOT, "docs", "openapi.yaml"); | |
| const OUT_PATH = path.join(ROOT, "src", "app", "docs", "lib", "openapi.generated.ts"); | |
| const HTTP_METHODS = ["get", "post", "put", "delete", "patch", "options", "head"]; | |
| const args = process.argv.slice(2); | |
| const includeManagement = args.includes("--include-management"); | |
| function summarizeEndpoint(rawPath, method, op) { | |
| const tags = Array.isArray(op?.tags) && op.tags.length > 0 ? op.tags : ["Other"]; | |
| return { | |
| path: rawPath, | |
| method: method.toUpperCase(), | |
| summary: typeof op?.summary === "string" ? op.summary : "", | |
| description: typeof op?.description === "string" ? op.description : "", | |
| tag: typeof tags[0] === "string" ? tags[0] : "Other", | |
| tags, | |
| requiresAuth: Array.isArray(op?.security) && op.security.length > 0, | |
| hasRequestBody: Boolean(op?.requestBody), | |
| }; | |
| } | |
| function isPublicV1(rawPath) { | |
| // Anything starting with /api/v1 is the OpenAI-compatible surface; everything | |
| // else under /api/* is internal management. Routes that don't start with /api | |
| // (rare) are kept because they are typically root-level surfaces. | |
| return rawPath.startsWith("/api/v1") || !rawPath.startsWith("/api/"); | |
| } | |
| function quote(value) { | |
| if (value === undefined || value === null) return "undefined"; | |
| return JSON.stringify(value); | |
| } | |
| function tsArray(values) { | |
| if (!values || values.length === 0) return "[]"; | |
| return `[${values.map((v) => quote(v)).join(", ")}]`; | |
| } | |
| function renderEndpoint(ep) { | |
| return ` { | |
| path: ${quote(ep.path)}, | |
| method: ${quote(ep.method)}, | |
| summary: ${quote(ep.summary)}, | |
| description: ${quote(ep.description)}, | |
| tag: ${quote(ep.tag)}, | |
| tags: ${tsArray(ep.tags)}, | |
| requiresAuth: ${ep.requiresAuth ? "true" : "false"}, | |
| hasRequestBody: ${ep.hasRequestBody ? "true" : "false"}, | |
| }`; | |
| } | |
| async function main() { | |
| const yamlText = await fs.readFile(OPENAPI_PATH, "utf8"); | |
| const spec = yaml.load(yamlText); | |
| if (!spec || typeof spec !== "object" || !spec.paths || typeof spec.paths !== "object") { | |
| throw new Error("openapi.yaml has no `paths` map"); | |
| } | |
| const version = spec.info && typeof spec.info.version === "string" ? spec.info.version : "0.0.0"; | |
| const title = | |
| spec.info && typeof spec.info.title === "string" ? spec.info.title : "OmniRoute API"; | |
| const endpoints = []; | |
| for (const [rawPath, pathItem] of Object.entries(spec.paths)) { | |
| if (!pathItem || typeof pathItem !== "object") continue; | |
| if (!includeManagement && !isPublicV1(rawPath)) continue; | |
| for (const method of HTTP_METHODS) { | |
| const op = pathItem[method]; | |
| if (!op) continue; | |
| endpoints.push(summarizeEndpoint(rawPath, method, op)); | |
| } | |
| } | |
| endpoints.sort((a, b) => { | |
| if (a.path !== b.path) return a.path.localeCompare(b.path); | |
| return a.method.localeCompare(b.method); | |
| }); | |
| const totalManagement = Object.entries(spec.paths).filter(([p]) => !isPublicV1(p)).length; | |
| const header = `// AUTO-GENERATED by scripts/docs/gen-openapi-module.mjs — DO NOT EDIT MANUALLY | |
| // Regenerate with: node scripts/docs/gen-openapi-module.mjs | |
| // | |
| // Source of truth: docs/openapi.yaml | |
| // | |
| // The Api Explorer consumes \`OPENAPI_ENDPOINTS\`; the spec metadata | |
| // (\`OPENAPI_VERSION\`, \`OPENAPI_TITLE\`) is surfaced in the page header. | |
| `; | |
| const body = ` | |
| export interface OpenApiEndpoint { | |
| /** Path template — may contain \`{param}\` placeholders. */ | |
| path: string; | |
| /** HTTP method in upper case (GET / POST / PUT / DELETE / PATCH / ...). */ | |
| method: string; | |
| /** Short one-line summary from the spec. */ | |
| summary: string; | |
| /** Long-form description (markdown is allowed). */ | |
| description: string; | |
| /** Primary tag — used for sidebar grouping in the Api Explorer. */ | |
| tag: string; | |
| /** All tags declared on the operation. */ | |
| tags: string[]; | |
| /** \`true\` when the operation declares a non-empty \`security\` array. */ | |
| requiresAuth: boolean; | |
| /** \`true\` when the operation declares a \`requestBody\`. */ | |
| hasRequestBody: boolean; | |
| } | |
| export const OPENAPI_VERSION = ${quote(version)}; | |
| export const OPENAPI_TITLE = ${quote(title)}; | |
| export const OPENAPI_ENDPOINTS: OpenApiEndpoint[] = [ | |
| ${endpoints.map(renderEndpoint).join(",\n")}${endpoints.length > 0 ? "," : ""} | |
| ]; | |
| export const OPENAPI_TAGS: string[] = Array.from( | |
| new Set(OPENAPI_ENDPOINTS.map((endpoint) => endpoint.tag)) | |
| ).sort(); | |
| `; | |
| await fs.mkdir(path.dirname(OUT_PATH), { recursive: true }); | |
| await fs.writeFile(OUT_PATH, `${header}${body}`, "utf8"); | |
| console.log( | |
| `[gen-openapi-module] wrote ${path.relative(ROOT, OUT_PATH)} (${endpoints.length} endpoints, v${version}, skipped ${totalManagement} management paths)` | |
| ); | |
| } | |
| main().catch((err) => { | |
| console.error(err); | |
| process.exit(1); | |
| }); | |