ts-class-to-openapi 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/README.md +258 -33
- package/dist/__test__/circular-reference.test.d.ts +1 -0
- package/dist/__test__/entities/circular.entity.d.ts +59 -0
- package/dist/__test__/entities/complex-generics.entity.d.ts +33 -0
- package/dist/__test__/entities/comprehensive-enum.entity.d.ts +23 -0
- package/dist/__test__/entities/enum.entity.d.ts +29 -0
- package/dist/__test__/entities/user-role-generic.entity.d.ts +13 -0
- package/dist/__test__/enum.test.d.ts +1 -0
- package/dist/__test__/generic-types.test.d.ts +1 -0
- package/dist/__test__/index.d.ts +5 -0
- package/dist/__test__/ref-pattern.test.d.ts +1 -0
- package/dist/__test__/singleton-behavior.test.d.ts +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.esm.js +785 -45
- package/dist/index.js +785 -44
- package/dist/run.js +785 -44
- package/dist/transformer.d.ts +269 -28
- package/dist/transformer.fixtures.d.ts +4 -0
- package/dist/types.d.ts +17 -3
- package/package.json +1 -1
package/dist/index.esm.js
CHANGED
|
@@ -36,6 +36,7 @@ const validatorDecorators = {
|
|
|
36
36
|
ArrayNotEmpty: { name: 'ArrayNotEmpty' },
|
|
37
37
|
ArrayMaxSize: { name: 'ArrayMaxSize' },
|
|
38
38
|
ArrayMinSize: { name: 'ArrayMinSize' },
|
|
39
|
+
IsEnum: { name: 'IsEnum', type: 'string' },
|
|
39
40
|
};
|
|
40
41
|
const constants = {
|
|
41
42
|
TS_CONFIG_DEFAULT_PATH,
|
|
@@ -94,6 +95,12 @@ class SchemaTransformer {
|
|
|
94
95
|
* @private
|
|
95
96
|
*/
|
|
96
97
|
loadedFiles = new Set();
|
|
98
|
+
/**
|
|
99
|
+
* Set of class names currently being processed to prevent circular references
|
|
100
|
+
* Key format: "fileName:className" for uniqueness across different files
|
|
101
|
+
* @private
|
|
102
|
+
*/
|
|
103
|
+
processingClasses = new Set();
|
|
97
104
|
/**
|
|
98
105
|
* Private constructor for singleton pattern.
|
|
99
106
|
*
|
|
@@ -146,20 +153,19 @@ class SchemaTransformer {
|
|
|
146
153
|
}
|
|
147
154
|
}
|
|
148
155
|
/**
|
|
149
|
-
*
|
|
156
|
+
* Transforms a class by its name into an OpenAPI schema object.
|
|
157
|
+
* Considers the context of the calling file to resolve ambiguous class names.
|
|
158
|
+
* Includes circular reference detection to prevent infinite recursion.
|
|
150
159
|
*
|
|
151
|
-
* @param className - The name of the class to
|
|
152
|
-
* @param
|
|
153
|
-
* @returns
|
|
160
|
+
* @param className - The name of the class to transform
|
|
161
|
+
* @param contextFilePath - Optional path to context file for resolving class ambiguity
|
|
162
|
+
* @returns Object containing the class name and its corresponding JSON schema
|
|
163
|
+
* @throws {Error} When the specified class cannot be found
|
|
154
164
|
* @private
|
|
155
165
|
*/
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
return sourceFile ? [sourceFile] : [];
|
|
160
|
-
}
|
|
161
|
-
// Only get source files that are not declaration files and not in node_modules
|
|
162
|
-
return this.program.getSourceFiles().filter(sf => {
|
|
166
|
+
transformByName(className, contextFilePath) {
|
|
167
|
+
// Get all relevant source files (not declaration files and not in node_modules)
|
|
168
|
+
const sourceFiles = this.program.getSourceFiles().filter(sf => {
|
|
163
169
|
if (sf.isDeclarationFile)
|
|
164
170
|
return false;
|
|
165
171
|
if (sf.fileName.includes('.d.ts'))
|
|
@@ -170,19 +176,46 @@ class SchemaTransformer {
|
|
|
170
176
|
this.loadedFiles.add(sf.fileName);
|
|
171
177
|
return true;
|
|
172
178
|
});
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
179
|
+
// If we have a context file, try to find the class in that file first
|
|
180
|
+
if (contextFilePath) {
|
|
181
|
+
const contextSourceFile = this.program.getSourceFile(contextFilePath);
|
|
182
|
+
if (contextSourceFile) {
|
|
183
|
+
const classNode = this.findClassByName(contextSourceFile, className);
|
|
184
|
+
if (classNode) {
|
|
185
|
+
const cacheKey = this.getCacheKey(contextSourceFile.fileName, className);
|
|
186
|
+
// Check cache first
|
|
187
|
+
if (this.classCache.has(cacheKey)) {
|
|
188
|
+
return this.classCache.get(cacheKey);
|
|
189
|
+
}
|
|
190
|
+
// Check for circular reference before processing
|
|
191
|
+
if (this.processingClasses.has(cacheKey)) {
|
|
192
|
+
// Return a $ref reference to break circular dependency (OpenAPI 3.1 style)
|
|
193
|
+
return {
|
|
194
|
+
name: className,
|
|
195
|
+
schema: {
|
|
196
|
+
$ref: `#/components/schemas/${className}`,
|
|
197
|
+
description: `Reference to ${className} (circular reference detected)`,
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
// Mark this class as being processed
|
|
202
|
+
this.processingClasses.add(cacheKey);
|
|
203
|
+
try {
|
|
204
|
+
const result = this.transformClass(classNode, contextSourceFile);
|
|
205
|
+
this.classCache.set(cacheKey, result);
|
|
206
|
+
this.cleanupCache();
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
finally {
|
|
210
|
+
// Always remove from processing set when done
|
|
211
|
+
this.processingClasses.delete(cacheKey);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// Fallback to searching all files, but prioritize files that are more likely to be relevant
|
|
217
|
+
const prioritizedFiles = this.prioritizeSourceFiles(sourceFiles, contextFilePath);
|
|
218
|
+
for (const sourceFile of prioritizedFiles) {
|
|
186
219
|
const classNode = this.findClassByName(sourceFile, className);
|
|
187
220
|
if (classNode && sourceFile?.fileName) {
|
|
188
221
|
const cacheKey = this.getCacheKey(sourceFile.fileName, className);
|
|
@@ -190,16 +223,67 @@ class SchemaTransformer {
|
|
|
190
223
|
if (this.classCache.has(cacheKey)) {
|
|
191
224
|
return this.classCache.get(cacheKey);
|
|
192
225
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
226
|
+
// Check for circular reference before processing
|
|
227
|
+
if (this.processingClasses.has(cacheKey)) {
|
|
228
|
+
// Return a $ref reference to break circular dependency (OpenAPI 3.1 style)
|
|
229
|
+
return {
|
|
230
|
+
name: className,
|
|
231
|
+
schema: {
|
|
232
|
+
$ref: `#/components/schemas/${className}`,
|
|
233
|
+
description: `Reference to ${className} (circular reference detected)`,
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
// Mark this class as being processed
|
|
238
|
+
this.processingClasses.add(cacheKey);
|
|
239
|
+
try {
|
|
240
|
+
const result = this.transformClass(classNode, sourceFile);
|
|
241
|
+
// Cache using fileName:className as key for uniqueness
|
|
242
|
+
this.classCache.set(cacheKey, result);
|
|
243
|
+
// Clean up cache if it gets too large
|
|
244
|
+
this.cleanupCache();
|
|
245
|
+
return result;
|
|
246
|
+
}
|
|
247
|
+
finally {
|
|
248
|
+
// Always remove from processing set when done
|
|
249
|
+
this.processingClasses.delete(cacheKey);
|
|
250
|
+
}
|
|
199
251
|
}
|
|
200
252
|
}
|
|
201
253
|
throw new Error(`Class ${className} not found`);
|
|
202
254
|
}
|
|
255
|
+
/**
|
|
256
|
+
* Prioritizes source files based on context to resolve class name conflicts.
|
|
257
|
+
* Gives priority to files in the same directory or with similar names.
|
|
258
|
+
*
|
|
259
|
+
* @param sourceFiles - Array of source files to prioritize
|
|
260
|
+
* @param contextFilePath - Optional path to context file for prioritization
|
|
261
|
+
* @returns Prioritized array of source files
|
|
262
|
+
* @private
|
|
263
|
+
*/
|
|
264
|
+
prioritizeSourceFiles(sourceFiles, contextFilePath) {
|
|
265
|
+
if (!contextFilePath) {
|
|
266
|
+
return sourceFiles;
|
|
267
|
+
}
|
|
268
|
+
const contextDir = contextFilePath.substring(0, contextFilePath.lastIndexOf('/'));
|
|
269
|
+
return sourceFiles.sort((a, b) => {
|
|
270
|
+
const aDir = a.fileName.substring(0, a.fileName.lastIndexOf('/'));
|
|
271
|
+
const bDir = b.fileName.substring(0, b.fileName.lastIndexOf('/'));
|
|
272
|
+
// Prioritize files in the same directory as context
|
|
273
|
+
const aInSameDir = aDir === contextDir ? 1 : 0;
|
|
274
|
+
const bInSameDir = bDir === contextDir ? 1 : 0;
|
|
275
|
+
if (aInSameDir !== bInSameDir) {
|
|
276
|
+
return bInSameDir - aInSameDir; // Higher priority first
|
|
277
|
+
}
|
|
278
|
+
// Prioritize non-test files over test files
|
|
279
|
+
const aIsTest = a.fileName.includes('test') || a.fileName.includes('spec') ? 0 : 1;
|
|
280
|
+
const bIsTest = b.fileName.includes('test') || b.fileName.includes('spec') ? 0 : 1;
|
|
281
|
+
if (aIsTest !== bIsTest) {
|
|
282
|
+
return bIsTest - aIsTest; // Non-test files first
|
|
283
|
+
}
|
|
284
|
+
return 0;
|
|
285
|
+
});
|
|
286
|
+
}
|
|
203
287
|
/**
|
|
204
288
|
* Gets the singleton instance of SchemaTransformer.
|
|
205
289
|
*
|
|
@@ -226,16 +310,88 @@ class SchemaTransformer {
|
|
|
226
310
|
/**
|
|
227
311
|
* Clears the current singleton instance. Useful for testing or when you need
|
|
228
312
|
* to create a new instance with different configuration.
|
|
313
|
+
* @private
|
|
229
314
|
*/
|
|
230
315
|
static clearInstance() {
|
|
231
|
-
SchemaTransformer.instance =
|
|
316
|
+
SchemaTransformer.instance = undefined;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Flag to prevent recursive disposal calls
|
|
320
|
+
* @private
|
|
321
|
+
*/
|
|
322
|
+
static disposingInProgress = false;
|
|
323
|
+
/**
|
|
324
|
+
* Completely disposes of the current singleton instance and releases all resources.
|
|
325
|
+
* This is a static method that can be called without having an instance reference.
|
|
326
|
+
* Ensures complete memory cleanup regardless of the current state.
|
|
327
|
+
*
|
|
328
|
+
* @example
|
|
329
|
+
* ```typescript
|
|
330
|
+
* SchemaTransformer.disposeInstance();
|
|
331
|
+
* // All resources released, next getInstance() will create fresh instance
|
|
332
|
+
* ```
|
|
333
|
+
*
|
|
334
|
+
* @public
|
|
335
|
+
*/
|
|
336
|
+
static disposeInstance() {
|
|
337
|
+
// Prevent recursive disposal calls
|
|
338
|
+
if (SchemaTransformer.disposingInProgress) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
SchemaTransformer.disposingInProgress = true;
|
|
342
|
+
try {
|
|
343
|
+
if (SchemaTransformer.instance) {
|
|
344
|
+
SchemaTransformer.instance.dispose();
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
catch (error) {
|
|
348
|
+
// Log any disposal errors but continue with cleanup
|
|
349
|
+
console.warn('Warning during static disposal:', error);
|
|
350
|
+
}
|
|
351
|
+
finally {
|
|
352
|
+
// Always ensure the static instance is cleared
|
|
353
|
+
SchemaTransformer.instance = undefined;
|
|
354
|
+
SchemaTransformer.disposingInProgress = false;
|
|
355
|
+
// Force garbage collection for cleanup
|
|
356
|
+
if (global.gc) {
|
|
357
|
+
global.gc();
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* @deprecated Use disposeInstance() instead for better clarity
|
|
363
|
+
* @private
|
|
364
|
+
*/
|
|
365
|
+
static dispose() {
|
|
366
|
+
SchemaTransformer.disposeInstance();
|
|
232
367
|
}
|
|
233
368
|
static getInstance(tsConfigPath, options) {
|
|
234
|
-
if (!SchemaTransformer.instance) {
|
|
369
|
+
if (!SchemaTransformer.instance || SchemaTransformer.isInstanceDisposed()) {
|
|
235
370
|
SchemaTransformer.instance = new SchemaTransformer(tsConfigPath, options);
|
|
236
371
|
}
|
|
237
372
|
return SchemaTransformer.instance;
|
|
238
373
|
}
|
|
374
|
+
/**
|
|
375
|
+
* Internal method to check if current instance is disposed
|
|
376
|
+
* @private
|
|
377
|
+
*/
|
|
378
|
+
static isInstanceDisposed() {
|
|
379
|
+
return SchemaTransformer.instance
|
|
380
|
+
? SchemaTransformer.instance.isDisposed()
|
|
381
|
+
: true;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Transforms a class using the singleton instance
|
|
385
|
+
* @param cls - The class constructor function to transform
|
|
386
|
+
* @param options - Optional configuration for memory management (only used if no instance exists)
|
|
387
|
+
* @returns Object containing the class name and its corresponding JSON schema
|
|
388
|
+
* @public
|
|
389
|
+
*/
|
|
390
|
+
static transformClass(cls, options) {
|
|
391
|
+
// Use the singleton instance instead of creating a temporary one
|
|
392
|
+
const transformer = SchemaTransformer.getInstance(undefined, options);
|
|
393
|
+
return transformer.transform(cls);
|
|
394
|
+
}
|
|
239
395
|
/**
|
|
240
396
|
* Transforms a class constructor function into an OpenAPI schema object.
|
|
241
397
|
*
|
|
@@ -270,31 +426,147 @@ class SchemaTransformer {
|
|
|
270
426
|
clearCache() {
|
|
271
427
|
this.classCache.clear();
|
|
272
428
|
this.loadedFiles.clear();
|
|
429
|
+
this.processingClasses.clear();
|
|
273
430
|
// Force garbage collection hint if available
|
|
274
431
|
if (global.gc) {
|
|
275
432
|
global.gc();
|
|
276
433
|
}
|
|
277
434
|
}
|
|
435
|
+
/**
|
|
436
|
+
* Completely disposes of the transformer instance and releases all resources.
|
|
437
|
+
* This includes clearing all caches, releasing TypeScript program resources,
|
|
438
|
+
* and resetting the singleton instance.
|
|
439
|
+
*
|
|
440
|
+
* After calling this method, you need to call getInstance() again to get a new instance.
|
|
441
|
+
*
|
|
442
|
+
* @example
|
|
443
|
+
* ```typescript
|
|
444
|
+
* const transformer = SchemaTransformer.getInstance();
|
|
445
|
+
* // ... use transformer
|
|
446
|
+
* transformer.dispose();
|
|
447
|
+
* // transformer is now unusable, need to get new instance
|
|
448
|
+
* const newTransformer = SchemaTransformer.getInstance();
|
|
449
|
+
* ```
|
|
450
|
+
*
|
|
451
|
+
* @private
|
|
452
|
+
*/
|
|
453
|
+
dispose() {
|
|
454
|
+
try {
|
|
455
|
+
// Clear all caches and sets completely
|
|
456
|
+
this.classCache.clear();
|
|
457
|
+
this.loadedFiles.clear();
|
|
458
|
+
this.processingClasses.clear();
|
|
459
|
+
// Release TypeScript program resources
|
|
460
|
+
// While TypeScript doesn't provide explicit disposal methods,
|
|
461
|
+
// we can help garbage collection by clearing all references
|
|
462
|
+
// Clear all references to TypeScript objects
|
|
463
|
+
// @ts-ignore - We're intentionally setting these to null for cleanup
|
|
464
|
+
this.program = null;
|
|
465
|
+
// @ts-ignore - We're intentionally setting these to null for cleanup
|
|
466
|
+
this.checker = null;
|
|
467
|
+
}
|
|
468
|
+
catch (error) {
|
|
469
|
+
// If there's any error during disposal, log it but continue
|
|
470
|
+
console.warn('Warning during transformer disposal:', error);
|
|
471
|
+
}
|
|
472
|
+
finally {
|
|
473
|
+
// Force garbage collection for cleanup
|
|
474
|
+
if (global.gc) {
|
|
475
|
+
global.gc();
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Completely resets the transformer by disposing current instance and creating a new one.
|
|
481
|
+
* This is useful when you need a fresh start with different TypeScript configuration
|
|
482
|
+
* or want to ensure all resources are properly released and recreated.
|
|
483
|
+
*
|
|
484
|
+
* @param tsConfigPath - Optional path to a specific TypeScript config file for the new instance
|
|
485
|
+
* @param options - Configuration options for memory management for the new instance
|
|
486
|
+
* @returns A fresh SchemaTransformer instance
|
|
487
|
+
*
|
|
488
|
+
* @example
|
|
489
|
+
* ```typescript
|
|
490
|
+
* const transformer = SchemaTransformer.getInstance();
|
|
491
|
+
* // ... use transformer
|
|
492
|
+
* const freshTransformer = transformer.reset('./new-tsconfig.json');
|
|
493
|
+
* ```
|
|
494
|
+
*
|
|
495
|
+
* @private
|
|
496
|
+
*/
|
|
497
|
+
reset(tsConfigPath, options) {
|
|
498
|
+
// Dispose current instance using static method to properly clear instance reference
|
|
499
|
+
SchemaTransformer.disposeInstance();
|
|
500
|
+
// Create and return new instance
|
|
501
|
+
return SchemaTransformer.getInstance(tsConfigPath, options);
|
|
502
|
+
}
|
|
278
503
|
/**
|
|
279
504
|
* Gets memory usage statistics for monitoring and debugging.
|
|
280
505
|
*
|
|
281
|
-
* @returns Object containing cache size
|
|
506
|
+
* @returns Object containing cache size, loaded files count, and processing status
|
|
282
507
|
*
|
|
283
508
|
* @example
|
|
284
509
|
* ```typescript
|
|
285
510
|
* const transformer = SchemaTransformer.getInstance();
|
|
286
511
|
* const stats = transformer.getMemoryStats();
|
|
287
512
|
* console.log(`Cache entries: ${stats.cacheSize}, Files loaded: ${stats.loadedFiles}`);
|
|
513
|
+
* console.log(`Currently processing: ${stats.currentlyProcessing} classes`);
|
|
288
514
|
* ```
|
|
289
515
|
*
|
|
290
|
-
* @
|
|
516
|
+
* @private
|
|
291
517
|
*/
|
|
292
518
|
getMemoryStats() {
|
|
293
519
|
return {
|
|
294
|
-
cacheSize: this.classCache
|
|
295
|
-
loadedFiles: this.loadedFiles
|
|
520
|
+
cacheSize: this.classCache?.size || 0,
|
|
521
|
+
loadedFiles: this.loadedFiles?.size || 0,
|
|
522
|
+
currentlyProcessing: this.processingClasses?.size || 0,
|
|
523
|
+
maxCacheSize: this.maxCacheSize || 0,
|
|
524
|
+
autoCleanup: this.autoCleanup || false,
|
|
525
|
+
isDisposed: !this.program || !this.checker,
|
|
296
526
|
};
|
|
297
527
|
}
|
|
528
|
+
/**
|
|
529
|
+
* Checks if the transformer instance has been disposed and is no longer usable.
|
|
530
|
+
*
|
|
531
|
+
* @returns True if the instance has been disposed
|
|
532
|
+
*
|
|
533
|
+
* @example
|
|
534
|
+
* ```typescript
|
|
535
|
+
* const transformer = SchemaTransformer.getInstance();
|
|
536
|
+
* transformer.dispose();
|
|
537
|
+
* console.log(transformer.isDisposed()); // true
|
|
538
|
+
* ```
|
|
539
|
+
*
|
|
540
|
+
* @private
|
|
541
|
+
*/
|
|
542
|
+
isDisposed() {
|
|
543
|
+
return (!this.program ||
|
|
544
|
+
!this.checker ||
|
|
545
|
+
!this.classCache ||
|
|
546
|
+
!this.loadedFiles ||
|
|
547
|
+
!this.processingClasses);
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Static method to check if there's an active singleton instance.
|
|
551
|
+
*
|
|
552
|
+
* @returns True if there's an active instance, false if disposed or never created
|
|
553
|
+
*
|
|
554
|
+
* @example
|
|
555
|
+
* ```typescript
|
|
556
|
+
* console.log(SchemaTransformer.hasActiveInstance()); // false
|
|
557
|
+
* const transformer = SchemaTransformer.getInstance();
|
|
558
|
+
* console.log(SchemaTransformer.hasActiveInstance()); // true
|
|
559
|
+
* SchemaTransformer.dispose();
|
|
560
|
+
* console.log(SchemaTransformer.hasActiveInstance()); // false
|
|
561
|
+
* ```
|
|
562
|
+
*
|
|
563
|
+
* @private
|
|
564
|
+
*/
|
|
565
|
+
static hasActiveInstance() {
|
|
566
|
+
return (SchemaTransformer.instance !== null &&
|
|
567
|
+
SchemaTransformer.instance !== undefined &&
|
|
568
|
+
!SchemaTransformer.isInstanceDisposed());
|
|
569
|
+
}
|
|
298
570
|
/**
|
|
299
571
|
* Finds a class declaration by name within a source file.
|
|
300
572
|
*
|
|
@@ -319,13 +591,14 @@ class SchemaTransformer {
|
|
|
319
591
|
* Transforms a TypeScript class declaration into a schema object.
|
|
320
592
|
*
|
|
321
593
|
* @param classNode - The TypeScript class declaration node
|
|
594
|
+
* @param sourceFile - The source file containing the class (for context)
|
|
322
595
|
* @returns Object containing class name and generated schema
|
|
323
596
|
* @private
|
|
324
597
|
*/
|
|
325
|
-
transformClass(classNode) {
|
|
598
|
+
transformClass(classNode, sourceFile) {
|
|
326
599
|
const className = classNode.name?.text || 'Unknown';
|
|
327
600
|
const properties = this.extractProperties(classNode);
|
|
328
|
-
const schema = this.generateSchema(properties);
|
|
601
|
+
const schema = this.generateSchema(properties, sourceFile?.fileName);
|
|
329
602
|
return { name: className, schema };
|
|
330
603
|
}
|
|
331
604
|
/**
|
|
@@ -369,6 +642,237 @@ class SchemaTransformer {
|
|
|
369
642
|
const type = this.checker.getTypeAtLocation(property);
|
|
370
643
|
return this.checker.typeToString(type);
|
|
371
644
|
}
|
|
645
|
+
/**
|
|
646
|
+
* Resolves generic types by analyzing the type alias and its arguments.
|
|
647
|
+
* For example, User<Role> where User is a type alias will be resolved to its structure.
|
|
648
|
+
*
|
|
649
|
+
* @param typeNode - The TypeScript type reference node with generic arguments
|
|
650
|
+
* @returns String representation of the resolved type or schema
|
|
651
|
+
* @private
|
|
652
|
+
*/
|
|
653
|
+
resolveGenericType(typeNode) {
|
|
654
|
+
const typeName = typeNode.typeName.text;
|
|
655
|
+
const typeArguments = typeNode.typeArguments;
|
|
656
|
+
if (!typeArguments || typeArguments.length === 0) {
|
|
657
|
+
return typeName;
|
|
658
|
+
}
|
|
659
|
+
// Try to resolve the type using the TypeScript type checker
|
|
660
|
+
const type = this.checker.getTypeAtLocation(typeNode);
|
|
661
|
+
const resolvedType = this.checker.typeToString(type);
|
|
662
|
+
// If we can resolve it to a meaningful structure, use that
|
|
663
|
+
if (resolvedType &&
|
|
664
|
+
resolvedType !== typeName &&
|
|
665
|
+
!resolvedType.includes('any')) {
|
|
666
|
+
// For type aliases like User<Role>, we want to create a synthetic type name
|
|
667
|
+
// that represents the resolved structure
|
|
668
|
+
const typeArgNames = typeArguments.map(arg => {
|
|
669
|
+
if (ts.isTypeReferenceNode(arg) && ts.isIdentifier(arg.typeName)) {
|
|
670
|
+
return arg.typeName.text;
|
|
671
|
+
}
|
|
672
|
+
return this.getTypeNodeToString(arg);
|
|
673
|
+
});
|
|
674
|
+
return `${typeName}_${typeArgNames.join('_')}`;
|
|
675
|
+
}
|
|
676
|
+
return typeName;
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Checks if a type string represents a resolved generic type.
|
|
680
|
+
*
|
|
681
|
+
* @param type - The type string to check
|
|
682
|
+
* @returns True if it's a resolved generic type
|
|
683
|
+
* @private
|
|
684
|
+
*/
|
|
685
|
+
isResolvedGenericType(type) {
|
|
686
|
+
// Simple heuristic: resolved generic types contain underscores and
|
|
687
|
+
// the parts after underscore should be known types
|
|
688
|
+
const parts = type.split('_');
|
|
689
|
+
return (parts.length > 1 &&
|
|
690
|
+
parts
|
|
691
|
+
.slice(1)
|
|
692
|
+
.every(part => this.isKnownType(part) || this.isPrimitiveType(part)));
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Checks if a type is a known class or interface.
|
|
696
|
+
*
|
|
697
|
+
* @param typeName - The type name to check
|
|
698
|
+
* @returns True if it's a known type
|
|
699
|
+
* @private
|
|
700
|
+
*/
|
|
701
|
+
isKnownType(typeName) {
|
|
702
|
+
// First check if it's a primitive type to avoid unnecessary lookups
|
|
703
|
+
if (this.isPrimitiveType(typeName)) {
|
|
704
|
+
return true;
|
|
705
|
+
}
|
|
706
|
+
try {
|
|
707
|
+
// Use a more conservative approach - check if we can find the class
|
|
708
|
+
// without actually transforming it to avoid side effects
|
|
709
|
+
const found = this.findClassInProject(typeName);
|
|
710
|
+
return found !== null;
|
|
711
|
+
}
|
|
712
|
+
catch {
|
|
713
|
+
return false;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Finds a class by name in the project without transforming it.
|
|
718
|
+
*
|
|
719
|
+
* @param className - The class name to find
|
|
720
|
+
* @returns True if found, false otherwise
|
|
721
|
+
* @private
|
|
722
|
+
*/
|
|
723
|
+
findClassInProject(className) {
|
|
724
|
+
const sourceFiles = this.program.getSourceFiles().filter(sf => {
|
|
725
|
+
if (sf.isDeclarationFile)
|
|
726
|
+
return false;
|
|
727
|
+
if (sf.fileName.includes('.d.ts'))
|
|
728
|
+
return false;
|
|
729
|
+
if (sf.fileName.includes('node_modules'))
|
|
730
|
+
return false;
|
|
731
|
+
return true;
|
|
732
|
+
});
|
|
733
|
+
for (const sourceFile of sourceFiles) {
|
|
734
|
+
const found = this.findClassByName(sourceFile, className);
|
|
735
|
+
if (found)
|
|
736
|
+
return true;
|
|
737
|
+
}
|
|
738
|
+
return false;
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Checks if a type is a primitive type.
|
|
742
|
+
*
|
|
743
|
+
* @param typeName - The type name to check
|
|
744
|
+
* @returns True if it's a primitive type
|
|
745
|
+
* @private
|
|
746
|
+
*/
|
|
747
|
+
isPrimitiveType(typeName) {
|
|
748
|
+
const lowerTypeName = typeName.toLowerCase();
|
|
749
|
+
// Check against all primitive types from constants
|
|
750
|
+
const primitiveTypes = [
|
|
751
|
+
constants.jsPrimitives.String.type.toLowerCase(),
|
|
752
|
+
constants.jsPrimitives.Number.type.toLowerCase(),
|
|
753
|
+
constants.jsPrimitives.Boolean.type.toLowerCase(),
|
|
754
|
+
constants.jsPrimitives.Date.type.toLowerCase(),
|
|
755
|
+
constants.jsPrimitives.Buffer.type.toLowerCase(),
|
|
756
|
+
constants.jsPrimitives.Uint8Array.type.toLowerCase(),
|
|
757
|
+
constants.jsPrimitives.File.type.toLowerCase(),
|
|
758
|
+
constants.jsPrimitives.UploadFile.type.toLowerCase(),
|
|
759
|
+
constants.jsPrimitives.BigInt.type.toLowerCase(),
|
|
760
|
+
];
|
|
761
|
+
return primitiveTypes.includes(lowerTypeName);
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Resolves a generic type schema by analyzing the type alias structure.
|
|
765
|
+
*
|
|
766
|
+
* @param resolvedTypeName - The resolved generic type name (e.g., User_Role)
|
|
767
|
+
* @returns OpenAPI schema for the resolved generic type
|
|
768
|
+
* @private
|
|
769
|
+
*/
|
|
770
|
+
resolveGenericTypeSchema(resolvedTypeName) {
|
|
771
|
+
const parts = resolvedTypeName.split('_');
|
|
772
|
+
const baseTypeName = parts[0];
|
|
773
|
+
const typeArgNames = parts.slice(1);
|
|
774
|
+
if (!baseTypeName) {
|
|
775
|
+
return null;
|
|
776
|
+
}
|
|
777
|
+
// Find the original type alias declaration
|
|
778
|
+
const typeAliasSymbol = this.findTypeAliasDeclaration(baseTypeName);
|
|
779
|
+
if (!typeAliasSymbol) {
|
|
780
|
+
return null;
|
|
781
|
+
}
|
|
782
|
+
// Create a schema based on the type alias structure, substituting type parameters
|
|
783
|
+
return this.createSchemaFromTypeAlias(typeAliasSymbol, typeArgNames);
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Finds a type alias declaration by name.
|
|
787
|
+
*
|
|
788
|
+
* @param typeName - The type alias name to find
|
|
789
|
+
* @returns The type alias declaration node or null
|
|
790
|
+
* @private
|
|
791
|
+
*/
|
|
792
|
+
findTypeAliasDeclaration(typeName) {
|
|
793
|
+
for (const sourceFile of this.program.getSourceFiles()) {
|
|
794
|
+
if (sourceFile.isDeclarationFile)
|
|
795
|
+
continue;
|
|
796
|
+
const findTypeAlias = (node) => {
|
|
797
|
+
if (ts.isTypeAliasDeclaration(node) && node.name.text === typeName) {
|
|
798
|
+
return node;
|
|
799
|
+
}
|
|
800
|
+
return ts.forEachChild(node, findTypeAlias) || null;
|
|
801
|
+
};
|
|
802
|
+
const result = findTypeAlias(sourceFile);
|
|
803
|
+
if (result)
|
|
804
|
+
return result;
|
|
805
|
+
}
|
|
806
|
+
return null;
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* Creates a schema from a type alias declaration, substituting type parameters.
|
|
810
|
+
*
|
|
811
|
+
* @param typeAlias - The type alias declaration
|
|
812
|
+
* @param typeArgNames - The concrete type arguments
|
|
813
|
+
* @returns OpenAPI schema for the type alias
|
|
814
|
+
* @private
|
|
815
|
+
*/
|
|
816
|
+
createSchemaFromTypeAlias(typeAlias, typeArgNames) {
|
|
817
|
+
const typeNode = typeAlias.type;
|
|
818
|
+
if (ts.isTypeLiteralNode(typeNode)) {
|
|
819
|
+
const schema = {
|
|
820
|
+
type: 'object',
|
|
821
|
+
properties: {},
|
|
822
|
+
required: [],
|
|
823
|
+
};
|
|
824
|
+
for (const member of typeNode.members) {
|
|
825
|
+
if (ts.isPropertySignature(member) &&
|
|
826
|
+
member.name &&
|
|
827
|
+
ts.isIdentifier(member.name)) {
|
|
828
|
+
const propertyName = member.name.text;
|
|
829
|
+
const isOptional = !!member.questionToken;
|
|
830
|
+
if (member.type) {
|
|
831
|
+
const propertyType = this.resolveTypeParameterInTypeAlias(member.type, typeAlias.typeParameters, typeArgNames);
|
|
832
|
+
const { type, format, nestedSchema } = this.mapTypeToSchema(propertyType);
|
|
833
|
+
if (nestedSchema) {
|
|
834
|
+
schema.properties[propertyName] = nestedSchema;
|
|
835
|
+
}
|
|
836
|
+
else {
|
|
837
|
+
schema.properties[propertyName] = { type };
|
|
838
|
+
if (format)
|
|
839
|
+
schema.properties[propertyName].format = format;
|
|
840
|
+
}
|
|
841
|
+
if (!isOptional) {
|
|
842
|
+
schema.required.push(propertyName);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
return schema;
|
|
848
|
+
}
|
|
849
|
+
return null;
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* Resolves type parameters in a type alias to concrete types.
|
|
853
|
+
*
|
|
854
|
+
* @param typeNode - The type node to resolve
|
|
855
|
+
* @param typeParameters - The type parameters of the type alias
|
|
856
|
+
* @param typeArgNames - The concrete type arguments
|
|
857
|
+
* @returns The resolved type string
|
|
858
|
+
* @private
|
|
859
|
+
*/
|
|
860
|
+
resolveTypeParameterInTypeAlias(typeNode, typeParameters, typeArgNames) {
|
|
861
|
+
if (ts.isTypeReferenceNode(typeNode) &&
|
|
862
|
+
ts.isIdentifier(typeNode.typeName)) {
|
|
863
|
+
const typeName = typeNode.typeName.text;
|
|
864
|
+
// Check if this is a type parameter
|
|
865
|
+
if (typeParameters) {
|
|
866
|
+
const paramIndex = typeParameters.findIndex(param => param.name.text === typeName);
|
|
867
|
+
if (paramIndex !== -1 && paramIndex < typeArgNames.length) {
|
|
868
|
+
const resolvedType = typeArgNames[paramIndex];
|
|
869
|
+
return resolvedType || typeName;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
return typeName;
|
|
873
|
+
}
|
|
874
|
+
return this.getTypeNodeToString(typeNode);
|
|
875
|
+
}
|
|
372
876
|
/**
|
|
373
877
|
* Converts a TypeScript type node to its string representation.
|
|
374
878
|
*
|
|
@@ -394,6 +898,7 @@ class SchemaTransformer {
|
|
|
394
898
|
return firstTypeArg.typeName.text;
|
|
395
899
|
}
|
|
396
900
|
}
|
|
901
|
+
return this.resolveGenericType(typeNode);
|
|
397
902
|
}
|
|
398
903
|
return typeNode.typeName.text;
|
|
399
904
|
}
|
|
@@ -492,19 +997,24 @@ class SchemaTransformer {
|
|
|
492
997
|
* Generates an OpenAPI schema from extracted property information.
|
|
493
998
|
*
|
|
494
999
|
* @param properties - Array of property information to process
|
|
1000
|
+
* @param contextFilePath - Optional context file path for resolving class references
|
|
495
1001
|
* @returns Complete OpenAPI schema object with properties and validation rules
|
|
496
1002
|
* @private
|
|
497
1003
|
*/
|
|
498
|
-
generateSchema(properties) {
|
|
1004
|
+
generateSchema(properties, contextFilePath) {
|
|
499
1005
|
const schema = {
|
|
500
1006
|
type: 'object',
|
|
501
1007
|
properties: {},
|
|
502
1008
|
required: [],
|
|
503
1009
|
};
|
|
504
1010
|
for (const property of properties) {
|
|
505
|
-
const { type, format, nestedSchema } = this.mapTypeToSchema(property.type);
|
|
1011
|
+
const { type, format, nestedSchema } = this.mapTypeToSchema(property.type, contextFilePath);
|
|
506
1012
|
if (nestedSchema) {
|
|
507
1013
|
schema.properties[property.name] = nestedSchema;
|
|
1014
|
+
// Skip decorator application for $ref schemas
|
|
1015
|
+
if (this.isRefSchema(nestedSchema)) {
|
|
1016
|
+
continue;
|
|
1017
|
+
}
|
|
508
1018
|
}
|
|
509
1019
|
else {
|
|
510
1020
|
schema.properties[property.name] = { type };
|
|
@@ -527,14 +1037,15 @@ class SchemaTransformer {
|
|
|
527
1037
|
* Handles primitive types, arrays, and nested objects recursively.
|
|
528
1038
|
*
|
|
529
1039
|
* @param type - The TypeScript type string to map
|
|
1040
|
+
* @param contextFilePath - Optional context file path for resolving class references
|
|
530
1041
|
* @returns Object containing OpenAPI type, optional format, and nested schema
|
|
531
1042
|
* @private
|
|
532
1043
|
*/
|
|
533
|
-
mapTypeToSchema(type) {
|
|
1044
|
+
mapTypeToSchema(type, contextFilePath) {
|
|
534
1045
|
// Handle arrays
|
|
535
1046
|
if (type.endsWith('[]')) {
|
|
536
1047
|
const elementType = type.slice(0, -2);
|
|
537
|
-
const elementSchema = this.mapTypeToSchema(elementType);
|
|
1048
|
+
const elementSchema = this.mapTypeToSchema(elementType, contextFilePath);
|
|
538
1049
|
const items = elementSchema.nestedSchema || {
|
|
539
1050
|
type: elementSchema.type,
|
|
540
1051
|
};
|
|
@@ -578,9 +1089,31 @@ class SchemaTransformer {
|
|
|
578
1089
|
format: constants.jsPrimitives.UploadFile.format,
|
|
579
1090
|
};
|
|
580
1091
|
default:
|
|
1092
|
+
// Check if it's a resolved generic type (e.g., User_Role)
|
|
1093
|
+
if (type.includes('_') && this.isResolvedGenericType(type)) {
|
|
1094
|
+
try {
|
|
1095
|
+
const genericSchema = this.resolveGenericTypeSchema(type);
|
|
1096
|
+
if (genericSchema) {
|
|
1097
|
+
return {
|
|
1098
|
+
type: constants.jsPrimitives.Object.value,
|
|
1099
|
+
nestedSchema: genericSchema,
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
catch (error) {
|
|
1104
|
+
console.warn(`Failed to resolve generic type ${type}:`, error);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
581
1107
|
// Handle nested objects
|
|
582
1108
|
try {
|
|
583
|
-
const nestedResult = this.transformByName(type);
|
|
1109
|
+
const nestedResult = this.transformByName(type, contextFilePath);
|
|
1110
|
+
// Check if it's a $ref schema (circular reference)
|
|
1111
|
+
if (nestedResult.schema.$ref) {
|
|
1112
|
+
return {
|
|
1113
|
+
type: constants.jsPrimitives.Object.value,
|
|
1114
|
+
nestedSchema: nestedResult.schema,
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
584
1117
|
return {
|
|
585
1118
|
type: constants.jsPrimitives.Object.value,
|
|
586
1119
|
nestedSchema: nestedResult.schema,
|
|
@@ -591,6 +1124,16 @@ class SchemaTransformer {
|
|
|
591
1124
|
}
|
|
592
1125
|
}
|
|
593
1126
|
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Checks if a schema is a $ref schema (circular reference).
|
|
1129
|
+
*
|
|
1130
|
+
* @param schema - The schema to check
|
|
1131
|
+
* @returns True if it's a $ref schema
|
|
1132
|
+
* @private
|
|
1133
|
+
*/
|
|
1134
|
+
isRefSchema(schema) {
|
|
1135
|
+
return '$ref' in schema;
|
|
1136
|
+
}
|
|
594
1137
|
/**
|
|
595
1138
|
* Applies class-validator decorators to schema properties.
|
|
596
1139
|
* Maps validation decorators to their corresponding OpenAPI schema constraints.
|
|
@@ -601,6 +1144,10 @@ class SchemaTransformer {
|
|
|
601
1144
|
* @private
|
|
602
1145
|
*/
|
|
603
1146
|
applyDecorators(decorators, schema, propertyName) {
|
|
1147
|
+
// Skip applying decorators to $ref schemas
|
|
1148
|
+
if (this.isRefSchema(schema)) {
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
604
1151
|
const isArrayType = schema.properties[propertyName].type ===
|
|
605
1152
|
constants.jsPrimitives.Array.value;
|
|
606
1153
|
for (const decorator of decorators) {
|
|
@@ -716,8 +1263,193 @@ class SchemaTransformer {
|
|
|
716
1263
|
case constants.validatorDecorators.ArrayMaxSize.name:
|
|
717
1264
|
schema.properties[propertyName].maxItems = decorator.arguments[0];
|
|
718
1265
|
break;
|
|
1266
|
+
case constants.validatorDecorators.IsEnum.name:
|
|
1267
|
+
this.applyEnumDecorator(decorator, schema, propertyName, isArrayType);
|
|
1268
|
+
break;
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
/**
|
|
1273
|
+
* Applies the @IsEnum decorator to a property, handling both primitive values and object enums.
|
|
1274
|
+
* Supports arrays of enum values as well.
|
|
1275
|
+
*
|
|
1276
|
+
* @param decorator - The IsEnum decorator information
|
|
1277
|
+
* @param schema - The schema object to modify
|
|
1278
|
+
* @param propertyName - The name of the property
|
|
1279
|
+
* @param isArrayType - Whether the property is an array type
|
|
1280
|
+
* @private
|
|
1281
|
+
*/
|
|
1282
|
+
applyEnumDecorator(decorator, schema, propertyName, isArrayType) {
|
|
1283
|
+
if (!decorator.arguments || decorator.arguments.length === 0) {
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
const enumArg = decorator.arguments[0];
|
|
1287
|
+
let enumValues = [];
|
|
1288
|
+
// Handle different enum argument types
|
|
1289
|
+
if (typeof enumArg === 'string') {
|
|
1290
|
+
// This is likely a reference to an enum type name
|
|
1291
|
+
// We need to try to resolve this to actual enum values
|
|
1292
|
+
enumValues = this.resolveEnumValues(enumArg);
|
|
1293
|
+
}
|
|
1294
|
+
else if (typeof enumArg === 'object' && enumArg !== null) {
|
|
1295
|
+
// Object enum - extract values
|
|
1296
|
+
if (Array.isArray(enumArg)) {
|
|
1297
|
+
// Already an array of values
|
|
1298
|
+
enumValues = enumArg;
|
|
1299
|
+
}
|
|
1300
|
+
else {
|
|
1301
|
+
// Enum object - get all values
|
|
1302
|
+
enumValues = Object.values(enumArg);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
// If we couldn't resolve enum values, fall back to string type without enum constraint
|
|
1306
|
+
if (enumValues.length === 0) {
|
|
1307
|
+
if (!isArrayType) {
|
|
1308
|
+
schema.properties[propertyName].type = 'string';
|
|
1309
|
+
}
|
|
1310
|
+
else if (schema.properties[propertyName].items) {
|
|
1311
|
+
schema.properties[propertyName].items.type = 'string';
|
|
1312
|
+
}
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
// Determine the type based on enum values
|
|
1316
|
+
let enumType = 'string';
|
|
1317
|
+
if (enumValues.length > 0) {
|
|
1318
|
+
const firstValue = enumValues[0];
|
|
1319
|
+
if (typeof firstValue === 'number') {
|
|
1320
|
+
enumType = 'number';
|
|
1321
|
+
}
|
|
1322
|
+
else if (typeof firstValue === 'boolean') {
|
|
1323
|
+
enumType = 'boolean';
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
// Apply enum to schema
|
|
1327
|
+
if (!isArrayType) {
|
|
1328
|
+
schema.properties[propertyName].type = enumType;
|
|
1329
|
+
schema.properties[propertyName].enum = enumValues;
|
|
1330
|
+
}
|
|
1331
|
+
else if (schema.properties[propertyName].items) {
|
|
1332
|
+
schema.properties[propertyName].items.type = enumType;
|
|
1333
|
+
schema.properties[propertyName].items.enum = enumValues;
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
/**
|
|
1337
|
+
* Attempts to resolve enum values from an enum type name.
|
|
1338
|
+
* This searches through the TypeScript AST to find the enum declaration
|
|
1339
|
+
* and extract its values.
|
|
1340
|
+
*
|
|
1341
|
+
* @param enumTypeName - The name of the enum type
|
|
1342
|
+
* @returns Array of enum values if found, empty array otherwise
|
|
1343
|
+
* @private
|
|
1344
|
+
*/
|
|
1345
|
+
resolveEnumValues(enumTypeName) {
|
|
1346
|
+
// Search for enum declarations in source files
|
|
1347
|
+
for (const sourceFile of this.program.getSourceFiles()) {
|
|
1348
|
+
if (sourceFile.isDeclarationFile)
|
|
1349
|
+
continue;
|
|
1350
|
+
if (sourceFile.fileName.includes('node_modules'))
|
|
1351
|
+
continue;
|
|
1352
|
+
const enumValues = this.findEnumValues(sourceFile, enumTypeName);
|
|
1353
|
+
if (enumValues.length > 0) {
|
|
1354
|
+
return enumValues;
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
return [];
|
|
1358
|
+
}
|
|
1359
|
+
/**
|
|
1360
|
+
* Finds enum values in a specific source file.
|
|
1361
|
+
*
|
|
1362
|
+
* @param sourceFile - The source file to search
|
|
1363
|
+
* @param enumTypeName - The name of the enum to find
|
|
1364
|
+
* @returns Array of enum values if found, empty array otherwise
|
|
1365
|
+
* @private
|
|
1366
|
+
*/
|
|
1367
|
+
findEnumValues(sourceFile, enumTypeName) {
|
|
1368
|
+
let enumValues = [];
|
|
1369
|
+
const visit = (node) => {
|
|
1370
|
+
// Handle TypeScript enum declarations
|
|
1371
|
+
if (ts.isEnumDeclaration(node) && node.name?.text === enumTypeName) {
|
|
1372
|
+
enumValues = this.extractEnumValues(node);
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
// Handle const object declarations (like const Status = { ... } as const)
|
|
1376
|
+
if (ts.isVariableStatement(node)) {
|
|
1377
|
+
for (const declaration of node.declarationList.declarations) {
|
|
1378
|
+
if (ts.isVariableDeclaration(declaration) &&
|
|
1379
|
+
ts.isIdentifier(declaration.name) &&
|
|
1380
|
+
declaration.name.text === enumTypeName &&
|
|
1381
|
+
declaration.initializer) {
|
|
1382
|
+
let initializer = declaration.initializer;
|
|
1383
|
+
// Handle "as const" assertions
|
|
1384
|
+
if (ts.isAsExpression(initializer) && initializer.expression) {
|
|
1385
|
+
initializer = initializer.expression;
|
|
1386
|
+
}
|
|
1387
|
+
enumValues = this.extractObjectEnumValues(initializer);
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
ts.forEachChild(node, visit);
|
|
1393
|
+
};
|
|
1394
|
+
visit(sourceFile);
|
|
1395
|
+
return enumValues;
|
|
1396
|
+
}
|
|
1397
|
+
/**
|
|
1398
|
+
* Extracts values from a TypeScript enum declaration.
|
|
1399
|
+
*
|
|
1400
|
+
* @param enumNode - The enum declaration node
|
|
1401
|
+
* @returns Array of enum values
|
|
1402
|
+
* @private
|
|
1403
|
+
*/
|
|
1404
|
+
extractEnumValues(enumNode) {
|
|
1405
|
+
const values = [];
|
|
1406
|
+
for (const member of enumNode.members) {
|
|
1407
|
+
if (member.initializer) {
|
|
1408
|
+
// Handle initialized enum members
|
|
1409
|
+
if (ts.isStringLiteral(member.initializer)) {
|
|
1410
|
+
values.push(member.initializer.text);
|
|
1411
|
+
}
|
|
1412
|
+
else if (ts.isNumericLiteral(member.initializer)) {
|
|
1413
|
+
values.push(Number(member.initializer.text));
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
else {
|
|
1417
|
+
// Handle auto-incremented numeric enums
|
|
1418
|
+
if (values.length === 0) {
|
|
1419
|
+
values.push(0);
|
|
1420
|
+
}
|
|
1421
|
+
else {
|
|
1422
|
+
const lastValue = values[values.length - 1];
|
|
1423
|
+
if (typeof lastValue === 'number') {
|
|
1424
|
+
values.push(lastValue + 1);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
return values;
|
|
1430
|
+
}
|
|
1431
|
+
/**
|
|
1432
|
+
* Extracts values from object literal enum (const object as const).
|
|
1433
|
+
*
|
|
1434
|
+
* @param initializer - The object literal initializer
|
|
1435
|
+
* @returns Array of enum values
|
|
1436
|
+
* @private
|
|
1437
|
+
*/
|
|
1438
|
+
extractObjectEnumValues(initializer) {
|
|
1439
|
+
const values = [];
|
|
1440
|
+
if (ts.isObjectLiteralExpression(initializer)) {
|
|
1441
|
+
for (const property of initializer.properties) {
|
|
1442
|
+
if (ts.isPropertyAssignment(property) && property.initializer) {
|
|
1443
|
+
if (ts.isStringLiteral(property.initializer)) {
|
|
1444
|
+
values.push(property.initializer.text);
|
|
1445
|
+
}
|
|
1446
|
+
else if (ts.isNumericLiteral(property.initializer)) {
|
|
1447
|
+
values.push(Number(property.initializer.text));
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
719
1450
|
}
|
|
720
1451
|
}
|
|
1452
|
+
return values;
|
|
721
1453
|
}
|
|
722
1454
|
/**
|
|
723
1455
|
* Applies sensible default behaviors for properties without class-validator decorators.
|
|
@@ -737,6 +1469,10 @@ class SchemaTransformer {
|
|
|
737
1469
|
* @private
|
|
738
1470
|
*/
|
|
739
1471
|
applyTypeBasedFormats(property, schema) {
|
|
1472
|
+
// Skip applying type-based formats to $ref schemas
|
|
1473
|
+
if (this.isRefSchema(schema)) {
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
740
1476
|
const propertyName = property.name;
|
|
741
1477
|
const propertyType = property.type.toLowerCase();
|
|
742
1478
|
const propertySchema = schema.properties[propertyName];
|
|
@@ -771,6 +1507,10 @@ class SchemaTransformer {
|
|
|
771
1507
|
* @private
|
|
772
1508
|
*/
|
|
773
1509
|
determineRequiredStatus(property, schema) {
|
|
1510
|
+
// Skip determining required status for $ref schemas
|
|
1511
|
+
if (this.isRefSchema(schema)) {
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
774
1514
|
const propertyName = property.name;
|
|
775
1515
|
// Check if already marked as required by IsNotEmpty or ArrayNotEmpty decorator
|
|
776
1516
|
const isAlreadyRequired = schema.required.includes(propertyName);
|
|
@@ -811,7 +1551,7 @@ class SchemaTransformer {
|
|
|
811
1551
|
* @public
|
|
812
1552
|
*/
|
|
813
1553
|
function transform(cls, options) {
|
|
814
|
-
return SchemaTransformer.
|
|
1554
|
+
return SchemaTransformer.transformClass(cls, options);
|
|
815
1555
|
}
|
|
816
1556
|
|
|
817
|
-
export { transform };
|
|
1557
|
+
export { SchemaTransformer, transform };
|