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);
});