Spaces:
Runtime error
Runtime error
File size: 5,780 Bytes
cd8bd0a | 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 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 | #!/usr/bin/env node
/**
* 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);
});
|