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 CHANGED
@@ -3,49 +3,96 @@
3
3
 
4
4
  // src/generators/typescript/util.ts
5
5
  function pascalCase(str) {
6
- return str.replace(/(^\w|-\w)/g, clearAndUpper).replace(/_/g, "");
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(schema.enum.map((value) => JSON.stringify(value)).join(" | "));
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
- (schema.anyOf || schema.oneOf)?.forEach((subSchema, index) => {
25
- if (index > 0) {
26
- writer.write(" | ");
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
- schemaToTypeLiteral(subSchema, doc, refPrefix)(writer);
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
- } else {
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 (!schemas) {
197
- return;
290
+ if (schemas) {
291
+ for (const [schemaName, schema] of Object.entries(schemas)) {
292
+ createModel(doc, file, schemaName, schema);
293
+ }
198
294
  }
199
- for (const [schemaName, schema] of Object.entries(schemas)) {
200
- createModel(doc, file, schemaName, schema);
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 import_ts_morph = require("ts-morph");
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").requiredOption("-b, --base <url>", "Base URL for the API calls").option("-o, --output <dir>", "Output directory", "./output").action(async (options) => {
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
- console.log(`Fetching OpenAPI spec from: ${config.url}...`);
342
- const res = await fetch(config.url);
343
- if (!res.ok) {
344
- throw new Error(`Failed to fetch OpenAPI spec: ${res.statusText}`);
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 import_ts_morph.Project();
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 templatePath = (0, import_node_path.join)(__dirname, "../templates", "core.ts");
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
- await (0, import_promises.readFile)(templatePath, "utf-8"),
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(/(^\w|-\w)/g, clearAndUpper).replace(/_/g, "");
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(schema.enum.map((value) => JSON.stringify(value)).join(" | "));
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
- (schema.anyOf || schema.oneOf)?.forEach((subSchema, index) => {
31
- if (index > 0) {
32
- writer.write(" | ");
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
- schemaToTypeLiteral(subSchema, doc, refPrefix)(writer);
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
- } else {
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 (!schemas) {
203
- return;
296
+ if (schemas) {
297
+ for (const [schemaName, schema] of Object.entries(schemas)) {
298
+ createModel(doc, file, schemaName, schema);
299
+ }
204
300
  }
205
- for (const [schemaName, schema] of Object.entries(schemas)) {
206
- createModel(doc, file, schemaName, schema);
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").requiredOption("-b, --base <url>", "Base URL for the API calls").option("-o, --output <dir>", "Output directory", "./output").action(async (options) => {
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
- console.log(`Fetching OpenAPI spec from: ${config.url}...`);
348
- const res = await fetch(config.url);
349
- if (!res.ok) {
350
- throw new Error(`Failed to fetch OpenAPI spec: ${res.statusText}`);
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 templatePath = join(__dirname, "../templates", "core.ts");
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
- await readFile(templatePath, "utf-8"),
493
+ templateContent,
362
494
  { overwrite: true }
363
495
  );
364
496
  await generateModels(modelsFile, config, openapi);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "api-sdk-gen",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -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 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 }
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
- }[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
- }
38
+ > = Api[Path][Method] extends { responses: infer R }
39
+ ? Status extends keyof R
40
+ ? R[Status]
41
+ : never
42
+ : never;