@icib.dev/api-client 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -15
- package/dist/api/apiClient.d.ts +1 -3620
- package/dist/api/apiClient.d.ts.map +1 -1
- package/dist/api/apiClient.js +2 -144
- package/dist/api/client.js +1 -1
- package/dist/api/contexts/items.d.ts +5 -0
- package/dist/api/contexts/items.d.ts.map +1 -0
- package/dist/api/contexts/items.js +8 -0
- package/dist/api/index.d.ts +1 -72
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +1 -72
- package/dist/api/types/index.d.ts +1 -1926
- package/dist/api/types/index.d.ts.map +1 -1
- package/dist/scripts/generate.js +670 -0
- package/dist/scripts/hash.js +49 -0
- package/dist/scripts/verify.js +52 -0
- package/package.json +14 -8
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync, mkdirSync, writeFileSync } from "fs";
|
|
3
|
+
import { dirname, join } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import SwaggerParser from "@apidevtools/swagger-parser";
|
|
6
|
+
import { normalizedJsonHash, computeClientHash, } from "./hash.js";
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const DEFAULT_OUT = "api";
|
|
9
|
+
function getDefaultUrl() {
|
|
10
|
+
const base = process.env.BASE_URL;
|
|
11
|
+
if (base) {
|
|
12
|
+
const normalized = base.replace(/\/$/, "");
|
|
13
|
+
return `${normalized}/docs/openapi`;
|
|
14
|
+
}
|
|
15
|
+
return "https://api.icib.dev/docs/?format=openapi";
|
|
16
|
+
}
|
|
17
|
+
function parseArgs() {
|
|
18
|
+
const args = process.argv.slice(2);
|
|
19
|
+
let url = getDefaultUrl();
|
|
20
|
+
let out = DEFAULT_OUT;
|
|
21
|
+
for (let i = 0; i < args.length; i++) {
|
|
22
|
+
if (args[i] === "--url" && args[i + 1]) {
|
|
23
|
+
url = args[++i];
|
|
24
|
+
}
|
|
25
|
+
else if (args[i] === "--out" && args[i + 1]) {
|
|
26
|
+
out = args[++i];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return { url, out };
|
|
30
|
+
}
|
|
31
|
+
async function fetchSpec(url) {
|
|
32
|
+
const res = await fetch(url);
|
|
33
|
+
if (!res.ok) {
|
|
34
|
+
throw new Error(`Failed to fetch spec: ${res.status} ${res.statusText}`);
|
|
35
|
+
}
|
|
36
|
+
return res.json();
|
|
37
|
+
}
|
|
38
|
+
async function loadRawSpec(urlOrPath) {
|
|
39
|
+
if (urlOrPath.startsWith("http://") || urlOrPath.startsWith("https://")) {
|
|
40
|
+
return fetchSpec(urlOrPath);
|
|
41
|
+
}
|
|
42
|
+
return JSON.parse(readFileSync(urlOrPath, "utf-8"));
|
|
43
|
+
}
|
|
44
|
+
async function parseSpec(spec) {
|
|
45
|
+
return (await SwaggerParser.parse(spec));
|
|
46
|
+
}
|
|
47
|
+
function getBaseUrl(doc) {
|
|
48
|
+
const oas2 = doc;
|
|
49
|
+
const oas3 = doc;
|
|
50
|
+
if (oas2.host) {
|
|
51
|
+
const scheme = oas2.schemes?.[0] ?? "https";
|
|
52
|
+
const basePath = oas2.basePath ?? "";
|
|
53
|
+
return `${scheme}://${oas2.host}${basePath}`;
|
|
54
|
+
}
|
|
55
|
+
if (oas3.servers?.[0]?.url) {
|
|
56
|
+
return oas3.servers[0].url.replace(/\/$/, "");
|
|
57
|
+
}
|
|
58
|
+
return "https://api.icib.dev/api";
|
|
59
|
+
}
|
|
60
|
+
/** Returns origin only (no basePath) for axios baseURL when path includes basePath */
|
|
61
|
+
function getOrigin(doc) {
|
|
62
|
+
const oas2 = doc;
|
|
63
|
+
if (oas2.host) {
|
|
64
|
+
const scheme = oas2.schemes?.[0] ?? "https";
|
|
65
|
+
return `${scheme}://${oas2.host}`;
|
|
66
|
+
}
|
|
67
|
+
return "https://api.icib.dev";
|
|
68
|
+
}
|
|
69
|
+
function getBasePath(doc) {
|
|
70
|
+
const oas2 = doc;
|
|
71
|
+
return oas2.basePath ?? "/api";
|
|
72
|
+
}
|
|
73
|
+
function getDefinitions(doc) {
|
|
74
|
+
return doc.definitions ?? doc.components?.schemas ?? {};
|
|
75
|
+
}
|
|
76
|
+
function getPaths(doc) {
|
|
77
|
+
return doc.paths ?? {};
|
|
78
|
+
}
|
|
79
|
+
function schemaToTsType(schema, definitions, refsSeen = new Set()) {
|
|
80
|
+
if (!schema)
|
|
81
|
+
return "unknown";
|
|
82
|
+
const ref = schema.$ref;
|
|
83
|
+
if (ref) {
|
|
84
|
+
const match = ref.match(/#\/definitions\/(.+)$/) ??
|
|
85
|
+
ref.match(/#\/components\/schemas\/(.+)$/);
|
|
86
|
+
const name = match?.[1];
|
|
87
|
+
if (name && !refsSeen.has(name)) {
|
|
88
|
+
refsSeen.add(name);
|
|
89
|
+
return name;
|
|
90
|
+
}
|
|
91
|
+
return name ?? "unknown";
|
|
92
|
+
}
|
|
93
|
+
const nullable = schema["x-nullable"] === true;
|
|
94
|
+
if (schema.type === "array") {
|
|
95
|
+
const items = schema.items;
|
|
96
|
+
const itemType = schemaToTsType(items, definitions, refsSeen);
|
|
97
|
+
const arr = `Array<${itemType}>`;
|
|
98
|
+
return nullable ? `${arr} | null` : arr;
|
|
99
|
+
}
|
|
100
|
+
if (schema.type === "object") {
|
|
101
|
+
if (schema.properties) {
|
|
102
|
+
const props = Object.entries(schema.properties).map(([k, v]) => {
|
|
103
|
+
const propSchema = v;
|
|
104
|
+
const optional = !(schema.required ?? []).includes(k);
|
|
105
|
+
const t = schemaToTsType(propSchema, definitions, refsSeen);
|
|
106
|
+
return ` ${k}${optional ? "?" : ""}: ${t};`;
|
|
107
|
+
});
|
|
108
|
+
return `{\n${props.join("\n")}\n}`;
|
|
109
|
+
}
|
|
110
|
+
return "Record<string, unknown>";
|
|
111
|
+
}
|
|
112
|
+
const prim = {
|
|
113
|
+
string: "string",
|
|
114
|
+
integer: "number",
|
|
115
|
+
number: "number",
|
|
116
|
+
boolean: "boolean",
|
|
117
|
+
};
|
|
118
|
+
let t = prim[schema.type] ?? "unknown";
|
|
119
|
+
if (schema.format === "date-time" || schema.format === "date")
|
|
120
|
+
t = "string";
|
|
121
|
+
if (schema.format === "uri")
|
|
122
|
+
t = "string";
|
|
123
|
+
return nullable ? `${t} | null` : t;
|
|
124
|
+
}
|
|
125
|
+
function generateTypes(definitions) {
|
|
126
|
+
const lines = [
|
|
127
|
+
"// Auto-generated types from OpenAPI definitions",
|
|
128
|
+
"",
|
|
129
|
+
];
|
|
130
|
+
for (const [name, schema] of Object.entries(definitions)) {
|
|
131
|
+
const s = schema;
|
|
132
|
+
if (s.$ref)
|
|
133
|
+
continue;
|
|
134
|
+
const props = [];
|
|
135
|
+
if (s.properties) {
|
|
136
|
+
const required = new Set(s.required ?? []);
|
|
137
|
+
for (const [propName, propSchema] of Object.entries(s.properties)) {
|
|
138
|
+
const optional = !required.has(propName);
|
|
139
|
+
const t = schemaToTsType(propSchema, definitions);
|
|
140
|
+
const desc = propSchema
|
|
141
|
+
.description ??
|
|
142
|
+
propSchema.title;
|
|
143
|
+
if (desc) {
|
|
144
|
+
props.push(` /** ${jsdocEscape(desc)} */`);
|
|
145
|
+
}
|
|
146
|
+
props.push(` ${propName}${optional ? "?" : ""}: ${t};`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const ifaceDesc = s.description;
|
|
150
|
+
if (s.type === "object" && !s.properties) {
|
|
151
|
+
if (ifaceDesc)
|
|
152
|
+
lines.push(`/** ${jsdocEscape(ifaceDesc)} */`);
|
|
153
|
+
lines.push(`export interface ${name} {\n [key: string]: unknown;\n}\n`);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
if (ifaceDesc)
|
|
157
|
+
lines.push(`/** ${jsdocEscape(ifaceDesc)} */`);
|
|
158
|
+
lines.push(`export interface ${name} {`);
|
|
159
|
+
lines.push(...props);
|
|
160
|
+
lines.push("}\n");
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
lines.push("export interface PaginatedResponse<T> {");
|
|
164
|
+
lines.push(" count: number;");
|
|
165
|
+
lines.push(" next: string | null;");
|
|
166
|
+
lines.push(" previous: string | null;");
|
|
167
|
+
lines.push(" results: T[];");
|
|
168
|
+
lines.push("}\n");
|
|
169
|
+
return lines.join("\n");
|
|
170
|
+
}
|
|
171
|
+
function extractOperations(paths, basePath, definitions) {
|
|
172
|
+
const ops = [];
|
|
173
|
+
const methods = ["get", "post", "put", "patch", "delete"];
|
|
174
|
+
for (const [path, pathItem] of Object.entries(paths)) {
|
|
175
|
+
const fullPath = basePath + (path.startsWith("/") ? path : `/${path}`);
|
|
176
|
+
for (const method of methods) {
|
|
177
|
+
const op = pathItem[method];
|
|
178
|
+
if (!op?.operationId)
|
|
179
|
+
continue;
|
|
180
|
+
const pathParamsMap = new Map();
|
|
181
|
+
const queryParams = [];
|
|
182
|
+
let bodyParam = null;
|
|
183
|
+
const pathParamNames = [...(path.match(/\{([^}]+)\}/g) ?? [])].map((m) => m.slice(1, -1));
|
|
184
|
+
const allParams = [
|
|
185
|
+
...(pathItem.parameters ?? []),
|
|
186
|
+
...(op.parameters ?? []),
|
|
187
|
+
];
|
|
188
|
+
for (const p of allParams) {
|
|
189
|
+
if (p.in === "path") {
|
|
190
|
+
if (!pathParamsMap.has(p.name)) {
|
|
191
|
+
pathParamsMap.set(p.name, p.description ?? "");
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
for (const name of pathParamNames) {
|
|
196
|
+
if (!pathParamsMap.has(name))
|
|
197
|
+
pathParamsMap.set(name, "");
|
|
198
|
+
}
|
|
199
|
+
const pathParams = Array.from(pathParamsMap.entries()).map(([name, description]) => ({
|
|
200
|
+
name,
|
|
201
|
+
description: description || undefined,
|
|
202
|
+
}));
|
|
203
|
+
for (const p of allParams) {
|
|
204
|
+
if (p.in === "path")
|
|
205
|
+
continue;
|
|
206
|
+
if (p.in === "query") {
|
|
207
|
+
queryParams.push({
|
|
208
|
+
name: p.name,
|
|
209
|
+
required: p.required ?? false,
|
|
210
|
+
schema: (p.schema ?? {
|
|
211
|
+
type: p.type ?? "string",
|
|
212
|
+
}),
|
|
213
|
+
description: p.description,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
else if (p.in === "body") {
|
|
217
|
+
const bodySchema = (p.schema ?? { type: "object" });
|
|
218
|
+
const propertyDescriptions = {};
|
|
219
|
+
if (bodySchema.properties) {
|
|
220
|
+
for (const [propName, propSchema] of Object.entries(bodySchema.properties)) {
|
|
221
|
+
const desc = propSchema
|
|
222
|
+
.description ??
|
|
223
|
+
propSchema.title;
|
|
224
|
+
if (desc)
|
|
225
|
+
propertyDescriptions[propName] = desc;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
bodyParam = {
|
|
229
|
+
name: p.name,
|
|
230
|
+
schema: bodySchema,
|
|
231
|
+
propertyDescriptions: Object.keys(propertyDescriptions).length > 0
|
|
232
|
+
? propertyDescriptions
|
|
233
|
+
: undefined,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const successResponse = op.responses?.["200"] ?? op.responses?.["201"];
|
|
238
|
+
const respSchema = successResponse?.schema;
|
|
239
|
+
const respDesc = successResponse?.description ?? "";
|
|
240
|
+
const xResponseType = successResponse?.["x-response-type"];
|
|
241
|
+
let responseType = "unknown";
|
|
242
|
+
if (respSchema) {
|
|
243
|
+
responseType = schemaToTsType(respSchema, definitions);
|
|
244
|
+
}
|
|
245
|
+
const producesBlob = xResponseType === "blob" ||
|
|
246
|
+
/File CSV|File.*CSV|Scarica|download|export|blob|binary/i.test(respDesc) ||
|
|
247
|
+
/\/download\/|\/export\/|download-unassigned|generate-csv|import_csv|import_csv\/|download-icon/i.test(fullPath);
|
|
248
|
+
ops.push({
|
|
249
|
+
operationId: op.operationId,
|
|
250
|
+
method,
|
|
251
|
+
path: fullPath,
|
|
252
|
+
pathParams,
|
|
253
|
+
queryParams,
|
|
254
|
+
bodyParam,
|
|
255
|
+
responseType: producesBlob ? "Blob" : responseType,
|
|
256
|
+
producesBlob,
|
|
257
|
+
summary: op.summary,
|
|
258
|
+
description: op.description,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return ops;
|
|
263
|
+
}
|
|
264
|
+
function groupByTag(ops, paths) {
|
|
265
|
+
const byTag = new Map();
|
|
266
|
+
for (const op of ops) {
|
|
267
|
+
let tag = "default";
|
|
268
|
+
for (const [path, pathItem] of Object.entries(paths)) {
|
|
269
|
+
for (const m of ["get", "post", "put", "patch", "delete"]) {
|
|
270
|
+
const o = pathItem[m];
|
|
271
|
+
if (o?.operationId === op.operationId && o.tags?.[0]) {
|
|
272
|
+
tag = o.tags[0];
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (!byTag.has(tag))
|
|
278
|
+
byTag.set(tag, []);
|
|
279
|
+
byTag.get(tag).push(op);
|
|
280
|
+
}
|
|
281
|
+
return byTag;
|
|
282
|
+
}
|
|
283
|
+
function sanitizeContextName(tag) {
|
|
284
|
+
return tag.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
285
|
+
}
|
|
286
|
+
/** Valid JS identifier for context (e.g. building-media -> buildingMedia) */
|
|
287
|
+
function contextToIdentifier(tag) {
|
|
288
|
+
return sanitizeIdentifier(sanitizeContextName(tag));
|
|
289
|
+
}
|
|
290
|
+
function toCamelCase(str) {
|
|
291
|
+
return str.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
292
|
+
}
|
|
293
|
+
function operationIdToFunctionName(operationId) {
|
|
294
|
+
const parts = operationId.split("_");
|
|
295
|
+
if (parts.length <= 1)
|
|
296
|
+
return sanitizeIdentifier(operationId);
|
|
297
|
+
const [context, ...rest] = parts;
|
|
298
|
+
const contextSafe = sanitizeIdentifier(context);
|
|
299
|
+
const action = rest
|
|
300
|
+
.map((p, i) => (i === 0 ? p : p.charAt(0).toUpperCase() + p.slice(1)))
|
|
301
|
+
.join("");
|
|
302
|
+
return contextSafe + action.charAt(0).toUpperCase() + action.slice(1);
|
|
303
|
+
}
|
|
304
|
+
/** Extract method name from operationId (e.g. allegati_list -> list, allegati_partial_update -> partialUpdate) */
|
|
305
|
+
function operationIdToMethodName(operationId) {
|
|
306
|
+
const parts = operationId.split("_");
|
|
307
|
+
if (parts.length <= 1)
|
|
308
|
+
return sanitizeIdentifier(operationId);
|
|
309
|
+
const [, ...rest] = parts;
|
|
310
|
+
return rest
|
|
311
|
+
.map((p, i) => (i === 0 ? p : p.charAt(0).toUpperCase() + p.slice(1)))
|
|
312
|
+
.join("");
|
|
313
|
+
}
|
|
314
|
+
function sanitizeIdentifier(name) {
|
|
315
|
+
return name
|
|
316
|
+
.replace(/-([a-z])/g, (_, c) => c.toUpperCase())
|
|
317
|
+
.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
318
|
+
}
|
|
319
|
+
/** Escape text for use inside JSDoc (avoid closing comment, handle newlines) */
|
|
320
|
+
function jsdocEscape(text) {
|
|
321
|
+
return text.replace(/\*\//g, "* /").replace(/\n/g, " ").trim();
|
|
322
|
+
}
|
|
323
|
+
function getTagDescription(doc, tag) {
|
|
324
|
+
const tags = doc
|
|
325
|
+
.tags;
|
|
326
|
+
return tags?.find((t) => t.name === tag)?.description;
|
|
327
|
+
}
|
|
328
|
+
function generateContextFile(tag, operations, definitions, tagDescription) {
|
|
329
|
+
const ctxName = sanitizeContextName(tag);
|
|
330
|
+
const exportName = contextToIdentifier(tag);
|
|
331
|
+
const clientVar = exportName === "client" ? "httpClient" : "client";
|
|
332
|
+
const hasBlobOps = operations.some((o) => o.producesBlob);
|
|
333
|
+
const clientImport = exportName === "client"
|
|
334
|
+
? hasBlobOps
|
|
335
|
+
? `import { client as httpClient, triggerBlobDownload, type BlobDownloadOptions, type BlobDownloadHeaders } from "../client.js";`
|
|
336
|
+
: `import { client as httpClient } from "../client.js";`
|
|
337
|
+
: hasBlobOps
|
|
338
|
+
? `import { client, triggerBlobDownload, type BlobDownloadOptions, type BlobDownloadHeaders } from "../client.js";`
|
|
339
|
+
: `import { client } from "../client.js";`;
|
|
340
|
+
const lines = [
|
|
341
|
+
`// Auto-generated API client for context: ${tag}`,
|
|
342
|
+
"",
|
|
343
|
+
clientImport,
|
|
344
|
+
"",
|
|
345
|
+
];
|
|
346
|
+
const usedTypes = new Set();
|
|
347
|
+
const builtins = new Set(["Blob", "Array", "Record"]);
|
|
348
|
+
const addUsedType = (t) => {
|
|
349
|
+
if (t === "unknown" || builtins.has(t))
|
|
350
|
+
return;
|
|
351
|
+
const match = t.match(/^([A-Z][a-zA-Z0-9]*)/);
|
|
352
|
+
if (match && !builtins.has(match[1]))
|
|
353
|
+
usedTypes.add(match[1]);
|
|
354
|
+
const arrMatch = t.match(/Array<([A-Z][a-zA-Z0-9]*)>/);
|
|
355
|
+
if (arrMatch && !builtins.has(arrMatch[1]))
|
|
356
|
+
usedTypes.add(arrMatch[1]);
|
|
357
|
+
};
|
|
358
|
+
for (const op of operations) {
|
|
359
|
+
if (op.bodyParam)
|
|
360
|
+
addUsedType(schemaToTsType(op.bodyParam.schema, definitions));
|
|
361
|
+
for (const q of op.queryParams)
|
|
362
|
+
addUsedType(schemaToTsType(q.schema, definitions));
|
|
363
|
+
addUsedType(op.responseType);
|
|
364
|
+
}
|
|
365
|
+
if (usedTypes.size > 0) {
|
|
366
|
+
lines.push(`import type { ${[...usedTypes].join(", ")} } from "../types/index.js";`);
|
|
367
|
+
lines.push("");
|
|
368
|
+
}
|
|
369
|
+
const seenNames = new Set();
|
|
370
|
+
const methodEntries = [];
|
|
371
|
+
for (const op of operations) {
|
|
372
|
+
let methodName = operationIdToMethodName(op.operationId);
|
|
373
|
+
if (seenNames.has(methodName)) {
|
|
374
|
+
let suffix = 1;
|
|
375
|
+
while (seenNames.has(`${methodName}${suffix}`))
|
|
376
|
+
suffix++;
|
|
377
|
+
methodName = `${methodName}${suffix}`;
|
|
378
|
+
}
|
|
379
|
+
seenNames.add(methodName);
|
|
380
|
+
const pathParamsType = op.pathParams.length > 0
|
|
381
|
+
? `{ ${op.pathParams.map((p) => `${p.name}: string | number`).join("; ")} }`
|
|
382
|
+
: null;
|
|
383
|
+
const queryParamsType = op.queryParams.length > 0
|
|
384
|
+
? `{ ${op.queryParams.map((q) => `${q.name}${q.required ? "" : "?"}: ${schemaToTsType(q.schema, definitions)}`).join("; ")} }`
|
|
385
|
+
: null;
|
|
386
|
+
const paramsParts = [];
|
|
387
|
+
if (pathParamsType)
|
|
388
|
+
paramsParts.push(pathParamsType);
|
|
389
|
+
if (queryParamsType)
|
|
390
|
+
paramsParts.push(queryParamsType);
|
|
391
|
+
const paramsType = paramsParts.length > 0 ? paramsParts.join(" & ") : "void";
|
|
392
|
+
const hasParams = op.pathParams.length > 0 || op.queryParams.length > 0;
|
|
393
|
+
const paramsRequired = op.pathParams.length > 0;
|
|
394
|
+
const paramsArg = hasParams
|
|
395
|
+
? `params${paramsRequired ? "" : "?"}: ${paramsType}`
|
|
396
|
+
: "";
|
|
397
|
+
const needsBody = op.method !== "get" && op.method !== "delete";
|
|
398
|
+
const bodyArg = op.bodyParam
|
|
399
|
+
? `data: ${schemaToTsType(op.bodyParam.schema, definitions)}`
|
|
400
|
+
: needsBody
|
|
401
|
+
? `data?: FormData | Record<string, unknown>`
|
|
402
|
+
: "";
|
|
403
|
+
const optionsArg = op.producesBlob ? `options?: BlobDownloadOptions` : "";
|
|
404
|
+
const args = [paramsArg, bodyArg, optionsArg].filter(Boolean).join(", ");
|
|
405
|
+
let pathExpr = `"${op.path}"`;
|
|
406
|
+
const pathParamNames = op.pathParams.map((p) => p.name);
|
|
407
|
+
if (op.pathParams.length > 0) {
|
|
408
|
+
const repl = op.path.replace(/\{([^}]+)\}/g, (_, name) => `\${String(params.${name})}`);
|
|
409
|
+
pathExpr = "`" + repl + "`";
|
|
410
|
+
}
|
|
411
|
+
const jsdocParts = [];
|
|
412
|
+
const summary = op.summary ?? op.description;
|
|
413
|
+
if (summary) {
|
|
414
|
+
jsdocParts.push(jsdocEscape(summary));
|
|
415
|
+
if (op.description && op.description !== op.summary) {
|
|
416
|
+
jsdocParts.push(jsdocEscape(op.description));
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (op.pathParams.length > 0) {
|
|
420
|
+
for (const p of op.pathParams) {
|
|
421
|
+
const desc = p.description
|
|
422
|
+
? jsdocEscape(p.description)
|
|
423
|
+
: "Path parameter";
|
|
424
|
+
jsdocParts.push(`@param params.${p.name} - ${desc}`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
for (const q of op.queryParams) {
|
|
428
|
+
if (q.description) {
|
|
429
|
+
jsdocParts.push(`@param params.${q.name} - ${jsdocEscape(q.description)}`);
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
jsdocParts.push(`@param params.${q.name} - Query parameter`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
if (op.bodyParam) {
|
|
436
|
+
const bodyDesc = op.bodyParam.propertyDescriptions
|
|
437
|
+
? Object.entries(op.bodyParam.propertyDescriptions)
|
|
438
|
+
.map(([k, v]) => `${k}: ${jsdocEscape(v)}`)
|
|
439
|
+
.join("; ")
|
|
440
|
+
: "Request body";
|
|
441
|
+
jsdocParts.push(`@param data - ${jsdocEscape(bodyDesc)}`);
|
|
442
|
+
}
|
|
443
|
+
if (op.producesBlob) {
|
|
444
|
+
jsdocParts.push(`@param options.download - When true, triggers a file download in the browser`);
|
|
445
|
+
jsdocParts.push(`@param options.filename - Suggested filename for the download`);
|
|
446
|
+
}
|
|
447
|
+
const methodLines = [];
|
|
448
|
+
if (jsdocParts.length > 0) {
|
|
449
|
+
methodLines.push(` /**`);
|
|
450
|
+
for (const line of jsdocParts) {
|
|
451
|
+
methodLines.push(` * ${line}`);
|
|
452
|
+
}
|
|
453
|
+
methodLines.push(` */`);
|
|
454
|
+
}
|
|
455
|
+
methodLines.push(` async ${methodName}(${args}) {`);
|
|
456
|
+
const http = clientVar;
|
|
457
|
+
if (op.producesBlob) {
|
|
458
|
+
if (op.method === "get" || op.method === "delete") {
|
|
459
|
+
if (op.pathParams.length > 0 && op.queryParams.length > 0) {
|
|
460
|
+
methodLines.push(` const { ${pathParamNames.join(", ")}, ...query } = params ?? {};`);
|
|
461
|
+
methodLines.push(` const res = await ${http}.${op.method}<Blob>(${pathExpr}, { responseType: "blob", params: query });`);
|
|
462
|
+
}
|
|
463
|
+
else if (op.pathParams.length > 0) {
|
|
464
|
+
methodLines.push(` const res = await ${http}.${op.method}<Blob>(${pathExpr}, { responseType: "blob" });`);
|
|
465
|
+
}
|
|
466
|
+
else if (op.queryParams.length > 0) {
|
|
467
|
+
methodLines.push(` const res = await ${http}.${op.method}<Blob>(${pathExpr}, { responseType: "blob", params });`);
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
methodLines.push(` const res = await ${http}.${op.method}<Blob>(${pathExpr}, { responseType: "blob" });`);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
const bodyVal = op.bodyParam || needsBody ? "data" : "undefined";
|
|
475
|
+
if (op.pathParams.length > 0) {
|
|
476
|
+
methodLines.push(` const res = await ${http}.${op.method}<Blob>(${pathExpr}, ${bodyVal}, { responseType: "blob" });`);
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
methodLines.push(` const res = await ${http}.${op.method}<Blob>(${pathExpr}, ${bodyVal}, { responseType: "blob" });`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
methodLines.push(` if (options?.download) triggerBlobDownload(res.data, res.headers as BlobDownloadHeaders, options.filename);`);
|
|
483
|
+
methodLines.push(` return res;`);
|
|
484
|
+
}
|
|
485
|
+
else if (op.method === "get" || op.method === "delete") {
|
|
486
|
+
if (op.pathParams.length > 0 && op.queryParams.length > 0) {
|
|
487
|
+
methodLines.push(` const { ${pathParamNames.join(", ")}, ...query } = params;`);
|
|
488
|
+
methodLines.push(` return ${http}.${op.method}<${op.responseType}>(${pathExpr}, { params: query });`);
|
|
489
|
+
}
|
|
490
|
+
else if (op.pathParams.length > 0) {
|
|
491
|
+
methodLines.push(` return ${http}.${op.method}<${op.responseType}>(${pathExpr});`);
|
|
492
|
+
}
|
|
493
|
+
else if (op.queryParams.length > 0) {
|
|
494
|
+
methodLines.push(` return ${http}.${op.method}<${op.responseType}>(${pathExpr}, { params });`);
|
|
495
|
+
}
|
|
496
|
+
else {
|
|
497
|
+
methodLines.push(` return ${http}.${op.method}<${op.responseType}>(${pathExpr});`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
const bodyArg = op.bodyParam || needsBody ? ", data" : "";
|
|
502
|
+
if (op.pathParams.length > 0) {
|
|
503
|
+
methodLines.push(` return ${http}.${op.method}<${op.responseType}>(${pathExpr}${bodyArg});`);
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
methodLines.push(` return ${http}.${op.method}<${op.responseType}>(${pathExpr}${bodyArg});`);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
methodLines.push(` }`);
|
|
510
|
+
methodEntries.push(methodLines.join("\n"));
|
|
511
|
+
}
|
|
512
|
+
const contextDesc = tagDescription
|
|
513
|
+
? jsdocEscape(tagDescription)
|
|
514
|
+
: `API client for ${tag} endpoints`;
|
|
515
|
+
lines.push(`/** ${contextDesc} */`);
|
|
516
|
+
lines.push(`export const ${exportName} = {`);
|
|
517
|
+
lines.push(methodEntries.join(",\n"));
|
|
518
|
+
lines.push("};");
|
|
519
|
+
return lines.join("\n");
|
|
520
|
+
}
|
|
521
|
+
function generateClient(baseUrl) {
|
|
522
|
+
return `// Auto-generated Axios client
|
|
523
|
+
import axios, { type AxiosInstance } from "axios";
|
|
524
|
+
|
|
525
|
+
let _token: string | null = null;
|
|
526
|
+
|
|
527
|
+
export function setAuthToken(token: string | null): void {
|
|
528
|
+
_token = token;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
export const client: AxiosInstance = axios.create({
|
|
532
|
+
baseURL: "${baseUrl}",
|
|
533
|
+
headers: {
|
|
534
|
+
"Content-Type": "application/json",
|
|
535
|
+
},
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
client.interceptors.request.use((config) => {
|
|
539
|
+
if (_token) {
|
|
540
|
+
config.headers.Authorization = \`Bearer \${_token}\`;
|
|
541
|
+
}
|
|
542
|
+
return config;
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
/** Options for blob/download endpoints */
|
|
546
|
+
export interface BlobDownloadOptions {
|
|
547
|
+
/** When true, triggers a file download in the browser */
|
|
548
|
+
download?: boolean;
|
|
549
|
+
/** Suggested filename (falls back to Content-Disposition or default) */
|
|
550
|
+
filename?: string;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/** Headers type for blob download (compatible with Axios response headers) */
|
|
554
|
+
export type BlobDownloadHeaders =
|
|
555
|
+
| import("axios").AxiosResponseHeaders
|
|
556
|
+
| import("axios").RawAxiosResponseHeaders
|
|
557
|
+
| Record<string, import("axios").AxiosHeaderValue>;
|
|
558
|
+
|
|
559
|
+
/** Triggers a blob download in the browser. No-op in Node.js. */
|
|
560
|
+
export function triggerBlobDownload(
|
|
561
|
+
blob: Blob,
|
|
562
|
+
headers: BlobDownloadHeaders,
|
|
563
|
+
suggestedFilename?: string
|
|
564
|
+
): void {
|
|
565
|
+
if (typeof document === "undefined") return;
|
|
566
|
+
const cdRaw = "get" in headers && typeof (headers as import("axios").AxiosHeaders).get === "function"
|
|
567
|
+
? (headers as import("axios").AxiosHeaders).get("content-disposition")
|
|
568
|
+
: (headers as Record<string, import("axios").AxiosHeaderValue>)["content-disposition"];
|
|
569
|
+
const cd = typeof cdRaw === "string" ? cdRaw : Array.isArray(cdRaw) ? cdRaw[0] : "";
|
|
570
|
+
const filename =
|
|
571
|
+
suggestedFilename ??
|
|
572
|
+
(cd && cd.includes("filename=")
|
|
573
|
+
? cd.split("filename=")[1]?.replace(/^["']|["']$/g, "").trim()
|
|
574
|
+
: "download");
|
|
575
|
+
const url = URL.createObjectURL(blob);
|
|
576
|
+
const a = document.createElement("a");
|
|
577
|
+
a.href = url;
|
|
578
|
+
a.download = filename;
|
|
579
|
+
a.click();
|
|
580
|
+
URL.revokeObjectURL(url);
|
|
581
|
+
}
|
|
582
|
+
`;
|
|
583
|
+
}
|
|
584
|
+
function generateApiClient(contextTags) {
|
|
585
|
+
const entries = contextTags.map((t) => ({
|
|
586
|
+
file: sanitizeContextName(t),
|
|
587
|
+
id: contextToIdentifier(t),
|
|
588
|
+
}));
|
|
589
|
+
const imports = entries
|
|
590
|
+
.map((e) => `import { ${e.id} } from "./contexts/${e.file}.js";`)
|
|
591
|
+
.join("\n");
|
|
592
|
+
const props = entries.map((e) => ` ${e.id}`).join(",\n");
|
|
593
|
+
return `// Auto-generated nested API client
|
|
594
|
+
${imports}
|
|
595
|
+
|
|
596
|
+
export const apiClient = {
|
|
597
|
+
${props},
|
|
598
|
+
};
|
|
599
|
+
`;
|
|
600
|
+
}
|
|
601
|
+
function generateIndex(contextTags) {
|
|
602
|
+
const exports = [
|
|
603
|
+
'export { client, setAuthToken } from "./client.js";',
|
|
604
|
+
'export { apiClient } from "./apiClient.js";',
|
|
605
|
+
'export * from "./types/index.js";',
|
|
606
|
+
"",
|
|
607
|
+
];
|
|
608
|
+
const reserved = new Set(["client"]);
|
|
609
|
+
for (const tag of contextTags) {
|
|
610
|
+
const ctxFile = sanitizeContextName(tag);
|
|
611
|
+
const ctxId = contextToIdentifier(tag);
|
|
612
|
+
const exportName = reserved.has(ctxId) ? `${ctxId}Context` : ctxId;
|
|
613
|
+
exports.push(`export { ${ctxId} as ${exportName} } from "./contexts/${ctxFile}.js";`);
|
|
614
|
+
}
|
|
615
|
+
return exports.join("\n");
|
|
616
|
+
}
|
|
617
|
+
async function main() {
|
|
618
|
+
const { url, out } = parseArgs();
|
|
619
|
+
console.log(`Fetching spec from ${url}...`);
|
|
620
|
+
const rawSpec = await loadRawSpec(url);
|
|
621
|
+
const doc = await parseSpec(rawSpec);
|
|
622
|
+
const baseUrl = getOrigin(doc);
|
|
623
|
+
const basePath = getBasePath(doc);
|
|
624
|
+
const definitions = getDefinitions(doc);
|
|
625
|
+
const paths = getPaths(doc);
|
|
626
|
+
console.log(`Base URL: ${baseUrl}`);
|
|
627
|
+
console.log(`Paths: ${Object.keys(paths).length}`);
|
|
628
|
+
console.log(`Definitions: ${Object.keys(definitions).length}`);
|
|
629
|
+
const ops = extractOperations(paths, basePath, definitions);
|
|
630
|
+
const byTag = groupByTag(ops, paths);
|
|
631
|
+
const cwd = process.cwd();
|
|
632
|
+
const outDir = join(cwd, out);
|
|
633
|
+
const typesDir = join(outDir, "types");
|
|
634
|
+
const contextsDir = join(outDir, "contexts");
|
|
635
|
+
mkdirSync(typesDir, { recursive: true });
|
|
636
|
+
mkdirSync(contextsDir, { recursive: true });
|
|
637
|
+
writeFileSync(join(typesDir, "index.ts"), generateTypes(definitions));
|
|
638
|
+
writeFileSync(join(outDir, "client.ts"), generateClient(baseUrl));
|
|
639
|
+
const sortedTags = [...byTag.keys()].sort();
|
|
640
|
+
for (const tag of sortedTags) {
|
|
641
|
+
const ctxName = sanitizeContextName(tag);
|
|
642
|
+
const tagDesc = getTagDescription(doc, tag);
|
|
643
|
+
const content = generateContextFile(tag, byTag.get(tag), definitions, tagDesc);
|
|
644
|
+
writeFileSync(join(contextsDir, `${ctxName}.ts`), content);
|
|
645
|
+
}
|
|
646
|
+
writeFileSync(join(outDir, "apiClient.ts"), generateApiClient(sortedTags));
|
|
647
|
+
writeFileSync(join(outDir, "index.ts"), generateIndex(sortedTags));
|
|
648
|
+
const docsHash = normalizedJsonHash(rawSpec);
|
|
649
|
+
const clientHash = computeClientHash(cwd, out);
|
|
650
|
+
const manifest = {
|
|
651
|
+
docsSource: url,
|
|
652
|
+
docsHash,
|
|
653
|
+
clientHash,
|
|
654
|
+
out,
|
|
655
|
+
generatedAt: new Date().toISOString(),
|
|
656
|
+
};
|
|
657
|
+
const manifestPath = join(cwd, "api-client.manifest.json");
|
|
658
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
659
|
+
console.log(`Generated API client in ${outDir}`);
|
|
660
|
+
console.log(` - types/index.ts`);
|
|
661
|
+
console.log(` - client.ts`);
|
|
662
|
+
console.log(` - apiClient.ts`);
|
|
663
|
+
console.log(` - contexts/*.ts (${sortedTags.length} files)`);
|
|
664
|
+
console.log(` - index.ts`);
|
|
665
|
+
console.log(` - manifest: ${manifestPath}`);
|
|
666
|
+
}
|
|
667
|
+
main().catch((err) => {
|
|
668
|
+
console.error(err);
|
|
669
|
+
process.exit(1);
|
|
670
|
+
});
|