jszy-swagger-doc-generator 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +327 -113
- package/dist/cli.js +34 -1
- package/dist/cli.js.map +1 -1
- package/dist/generators/swagger.generator.d.ts +90 -0
- package/dist/generators/swagger.generator.js +626 -0
- package/dist/generators/swagger.generator.js.map +1 -0
- package/dist/helpers/handlebars.helpers.d.ts +4 -0
- package/dist/helpers/handlebars.helpers.js +92 -0
- package/dist/helpers/handlebars.helpers.js.map +1 -0
- package/dist/helpers/string.helpers.d.ts +8 -0
- package/dist/helpers/string.helpers.js +25 -0
- package/dist/helpers/string.helpers.js.map +1 -0
- package/dist/helpers/template.helpers.d.ts +4 -0
- package/dist/helpers/template.helpers.js +105 -0
- package/dist/helpers/template.helpers.js.map +1 -0
- package/dist/helpers/type.helpers.d.ts +18 -0
- package/dist/helpers/type.helpers.js +229 -0
- package/dist/helpers/type.helpers.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +432 -159
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +97 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +1 -1
- package/src/cli.ts +35 -1
- package/src/generators/swagger.generator.ts +664 -0
- package/src/helpers/template.helpers.ts +72 -0
- package/src/helpers/type.helpers.ts +232 -0
- package/src/index.ts +465 -162
- package/src/types.ts +98 -0
- package/templates/hooks/individual-hook.hbs +55 -0
- package/templates/hooks/react-hook.hbs +14 -0
- package/templates/types/type-definition.hbs +5 -0
- package/test-openapi-swagger.json +454 -0
package/dist/index.js
CHANGED
|
@@ -40,6 +40,8 @@ exports.SwaggerDocGenerator = void 0;
|
|
|
40
40
|
const axios_1 = __importDefault(require("axios"));
|
|
41
41
|
const fs = __importStar(require("fs"));
|
|
42
42
|
const path = __importStar(require("path"));
|
|
43
|
+
const template_helpers_1 = require("./helpers/template.helpers");
|
|
44
|
+
const type_helpers_1 = require("./helpers/type.helpers");
|
|
43
45
|
/**
|
|
44
46
|
* Transforms a string to PascalCase
|
|
45
47
|
*/
|
|
@@ -60,76 +62,27 @@ function toCamelCase(str) {
|
|
|
60
62
|
})
|
|
61
63
|
.replace(/\s+/g, '');
|
|
62
64
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
return
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
// Handle allOf (used for composition/references)
|
|
75
|
-
if (typeDef.allOf && Array.isArray(typeDef.allOf)) {
|
|
76
|
-
return typeDef.allOf.map((item) => {
|
|
77
|
-
if (item.$ref) {
|
|
78
|
-
return item.$ref.split('/').pop();
|
|
79
|
-
}
|
|
80
|
-
else if (item.type) {
|
|
81
|
-
return convertTypeToTs(item, schemaComponents);
|
|
82
|
-
}
|
|
83
|
-
return 'any';
|
|
84
|
-
}).filter(Boolean).join(' & ') || 'any';
|
|
85
|
-
}
|
|
86
|
-
if (Array.isArray(typeDef.type)) {
|
|
87
|
-
// Handle union types like ["string", "null"]
|
|
88
|
-
if (typeDef.type.includes('null')) {
|
|
89
|
-
const nonNullType = typeDef.type.find((t) => t !== 'null');
|
|
90
|
-
return `${convertTypeToTs({ ...typeDef, type: nonNullType }, schemaComponents)} | null`;
|
|
91
|
-
}
|
|
92
|
-
return 'any';
|
|
65
|
+
class SwaggerDocGenerator {
|
|
66
|
+
/**
|
|
67
|
+
* Transforms a string to PascalCase
|
|
68
|
+
*/
|
|
69
|
+
toPascalCase(str) {
|
|
70
|
+
return str
|
|
71
|
+
.replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) => {
|
|
72
|
+
return index === 0 ? word.toUpperCase() : word.toUpperCase();
|
|
73
|
+
})
|
|
74
|
+
.replace(/\s+/g, '');
|
|
93
75
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
case 'integer':
|
|
104
|
-
case 'number':
|
|
105
|
-
return 'number';
|
|
106
|
-
case 'boolean':
|
|
107
|
-
return 'boolean';
|
|
108
|
-
case 'array':
|
|
109
|
-
if (typeDef.items) {
|
|
110
|
-
return `${convertTypeToTs(typeDef.items, schemaComponents)}[]`;
|
|
111
|
-
}
|
|
112
|
-
return 'any[]';
|
|
113
|
-
case 'object':
|
|
114
|
-
if (typeDef.properties) {
|
|
115
|
-
// Inline object definition
|
|
116
|
-
const fields = Object.entries(typeDef.properties)
|
|
117
|
-
.map(([propName, propSchema]) => {
|
|
118
|
-
const required = typeDef.required && typeDef.required.includes(propName);
|
|
119
|
-
const optional = !required ? '?' : '';
|
|
120
|
-
return ` ${propName}${optional}: ${convertTypeToTs(propSchema, schemaComponents)};`;
|
|
121
|
-
})
|
|
122
|
-
.join('\n');
|
|
123
|
-
return `{\n${fields}\n}`;
|
|
124
|
-
}
|
|
125
|
-
return 'Record<string, any>';
|
|
126
|
-
case 'null':
|
|
127
|
-
return 'null';
|
|
128
|
-
default:
|
|
129
|
-
return 'any';
|
|
76
|
+
/**
|
|
77
|
+
* Transforms a string to camelCase
|
|
78
|
+
*/
|
|
79
|
+
toCamelCase(str) {
|
|
80
|
+
return str
|
|
81
|
+
.replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) => {
|
|
82
|
+
return index === 0 ? word.toLowerCase() : word.toUpperCase();
|
|
83
|
+
})
|
|
84
|
+
.replace(/\s+/g, '');
|
|
130
85
|
}
|
|
131
|
-
}
|
|
132
|
-
class SwaggerDocGenerator {
|
|
133
86
|
/**
|
|
134
87
|
* Fetches the Swagger/OpenAPI JSON from a given URL
|
|
135
88
|
*/
|
|
@@ -242,6 +195,149 @@ class SwaggerDocGenerator {
|
|
|
242
195
|
});
|
|
243
196
|
return typeDefs;
|
|
244
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* Generates React hooks from the paths in Swagger doc organized by tag
|
|
200
|
+
*/
|
|
201
|
+
/**
|
|
202
|
+
* Converts OpenAPI types to TypeScript types
|
|
203
|
+
*/
|
|
204
|
+
convertTypeToTs(typeDef, schemaComponents) {
|
|
205
|
+
if (!typeDef)
|
|
206
|
+
return 'any';
|
|
207
|
+
if (typeDef.$ref) {
|
|
208
|
+
// Extract the type name from the reference
|
|
209
|
+
const refTypeName = typeDef.$ref.split('/').pop();
|
|
210
|
+
return refTypeName || 'any';
|
|
211
|
+
}
|
|
212
|
+
// Handle allOf (used for composition/references) - combine all properties
|
|
213
|
+
if (typeDef.allOf && Array.isArray(typeDef.allOf)) {
|
|
214
|
+
const combinedProperties = {};
|
|
215
|
+
const refTypes = [];
|
|
216
|
+
for (const item of typeDef.allOf) {
|
|
217
|
+
if (item.$ref) {
|
|
218
|
+
// Extract the type name from the reference
|
|
219
|
+
const refTypeName = item.$ref.split('/').pop();
|
|
220
|
+
if (refTypeName) {
|
|
221
|
+
refTypes.push(refTypeName);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
else if (item.type === 'object' && item.properties) {
|
|
225
|
+
// Combine properties from inline object definitions
|
|
226
|
+
Object.assign(combinedProperties, item.properties);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (refTypes.length > 0 && Object.keys(combinedProperties).length > 0) {
|
|
230
|
+
// We have both references and inline properties
|
|
231
|
+
const inlineDef = {
|
|
232
|
+
type: 'object',
|
|
233
|
+
properties: combinedProperties,
|
|
234
|
+
required: typeDef.required
|
|
235
|
+
};
|
|
236
|
+
const inlineType = this.convertTypeToTs(inlineDef, schemaComponents);
|
|
237
|
+
return `${refTypes.join(' & ')} & ${inlineType}`;
|
|
238
|
+
}
|
|
239
|
+
else if (refTypes.length > 0) {
|
|
240
|
+
// Only references
|
|
241
|
+
return refTypes.join(' & ');
|
|
242
|
+
}
|
|
243
|
+
else if (Object.keys(combinedProperties).length > 0) {
|
|
244
|
+
// Only inline properties
|
|
245
|
+
return this.convertTypeToTs({
|
|
246
|
+
type: 'object',
|
|
247
|
+
properties: combinedProperties,
|
|
248
|
+
required: typeDef.required
|
|
249
|
+
}, schemaComponents);
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
return 'any';
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// Handle oneOf (union types)
|
|
256
|
+
if (typeDef.oneOf && Array.isArray(typeDef.oneOf)) {
|
|
257
|
+
return typeDef.oneOf.map((item) => {
|
|
258
|
+
if (item.$ref) {
|
|
259
|
+
return item.$ref.split('/').pop();
|
|
260
|
+
}
|
|
261
|
+
else if (item.type) {
|
|
262
|
+
return this.convertTypeToTs(item, schemaComponents);
|
|
263
|
+
}
|
|
264
|
+
return 'any';
|
|
265
|
+
}).filter(Boolean).join(' | ') || 'any';
|
|
266
|
+
}
|
|
267
|
+
// Handle anyOf (union types)
|
|
268
|
+
if (typeDef.anyOf && Array.isArray(typeDef.anyOf)) {
|
|
269
|
+
return typeDef.anyOf.map((item) => {
|
|
270
|
+
if (item.$ref) {
|
|
271
|
+
return item.$ref.split('/').pop();
|
|
272
|
+
}
|
|
273
|
+
else if (item.type) {
|
|
274
|
+
return this.convertTypeToTs(item, schemaComponents);
|
|
275
|
+
}
|
|
276
|
+
return 'any';
|
|
277
|
+
}).filter(Boolean).join(' | ') || 'any';
|
|
278
|
+
}
|
|
279
|
+
if (Array.isArray(typeDef.type)) {
|
|
280
|
+
// Handle union types like ["string", "null"]
|
|
281
|
+
if (typeDef.type.includes('null')) {
|
|
282
|
+
const nonNullTypes = typeDef.type.filter((t) => t !== 'null');
|
|
283
|
+
if (nonNullTypes.length === 1) {
|
|
284
|
+
return `${this.convertTypeToTs({ ...typeDef, type: nonNullTypes[0] }, schemaComponents)} | null`;
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
// Handle complex union types with null
|
|
288
|
+
const nonNullTypeStr = nonNullTypes
|
|
289
|
+
.map((t) => this.convertTypeToTs({ ...typeDef, type: t }, schemaComponents))
|
|
290
|
+
.join(' | ');
|
|
291
|
+
return `${nonNullTypeStr} | null`;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// Handle other array type unions
|
|
295
|
+
return typeDef.type
|
|
296
|
+
.map((t) => this.convertTypeToTs({ ...typeDef, type: t }, schemaComponents))
|
|
297
|
+
.join(' | ') || 'any';
|
|
298
|
+
}
|
|
299
|
+
switch (typeDef.type) {
|
|
300
|
+
case 'string':
|
|
301
|
+
if (typeDef.enum) {
|
|
302
|
+
return `"${typeDef.enum.join('" | "')}"`;
|
|
303
|
+
}
|
|
304
|
+
if (typeDef.format === 'date' || typeDef.format === 'date-time') {
|
|
305
|
+
return 'string';
|
|
306
|
+
}
|
|
307
|
+
return 'string';
|
|
308
|
+
case 'integer':
|
|
309
|
+
case 'number':
|
|
310
|
+
return 'number';
|
|
311
|
+
case 'boolean':
|
|
312
|
+
return 'boolean';
|
|
313
|
+
case 'array':
|
|
314
|
+
if (typeDef.items) {
|
|
315
|
+
return `${this.convertTypeToTs(typeDef.items, schemaComponents)}[]`;
|
|
316
|
+
}
|
|
317
|
+
return 'any[]';
|
|
318
|
+
case 'object':
|
|
319
|
+
if (typeDef.properties) {
|
|
320
|
+
// Inline object definition
|
|
321
|
+
const fields = Object.entries(typeDef.properties)
|
|
322
|
+
.map(([propName, propSchema]) => {
|
|
323
|
+
const required = typeDef.required && typeDef.required.includes(propName);
|
|
324
|
+
const optional = !required ? '?' : '';
|
|
325
|
+
const type = this.convertTypeToTs(propSchema, schemaComponents);
|
|
326
|
+
// Get the description for JSDoc if available
|
|
327
|
+
const propDescription = propSchema.description || propSchema.title;
|
|
328
|
+
const jsDoc = propDescription ? ` /** ${propDescription} */\n` : '';
|
|
329
|
+
return `${jsDoc} ${propName}${optional}: ${type};`;
|
|
330
|
+
})
|
|
331
|
+
.join('\n');
|
|
332
|
+
return `{\n${fields}\n }`;
|
|
333
|
+
}
|
|
334
|
+
return 'Record<string, any>';
|
|
335
|
+
case 'null':
|
|
336
|
+
return 'null';
|
|
337
|
+
default:
|
|
338
|
+
return 'any';
|
|
339
|
+
}
|
|
340
|
+
}
|
|
245
341
|
/**
|
|
246
342
|
* Generates a single TypeScript type definition
|
|
247
343
|
*/
|
|
@@ -250,15 +346,15 @@ class SwaggerDocGenerator {
|
|
|
250
346
|
// Enum type
|
|
251
347
|
return `export type ${typeName} = ${schema.enum.map((val) => `'${val}'`).join(' | ')};\n`;
|
|
252
348
|
}
|
|
253
|
-
if (schema.oneOf || schema.anyOf
|
|
254
|
-
// Union type or complex type
|
|
255
|
-
const typeOption = schema.oneOf ? 'oneOf' :
|
|
349
|
+
if (schema.oneOf || schema.anyOf) {
|
|
350
|
+
// Union type or complex type (oneOf/anyOf)
|
|
351
|
+
const typeOption = schema.oneOf ? 'oneOf' : 'anyOf';
|
|
256
352
|
const types = schema[typeOption].map((item) => {
|
|
257
353
|
if (item.$ref) {
|
|
258
354
|
return item.$ref.split('/').pop();
|
|
259
355
|
}
|
|
260
356
|
else if (item.type) {
|
|
261
|
-
return convertTypeToTs(item, allSchemas);
|
|
357
|
+
return this.convertTypeToTs(item, allSchemas);
|
|
262
358
|
}
|
|
263
359
|
else {
|
|
264
360
|
return 'any';
|
|
@@ -266,6 +362,24 @@ class SwaggerDocGenerator {
|
|
|
266
362
|
}).filter(Boolean);
|
|
267
363
|
return `export type ${typeName} = ${types.join(' | ')};\n`;
|
|
268
364
|
}
|
|
365
|
+
if (schema.allOf) {
|
|
366
|
+
// Handle allOf - composition of multiple schemas
|
|
367
|
+
const allParts = [];
|
|
368
|
+
for (const part of schema.allOf) {
|
|
369
|
+
if (part.$ref) {
|
|
370
|
+
const refTypeName = part.$ref.split('/').pop();
|
|
371
|
+
if (refTypeName) {
|
|
372
|
+
allParts.push(refTypeName);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
else if (part.type === 'object' && part.properties) {
|
|
376
|
+
// Create a temporary interface for inline object
|
|
377
|
+
const inlineInterface = this.generateInlineObjectInterface(part, `${typeName}Inline`, allSchemas);
|
|
378
|
+
allParts.push(inlineInterface);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return `export type ${typeName} = ${allParts.join(' & ')};\n`;
|
|
382
|
+
}
|
|
269
383
|
if (schema.type === 'object') {
|
|
270
384
|
// Object type
|
|
271
385
|
let result = `export interface ${typeName} {\n`;
|
|
@@ -273,18 +387,39 @@ class SwaggerDocGenerator {
|
|
|
273
387
|
Object.entries(schema.properties).forEach(([propName, propSchema]) => {
|
|
274
388
|
const required = schema.required && schema.required.includes(propName);
|
|
275
389
|
const optional = !required ? '?' : '';
|
|
276
|
-
// Add JSDoc comment if available
|
|
277
|
-
|
|
278
|
-
|
|
390
|
+
// Add JSDoc comment if available (using title or description)
|
|
391
|
+
const jsDoc = propSchema.title || propSchema.description;
|
|
392
|
+
if (jsDoc) {
|
|
393
|
+
result += ` /** ${jsDoc} */\n`;
|
|
279
394
|
}
|
|
280
|
-
result += ` ${propName}${optional}: ${convertTypeToTs(propSchema, allSchemas)};\n`;
|
|
395
|
+
result += ` ${propName}${optional}: ${this.convertTypeToTs(propSchema, allSchemas)};\n`;
|
|
281
396
|
});
|
|
282
397
|
}
|
|
283
398
|
result += '}\n';
|
|
284
399
|
return result;
|
|
285
400
|
}
|
|
286
401
|
// For other types (string, number, etc.) that might have additional properties
|
|
287
|
-
return `export type ${typeName} = ${convertTypeToTs(schema, allSchemas)};\n`;
|
|
402
|
+
return `export type ${typeName} = ${this.convertTypeToTs(schema, allSchemas)};\n`;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Generates an inline object interface for allOf composition
|
|
406
|
+
*/
|
|
407
|
+
generateInlineObjectInterface(schema, tempName, allSchemas) {
|
|
408
|
+
if (!schema.properties)
|
|
409
|
+
return 'any';
|
|
410
|
+
let result = '{\n';
|
|
411
|
+
Object.entries(schema.properties).forEach(([propName, propSchema]) => {
|
|
412
|
+
const required = schema.required && schema.required.includes(propName);
|
|
413
|
+
const optional = !required ? '?' : '';
|
|
414
|
+
// Add JSDoc comment if available (using title or description)
|
|
415
|
+
const jsDoc = propSchema.title || propSchema.description;
|
|
416
|
+
if (jsDoc) {
|
|
417
|
+
result += ` /** ${jsDoc} */\n`;
|
|
418
|
+
}
|
|
419
|
+
result += ` ${propName}${optional}: ${this.convertTypeToTs(propSchema, allSchemas)};\n`;
|
|
420
|
+
});
|
|
421
|
+
result += ' }';
|
|
422
|
+
return result;
|
|
288
423
|
}
|
|
289
424
|
/**
|
|
290
425
|
* Generates React hooks from the paths in Swagger doc organized by tag
|
|
@@ -557,19 +692,34 @@ class SwaggerDocGenerator {
|
|
|
557
692
|
}
|
|
558
693
|
// Create a unique interface name based on the operation ID
|
|
559
694
|
const operationId = endpointInfo.operationId || this.generateOperationId(path, method);
|
|
560
|
-
|
|
695
|
+
// Extract action name from operationId to create cleaner parameter interface names
|
|
696
|
+
// e.g. configController_updateConfig -> UpdateConfigParams instead of ConfigController_updateConfigParams
|
|
697
|
+
let interfaceName;
|
|
698
|
+
if (operationId.includes('_')) {
|
|
699
|
+
const parts = operationId.split('_');
|
|
700
|
+
if (parts.length >= 2) {
|
|
701
|
+
// Use just the action part in the interface name
|
|
702
|
+
interfaceName = `${toPascalCase(parts[parts.length - 1])}Params`;
|
|
703
|
+
}
|
|
704
|
+
else {
|
|
705
|
+
interfaceName = `${toPascalCase(operationId)}Params`;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
else {
|
|
709
|
+
interfaceName = `${toPascalCase(operationId)}Params`;
|
|
710
|
+
}
|
|
561
711
|
let paramsInterface = `export interface ${interfaceName} {\n`;
|
|
562
712
|
// Add path parameters
|
|
563
713
|
pathParams.forEach((param) => {
|
|
564
714
|
const required = param.required ? '' : '?';
|
|
565
|
-
const type = convertTypeToTs(param.schema || {}, schemas);
|
|
566
|
-
paramsInterface += ` ${toCamelCase(param.name)}${required}: ${type};\n`;
|
|
715
|
+
const type = this.convertTypeToTs(param.schema || {}, schemas);
|
|
716
|
+
paramsInterface += ` ${this.toCamelCase(param.name)}${required}: ${type};\n`;
|
|
567
717
|
});
|
|
568
718
|
// Add query parameters
|
|
569
719
|
queryParams.forEach((param) => {
|
|
570
720
|
const required = param.required ? '' : '?';
|
|
571
|
-
const type = convertTypeToTs(param.schema || {}, schemas);
|
|
572
|
-
paramsInterface += ` ${toCamelCase(param.name)}${required}: ${type};\n`;
|
|
721
|
+
const type = this.convertTypeToTs(param.schema || {}, schemas);
|
|
722
|
+
paramsInterface += ` ${this.toCamelCase(param.name)}${required}: ${type};\n`;
|
|
573
723
|
});
|
|
574
724
|
paramsInterface += '}\n';
|
|
575
725
|
return paramsInterface;
|
|
@@ -579,17 +729,38 @@ class SwaggerDocGenerator {
|
|
|
579
729
|
*/
|
|
580
730
|
generateReactQueryHook(path, method, endpointInfo, schemas) {
|
|
581
731
|
const operationId = endpointInfo.operationId || this.generateOperationId(path, method);
|
|
582
|
-
|
|
732
|
+
// Extract action name from operationId to create cleaner hook names
|
|
733
|
+
// e.g. configController_updateConfig -> useUpdateConfig instead of useConfigController_updateConfig
|
|
734
|
+
let hookName = `use${this.toPascalCase(operationId)}`;
|
|
735
|
+
// Check if operationId follows pattern controller_action and simplify to action
|
|
736
|
+
if (operationId.includes('_')) {
|
|
737
|
+
const parts = operationId.split('_');
|
|
738
|
+
if (parts.length >= 2) {
|
|
739
|
+
// Use just the action part as the hook name
|
|
740
|
+
hookName = `use${this.toPascalCase(parts[parts.length - 1])}`;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
else {
|
|
744
|
+
// For operationIds without underscores, keep the original naming
|
|
745
|
+
hookName = `use${this.toPascalCase(operationId)}`;
|
|
746
|
+
}
|
|
583
747
|
const hookType = method.toLowerCase() === 'get' ? 'useQuery' : 'useMutation';
|
|
584
748
|
// Use unique parameter interface name
|
|
585
749
|
const pathParams = endpointInfo.parameters?.filter((p) => p.in === 'path') || [];
|
|
586
750
|
const queryParams = endpointInfo.parameters?.filter((p) => p.in === 'query') || [];
|
|
587
|
-
// Determine response type
|
|
751
|
+
// Determine response type by checking common success response codes
|
|
588
752
|
let responseType = 'any';
|
|
589
|
-
if (endpointInfo.responses
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
753
|
+
if (endpointInfo.responses) {
|
|
754
|
+
// Check for success responses in order of preference: 200, 201, 204, etc.
|
|
755
|
+
const successCodes = ['200', '201', '204', '202', '203', '205'];
|
|
756
|
+
for (const code of successCodes) {
|
|
757
|
+
if (endpointInfo.responses[code]) {
|
|
758
|
+
const responseSchema = endpointInfo.responses[code].content?.['application/json']?.schema;
|
|
759
|
+
if (responseSchema) {
|
|
760
|
+
responseType = this.convertTypeToTs(responseSchema, schemas);
|
|
761
|
+
break; // Use the first success response found
|
|
762
|
+
}
|
|
763
|
+
}
|
|
593
764
|
}
|
|
594
765
|
}
|
|
595
766
|
// Generate request body parameter if needed
|
|
@@ -598,81 +769,39 @@ class SwaggerDocGenerator {
|
|
|
598
769
|
if (method.toLowerCase() !== 'get' && method.toLowerCase() !== 'delete' && endpointInfo.requestBody) {
|
|
599
770
|
const bodySchema = endpointInfo.requestBody.content?.['application/json']?.schema;
|
|
600
771
|
if (bodySchema) {
|
|
601
|
-
requestBodyType = convertTypeToTs(bodySchema, schemas);
|
|
772
|
+
requestBodyType = this.convertTypeToTs(bodySchema, schemas);
|
|
602
773
|
hasBody = true;
|
|
603
774
|
}
|
|
604
775
|
}
|
|
605
|
-
// Format the path for use in the code (handle path parameters)
|
|
606
|
-
const
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
hookCode += ` });\n`;
|
|
634
|
-
hookCode += `};\n`;
|
|
635
|
-
}
|
|
776
|
+
// Format the path for use in the code (handle path parameters) - without base URL
|
|
777
|
+
const formattedPath = path.replace(/{(\w+)}/g, (_, param) => `\${params.${this.toCamelCase(param)}}`);
|
|
778
|
+
// Prepare data for the template
|
|
779
|
+
const hookData = {
|
|
780
|
+
hookName: hookName,
|
|
781
|
+
operationId: operationId,
|
|
782
|
+
method: method.toLowerCase(),
|
|
783
|
+
responseType: responseType,
|
|
784
|
+
requestBodyType: requestBodyType,
|
|
785
|
+
hasParams: pathParams.length > 0 || queryParams.length > 0,
|
|
786
|
+
hasPathParams: pathParams.length > 0,
|
|
787
|
+
paramInterfaceName: `${hookName.replace('use', '')}Params`,
|
|
788
|
+
formattedPath: formattedPath,
|
|
789
|
+
isGetRequest: method.toLowerCase() === 'get'
|
|
790
|
+
};
|
|
791
|
+
// Load and compile the individual hook template
|
|
792
|
+
const fs = require('fs');
|
|
793
|
+
const pathModule = require('path');
|
|
794
|
+
const templatePath = pathModule.join(__dirname, '..', 'templates', 'hooks', 'individual-hook.hbs');
|
|
795
|
+
try {
|
|
796
|
+
const templateSource = fs.readFileSync(templatePath, 'utf8');
|
|
797
|
+
const Handlebars = require('handlebars');
|
|
798
|
+
const template = Handlebars.compile(templateSource);
|
|
799
|
+
return template(hookData);
|
|
800
|
+
}
|
|
801
|
+
catch (error) {
|
|
802
|
+
console.error(`Error reading template file: ${error.message}`);
|
|
803
|
+
return `// Error generating hook for ${operationId}: ${error.message}`;
|
|
636
804
|
}
|
|
637
|
-
else {
|
|
638
|
-
// For non-GET requests, use useMutation
|
|
639
|
-
const hasPathParams = pathParams.length > 0;
|
|
640
|
-
if (hasPathParams) {
|
|
641
|
-
const paramInterfaceName = `${toPascalCase(operationId)}Params`;
|
|
642
|
-
hookCode += `export const ${hookName} = () => {\n`;
|
|
643
|
-
hookCode += ` const queryClient = useQueryClient();\n\n`;
|
|
644
|
-
hookCode += ` return useMutation({\n`;
|
|
645
|
-
hookCode += ` mutationFn: async ({ params, data }: { params: ${paramInterfaceName}; data: ${requestBodyType} }) => {\n`;
|
|
646
|
-
// Format the path for use in the code (handle path parameters)
|
|
647
|
-
let formattedPath = path.replace(/{(\w+)}/g, (_, param) => `\${params.${toCamelCase(param)}}`);
|
|
648
|
-
const pathWithParams = `\`\${process.env.REACT_APP_API_BASE_URL || ''}${formattedPath}\``;
|
|
649
|
-
hookCode += ` const response = await axios.${method.toLowerCase()}<${responseType}>(${pathWithParams}, data);\n`;
|
|
650
|
-
hookCode += ` return response.data;\n`;
|
|
651
|
-
hookCode += ` },\n`;
|
|
652
|
-
hookCode += ` onSuccess: () => {\n`;
|
|
653
|
-
hookCode += ` // Invalidate and refetch related queries\n`;
|
|
654
|
-
hookCode += ` queryClient.invalidateQueries({ queryKey: ['${operationId}'] });\n`;
|
|
655
|
-
hookCode += ` },\n`;
|
|
656
|
-
hookCode += ` });\n`;
|
|
657
|
-
hookCode += `};\n`;
|
|
658
|
-
}
|
|
659
|
-
else {
|
|
660
|
-
hookCode += `export const ${hookName} = () => {\n`;
|
|
661
|
-
hookCode += ` const queryClient = useQueryClient();\n\n`;
|
|
662
|
-
hookCode += ` return useMutation({\n`;
|
|
663
|
-
hookCode += ` mutationFn: async (data: ${requestBodyType}) => {\n`;
|
|
664
|
-
hookCode += ` const response = await axios.${method.toLowerCase()}<${responseType}>(${axiosPath}, data);\n`;
|
|
665
|
-
hookCode += ` return response.data;\n`;
|
|
666
|
-
hookCode += ` },\n`;
|
|
667
|
-
hookCode += ` onSuccess: () => {\n`;
|
|
668
|
-
hookCode += ` // Invalidate and refetch related queries\n`;
|
|
669
|
-
hookCode += ` queryClient.invalidateQueries({ queryKey: ['${operationId}'] });\n`;
|
|
670
|
-
hookCode += ` },\n`;
|
|
671
|
-
hookCode += ` });\n`;
|
|
672
|
-
hookCode += `};\n`;
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
return hookCode;
|
|
676
805
|
}
|
|
677
806
|
/**
|
|
678
807
|
* Generate operation ID from path and method if not provided
|
|
@@ -780,6 +909,150 @@ class SwaggerDocGenerator {
|
|
|
780
909
|
}
|
|
781
910
|
}
|
|
782
911
|
}
|
|
912
|
+
/**
|
|
913
|
+
* Generates frontend resources using Handlebars templates
|
|
914
|
+
*/
|
|
915
|
+
generateHandlebarsResources(swaggerDoc, templatePaths = {}) {
|
|
916
|
+
const resourcesByTag = new Map();
|
|
917
|
+
const schemas = swaggerDoc.components?.schemas || {};
|
|
918
|
+
// Group endpoints by tag
|
|
919
|
+
const endpointsByTag = {};
|
|
920
|
+
Object.entries(swaggerDoc.paths).forEach(([path, methods]) => {
|
|
921
|
+
Object.entries(methods).forEach(([method, endpointInfo]) => {
|
|
922
|
+
// Determine the tag for this endpoint
|
|
923
|
+
const tag = (endpointInfo.tags && endpointInfo.tags[0]) ? endpointInfo.tags[0] : 'General';
|
|
924
|
+
if (!endpointsByTag[tag]) {
|
|
925
|
+
endpointsByTag[tag] = [];
|
|
926
|
+
}
|
|
927
|
+
endpointsByTag[tag].push({ path, method, endpointInfo });
|
|
928
|
+
});
|
|
929
|
+
});
|
|
930
|
+
// Generate resources for each tag
|
|
931
|
+
Object.entries(endpointsByTag).forEach(([tag, endpoints]) => {
|
|
932
|
+
// Prepare context for templates
|
|
933
|
+
const context = {
|
|
934
|
+
title: swaggerDoc.info.title,
|
|
935
|
+
description: swaggerDoc.info.description || swaggerDoc.info.title,
|
|
936
|
+
version: swaggerDoc.info.version,
|
|
937
|
+
tag: tag,
|
|
938
|
+
endpoints: endpoints.map(e => ({
|
|
939
|
+
path: e.path,
|
|
940
|
+
method: e.method.toUpperCase(),
|
|
941
|
+
operationId: e.endpointInfo.operationId || this.generateOperationId(e.path, e.method),
|
|
942
|
+
summary: e.endpointInfo.summary,
|
|
943
|
+
description: e.endpointInfo.description,
|
|
944
|
+
parameters: e.endpointInfo.parameters || [],
|
|
945
|
+
responses: e.endpointInfo.responses,
|
|
946
|
+
requestBody: e.endpointInfo.requestBody
|
|
947
|
+
})),
|
|
948
|
+
schemas: schemas,
|
|
949
|
+
hasImportTypes: false,
|
|
950
|
+
usedTypeNames: [],
|
|
951
|
+
paramInterfaces: [],
|
|
952
|
+
hooks: [],
|
|
953
|
+
typeDefinitions: []
|
|
954
|
+
};
|
|
955
|
+
// Find types used in this tag
|
|
956
|
+
const directlyUsedSchemas = new Set();
|
|
957
|
+
if (schemas) {
|
|
958
|
+
Object.entries(schemas).forEach(([typeName, schema]) => {
|
|
959
|
+
if (this.isSchemaUsedInEndpoints(typeName, endpoints, schemas)) {
|
|
960
|
+
directlyUsedSchemas.add(typeName);
|
|
961
|
+
}
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
const allNeededSchemas = this.findAllReferencedSchemas(directlyUsedSchemas, schemas);
|
|
965
|
+
// Generate TypeScript types
|
|
966
|
+
let typesContent = '';
|
|
967
|
+
if (schemas) {
|
|
968
|
+
for (const typeName of allNeededSchemas) {
|
|
969
|
+
const schema = schemas[typeName];
|
|
970
|
+
if (schema) {
|
|
971
|
+
const typeDef = (0, type_helpers_1.generateSingleTypeDefinition)(typeName, schema, schemas);
|
|
972
|
+
typesContent += typeDef + '\n';
|
|
973
|
+
context.typeDefinitions.push(typeDef);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
// Check if there are used types for import
|
|
978
|
+
if (allNeededSchemas.size > 0) {
|
|
979
|
+
context.hasImportTypes = true;
|
|
980
|
+
context.usedTypeNames = Array.from(allNeededSchemas);
|
|
981
|
+
}
|
|
982
|
+
// Generate parameter interfaces
|
|
983
|
+
const allParamInterfaces = [];
|
|
984
|
+
endpoints.forEach(({ path, method, endpointInfo }) => {
|
|
985
|
+
const paramInterface = this.generateParamInterface(path, method, endpointInfo, schemas);
|
|
986
|
+
if (paramInterface && !allParamInterfaces.includes(paramInterface)) {
|
|
987
|
+
allParamInterfaces.push(paramInterface);
|
|
988
|
+
}
|
|
989
|
+
});
|
|
990
|
+
context.paramInterfaces = allParamInterfaces;
|
|
991
|
+
// Generate individual hooks
|
|
992
|
+
const allHooks = [];
|
|
993
|
+
const endpointHookContents = [];
|
|
994
|
+
endpoints.forEach(({ path, method, endpointInfo }) => {
|
|
995
|
+
const hookContent = this.generateReactQueryHook(path, method, endpointInfo, schemas);
|
|
996
|
+
allHooks.push(hookContent);
|
|
997
|
+
endpointHookContents.push(hookContent); // Store for template context
|
|
998
|
+
});
|
|
999
|
+
context.hooks = allHooks;
|
|
1000
|
+
context.endpointHooks = endpointHookContents;
|
|
1001
|
+
// Generate resources using specified templates
|
|
1002
|
+
let hooksContent = '';
|
|
1003
|
+
if (templatePaths.hooks) {
|
|
1004
|
+
try {
|
|
1005
|
+
// Add utility functions to context for use in templates
|
|
1006
|
+
context['camelCase'] = (str) => this.toCamelCase(str);
|
|
1007
|
+
context['pascalCase'] = (str) => this.toPascalCase(str);
|
|
1008
|
+
hooksContent = (0, template_helpers_1.compileTemplate)(templatePaths.hooks, context);
|
|
1009
|
+
}
|
|
1010
|
+
catch (error) {
|
|
1011
|
+
// If template doesn't exist or fails, fall back to default generation
|
|
1012
|
+
console.warn(`Failed to compile hooks template: ${templatePaths.hooks}`, error);
|
|
1013
|
+
// Use the existing method as fallback
|
|
1014
|
+
hooksContent = `// ${this.toPascalCase(tag)} API Hooks\n`;
|
|
1015
|
+
hooksContent += `import { useQuery, useMutation, useQueryClient } from 'react-query';\n`;
|
|
1016
|
+
hooksContent += `import axios from 'axios';\n`;
|
|
1017
|
+
if (context.hasImportTypes) {
|
|
1018
|
+
hooksContent += `import type { ${context.usedTypeNames.join(', ')} } from './${this.toCamelCase(tag)}.types';\n\n`;
|
|
1019
|
+
}
|
|
1020
|
+
else {
|
|
1021
|
+
hooksContent += `\n`;
|
|
1022
|
+
}
|
|
1023
|
+
allParamInterfaces.forEach(interfaceCode => {
|
|
1024
|
+
hooksContent += interfaceCode + '\n';
|
|
1025
|
+
});
|
|
1026
|
+
allHooks.forEach(hookCode => {
|
|
1027
|
+
hooksContent += hookCode + '\n';
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
else {
|
|
1032
|
+
// Default generation if no template is provided
|
|
1033
|
+
hooksContent = `// ${this.toPascalCase(tag)} API Hooks\n`;
|
|
1034
|
+
hooksContent += `import { useQuery, useMutation, useQueryClient } from 'react-query';\n`;
|
|
1035
|
+
hooksContent += `import axios from 'axios';\n`;
|
|
1036
|
+
if (context.hasImportTypes) {
|
|
1037
|
+
hooksContent += `import type { ${context.usedTypeNames.join(', ')} } from './${this.toCamelCase(tag)}.types';\n\n`;
|
|
1038
|
+
}
|
|
1039
|
+
else {
|
|
1040
|
+
hooksContent += `\n`;
|
|
1041
|
+
}
|
|
1042
|
+
allParamInterfaces.forEach(interfaceCode => {
|
|
1043
|
+
hooksContent += interfaceCode + '\n';
|
|
1044
|
+
});
|
|
1045
|
+
allHooks.forEach(hookCode => {
|
|
1046
|
+
hooksContent += hookCode + '\n';
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
resourcesByTag.set(tag, {
|
|
1050
|
+
hooks: hooksContent,
|
|
1051
|
+
types: typesContent
|
|
1052
|
+
});
|
|
1053
|
+
});
|
|
1054
|
+
return resourcesByTag;
|
|
1055
|
+
}
|
|
783
1056
|
}
|
|
784
1057
|
exports.SwaggerDocGenerator = SwaggerDocGenerator;
|
|
785
1058
|
//# sourceMappingURL=index.js.map
|