| #!/usr/bin/env node
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
|
|
| import { promises as fs } from "node:fs";
|
| import path from "node:path";
|
| import { fileURLToPath } from "node:url";
|
| import 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", "reference", "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) {
|
|
|
|
|
|
|
| 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/reference/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);
|
| });
|
|
|