api-sdk-gen 1.0.4 → 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 +172 -48
- package/dist/index.mjs +170 -46
- package/package.json +1 -1
- package/templates/core.ts +13 -2
package/dist/index.js
CHANGED
|
@@ -3,17 +3,15 @@
|
|
|
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) {
|
|
@@ -22,34 +20,79 @@ function schemaToTypeLiteral(schema, doc, refPrefix) {
|
|
|
22
20
|
writer.write(
|
|
23
21
|
schema.enum.map((value) => JSON.stringify(value)).join(" | ")
|
|
24
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
|
+
}
|
|
25
41
|
} else if (schema.anyOf || schema.oneOf) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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);
|
|
29
50
|
}
|
|
30
|
-
|
|
31
|
-
|
|
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
|
+
}
|
|
32
94
|
} else if (schema.type) {
|
|
33
95
|
switch (schema.type) {
|
|
34
|
-
case "object": {
|
|
35
|
-
const requiredProps = new Set(schema.required || []);
|
|
36
|
-
writer.write("{").newLine();
|
|
37
|
-
for (const [propName, propSchema] of Object.entries(
|
|
38
|
-
schema.properties || {}
|
|
39
|
-
)) {
|
|
40
|
-
writer.indent(() => {
|
|
41
|
-
writer.write(propName);
|
|
42
|
-
if (!requiredProps.has(propName)) {
|
|
43
|
-
writer.write("?");
|
|
44
|
-
}
|
|
45
|
-
writer.write(": ");
|
|
46
|
-
schemaToTypeLiteral(propSchema, doc, refPrefix)(writer);
|
|
47
|
-
writer.write(";").newLine();
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
writer.write("}");
|
|
51
|
-
break;
|
|
52
|
-
}
|
|
53
96
|
case "array":
|
|
54
97
|
if (Array.isArray(schema.items)) {
|
|
55
98
|
writer.write("[");
|
|
@@ -118,12 +161,33 @@ var jsonSchemaTags = [
|
|
|
118
161
|
"format",
|
|
119
162
|
"example"
|
|
120
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
|
+
}
|
|
121
185
|
function createInterfaceModel(doc, file, name, schema) {
|
|
122
186
|
if (!schema.properties || Object.keys(schema.properties).length === 0) {
|
|
123
187
|
file.addTypeAlias({
|
|
124
188
|
name: pascalCase(name),
|
|
125
189
|
isExported: true,
|
|
126
|
-
type: "{}"
|
|
190
|
+
type: schema.additionalProperties ? "Record<string, unknown>" : "{}"
|
|
127
191
|
});
|
|
128
192
|
return;
|
|
129
193
|
}
|
|
@@ -162,12 +226,23 @@ function generateSchemaDocs(schema, doc) {
|
|
|
162
226
|
});
|
|
163
227
|
}
|
|
164
228
|
for (const tagName of jsonSchemaTags) {
|
|
229
|
+
if (tagName === "pattern" && shouldIgnorePattern(schema)) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
165
232
|
const value = schema[tagName];
|
|
166
233
|
if (value !== void 0) {
|
|
167
234
|
jsdoc.tags?.push({
|
|
168
235
|
tagName,
|
|
169
236
|
text: String(value)
|
|
170
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
|
+
}
|
|
171
246
|
}
|
|
172
247
|
}
|
|
173
248
|
if (!jsdoc.tags?.length && !jsdoc.description) {
|
|
@@ -184,10 +259,7 @@ function generatePropertySignature(doc, propName, propSchema) {
|
|
|
184
259
|
};
|
|
185
260
|
}
|
|
186
261
|
function createModel(doc, file, name, schema) {
|
|
187
|
-
if ("$ref" in schema) {
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
if (schema.type === "object" && schema.properties) {
|
|
262
|
+
if (!("$ref" in schema) && (schema.type === "object" || !schema.type && schema.properties) && schema.properties) {
|
|
191
263
|
createInterfaceModel(doc, file, name, schema);
|
|
192
264
|
return;
|
|
193
265
|
}
|
|
@@ -199,19 +271,49 @@ function createModel(doc, file, name, schema) {
|
|
|
199
271
|
docs: jsdoc ? [jsdoc] : []
|
|
200
272
|
});
|
|
201
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
|
+
}
|
|
202
288
|
async function generateModels(file, config, doc) {
|
|
203
289
|
const schemas = doc?.components?.schemas;
|
|
204
|
-
if (
|
|
205
|
-
|
|
290
|
+
if (schemas) {
|
|
291
|
+
for (const [schemaName, schema] of Object.entries(schemas)) {
|
|
292
|
+
createModel(doc, file, schemaName, schema);
|
|
293
|
+
}
|
|
206
294
|
}
|
|
207
|
-
|
|
208
|
-
|
|
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
|
+
}
|
|
209
311
|
}
|
|
210
312
|
}
|
|
211
313
|
|
|
212
314
|
// src/index.ts
|
|
213
315
|
var import_promises = require("fs/promises");
|
|
214
|
-
var
|
|
316
|
+
var import_ts_morph2 = require("ts-morph");
|
|
215
317
|
var import_node_path = require("path");
|
|
216
318
|
|
|
217
319
|
// src/generators/typescript/api_generator.ts
|
|
@@ -339,28 +441,50 @@ async function generateApiType(file, config, doc) {
|
|
|
339
441
|
// src/index.ts
|
|
340
442
|
var import_commander = require("commander");
|
|
341
443
|
var program = new import_commander.Command();
|
|
342
|
-
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) => {
|
|
343
445
|
const config = {
|
|
344
446
|
url: options.url,
|
|
345
447
|
api_base: options.base,
|
|
346
448
|
outputDir: options.output
|
|
347
449
|
};
|
|
348
450
|
try {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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);
|
|
353
463
|
}
|
|
354
|
-
const openapi = await res.json();
|
|
355
464
|
console.log(`Generating SDK to: ${config.outputDir}...`);
|
|
356
465
|
await (0, import_promises.mkdir)(config.outputDir, { recursive: true });
|
|
357
|
-
const project = new
|
|
466
|
+
const project = new import_ts_morph2.Project();
|
|
358
467
|
const modelsFile = project.createSourceFile((0, import_node_path.join)(config.outputDir, "models.ts"), "", { overwrite: true });
|
|
359
468
|
const apiFile = project.createSourceFile((0, import_node_path.join)(config.outputDir, "api.ts"), "", { overwrite: true });
|
|
360
|
-
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
|
+
}
|
|
361
485
|
const coreFile = project.createSourceFile(
|
|
362
486
|
(0, import_node_path.join)(config.outputDir, "core.ts"),
|
|
363
|
-
|
|
487
|
+
templateContent,
|
|
364
488
|
{ overwrite: true }
|
|
365
489
|
);
|
|
366
490
|
await generateModels(modelsFile, config, openapi);
|
package/dist/index.mjs
CHANGED
|
@@ -9,17 +9,15 @@ 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) {
|
|
@@ -28,34 +26,79 @@ function schemaToTypeLiteral(schema, doc, refPrefix) {
|
|
|
28
26
|
writer.write(
|
|
29
27
|
schema.enum.map((value) => JSON.stringify(value)).join(" | ")
|
|
30
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
|
+
}
|
|
31
47
|
} else if (schema.anyOf || schema.oneOf) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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);
|
|
35
56
|
}
|
|
36
|
-
|
|
37
|
-
|
|
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
|
+
}
|
|
38
100
|
} else if (schema.type) {
|
|
39
101
|
switch (schema.type) {
|
|
40
|
-
case "object": {
|
|
41
|
-
const requiredProps = new Set(schema.required || []);
|
|
42
|
-
writer.write("{").newLine();
|
|
43
|
-
for (const [propName, propSchema] of Object.entries(
|
|
44
|
-
schema.properties || {}
|
|
45
|
-
)) {
|
|
46
|
-
writer.indent(() => {
|
|
47
|
-
writer.write(propName);
|
|
48
|
-
if (!requiredProps.has(propName)) {
|
|
49
|
-
writer.write("?");
|
|
50
|
-
}
|
|
51
|
-
writer.write(": ");
|
|
52
|
-
schemaToTypeLiteral(propSchema, doc, refPrefix)(writer);
|
|
53
|
-
writer.write(";").newLine();
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
writer.write("}");
|
|
57
|
-
break;
|
|
58
|
-
}
|
|
59
102
|
case "array":
|
|
60
103
|
if (Array.isArray(schema.items)) {
|
|
61
104
|
writer.write("[");
|
|
@@ -124,12 +167,33 @@ var jsonSchemaTags = [
|
|
|
124
167
|
"format",
|
|
125
168
|
"example"
|
|
126
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
|
+
}
|
|
127
191
|
function createInterfaceModel(doc, file, name, schema) {
|
|
128
192
|
if (!schema.properties || Object.keys(schema.properties).length === 0) {
|
|
129
193
|
file.addTypeAlias({
|
|
130
194
|
name: pascalCase(name),
|
|
131
195
|
isExported: true,
|
|
132
|
-
type: "{}"
|
|
196
|
+
type: schema.additionalProperties ? "Record<string, unknown>" : "{}"
|
|
133
197
|
});
|
|
134
198
|
return;
|
|
135
199
|
}
|
|
@@ -168,12 +232,23 @@ function generateSchemaDocs(schema, doc) {
|
|
|
168
232
|
});
|
|
169
233
|
}
|
|
170
234
|
for (const tagName of jsonSchemaTags) {
|
|
235
|
+
if (tagName === "pattern" && shouldIgnorePattern(schema)) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
171
238
|
const value = schema[tagName];
|
|
172
239
|
if (value !== void 0) {
|
|
173
240
|
jsdoc.tags?.push({
|
|
174
241
|
tagName,
|
|
175
242
|
text: String(value)
|
|
176
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
|
+
}
|
|
177
252
|
}
|
|
178
253
|
}
|
|
179
254
|
if (!jsdoc.tags?.length && !jsdoc.description) {
|
|
@@ -190,10 +265,7 @@ function generatePropertySignature(doc, propName, propSchema) {
|
|
|
190
265
|
};
|
|
191
266
|
}
|
|
192
267
|
function createModel(doc, file, name, schema) {
|
|
193
|
-
if ("$ref" in schema) {
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
if (schema.type === "object" && schema.properties) {
|
|
268
|
+
if (!("$ref" in schema) && (schema.type === "object" || !schema.type && schema.properties) && schema.properties) {
|
|
197
269
|
createInterfaceModel(doc, file, name, schema);
|
|
198
270
|
return;
|
|
199
271
|
}
|
|
@@ -205,13 +277,43 @@ function createModel(doc, file, name, schema) {
|
|
|
205
277
|
docs: jsdoc ? [jsdoc] : []
|
|
206
278
|
});
|
|
207
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
|
+
}
|
|
208
294
|
async function generateModels(file, config, doc) {
|
|
209
295
|
const schemas = doc?.components?.schemas;
|
|
210
|
-
if (
|
|
211
|
-
|
|
296
|
+
if (schemas) {
|
|
297
|
+
for (const [schemaName, schema] of Object.entries(schemas)) {
|
|
298
|
+
createModel(doc, file, schemaName, schema);
|
|
299
|
+
}
|
|
212
300
|
}
|
|
213
|
-
|
|
214
|
-
|
|
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
|
+
}
|
|
215
317
|
}
|
|
216
318
|
}
|
|
217
319
|
|
|
@@ -345,28 +447,50 @@ async function generateApiType(file, config, doc) {
|
|
|
345
447
|
// src/index.ts
|
|
346
448
|
import { Command } from "commander";
|
|
347
449
|
var program = new Command();
|
|
348
|
-
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) => {
|
|
349
451
|
const config = {
|
|
350
452
|
url: options.url,
|
|
351
453
|
api_base: options.base,
|
|
352
454
|
outputDir: options.output
|
|
353
455
|
};
|
|
354
456
|
try {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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);
|
|
359
469
|
}
|
|
360
|
-
const openapi = await res.json();
|
|
361
470
|
console.log(`Generating SDK to: ${config.outputDir}...`);
|
|
362
471
|
await mkdir(config.outputDir, { recursive: true });
|
|
363
472
|
const project = new Project();
|
|
364
473
|
const modelsFile = project.createSourceFile(join(config.outputDir, "models.ts"), "", { overwrite: true });
|
|
365
474
|
const apiFile = project.createSourceFile(join(config.outputDir, "api.ts"), "", { overwrite: true });
|
|
366
|
-
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
|
+
}
|
|
367
491
|
const coreFile = project.createSourceFile(
|
|
368
492
|
join(config.outputDir, "core.ts"),
|
|
369
|
-
|
|
493
|
+
templateContent,
|
|
370
494
|
{ overwrite: true }
|
|
371
495
|
);
|
|
372
496
|
await generateModels(modelsFile, config, openapi);
|
package/package.json
CHANGED
package/templates/core.ts
CHANGED
|
@@ -27,5 +27,16 @@ type Pretty<T> = {
|
|
|
27
27
|
[K in keyof T]: T[K];
|
|
28
28
|
};
|
|
29
29
|
|
|
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<
|
|
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']
|
|
37
|
+
: never
|
|
38
|
+
> = Api[Path][Method] extends { responses: infer R }
|
|
39
|
+
? Status extends keyof R
|
|
40
|
+
? R[Status]
|
|
41
|
+
: never
|
|
42
|
+
: never;
|