api-sdk-gen 1.0.3 → 1.0.5
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/dist/index.js +181 -49
- package/dist/index.mjs +179 -47
- package/package.json +1 -1
- package/templates/core copy.ts +214 -0
- package/templates/core.ts +12 -184
package/dist/index.js
CHANGED
|
@@ -3,49 +3,96 @@
|
|
|
3
3
|
|
|
4
4
|
// src/generators/typescript/util.ts
|
|
5
5
|
function pascalCase(str) {
|
|
6
|
-
return str.replace(/(
|
|
7
|
-
}
|
|
8
|
-
function clearAndUpper(text) {
|
|
9
|
-
return text.replace(/-/, "").toUpperCase();
|
|
6
|
+
return str.replace(/(^|[-_])(\w)/g, (_, __, c) => c.toUpperCase());
|
|
10
7
|
}
|
|
11
8
|
function resolveSchema(ref) {
|
|
12
9
|
const parts = ref.split("#/components/schemas/");
|
|
13
|
-
return pascalCase(parts[1]);
|
|
10
|
+
return pascalCase(parts[1] || parts[0]);
|
|
14
11
|
}
|
|
15
12
|
|
|
16
13
|
// src/generators/typescript/schema_to_ts.ts
|
|
14
|
+
var import_ts_morph = require("ts-morph");
|
|
17
15
|
function schemaToTypeLiteral(schema, doc, refPrefix) {
|
|
18
16
|
return (writer) => {
|
|
19
17
|
if ("$ref" in schema) {
|
|
20
18
|
writer.write((refPrefix ?? "") + resolveSchema(schema.$ref));
|
|
21
19
|
} else if (schema.enum) {
|
|
22
|
-
writer.write(
|
|
20
|
+
writer.write(
|
|
21
|
+
schema.enum.map((value) => JSON.stringify(value)).join(" | ")
|
|
22
|
+
);
|
|
23
|
+
} else if (schema.allOf && schema.allOf.length > 0) {
|
|
24
|
+
const uniqueTypes = /* @__PURE__ */ new Set();
|
|
25
|
+
for (const subSchema of schema.allOf) {
|
|
26
|
+
const subWriter = new import_ts_morph.CodeBlockWriter();
|
|
27
|
+
schemaToTypeLiteral(subSchema, doc, refPrefix)(subWriter);
|
|
28
|
+
const str = subWriter.toString().trim();
|
|
29
|
+
if (str) uniqueTypes.add(str);
|
|
30
|
+
}
|
|
31
|
+
const typesArray = Array.from(uniqueTypes);
|
|
32
|
+
if (typesArray.length > 1) {
|
|
33
|
+
writer.write("(");
|
|
34
|
+
writer.write(typesArray.join(" & "));
|
|
35
|
+
writer.write(")");
|
|
36
|
+
} else if (typesArray.length === 1) {
|
|
37
|
+
writer.write(typesArray[0]);
|
|
38
|
+
} else {
|
|
39
|
+
writer.write("unknown");
|
|
40
|
+
}
|
|
23
41
|
} else if (schema.anyOf || schema.oneOf) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
42
|
+
const items = schema.anyOf || schema.oneOf;
|
|
43
|
+
if (items && items.length > 0) {
|
|
44
|
+
const uniqueTypes = /* @__PURE__ */ new Set();
|
|
45
|
+
for (const subSchema of items) {
|
|
46
|
+
const subWriter = new import_ts_morph.CodeBlockWriter();
|
|
47
|
+
schemaToTypeLiteral(subSchema, doc, refPrefix)(subWriter);
|
|
48
|
+
const str = subWriter.toString().trim();
|
|
49
|
+
if (str) uniqueTypes.add(str);
|
|
27
50
|
}
|
|
28
|
-
|
|
29
|
-
|
|
51
|
+
const typesArray = Array.from(uniqueTypes);
|
|
52
|
+
if (typesArray.length > 1) {
|
|
53
|
+
writer.write("(");
|
|
54
|
+
writer.write(typesArray.join(" | "));
|
|
55
|
+
writer.write(")");
|
|
56
|
+
} else if (typesArray.length === 1) {
|
|
57
|
+
writer.write(typesArray[0]);
|
|
58
|
+
} else {
|
|
59
|
+
writer.write("unknown");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
} else if (schema.type === "object" || !schema.type && schema.properties) {
|
|
63
|
+
const properties = schema.properties || {};
|
|
64
|
+
const propEntries = Object.entries(properties);
|
|
65
|
+
if (propEntries.length === 0) {
|
|
66
|
+
if (schema.additionalProperties && typeof schema.additionalProperties === "object" && Object.keys(schema.additionalProperties).length > 0) {
|
|
67
|
+
writer.write("Record<string, ");
|
|
68
|
+
schemaToTypeLiteral(schema.additionalProperties, doc, refPrefix)(writer);
|
|
69
|
+
writer.write(">");
|
|
70
|
+
} else {
|
|
71
|
+
writer.write("Record<string, unknown>");
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
const requiredProps = new Set(schema.required || []);
|
|
75
|
+
writer.write("{").newLine();
|
|
76
|
+
for (const [propName, propSchema] of propEntries) {
|
|
77
|
+
writer.indent(() => {
|
|
78
|
+
writer.write(propName);
|
|
79
|
+
if (!requiredProps.has(propName)) {
|
|
80
|
+
writer.write("?");
|
|
81
|
+
}
|
|
82
|
+
writer.write(": ");
|
|
83
|
+
schemaToTypeLiteral(propSchema, doc, refPrefix)(writer);
|
|
84
|
+
writer.write(";").newLine();
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
if (schema.additionalProperties) {
|
|
88
|
+
writer.indent(() => {
|
|
89
|
+
writer.write("[k: string]: unknown;").newLine();
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
writer.write("}");
|
|
93
|
+
}
|
|
30
94
|
} else if (schema.type) {
|
|
31
95
|
switch (schema.type) {
|
|
32
|
-
case "object": {
|
|
33
|
-
const requiredProps = new Set(schema.required || []);
|
|
34
|
-
writer.write("{").newLine();
|
|
35
|
-
for (const [propName, propSchema] of Object.entries(schema.properties || {})) {
|
|
36
|
-
writer.indent(() => {
|
|
37
|
-
writer.write(propName);
|
|
38
|
-
if (!requiredProps.has(propName)) {
|
|
39
|
-
writer.write("?");
|
|
40
|
-
}
|
|
41
|
-
writer.write(": ");
|
|
42
|
-
schemaToTypeLiteral(propSchema, doc, refPrefix)(writer);
|
|
43
|
-
writer.write(";").newLine();
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
writer.write("}");
|
|
47
|
-
break;
|
|
48
|
-
}
|
|
49
96
|
case "array":
|
|
50
97
|
if (Array.isArray(schema.items)) {
|
|
51
98
|
writer.write("[");
|
|
@@ -66,12 +113,16 @@ function schemaToTypeLiteral(schema, doc, refPrefix) {
|
|
|
66
113
|
break;
|
|
67
114
|
case "string": {
|
|
68
115
|
const s = schema;
|
|
116
|
+
if (s.format === "date-time" || s.format === "date") {
|
|
117
|
+
writer.write("Date | string");
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
69
120
|
const isFile = s.format === "binary" || s.format === "base64" || s.contentMediaType;
|
|
70
121
|
if (isFile) {
|
|
71
122
|
writer.write("File | Blob");
|
|
72
|
-
|
|
73
|
-
writer.write(s.type);
|
|
123
|
+
break;
|
|
74
124
|
}
|
|
125
|
+
writer.write(s.type);
|
|
75
126
|
break;
|
|
76
127
|
}
|
|
77
128
|
case "number":
|
|
@@ -110,12 +161,33 @@ var jsonSchemaTags = [
|
|
|
110
161
|
"format",
|
|
111
162
|
"example"
|
|
112
163
|
];
|
|
164
|
+
var standardFormatsWithRedundantPatterns = /* @__PURE__ */ new Set([
|
|
165
|
+
"date",
|
|
166
|
+
"date-time",
|
|
167
|
+
"uuid",
|
|
168
|
+
"email",
|
|
169
|
+
"ipv4",
|
|
170
|
+
"ipv6",
|
|
171
|
+
"uri",
|
|
172
|
+
"url"
|
|
173
|
+
]);
|
|
174
|
+
function shouldIgnorePattern(schema) {
|
|
175
|
+
const format = schema.format;
|
|
176
|
+
if (format && standardFormatsWithRedundantPatterns.has(format)) {
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
const subFormats = (schema.oneOf || schema.anyOf)?.map((s) => s?.format).filter(Boolean);
|
|
180
|
+
if (subFormats && subFormats.some((f) => standardFormatsWithRedundantPatterns.has(f))) {
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
113
185
|
function createInterfaceModel(doc, file, name, schema) {
|
|
114
186
|
if (!schema.properties || Object.keys(schema.properties).length === 0) {
|
|
115
187
|
file.addTypeAlias({
|
|
116
188
|
name: pascalCase(name),
|
|
117
189
|
isExported: true,
|
|
118
|
-
type: "{}"
|
|
190
|
+
type: schema.additionalProperties ? "Record<string, unknown>" : "{}"
|
|
119
191
|
});
|
|
120
192
|
return;
|
|
121
193
|
}
|
|
@@ -154,12 +226,23 @@ function generateSchemaDocs(schema, doc) {
|
|
|
154
226
|
});
|
|
155
227
|
}
|
|
156
228
|
for (const tagName of jsonSchemaTags) {
|
|
229
|
+
if (tagName === "pattern" && shouldIgnorePattern(schema)) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
157
232
|
const value = schema[tagName];
|
|
158
233
|
if (value !== void 0) {
|
|
159
234
|
jsdoc.tags?.push({
|
|
160
235
|
tagName,
|
|
161
236
|
text: String(value)
|
|
162
237
|
});
|
|
238
|
+
} else if (tagName === "format") {
|
|
239
|
+
const subFormats = (schema.oneOf || schema.anyOf)?.map((sub) => sub?.format).filter(Boolean);
|
|
240
|
+
if (subFormats && subFormats.length > 0) {
|
|
241
|
+
jsdoc.tags?.push({
|
|
242
|
+
tagName: "format",
|
|
243
|
+
text: Array.from(new Set(subFormats)).join(" | ")
|
|
244
|
+
});
|
|
245
|
+
}
|
|
163
246
|
}
|
|
164
247
|
}
|
|
165
248
|
if (!jsdoc.tags?.length && !jsdoc.description) {
|
|
@@ -176,10 +259,7 @@ function generatePropertySignature(doc, propName, propSchema) {
|
|
|
176
259
|
};
|
|
177
260
|
}
|
|
178
261
|
function createModel(doc, file, name, schema) {
|
|
179
|
-
if ("$ref" in schema) {
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
if (schema.type === "object" && schema.properties) {
|
|
262
|
+
if (!("$ref" in schema) && (schema.type === "object" || !schema.type && schema.properties) && schema.properties) {
|
|
183
263
|
createInterfaceModel(doc, file, name, schema);
|
|
184
264
|
return;
|
|
185
265
|
}
|
|
@@ -191,19 +271,49 @@ function createModel(doc, file, name, schema) {
|
|
|
191
271
|
docs: jsdoc ? [jsdoc] : []
|
|
192
272
|
});
|
|
193
273
|
}
|
|
274
|
+
function findReferencedSchemas(obj, refs = /* @__PURE__ */ new Set()) {
|
|
275
|
+
if (!obj || typeof obj !== "object") return refs;
|
|
276
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
277
|
+
if (k === "$ref" && typeof v === "string") {
|
|
278
|
+
const parts = v.split("#/components/schemas/");
|
|
279
|
+
if (parts.length > 1 && parts[1]) {
|
|
280
|
+
refs.add(parts[1]);
|
|
281
|
+
}
|
|
282
|
+
} else {
|
|
283
|
+
findReferencedSchemas(v, refs);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return refs;
|
|
287
|
+
}
|
|
194
288
|
async function generateModels(file, config, doc) {
|
|
195
289
|
const schemas = doc?.components?.schemas;
|
|
196
|
-
if (
|
|
197
|
-
|
|
290
|
+
if (schemas) {
|
|
291
|
+
for (const [schemaName, schema] of Object.entries(schemas)) {
|
|
292
|
+
createModel(doc, file, schemaName, schema);
|
|
293
|
+
}
|
|
198
294
|
}
|
|
199
|
-
|
|
200
|
-
|
|
295
|
+
const referencedSchemas = findReferencedSchemas(doc);
|
|
296
|
+
for (const refName of referencedSchemas) {
|
|
297
|
+
const pascalName = pascalCase(refName);
|
|
298
|
+
if (!file.getTypeAlias(pascalName) && !file.getInterface(pascalName)) {
|
|
299
|
+
file.addTypeAlias({
|
|
300
|
+
name: pascalName,
|
|
301
|
+
isExported: true,
|
|
302
|
+
type: "Record<string, unknown>",
|
|
303
|
+
docs: [
|
|
304
|
+
{
|
|
305
|
+
description: `Aviso: Este schema n\xE3o existe ou n\xE3o foi declarado na especifica\xE7\xE3o OpenAPI ('#/components/schemas/${refName}').
|
|
306
|
+
Gerado automaticamente como fallback para evitar quebras de refer\xEAncia e erros de compila\xE7\xE3o.`
|
|
307
|
+
}
|
|
308
|
+
]
|
|
309
|
+
});
|
|
310
|
+
}
|
|
201
311
|
}
|
|
202
312
|
}
|
|
203
313
|
|
|
204
314
|
// src/index.ts
|
|
205
315
|
var import_promises = require("fs/promises");
|
|
206
|
-
var
|
|
316
|
+
var import_ts_morph2 = require("ts-morph");
|
|
207
317
|
var import_node_path = require("path");
|
|
208
318
|
|
|
209
319
|
// src/generators/typescript/api_generator.ts
|
|
@@ -331,28 +441,50 @@ async function generateApiType(file, config, doc) {
|
|
|
331
441
|
// src/index.ts
|
|
332
442
|
var import_commander = require("commander");
|
|
333
443
|
var program = new import_commander.Command();
|
|
334
|
-
program.name("api-sdk-gen").description("Generate TypeScript SDK from OpenAPI specification").version("1.0.0").requiredOption("-u, --url <url>", "URL of the OpenAPI JSON specification").
|
|
444
|
+
program.name("api-sdk-gen").description("Generate TypeScript SDK from OpenAPI specification").version("1.0.0").requiredOption("-u, --url <url>", "URL or file path of the OpenAPI JSON specification").option("-b, --base <url>", "Base URL for the API calls", "http://localhost").option("-o, --output <dir>", "Output directory", "./output").action(async (options) => {
|
|
335
445
|
const config = {
|
|
336
446
|
url: options.url,
|
|
337
447
|
api_base: options.base,
|
|
338
448
|
outputDir: options.output
|
|
339
449
|
};
|
|
340
450
|
try {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
451
|
+
let openapi;
|
|
452
|
+
if (config.url.startsWith("http://") || config.url.startsWith("https://")) {
|
|
453
|
+
console.log(`Fetching OpenAPI spec from: ${config.url}...`);
|
|
454
|
+
const res = await fetch(config.url);
|
|
455
|
+
if (!res.ok) {
|
|
456
|
+
throw new Error(`Failed to fetch OpenAPI spec: ${res.statusText}`);
|
|
457
|
+
}
|
|
458
|
+
openapi = await res.json();
|
|
459
|
+
} else {
|
|
460
|
+
console.log(`Reading OpenAPI spec from file: ${config.url}...`);
|
|
461
|
+
const content = await (0, import_promises.readFile)(config.url, "utf-8");
|
|
462
|
+
openapi = JSON.parse(content);
|
|
345
463
|
}
|
|
346
|
-
const openapi = await res.json();
|
|
347
464
|
console.log(`Generating SDK to: ${config.outputDir}...`);
|
|
348
465
|
await (0, import_promises.mkdir)(config.outputDir, { recursive: true });
|
|
349
|
-
const project = new
|
|
466
|
+
const project = new import_ts_morph2.Project();
|
|
350
467
|
const modelsFile = project.createSourceFile((0, import_node_path.join)(config.outputDir, "models.ts"), "", { overwrite: true });
|
|
351
468
|
const apiFile = project.createSourceFile((0, import_node_path.join)(config.outputDir, "api.ts"), "", { overwrite: true });
|
|
352
|
-
const
|
|
469
|
+
const templatePathCandidates = [
|
|
470
|
+
(0, import_node_path.join)(__dirname, "../templates", "core.ts"),
|
|
471
|
+
(0, import_node_path.join)(__dirname, "templates", "core.ts"),
|
|
472
|
+
(0, import_node_path.join)(process.cwd(), "templates", "core.ts")
|
|
473
|
+
];
|
|
474
|
+
let templateContent = "";
|
|
475
|
+
for (const candidate of templatePathCandidates) {
|
|
476
|
+
try {
|
|
477
|
+
templateContent = await (0, import_promises.readFile)(candidate, "utf-8");
|
|
478
|
+
break;
|
|
479
|
+
} catch {
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (!templateContent) {
|
|
483
|
+
throw new Error("Template core.ts not found");
|
|
484
|
+
}
|
|
353
485
|
const coreFile = project.createSourceFile(
|
|
354
486
|
(0, import_node_path.join)(config.outputDir, "core.ts"),
|
|
355
|
-
|
|
487
|
+
templateContent,
|
|
356
488
|
{ overwrite: true }
|
|
357
489
|
);
|
|
358
490
|
await generateModels(modelsFile, config, openapi);
|
package/dist/index.mjs
CHANGED
|
@@ -9,49 +9,96 @@ var __dirname = /* @__PURE__ */ getDirname();
|
|
|
9
9
|
|
|
10
10
|
// src/generators/typescript/util.ts
|
|
11
11
|
function pascalCase(str) {
|
|
12
|
-
return str.replace(/(
|
|
13
|
-
}
|
|
14
|
-
function clearAndUpper(text) {
|
|
15
|
-
return text.replace(/-/, "").toUpperCase();
|
|
12
|
+
return str.replace(/(^|[-_])(\w)/g, (_, __, c) => c.toUpperCase());
|
|
16
13
|
}
|
|
17
14
|
function resolveSchema(ref) {
|
|
18
15
|
const parts = ref.split("#/components/schemas/");
|
|
19
|
-
return pascalCase(parts[1]);
|
|
16
|
+
return pascalCase(parts[1] || parts[0]);
|
|
20
17
|
}
|
|
21
18
|
|
|
22
19
|
// src/generators/typescript/schema_to_ts.ts
|
|
20
|
+
import { CodeBlockWriter } from "ts-morph";
|
|
23
21
|
function schemaToTypeLiteral(schema, doc, refPrefix) {
|
|
24
22
|
return (writer) => {
|
|
25
23
|
if ("$ref" in schema) {
|
|
26
24
|
writer.write((refPrefix ?? "") + resolveSchema(schema.$ref));
|
|
27
25
|
} else if (schema.enum) {
|
|
28
|
-
writer.write(
|
|
26
|
+
writer.write(
|
|
27
|
+
schema.enum.map((value) => JSON.stringify(value)).join(" | ")
|
|
28
|
+
);
|
|
29
|
+
} else if (schema.allOf && schema.allOf.length > 0) {
|
|
30
|
+
const uniqueTypes = /* @__PURE__ */ new Set();
|
|
31
|
+
for (const subSchema of schema.allOf) {
|
|
32
|
+
const subWriter = new CodeBlockWriter();
|
|
33
|
+
schemaToTypeLiteral(subSchema, doc, refPrefix)(subWriter);
|
|
34
|
+
const str = subWriter.toString().trim();
|
|
35
|
+
if (str) uniqueTypes.add(str);
|
|
36
|
+
}
|
|
37
|
+
const typesArray = Array.from(uniqueTypes);
|
|
38
|
+
if (typesArray.length > 1) {
|
|
39
|
+
writer.write("(");
|
|
40
|
+
writer.write(typesArray.join(" & "));
|
|
41
|
+
writer.write(")");
|
|
42
|
+
} else if (typesArray.length === 1) {
|
|
43
|
+
writer.write(typesArray[0]);
|
|
44
|
+
} else {
|
|
45
|
+
writer.write("unknown");
|
|
46
|
+
}
|
|
29
47
|
} else if (schema.anyOf || schema.oneOf) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
48
|
+
const items = schema.anyOf || schema.oneOf;
|
|
49
|
+
if (items && items.length > 0) {
|
|
50
|
+
const uniqueTypes = /* @__PURE__ */ new Set();
|
|
51
|
+
for (const subSchema of items) {
|
|
52
|
+
const subWriter = new CodeBlockWriter();
|
|
53
|
+
schemaToTypeLiteral(subSchema, doc, refPrefix)(subWriter);
|
|
54
|
+
const str = subWriter.toString().trim();
|
|
55
|
+
if (str) uniqueTypes.add(str);
|
|
33
56
|
}
|
|
34
|
-
|
|
35
|
-
|
|
57
|
+
const typesArray = Array.from(uniqueTypes);
|
|
58
|
+
if (typesArray.length > 1) {
|
|
59
|
+
writer.write("(");
|
|
60
|
+
writer.write(typesArray.join(" | "));
|
|
61
|
+
writer.write(")");
|
|
62
|
+
} else if (typesArray.length === 1) {
|
|
63
|
+
writer.write(typesArray[0]);
|
|
64
|
+
} else {
|
|
65
|
+
writer.write("unknown");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
} else if (schema.type === "object" || !schema.type && schema.properties) {
|
|
69
|
+
const properties = schema.properties || {};
|
|
70
|
+
const propEntries = Object.entries(properties);
|
|
71
|
+
if (propEntries.length === 0) {
|
|
72
|
+
if (schema.additionalProperties && typeof schema.additionalProperties === "object" && Object.keys(schema.additionalProperties).length > 0) {
|
|
73
|
+
writer.write("Record<string, ");
|
|
74
|
+
schemaToTypeLiteral(schema.additionalProperties, doc, refPrefix)(writer);
|
|
75
|
+
writer.write(">");
|
|
76
|
+
} else {
|
|
77
|
+
writer.write("Record<string, unknown>");
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
const requiredProps = new Set(schema.required || []);
|
|
81
|
+
writer.write("{").newLine();
|
|
82
|
+
for (const [propName, propSchema] of propEntries) {
|
|
83
|
+
writer.indent(() => {
|
|
84
|
+
writer.write(propName);
|
|
85
|
+
if (!requiredProps.has(propName)) {
|
|
86
|
+
writer.write("?");
|
|
87
|
+
}
|
|
88
|
+
writer.write(": ");
|
|
89
|
+
schemaToTypeLiteral(propSchema, doc, refPrefix)(writer);
|
|
90
|
+
writer.write(";").newLine();
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
if (schema.additionalProperties) {
|
|
94
|
+
writer.indent(() => {
|
|
95
|
+
writer.write("[k: string]: unknown;").newLine();
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
writer.write("}");
|
|
99
|
+
}
|
|
36
100
|
} else if (schema.type) {
|
|
37
101
|
switch (schema.type) {
|
|
38
|
-
case "object": {
|
|
39
|
-
const requiredProps = new Set(schema.required || []);
|
|
40
|
-
writer.write("{").newLine();
|
|
41
|
-
for (const [propName, propSchema] of Object.entries(schema.properties || {})) {
|
|
42
|
-
writer.indent(() => {
|
|
43
|
-
writer.write(propName);
|
|
44
|
-
if (!requiredProps.has(propName)) {
|
|
45
|
-
writer.write("?");
|
|
46
|
-
}
|
|
47
|
-
writer.write(": ");
|
|
48
|
-
schemaToTypeLiteral(propSchema, doc, refPrefix)(writer);
|
|
49
|
-
writer.write(";").newLine();
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
writer.write("}");
|
|
53
|
-
break;
|
|
54
|
-
}
|
|
55
102
|
case "array":
|
|
56
103
|
if (Array.isArray(schema.items)) {
|
|
57
104
|
writer.write("[");
|
|
@@ -72,12 +119,16 @@ function schemaToTypeLiteral(schema, doc, refPrefix) {
|
|
|
72
119
|
break;
|
|
73
120
|
case "string": {
|
|
74
121
|
const s = schema;
|
|
122
|
+
if (s.format === "date-time" || s.format === "date") {
|
|
123
|
+
writer.write("Date | string");
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
75
126
|
const isFile = s.format === "binary" || s.format === "base64" || s.contentMediaType;
|
|
76
127
|
if (isFile) {
|
|
77
128
|
writer.write("File | Blob");
|
|
78
|
-
|
|
79
|
-
writer.write(s.type);
|
|
129
|
+
break;
|
|
80
130
|
}
|
|
131
|
+
writer.write(s.type);
|
|
81
132
|
break;
|
|
82
133
|
}
|
|
83
134
|
case "number":
|
|
@@ -116,12 +167,33 @@ var jsonSchemaTags = [
|
|
|
116
167
|
"format",
|
|
117
168
|
"example"
|
|
118
169
|
];
|
|
170
|
+
var standardFormatsWithRedundantPatterns = /* @__PURE__ */ new Set([
|
|
171
|
+
"date",
|
|
172
|
+
"date-time",
|
|
173
|
+
"uuid",
|
|
174
|
+
"email",
|
|
175
|
+
"ipv4",
|
|
176
|
+
"ipv6",
|
|
177
|
+
"uri",
|
|
178
|
+
"url"
|
|
179
|
+
]);
|
|
180
|
+
function shouldIgnorePattern(schema) {
|
|
181
|
+
const format = schema.format;
|
|
182
|
+
if (format && standardFormatsWithRedundantPatterns.has(format)) {
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
const subFormats = (schema.oneOf || schema.anyOf)?.map((s) => s?.format).filter(Boolean);
|
|
186
|
+
if (subFormats && subFormats.some((f) => standardFormatsWithRedundantPatterns.has(f))) {
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
119
191
|
function createInterfaceModel(doc, file, name, schema) {
|
|
120
192
|
if (!schema.properties || Object.keys(schema.properties).length === 0) {
|
|
121
193
|
file.addTypeAlias({
|
|
122
194
|
name: pascalCase(name),
|
|
123
195
|
isExported: true,
|
|
124
|
-
type: "{}"
|
|
196
|
+
type: schema.additionalProperties ? "Record<string, unknown>" : "{}"
|
|
125
197
|
});
|
|
126
198
|
return;
|
|
127
199
|
}
|
|
@@ -160,12 +232,23 @@ function generateSchemaDocs(schema, doc) {
|
|
|
160
232
|
});
|
|
161
233
|
}
|
|
162
234
|
for (const tagName of jsonSchemaTags) {
|
|
235
|
+
if (tagName === "pattern" && shouldIgnorePattern(schema)) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
163
238
|
const value = schema[tagName];
|
|
164
239
|
if (value !== void 0) {
|
|
165
240
|
jsdoc.tags?.push({
|
|
166
241
|
tagName,
|
|
167
242
|
text: String(value)
|
|
168
243
|
});
|
|
244
|
+
} else if (tagName === "format") {
|
|
245
|
+
const subFormats = (schema.oneOf || schema.anyOf)?.map((sub) => sub?.format).filter(Boolean);
|
|
246
|
+
if (subFormats && subFormats.length > 0) {
|
|
247
|
+
jsdoc.tags?.push({
|
|
248
|
+
tagName: "format",
|
|
249
|
+
text: Array.from(new Set(subFormats)).join(" | ")
|
|
250
|
+
});
|
|
251
|
+
}
|
|
169
252
|
}
|
|
170
253
|
}
|
|
171
254
|
if (!jsdoc.tags?.length && !jsdoc.description) {
|
|
@@ -182,10 +265,7 @@ function generatePropertySignature(doc, propName, propSchema) {
|
|
|
182
265
|
};
|
|
183
266
|
}
|
|
184
267
|
function createModel(doc, file, name, schema) {
|
|
185
|
-
if ("$ref" in schema) {
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
if (schema.type === "object" && schema.properties) {
|
|
268
|
+
if (!("$ref" in schema) && (schema.type === "object" || !schema.type && schema.properties) && schema.properties) {
|
|
189
269
|
createInterfaceModel(doc, file, name, schema);
|
|
190
270
|
return;
|
|
191
271
|
}
|
|
@@ -197,13 +277,43 @@ function createModel(doc, file, name, schema) {
|
|
|
197
277
|
docs: jsdoc ? [jsdoc] : []
|
|
198
278
|
});
|
|
199
279
|
}
|
|
280
|
+
function findReferencedSchemas(obj, refs = /* @__PURE__ */ new Set()) {
|
|
281
|
+
if (!obj || typeof obj !== "object") return refs;
|
|
282
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
283
|
+
if (k === "$ref" && typeof v === "string") {
|
|
284
|
+
const parts = v.split("#/components/schemas/");
|
|
285
|
+
if (parts.length > 1 && parts[1]) {
|
|
286
|
+
refs.add(parts[1]);
|
|
287
|
+
}
|
|
288
|
+
} else {
|
|
289
|
+
findReferencedSchemas(v, refs);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return refs;
|
|
293
|
+
}
|
|
200
294
|
async function generateModels(file, config, doc) {
|
|
201
295
|
const schemas = doc?.components?.schemas;
|
|
202
|
-
if (
|
|
203
|
-
|
|
296
|
+
if (schemas) {
|
|
297
|
+
for (const [schemaName, schema] of Object.entries(schemas)) {
|
|
298
|
+
createModel(doc, file, schemaName, schema);
|
|
299
|
+
}
|
|
204
300
|
}
|
|
205
|
-
|
|
206
|
-
|
|
301
|
+
const referencedSchemas = findReferencedSchemas(doc);
|
|
302
|
+
for (const refName of referencedSchemas) {
|
|
303
|
+
const pascalName = pascalCase(refName);
|
|
304
|
+
if (!file.getTypeAlias(pascalName) && !file.getInterface(pascalName)) {
|
|
305
|
+
file.addTypeAlias({
|
|
306
|
+
name: pascalName,
|
|
307
|
+
isExported: true,
|
|
308
|
+
type: "Record<string, unknown>",
|
|
309
|
+
docs: [
|
|
310
|
+
{
|
|
311
|
+
description: `Aviso: Este schema n\xE3o existe ou n\xE3o foi declarado na especifica\xE7\xE3o OpenAPI ('#/components/schemas/${refName}').
|
|
312
|
+
Gerado automaticamente como fallback para evitar quebras de refer\xEAncia e erros de compila\xE7\xE3o.`
|
|
313
|
+
}
|
|
314
|
+
]
|
|
315
|
+
});
|
|
316
|
+
}
|
|
207
317
|
}
|
|
208
318
|
}
|
|
209
319
|
|
|
@@ -337,28 +447,50 @@ async function generateApiType(file, config, doc) {
|
|
|
337
447
|
// src/index.ts
|
|
338
448
|
import { Command } from "commander";
|
|
339
449
|
var program = new Command();
|
|
340
|
-
program.name("api-sdk-gen").description("Generate TypeScript SDK from OpenAPI specification").version("1.0.0").requiredOption("-u, --url <url>", "URL of the OpenAPI JSON specification").
|
|
450
|
+
program.name("api-sdk-gen").description("Generate TypeScript SDK from OpenAPI specification").version("1.0.0").requiredOption("-u, --url <url>", "URL or file path of the OpenAPI JSON specification").option("-b, --base <url>", "Base URL for the API calls", "http://localhost").option("-o, --output <dir>", "Output directory", "./output").action(async (options) => {
|
|
341
451
|
const config = {
|
|
342
452
|
url: options.url,
|
|
343
453
|
api_base: options.base,
|
|
344
454
|
outputDir: options.output
|
|
345
455
|
};
|
|
346
456
|
try {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
457
|
+
let openapi;
|
|
458
|
+
if (config.url.startsWith("http://") || config.url.startsWith("https://")) {
|
|
459
|
+
console.log(`Fetching OpenAPI spec from: ${config.url}...`);
|
|
460
|
+
const res = await fetch(config.url);
|
|
461
|
+
if (!res.ok) {
|
|
462
|
+
throw new Error(`Failed to fetch OpenAPI spec: ${res.statusText}`);
|
|
463
|
+
}
|
|
464
|
+
openapi = await res.json();
|
|
465
|
+
} else {
|
|
466
|
+
console.log(`Reading OpenAPI spec from file: ${config.url}...`);
|
|
467
|
+
const content = await readFile(config.url, "utf-8");
|
|
468
|
+
openapi = JSON.parse(content);
|
|
351
469
|
}
|
|
352
|
-
const openapi = await res.json();
|
|
353
470
|
console.log(`Generating SDK to: ${config.outputDir}...`);
|
|
354
471
|
await mkdir(config.outputDir, { recursive: true });
|
|
355
472
|
const project = new Project();
|
|
356
473
|
const modelsFile = project.createSourceFile(join(config.outputDir, "models.ts"), "", { overwrite: true });
|
|
357
474
|
const apiFile = project.createSourceFile(join(config.outputDir, "api.ts"), "", { overwrite: true });
|
|
358
|
-
const
|
|
475
|
+
const templatePathCandidates = [
|
|
476
|
+
join(__dirname, "../templates", "core.ts"),
|
|
477
|
+
join(__dirname, "templates", "core.ts"),
|
|
478
|
+
join(process.cwd(), "templates", "core.ts")
|
|
479
|
+
];
|
|
480
|
+
let templateContent = "";
|
|
481
|
+
for (const candidate of templatePathCandidates) {
|
|
482
|
+
try {
|
|
483
|
+
templateContent = await readFile(candidate, "utf-8");
|
|
484
|
+
break;
|
|
485
|
+
} catch {
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
if (!templateContent) {
|
|
489
|
+
throw new Error("Template core.ts not found");
|
|
490
|
+
}
|
|
359
491
|
const coreFile = project.createSourceFile(
|
|
360
492
|
join(config.outputDir, "core.ts"),
|
|
361
|
-
|
|
493
|
+
templateContent,
|
|
362
494
|
{ overwrite: true }
|
|
363
495
|
);
|
|
364
496
|
await generateModels(modelsFile, config, openapi);
|
package/package.json
CHANGED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
type HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete';
|
|
2
|
+
|
|
3
|
+
interface BaseOperation {
|
|
4
|
+
parameters?: {
|
|
5
|
+
query?: Record<string, unknown>;
|
|
6
|
+
path?: Record<string, string>;
|
|
7
|
+
header?: Record<string, string>;
|
|
8
|
+
}
|
|
9
|
+
requestBody?: Record<string, any>;
|
|
10
|
+
responses?: Record<string | number, {
|
|
11
|
+
headers?: Record<string, Record<string, any>>;
|
|
12
|
+
content?: Record<string, any>
|
|
13
|
+
}>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type ApiDoc = Record<string, Partial<BaseOperation & Record<HttpMethod, BaseOperation>>>;
|
|
17
|
+
|
|
18
|
+
type BaseRequest = {
|
|
19
|
+
path?: Record<string, string>;
|
|
20
|
+
query?: Record<string, string>;
|
|
21
|
+
header?: Record<string, string>;
|
|
22
|
+
contentType?: string;
|
|
23
|
+
body?: any;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type Pretty<T> = {
|
|
27
|
+
[K in keyof T]: T[K];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type DeepPretty<T> = T extends object ? Pretty<{
|
|
31
|
+
[K in keyof T]: DeepPretty<T[K]>;
|
|
32
|
+
}> : T;
|
|
33
|
+
|
|
34
|
+
type GetOp<T extends ApiDoc, P extends keyof T, M extends keyof T[P] & HttpMethod> =
|
|
35
|
+
T[P][M] extends infer Op ? (Op extends BaseOperation ? Op : never) : never;
|
|
36
|
+
|
|
37
|
+
type RequiredKeys<T> = { [K in keyof T]-?: (Record<string, never>) extends Pick<T, K> ? never : K }[keyof T];
|
|
38
|
+
|
|
39
|
+
type OptionalIfEmpty<K extends string, T> =
|
|
40
|
+
[T] extends [never] ? unknown :
|
|
41
|
+
[T] extends [undefined] ? unknown :
|
|
42
|
+
RequiredKeys<T> extends never
|
|
43
|
+
? (keyof T extends never ? unknown : { [P in K]?: T })
|
|
44
|
+
: { [P in K]: T };
|
|
45
|
+
|
|
46
|
+
type OpBody<Op extends BaseOperation> =
|
|
47
|
+
Op['requestBody'] extends infer RB
|
|
48
|
+
? RB extends Record<string, unknown>
|
|
49
|
+
? { [K in keyof RB]: {
|
|
50
|
+
contentType: K,
|
|
51
|
+
body: K extends 'multipart/form-data' ? (RB[K] | FormData) : RB[K]
|
|
52
|
+
} }[keyof RB]
|
|
53
|
+
: unknown
|
|
54
|
+
: unknown;
|
|
55
|
+
|
|
56
|
+
type ToNumber<S> = S extends number ? S : S extends `${infer N extends number}` ? N : S;
|
|
57
|
+
|
|
58
|
+
type ApiFetchResponse<Op extends BaseOperation> =
|
|
59
|
+
Op['responses'] extends infer R
|
|
60
|
+
? R extends Record<string | number, any>
|
|
61
|
+
? {
|
|
62
|
+
[S in keyof R]: S extends number | string
|
|
63
|
+
? R[S] extends { content: infer C }
|
|
64
|
+
? C extends Record<string, any>
|
|
65
|
+
? { [CT in keyof C]: {
|
|
66
|
+
status: ToNumber<S>,
|
|
67
|
+
contentType: CT,
|
|
68
|
+
body: C[CT]
|
|
69
|
+
} }[keyof C]
|
|
70
|
+
: { status: ToNumber<S>, contentType: null, body: null }
|
|
71
|
+
: { status: ToNumber<S>, contentType: null, body: null }
|
|
72
|
+
: never
|
|
73
|
+
}[keyof R]
|
|
74
|
+
: { status: number; contentType: string; body: any }
|
|
75
|
+
: { status: number; contentType: string; body: any };
|
|
76
|
+
|
|
77
|
+
type ApiFetchOptions<T extends ApiDoc, P extends keyof T, M extends keyof T[P] & HttpMethod, Op = GetOp<T, P, M>> =
|
|
78
|
+
{ method: M } &
|
|
79
|
+
(Op extends BaseOperation ?
|
|
80
|
+
OptionalIfEmpty<'path', NonNullable<Op['parameters']>['path']> &
|
|
81
|
+
OptionalIfEmpty<'query', NonNullable<Op['parameters']>['query']> &
|
|
82
|
+
OptionalIfEmpty<'header', NonNullable<Op['parameters']>['header']> &
|
|
83
|
+
OpBody<Op>
|
|
84
|
+
: unknown);
|
|
85
|
+
|
|
86
|
+
type ApiFetch<T extends ApiDoc> = <
|
|
87
|
+
P extends keyof T,
|
|
88
|
+
M extends keyof T[P] & HttpMethod
|
|
89
|
+
>(
|
|
90
|
+
path: P,
|
|
91
|
+
options: ApiFetchOptions<T, P, M>
|
|
92
|
+
) => Promise<DeepPretty<ApiFetchResponse<GetOp<T, P, M>>>>;
|
|
93
|
+
|
|
94
|
+
export type Request = {
|
|
95
|
+
url: URL;
|
|
96
|
+
init: RequestInit;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function buildApiRequest(
|
|
100
|
+
baseUrl: string | URL,
|
|
101
|
+
path: string,
|
|
102
|
+
options: BaseRequest
|
|
103
|
+
): Request {
|
|
104
|
+
const headers = new Headers();
|
|
105
|
+
const opts = options as any;
|
|
106
|
+
let body: any = opts.body;
|
|
107
|
+
const url = new URL(path as string, baseUrl);
|
|
108
|
+
|
|
109
|
+
if (opts.header) {
|
|
110
|
+
Object.entries(opts.header).forEach(([key, value]) => {
|
|
111
|
+
if (Array.isArray(value)) {
|
|
112
|
+
value.forEach(v => {
|
|
113
|
+
headers.append(key, v)
|
|
114
|
+
});
|
|
115
|
+
} else {
|
|
116
|
+
headers.set(key, value as string);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (opts.body) {
|
|
122
|
+
headers.append('Content-Type', opts.contentType);
|
|
123
|
+
|
|
124
|
+
if (opts.contentType === 'application/json') {
|
|
125
|
+
body = JSON.stringify(body);
|
|
126
|
+
} else if (opts.contentType === 'multipart/form-data' && !(body instanceof FormData)) {
|
|
127
|
+
const form = new FormData();
|
|
128
|
+
|
|
129
|
+
Object.entries(body).forEach(([key, value]) => {
|
|
130
|
+
if (Array.isArray(value)) {
|
|
131
|
+
value.forEach(v => {
|
|
132
|
+
form.append(key, v)
|
|
133
|
+
});
|
|
134
|
+
} else {
|
|
135
|
+
form.set(key, value as string);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
body = form;
|
|
140
|
+
} else if (opts.contentType === 'application/x-www-form-urlencoded') {
|
|
141
|
+
body = new URLSearchParams(body);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (opts.path) {
|
|
146
|
+
url.pathname = url.pathname.replace(/\{(.+?)\}/g, (text, key) => key in opts.path ? opts.path[key] : text);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (opts.query) {
|
|
150
|
+
Object.entries(opts.query).forEach(([key, value]) => {
|
|
151
|
+
if (Array.isArray(value)) {
|
|
152
|
+
value.forEach(v => {
|
|
153
|
+
url.searchParams.append(key, v)
|
|
154
|
+
});
|
|
155
|
+
} else {
|
|
156
|
+
url.searchParams.set(key, value as string);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
url,
|
|
163
|
+
init: {
|
|
164
|
+
method: opts.method.toUpperCase(),
|
|
165
|
+
headers,
|
|
166
|
+
body,
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function buildResponse<T extends ApiFetchResponse<any>>(response: Response): Promise<T> {
|
|
172
|
+
const contentType = response.headers.get('Content-Type') || null;
|
|
173
|
+
|
|
174
|
+
if (contentType?.startsWith('application/json')) {
|
|
175
|
+
return {
|
|
176
|
+
status: response.status,
|
|
177
|
+
contentType,
|
|
178
|
+
body: await response.json(),
|
|
179
|
+
} as T;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (contentType?.startsWith('application/xml')) {
|
|
183
|
+
return {
|
|
184
|
+
status: response.status,
|
|
185
|
+
contentType,
|
|
186
|
+
body: new DOMParser().parseFromString(await response.text(), 'application/xml'),
|
|
187
|
+
} as T;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (contentType?.startsWith('text/plain')) {
|
|
191
|
+
return {
|
|
192
|
+
status: response.status,
|
|
193
|
+
contentType,
|
|
194
|
+
body: await response.text(),
|
|
195
|
+
} as T;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
status: response.status,
|
|
200
|
+
contentType,
|
|
201
|
+
body: await response.blob(),
|
|
202
|
+
} as T;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function createApi<T extends ApiDoc>(
|
|
206
|
+
baseUrl: string | URL,
|
|
207
|
+
adapter?: (request: Request) => Promise<Response>
|
|
208
|
+
) {
|
|
209
|
+
adapter ??= (request) => fetch(request.url, request.init);
|
|
210
|
+
|
|
211
|
+
const api = async (path: string, options: any) => buildResponse(await adapter(buildApiRequest(baseUrl, path, options)));
|
|
212
|
+
|
|
213
|
+
return api as ApiFetch<T>;
|
|
214
|
+
}
|
package/templates/core.ts
CHANGED
|
@@ -27,188 +27,16 @@ type Pretty<T> = {
|
|
|
27
27
|
[K in keyof T]: T[K];
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
-
type
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
type RequiredKeys<T> = { [K in keyof T]-?: (Record<string, never>) extends Pick<T, K> ? never : K }[keyof T];
|
|
38
|
-
|
|
39
|
-
type OptionalIfEmpty<K extends string, T> =
|
|
40
|
-
[T] extends [never] ? unknown :
|
|
41
|
-
[T] extends [undefined] ? unknown :
|
|
42
|
-
RequiredKeys<T> extends never
|
|
43
|
-
? (keyof T extends never ? unknown : { [P in K]?: T })
|
|
44
|
-
: { [P in K]: T };
|
|
45
|
-
|
|
46
|
-
type OpBody<Op extends BaseOperation> =
|
|
47
|
-
Op['requestBody'] extends infer RB
|
|
48
|
-
? RB extends Record<string, unknown>
|
|
49
|
-
? { [K in keyof RB]: {
|
|
50
|
-
contentType: K,
|
|
51
|
-
body: K extends 'multipart/form-data' ? (RB[K] | FormData) : RB[K]
|
|
52
|
-
} }[keyof RB]
|
|
53
|
-
: unknown
|
|
54
|
-
: unknown;
|
|
55
|
-
|
|
56
|
-
type ToNumber<S> = S extends number ? S : S extends `${infer N extends number}` ? N : S;
|
|
57
|
-
|
|
58
|
-
type ApiFetchResponse<Op extends BaseOperation> =
|
|
59
|
-
Op['responses'] extends infer R
|
|
60
|
-
? R extends Record<string | number, any>
|
|
61
|
-
? {
|
|
62
|
-
[S in keyof R]: S extends number | string
|
|
63
|
-
? R[S] extends { content: infer C }
|
|
64
|
-
? C extends Record<string, any>
|
|
65
|
-
? { [CT in keyof C]: {
|
|
66
|
-
status: ToNumber<S>,
|
|
67
|
-
contentType: CT,
|
|
68
|
-
body: C[CT]
|
|
69
|
-
} }[keyof C]
|
|
70
|
-
: { status: ToNumber<S>, contentType: null, body: null }
|
|
71
|
-
: { status: ToNumber<S>, contentType: null, body: null }
|
|
30
|
+
export type QueryParams<Api extends ApiDoc, Path extends keyof Api, Method extends keyof Api[Path]> = Api[Path][Method] extends { parameters: infer P } ? P : never;
|
|
31
|
+
export type ResponseStatus<
|
|
32
|
+
Api extends ApiDoc,
|
|
33
|
+
Path extends keyof Api,
|
|
34
|
+
Method extends keyof Api[Path],
|
|
35
|
+
Status extends Api[Path][Method] extends { responses: Record<string | number, any> }
|
|
36
|
+
? keyof Api[Path][Method]['responses']
|
|
72
37
|
: never
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
{ method: M } &
|
|
79
|
-
(Op extends BaseOperation ?
|
|
80
|
-
OptionalIfEmpty<'path', NonNullable<Op['parameters']>['path']> &
|
|
81
|
-
OptionalIfEmpty<'query', NonNullable<Op['parameters']>['query']> &
|
|
82
|
-
OptionalIfEmpty<'header', NonNullable<Op['parameters']>['header']> &
|
|
83
|
-
OpBody<Op>
|
|
84
|
-
: unknown);
|
|
85
|
-
|
|
86
|
-
type ApiFetch<T extends ApiDoc> = <
|
|
87
|
-
P extends keyof T,
|
|
88
|
-
M extends keyof T[P] & HttpMethod
|
|
89
|
-
>(
|
|
90
|
-
path: P,
|
|
91
|
-
options: ApiFetchOptions<T, P, M>
|
|
92
|
-
) => Promise<DeepPretty<ApiFetchResponse<GetOp<T, P, M>>>>;
|
|
93
|
-
|
|
94
|
-
export type Request = {
|
|
95
|
-
url: URL;
|
|
96
|
-
init: RequestInit;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export function buildApiRequest(
|
|
100
|
-
baseUrl: string | URL,
|
|
101
|
-
path: string,
|
|
102
|
-
options: BaseRequest
|
|
103
|
-
): Request {
|
|
104
|
-
const headers = new Headers();
|
|
105
|
-
const opts = options as any;
|
|
106
|
-
let body: any = opts.body;
|
|
107
|
-
const url = new URL(path as string, baseUrl);
|
|
108
|
-
|
|
109
|
-
if (opts.header) {
|
|
110
|
-
Object.entries(opts.header).forEach(([key, value]) => {
|
|
111
|
-
if (Array.isArray(value)) {
|
|
112
|
-
value.forEach(v => {
|
|
113
|
-
headers.append(key, v)
|
|
114
|
-
});
|
|
115
|
-
} else {
|
|
116
|
-
headers.set(key, value as string);
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (opts.body) {
|
|
122
|
-
headers.append('Content-Type', opts.contentType);
|
|
123
|
-
|
|
124
|
-
if (opts.contentType === 'application/json') {
|
|
125
|
-
body = JSON.stringify(body);
|
|
126
|
-
} else if (opts.contentType === 'multipart/form-data' && !(body instanceof FormData)) {
|
|
127
|
-
const form = new FormData();
|
|
128
|
-
|
|
129
|
-
Object.entries(body).forEach(([key, value]) => {
|
|
130
|
-
if (Array.isArray(value)) {
|
|
131
|
-
value.forEach(v => {
|
|
132
|
-
form.append(key, v)
|
|
133
|
-
});
|
|
134
|
-
} else {
|
|
135
|
-
form.set(key, value as string);
|
|
136
|
-
}
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
body = form;
|
|
140
|
-
} else if (opts.contentType === 'application/x-www-form-urlencoded') {
|
|
141
|
-
body = new URLSearchParams(body);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (opts.path) {
|
|
146
|
-
url.pathname = url.pathname.replace(/\{(.+?)\}/g, (text, key) => key in opts.path ? opts.path[key] : text);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (opts.query) {
|
|
150
|
-
Object.entries(opts.query).forEach(([key, value]) => {
|
|
151
|
-
if (Array.isArray(value)) {
|
|
152
|
-
value.forEach(v => {
|
|
153
|
-
url.searchParams.append(key, v)
|
|
154
|
-
});
|
|
155
|
-
} else {
|
|
156
|
-
url.searchParams.set(key, value as string);
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return {
|
|
162
|
-
url,
|
|
163
|
-
init: {
|
|
164
|
-
method: opts.method.toUpperCase(),
|
|
165
|
-
headers,
|
|
166
|
-
body,
|
|
167
|
-
}
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
export async function buildResponse<T extends ApiFetchResponse<any>>(response: Response): Promise<T> {
|
|
172
|
-
const contentType = response.headers.get('Content-Type') || null;
|
|
173
|
-
|
|
174
|
-
if (contentType?.startsWith('application/json')) {
|
|
175
|
-
return {
|
|
176
|
-
status: response.status,
|
|
177
|
-
contentType,
|
|
178
|
-
body: await response.json(),
|
|
179
|
-
} as T;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (contentType?.startsWith('application/xml')) {
|
|
183
|
-
return {
|
|
184
|
-
status: response.status,
|
|
185
|
-
contentType,
|
|
186
|
-
body: new DOMParser().parseFromString(await response.text(), 'application/xml'),
|
|
187
|
-
} as T;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
if (contentType?.startsWith('text/plain')) {
|
|
191
|
-
return {
|
|
192
|
-
status: response.status,
|
|
193
|
-
contentType,
|
|
194
|
-
body: await response.text(),
|
|
195
|
-
} as T;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return {
|
|
199
|
-
status: response.status,
|
|
200
|
-
contentType,
|
|
201
|
-
body: await response.blob(),
|
|
202
|
-
} as T;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
export function createApi<T extends ApiDoc>(
|
|
206
|
-
baseUrl: string | URL,
|
|
207
|
-
adapter?: (request: Request) => Promise<Response>
|
|
208
|
-
) {
|
|
209
|
-
adapter ??= (request) => fetch(request.url, request.init);
|
|
210
|
-
|
|
211
|
-
const api = async (path: string, options: any) => buildResponse(await adapter(buildApiRequest(baseUrl, path, options)));
|
|
212
|
-
|
|
213
|
-
return api as ApiFetch<T>;
|
|
214
|
-
}
|
|
38
|
+
> = Api[Path][Method] extends { responses: infer R }
|
|
39
|
+
? Status extends keyof R
|
|
40
|
+
? R[Status]
|
|
41
|
+
: never
|
|
42
|
+
: never;
|