ts-class-to-openapi 1.0.5 → 1.0.6

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.
Files changed (42) hide show
  1. package/README.md +368 -882
  2. package/dist/__test__/entities/additional-test-classes.d.ts +12 -0
  3. package/dist/__test__/entities/decorated-classes.d.ts +54 -0
  4. package/dist/__test__/entities/nested-classes.d.ts +70 -0
  5. package/dist/__test__/entities/pure-classes.d.ts +37 -0
  6. package/dist/__test__/entities/schema-validation-classes.d.ts +35 -0
  7. package/dist/__test__/index.d.ts +3 -9
  8. package/dist/__test__/test.d.ts +4 -0
  9. package/dist/index.d.ts +2 -2
  10. package/dist/index.esm.js +475 -1324
  11. package/dist/index.js +474 -1324
  12. package/dist/run.d.ts +1 -1
  13. package/dist/run.js +1062 -1350
  14. package/dist/transformer.d.ts +1 -575
  15. package/dist/transformer.fixtures.d.ts +21 -0
  16. package/dist/types.d.ts +38 -3
  17. package/package.json +16 -15
  18. package/dist/__test__/entities/address.entity.d.ts +0 -5
  19. package/dist/__test__/entities/array.entity.d.ts +0 -7
  20. package/dist/__test__/entities/broken.entity.d.ts +0 -7
  21. package/dist/__test__/entities/circular.entity.d.ts +0 -59
  22. package/dist/__test__/entities/complete.entity.d.ts +0 -16
  23. package/dist/__test__/entities/complex-generics.entity.d.ts +0 -33
  24. package/dist/__test__/entities/comprehensive-enum.entity.d.ts +0 -23
  25. package/dist/__test__/entities/enum.entity.d.ts +0 -29
  26. package/dist/__test__/entities/generic.entity.d.ts +0 -11
  27. package/dist/__test__/entities/optional-properties.entity.d.ts +0 -11
  28. package/dist/__test__/entities/plain.entity.d.ts +0 -19
  29. package/dist/__test__/entities/simple.entity.d.ts +0 -5
  30. package/dist/__test__/entities/upload.entity.d.ts +0 -8
  31. package/dist/__test__/entities/user-role-generic.entity.d.ts +0 -13
  32. package/dist/__test__/plain.test.d.ts +0 -1
  33. package/dist/__test__/ref-pattern.test.d.ts +0 -1
  34. package/dist/__test__/singleton-behavior.test.d.ts +0 -1
  35. package/dist/__test__/test-entities/duplicate-name.entity.d.ts +0 -5
  36. package/dist/__test__/test-entities/generic.entity.d.ts +0 -11
  37. /package/dist/__test__/{circular-reference.test.d.ts → testCases/debug.test.d.ts} +0 -0
  38. /package/dist/__test__/{enum.test.d.ts → testCases/decorated-classes.test.d.ts} +0 -0
  39. /package/dist/__test__/{generic-types.test.d.ts → testCases/edge-cases.test.d.ts} +0 -0
  40. /package/dist/__test__/{integration.test.d.ts → testCases/nested-classes.test.d.ts} +0 -0
  41. /package/dist/__test__/{main.test.d.ts → testCases/pure-classes.test.d.ts} +0 -0
  42. /package/dist/__test__/{optional-properties.test.d.ts → testCases/schema-validation.test.d.ts} +0 -0
package/dist/index.esm.js CHANGED
@@ -4,113 +4,57 @@ import path from 'path';
4
4
  const TS_CONFIG_DEFAULT_PATH = path.resolve(process.cwd(), 'tsconfig.json');
5
5
  const jsPrimitives = {
6
6
  String: { type: 'String', value: 'string' },
7
- Number: { type: 'Number', value: 'number', format: 'float' },
7
+ Any: { type: 'Any'},
8
+ Unknown: { type: 'Unknown'},
9
+ Number: { type: 'Number', value: 'number', format: 'double' },
8
10
  Boolean: { type: 'Boolean', value: 'boolean' },
9
- Symbol: { type: 'Symbol', value: 'symbol' },
11
+ Symbol: { type: 'Symbol', value: 'string' },
10
12
  BigInt: { type: 'BigInt', value: 'integer', format: 'int64' },
11
- null: { type: 'null', value: 'null' },
13
+ null: { type: 'null'},
12
14
  Object: { type: 'Object', value: 'object' },
13
15
  Array: { type: 'Array', value: 'array' },
14
16
  Date: { type: 'Date', value: 'string', format: 'date-time' },
15
- Function: { type: 'Function', value: 'function' },
16
- Buffer: { type: 'Buffer', value: 'string', format: 'binary' },
17
- Uint8Array: { type: 'Uint8Array', value: 'string', format: 'binary' },
17
+ Buffer: { type: 'Buffer', value: 'string'},
18
+ Uint8Array: { type: 'Uint8Array', value: 'string'},
18
19
  UploadFile: { type: 'UploadFile', value: 'string', format: 'binary' },
19
- File: { type: 'File', value: 'string', format: 'binary' },
20
+ File: { type: 'File', value: 'string'},
20
21
  };
21
22
  const validatorDecorators = {
22
- Length: { name: 'Length', type: 'string' },
23
- MinLength: { name: 'MinLength', type: 'string' },
24
- MaxLength: { name: 'MaxLength', type: 'string' },
23
+ Length: { name: 'Length'},
24
+ MinLength: { name: 'MinLength'},
25
+ MaxLength: { name: 'MaxLength'},
25
26
  IsInt: { name: 'IsInt', type: 'integer', format: 'int32' },
26
- IsNumber: { name: 'IsNumber', type: 'number', format: 'double' },
27
- IsString: { name: 'IsString', type: 'string', format: 'string' },
28
- IsPositive: { name: 'IsPositive', type: 'number' },
27
+ IsNumber: { name: 'IsNumber', type: 'number'},
28
+ IsString: { name: 'IsString', type: 'string'},
29
+ IsPositive: { name: 'IsPositive'},
29
30
  IsDate: { name: 'IsDate', type: 'string', format: 'date-time' },
30
- IsEmail: { name: 'IsEmail', type: 'string', format: 'email' },
31
+ IsEmail: { name: 'IsEmail', format: 'email' },
31
32
  IsNotEmpty: { name: 'IsNotEmpty' },
33
+ IsOptional: { name: 'IsOptional' },
32
34
  IsBoolean: { name: 'IsBoolean', type: 'boolean' },
33
- IsArray: { name: 'IsArray', type: 'array' },
35
+ IsArray: { name: 'IsArray'},
34
36
  Min: { name: 'Min' },
35
37
  Max: { name: 'Max' },
36
38
  ArrayNotEmpty: { name: 'ArrayNotEmpty' },
37
39
  ArrayMaxSize: { name: 'ArrayMaxSize' },
38
40
  ArrayMinSize: { name: 'ArrayMinSize' },
39
- IsEnum: { name: 'IsEnum', type: 'string' },
41
+ IsEnum: { name: 'IsEnum'},
40
42
  };
41
43
  const constants = {
42
44
  TS_CONFIG_DEFAULT_PATH,
43
45
  jsPrimitives,
44
- validatorDecorators,
45
- };
46
+ validatorDecorators};
46
47
 
47
- /**
48
- * Transforms class-validator decorated classes into OpenAPI schema objects.
49
- * Analyzes TypeScript source files directly using the TypeScript compiler API.
50
- * Implemented as a singleton for performance optimization.
51
- *
52
- * @example
53
- * ```typescript
54
- * const transformer = SchemaTransformer.getInstance();
55
- * const schema = transformer.transform(User);
56
- * console.log(schema);
57
- * ```
58
- *
59
- * @public
60
- */
61
48
  class SchemaTransformer {
62
- /**
63
- * Singleton instance
64
- * @private
65
- */
66
49
  static instance = null;
67
- /**
68
- * TypeScript program instance for analyzing source files.
69
- * @private
70
- */
71
50
  program;
72
- /**
73
- * TypeScript type checker for resolving types.
74
- * @private
75
- */
76
51
  checker;
77
- /**
78
- * Cache for storing transformed class schemas to avoid reprocessing.
79
- * Key format: "fileName:className" for uniqueness across different files.
80
- * @private
81
- */
82
52
  classCache = new Map();
83
- /**
84
- * Maximum number of entries to keep in cache before cleanup
85
- * @private
86
- */
87
53
  maxCacheSize;
88
- /**
89
- * Whether to automatically clean up cache
90
- * @private
91
- */
92
54
  autoCleanup;
93
- /**
94
- * Set of file paths that have been loaded to avoid redundant processing
95
- * @private
96
- */
97
55
  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
56
  processingClasses = new Set();
104
- /**
105
- * Private constructor for singleton pattern.
106
- *
107
- * @param tsConfigPath - Optional path to a specific TypeScript config file
108
- * @param options - Configuration options for memory management
109
- * @throws {Error} When TypeScript configuration cannot be loaded
110
- * @private
111
- */
112
57
  constructor(tsConfigPath = constants.TS_CONFIG_DEFAULT_PATH, options = {}) {
113
- // Initialize configuration with defaults
114
58
  this.maxCacheSize = options.maxCacheSize ?? 100;
115
59
  this.autoCleanup = options.autoCleanup ?? true;
116
60
  const { config, error } = ts.readConfigFile(tsConfigPath || 'tsconfig.json', ts.sys.readFile);
@@ -122,493 +66,7 @@ class SchemaTransformer {
122
66
  this.program = ts.createProgram(fileNames, tsOptions);
123
67
  this.checker = this.program.getTypeChecker();
124
68
  }
125
- /**
126
- * Generates a unique cache key using file name and class name.
127
- *
128
- * @param fileName - The source file name
129
- * @param className - The class name
130
- * @returns Unique cache key in format "fileName:className"
131
- * @private
132
- */
133
- getCacheKey(fileName, className) {
134
- return `${fileName}:${className}`;
135
- }
136
- /**
137
- * Cleans up cache when it exceeds maximum size to prevent memory leaks.
138
- * Removes oldest entries using LRU strategy.
139
- * @private
140
- */
141
- cleanupCache() {
142
- if (!this.autoCleanup || this.classCache.size <= this.maxCacheSize) {
143
- return;
144
- }
145
- const entries = Array.from(this.classCache.entries());
146
- const toDelete = entries.slice(0, Math.floor(this.maxCacheSize / 2));
147
- for (const [key] of toDelete) {
148
- this.classCache.delete(key);
149
- }
150
- // Force garbage collection hint
151
- if (global.gc) {
152
- global.gc();
153
- }
154
- }
155
- /**
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.
159
- *
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
164
- * @private
165
- */
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 => {
169
- if (sf.isDeclarationFile)
170
- return false;
171
- if (sf.fileName.includes('.d.ts'))
172
- return false;
173
- if (sf.fileName.includes('node_modules'))
174
- return false;
175
- // Mark file as loaded for memory tracking
176
- this.loadedFiles.add(sf.fileName);
177
- return true;
178
- });
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) {
219
- const classNode = this.findClassByName(sourceFile, className);
220
- if (classNode && sourceFile?.fileName) {
221
- const cacheKey = this.getCacheKey(sourceFile.fileName, className);
222
- // Check cache first using fileName:className as key
223
- if (this.classCache.has(cacheKey)) {
224
- return this.classCache.get(cacheKey);
225
- }
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
- }
251
- }
252
- }
253
- throw new Error(`Class ${className} not found`);
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
- }
287
- /**
288
- * Gets the singleton instance of SchemaTransformer.
289
- *
290
- * @param tsConfigPath - Optional path to a specific TypeScript config file (only used on first call)
291
- * @param options - Configuration options for memory management (only used on first call)
292
- * @returns The singleton instance
293
- *
294
- * @example
295
- * ```typescript
296
- * const transformer = SchemaTransformer.getInstance();
297
- * ```
298
- *
299
- * @example
300
- * ```typescript
301
- * // With memory optimization options
302
- * const transformer = SchemaTransformer.getInstance('./tsconfig.json', {
303
- * maxCacheSize: 50,
304
- * autoCleanup: true
305
- * });
306
- * ```
307
- *
308
- * @public
309
- */
310
- /**
311
- * Clears the current singleton instance. Useful for testing or when you need
312
- * to create a new instance with different configuration.
313
- * @private
314
- */
315
- static clearInstance() {
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();
367
- }
368
- static getInstance(tsConfigPath, options) {
369
- if (!SchemaTransformer.instance || SchemaTransformer.isInstanceDisposed()) {
370
- SchemaTransformer.instance = new SchemaTransformer(tsConfigPath, options);
371
- }
372
- return SchemaTransformer.instance;
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
- }
395
- /**
396
- * Transforms a class constructor function into an OpenAPI schema object.
397
- *
398
- * @param cls - The class constructor function to transform
399
- * @returns Object containing the class name and its corresponding JSON schema
400
- *
401
- * @example
402
- * ```typescript
403
- * import { User } from './entities/user.js';
404
- * const transformer = SchemaTransformer.getInstance();
405
- * const schema = transformer.transform(User);
406
- * ```
407
- *
408
- * @public
409
- */
410
- transform(cls) {
411
- return this.transformByName(cls.name);
412
- }
413
- /**
414
- * Clears all cached schemas and loaded file references to free memory.
415
- * Useful for long-running applications or when processing many different classes.
416
- *
417
- * @example
418
- * ```typescript
419
- * const transformer = SchemaTransformer.getInstance();
420
- * // After processing many classes...
421
- * transformer.clearCache();
422
- * ```
423
- *
424
- * @public
425
- */
426
- clearCache() {
427
- this.classCache.clear();
428
- this.loadedFiles.clear();
429
- this.processingClasses.clear();
430
- // Force garbage collection hint if available
431
- if (global.gc) {
432
- global.gc();
433
- }
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
- }
503
- /**
504
- * Gets memory usage statistics for monitoring and debugging.
505
- *
506
- * @returns Object containing cache size, loaded files count, and processing status
507
- *
508
- * @example
509
- * ```typescript
510
- * const transformer = SchemaTransformer.getInstance();
511
- * const stats = transformer.getMemoryStats();
512
- * console.log(`Cache entries: ${stats.cacheSize}, Files loaded: ${stats.loadedFiles}`);
513
- * console.log(`Currently processing: ${stats.currentlyProcessing} classes`);
514
- * ```
515
- *
516
- * @private
517
- */
518
- getMemoryStats() {
519
- return {
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,
526
- };
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
- }
570
- /**
571
- * Finds a class declaration by name within a source file.
572
- *
573
- * @param sourceFile - The TypeScript source file to search in
574
- * @param className - The name of the class to find
575
- * @returns The class declaration node if found, undefined otherwise
576
- * @private
577
- */
578
- findClassByName(sourceFile, className) {
579
- let result;
580
- const visit = (node) => {
581
- if (ts.isClassDeclaration(node) && node.name?.text === className) {
582
- result = node;
583
- return;
584
- }
585
- ts.forEachChild(node, visit);
586
- };
587
- visit(sourceFile);
588
- return result;
589
- }
590
- /**
591
- * Transforms a TypeScript class declaration into a schema object.
592
- *
593
- * @param classNode - The TypeScript class declaration node
594
- * @param sourceFile - The source file containing the class (for context)
595
- * @returns Object containing class name and generated schema
596
- * @private
597
- */
598
- transformClass(classNode, sourceFile) {
599
- const className = classNode.name?.text || 'Unknown';
600
- const properties = this.extractProperties(classNode);
601
- const schema = this.generateSchema(properties, sourceFile?.fileName);
602
- return { name: className, schema };
603
- }
604
- /**
605
- * Extracts property information from a class declaration.
606
- *
607
- * @param classNode - The TypeScript class declaration node
608
- * @returns Array of property information including names, types, decorators, and optional status
609
- * @private
610
- */
611
- extractProperties(classNode) {
69
+ getPropertiesByClassDeclaration(classNode) {
612
70
  const properties = [];
613
71
  for (const member of classNode.members) {
614
72
  if (ts.isPropertyDeclaration(member) &&
@@ -618,272 +76,35 @@ class SchemaTransformer {
618
76
  const type = this.getPropertyType(member);
619
77
  const decorators = this.extractDecorators(member);
620
78
  const isOptional = !!member.questionToken;
621
- properties.push({
79
+ const isGeneric = this.isPropertyTypeGeneric(member);
80
+ const isPrimitive = this.isPrimitiveType(type);
81
+ const property = {
622
82
  name: propertyName,
623
83
  type,
624
84
  decorators,
625
85
  isOptional,
626
- });
86
+ isGeneric,
87
+ originalProperty: member,
88
+ isPrimitive,
89
+ isClassType: this.isClassType(member),
90
+ isArray: this.isArrayProperty(member),
91
+ };
92
+ properties.push(property);
627
93
  }
628
94
  }
629
95
  return properties;
630
96
  }
631
- /**
632
- * Gets the TypeScript type of a property as a string.
633
- *
634
- * @param property - The property declaration to analyze
635
- * @returns String representation of the property's type
636
- * @private
637
- */
638
97
  getPropertyType(property) {
639
98
  if (property.type) {
640
99
  return this.getTypeNodeToString(property.type);
641
100
  }
642
101
  const type = this.checker.getTypeAtLocation(property);
643
- return this.checker.typeToString(type);
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);
102
+ return this.getStringFromType(type);
875
103
  }
876
- /**
877
- * Converts a TypeScript type node to its string representation.
878
- *
879
- * @param typeNode - The TypeScript type node to convert
880
- * @returns String representation of the type
881
- * @private
882
- */
883
104
  getTypeNodeToString(typeNode) {
884
105
  if (ts.isTypeReferenceNode(typeNode) &&
885
106
  ts.isIdentifier(typeNode.typeName)) {
886
- if (typeNode.typeName.text.toLowerCase().includes('uploadfile')) {
107
+ if (typeNode.typeName.text.toLowerCase() === 'uploadfile') {
887
108
  return 'UploadFile';
888
109
  }
889
110
  if (typeNode.typeArguments && typeNode.typeArguments.length > 0) {
@@ -891,12 +112,9 @@ class SchemaTransformer {
891
112
  if (firstTypeArg &&
892
113
  ts.isTypeReferenceNode(firstTypeArg) &&
893
114
  ts.isIdentifier(firstTypeArg.typeName)) {
894
- if (firstTypeArg.typeName.text.toLowerCase().includes('uploadfile')) {
115
+ if (firstTypeArg.typeName.text.toLowerCase() === 'uploadfile') {
895
116
  return 'UploadFile';
896
117
  }
897
- if (typeNode.typeName.text === 'BaseDto') {
898
- return firstTypeArg.typeName.text;
899
- }
900
118
  }
901
119
  return this.resolveGenericType(typeNode);
902
120
  }
@@ -935,13 +153,24 @@ class SchemaTransformer {
935
153
  return typeText;
936
154
  }
937
155
  }
938
- /**
939
- * Extracts decorator information from a property declaration.
940
- *
941
- * @param member - The property declaration to analyze
942
- * @returns Array of decorator information including names and arguments
943
- * @private
944
- */
156
+ resolveGenericType(typeNode) {
157
+ const typeName = typeNode.typeName.text;
158
+ const typeArguments = typeNode.typeArguments;
159
+ if (!typeArguments || typeArguments.length === 0) {
160
+ return typeName;
161
+ }
162
+ const type = this.checker.getTypeAtLocation(typeNode);
163
+ const resolvedType = this.getStringFromType(type);
164
+ if (resolvedType &&
165
+ resolvedType !== typeName &&
166
+ !resolvedType.includes('any')) {
167
+ return resolvedType;
168
+ }
169
+ return typeName;
170
+ }
171
+ getStringFromType(type) {
172
+ return this.checker.typeToString(type);
173
+ }
945
174
  extractDecorators(member) {
946
175
  const decorators = [];
947
176
  if (member.modifiers) {
@@ -960,26 +189,12 @@ class SchemaTransformer {
960
189
  }
961
190
  return decorators;
962
191
  }
963
- /**
964
- * Gets the name of a decorator from a call expression.
965
- *
966
- * @param callExpression - The decorator call expression
967
- * @returns The decorator name or "unknown" if not identifiable
968
- * @private
969
- */
970
192
  getDecoratorName(callExpression) {
971
193
  if (ts.isIdentifier(callExpression.expression)) {
972
194
  return callExpression.expression.text;
973
195
  }
974
196
  return 'unknown';
975
197
  }
976
- /**
977
- * Extracts arguments from a decorator call expression.
978
- *
979
- * @param callExpression - The decorator call expression
980
- * @returns Array of parsed decorator arguments
981
- * @private
982
- */
983
198
  getDecoratorArguments(callExpression) {
984
199
  return callExpression.arguments.map(arg => {
985
200
  if (ts.isNumericLiteral(arg))
@@ -993,565 +208,501 @@ class SchemaTransformer {
993
208
  return arg.getText();
994
209
  });
995
210
  }
996
- /**
997
- * Generates an OpenAPI schema from extracted property information.
998
- *
999
- * @param properties - Array of property information to process
1000
- * @param contextFilePath - Optional context file path for resolving class references
1001
- * @returns Complete OpenAPI schema object with properties and validation rules
1002
- * @private
1003
- */
1004
- generateSchema(properties, contextFilePath) {
1005
- const schema = {
1006
- type: 'object',
1007
- properties: {},
1008
- required: [],
1009
- };
1010
- for (const property of properties) {
1011
- const { type, format, nestedSchema } = this.mapTypeToSchema(property.type, contextFilePath);
1012
- if (nestedSchema) {
1013
- schema.properties[property.name] = nestedSchema;
1014
- // Skip decorator application for $ref schemas
1015
- if (this.isRefSchema(nestedSchema)) {
1016
- continue;
211
+ isPropertyTypeGeneric(property) {
212
+ if (property.type && this.isGenericTypeFromNode(property.type)) {
213
+ return true;
214
+ }
215
+ try {
216
+ const type = this.checker.getTypeAtLocation(property);
217
+ return this.isGenericTypeFromSymbol(type);
218
+ }
219
+ catch (error) {
220
+ console.warn('Error analyzing property type for generics:', error);
221
+ return false;
222
+ }
223
+ }
224
+ isGenericTypeFromNode(typeNode) {
225
+ if (ts.isTypeReferenceNode(typeNode) && typeNode.typeArguments) {
226
+ return typeNode.typeArguments.length > 0;
227
+ }
228
+ // Check for mapped types (e.g., { [K in keyof T]: T[K] })
229
+ if (ts.isMappedTypeNode(typeNode)) {
230
+ return true;
231
+ }
232
+ // Check for conditional types (e.g., T extends U ? X : Y)
233
+ if (ts.isConditionalTypeNode(typeNode)) {
234
+ return true;
235
+ }
236
+ // Check for indexed access types (e.g., T[K])
237
+ if (ts.isIndexedAccessTypeNode(typeNode)) {
238
+ return true;
239
+ }
240
+ // Check for type operators like keyof, typeof
241
+ if (ts.isTypeOperatorNode(typeNode)) {
242
+ return true;
243
+ }
244
+ return false;
245
+ }
246
+ isGenericTypeFromSymbol(type) {
247
+ // First check if it's a simple array type - these should NOT be considered generic
248
+ if (this.isSimpleArrayType(type)) {
249
+ return false;
250
+ }
251
+ // Check if the type has type parameters
252
+ if (type.aliasTypeArguments && type.aliasTypeArguments.length > 0) {
253
+ return true;
254
+ }
255
+ // Check if it's a type reference with type arguments
256
+ // But exclude simple arrays which internally use Array<T> representation
257
+ if (type.typeArguments && type.typeArguments.length > 0) {
258
+ const symbol = type.getSymbol();
259
+ if (symbol && symbol.getName() === 'Array') {
260
+ // This is Array<T> - only consider it generic if T itself is a utility type
261
+ const elementType = type.typeArguments[0];
262
+ if (elementType) {
263
+ return this.isUtilityTypeFromType(elementType);
1017
264
  }
265
+ return false;
1018
266
  }
1019
- else {
1020
- schema.properties[property.name] = { type };
1021
- if (format)
1022
- schema.properties[property.name].format = format;
267
+ const elementType = type.typeArguments[0];
268
+ return this.isUtilityTypeFromType(elementType);
269
+ }
270
+ // Check type flags for generic indicators
271
+ if (type.flags & ts.TypeFlags.TypeParameter) {
272
+ return true;
273
+ }
274
+ if (type.flags & ts.TypeFlags.Conditional) {
275
+ return true;
276
+ }
277
+ if (type.flags & ts.TypeFlags.Index) {
278
+ return true;
279
+ }
280
+ if (type.flags & ts.TypeFlags.IndexedAccess) {
281
+ return true;
282
+ }
283
+ // Check if the type symbol indicates a generic type
284
+ const symbol = type.getSymbol();
285
+ if (symbol && symbol.declarations) {
286
+ for (const declaration of symbol.declarations) {
287
+ // Check for type alias declarations with type parameters
288
+ if (ts.isTypeAliasDeclaration(declaration) &&
289
+ declaration.typeParameters) {
290
+ return true;
291
+ }
292
+ // Check for interface declarations with type parameters
293
+ if (ts.isInterfaceDeclaration(declaration) &&
294
+ declaration.typeParameters) {
295
+ return true;
296
+ }
297
+ // Check for class declarations with type parameters
298
+ if (ts.isClassDeclaration(declaration) && declaration.typeParameters) {
299
+ return true;
300
+ }
301
+ }
302
+ }
303
+ return false;
304
+ }
305
+ isUtilityTypeFromType(type) {
306
+ if (!type.aliasSymbol)
307
+ return false;
308
+ const aliasName = type.aliasSymbol.getName();
309
+ const utilityTypes = [
310
+ 'Partial',
311
+ 'Required',
312
+ 'Readonly',
313
+ 'Pick',
314
+ 'Omit',
315
+ 'Record',
316
+ 'Exclude',
317
+ 'Extract',
318
+ 'NonNullable',
319
+ ];
320
+ return utilityTypes.includes(aliasName);
321
+ }
322
+ isSimpleArrayType(type) {
323
+ const symbol = type.getSymbol();
324
+ if (!symbol || symbol.getName() !== 'Array') {
325
+ return false;
326
+ }
327
+ // Check if this is Array<T> where T is a simple, non-generic type
328
+ if (type.typeArguments &&
329
+ type.typeArguments.length === 1) {
330
+ const elementType = type.typeArguments[0];
331
+ if (!elementType)
332
+ return false;
333
+ // If the element type is a utility type, then this array should be considered generic
334
+ if (this.isUtilityTypeFromType(elementType)) {
335
+ return false;
336
+ }
337
+ // If the element type itself has generic parameters, this array is generic
338
+ if (elementType.typeArguments &&
339
+ elementType.typeArguments.length > 0) {
340
+ return false;
341
+ }
342
+ return true;
343
+ }
344
+ return false;
345
+ }
346
+ isPrimitiveType(typeName) {
347
+ const lowerTypeName = typeName.toLowerCase();
348
+ // Check against all primitive types from constants
349
+ const primitiveTypes = [
350
+ constants.jsPrimitives.String.type.toLowerCase(),
351
+ constants.jsPrimitives.Number.type.toLowerCase(),
352
+ constants.jsPrimitives.Boolean.type.toLowerCase(),
353
+ constants.jsPrimitives.Date.type.toLowerCase(),
354
+ constants.jsPrimitives.Buffer.type.toLowerCase(),
355
+ constants.jsPrimitives.Uint8Array.type.toLowerCase(),
356
+ constants.jsPrimitives.File.type.toLowerCase(),
357
+ constants.jsPrimitives.UploadFile.type.toLowerCase(),
358
+ constants.jsPrimitives.BigInt.type.toLowerCase(),
359
+ constants.jsPrimitives.Symbol.type.toLowerCase(),
360
+ constants.jsPrimitives.null.type.toLowerCase(),
361
+ constants.jsPrimitives.Object.type.toLowerCase(),
362
+ constants.jsPrimitives.Array.type.toLowerCase(),
363
+ constants.jsPrimitives.Any.type.toLowerCase(),
364
+ constants.jsPrimitives.Unknown.type.toLowerCase(),
365
+ ];
366
+ const primitivesArray = primitiveTypes.map(t => t.concat('[]'));
367
+ return (primitiveTypes.includes(lowerTypeName) ||
368
+ primitivesArray.includes(lowerTypeName));
369
+ }
370
+ static getInstance(tsConfigPath, options) {
371
+ if (!SchemaTransformer.instance) {
372
+ SchemaTransformer.instance = new SchemaTransformer(tsConfigPath, options);
373
+ }
374
+ return SchemaTransformer.instance;
375
+ }
376
+ getSourceFileByClassName(className, sourceOptions) {
377
+ let sourceFiles = [];
378
+ if (sourceOptions?.isExternal) {
379
+ sourceFiles = this.program.getSourceFiles().filter(sf => {
380
+ return (sf.fileName.includes(sourceOptions.packageName) &&
381
+ (!sourceOptions.filePath || sf.fileName === sourceOptions.filePath));
382
+ });
383
+ }
384
+ else {
385
+ sourceFiles = this.program.getSourceFiles().filter(sf => {
386
+ if (sf.isDeclarationFile)
387
+ return false;
388
+ if (sf.fileName.includes('.d.ts'))
389
+ return false;
390
+ if (sf.fileName.includes('node_modules'))
391
+ return false;
392
+ return true;
393
+ });
394
+ }
395
+ for (const sourceFile of sourceFiles) {
396
+ let node;
397
+ const found = sourceFile.statements.some(stmt => {
398
+ node = stmt;
399
+ return (ts.isClassDeclaration(stmt) &&
400
+ stmt.name &&
401
+ stmt.name.text === className);
402
+ });
403
+ if (found) {
404
+ return { sourceFile, node: node };
1023
405
  }
1024
- // Apply decorators if present
1025
- this.applyDecorators(property.decorators, schema, property.name);
1026
- // If no decorators are present, apply type-based format specifications
1027
- if (property.decorators.length === 0) {
1028
- this.applyTypeBasedFormats(property, schema);
406
+ }
407
+ }
408
+ isClassType(propertyDeclaration) {
409
+ // If there's no explicit type annotation, we can't determine reliably
410
+ if (!propertyDeclaration.type) {
411
+ return false;
412
+ }
413
+ // Check if the original property type is an array type
414
+ if (this.isArrayProperty(propertyDeclaration) &&
415
+ ts.isTypeReferenceNode(propertyDeclaration.type
416
+ .elementType)) {
417
+ const type = this.checker.getTypeAtLocation(propertyDeclaration.type.elementType);
418
+ const symbol = type.getSymbol();
419
+ if (symbol && symbol.declarations) {
420
+ return symbol.declarations.some(decl => ts.isClassDeclaration(decl));
421
+ }
422
+ }
423
+ else if (ts.isTypeReferenceNode(propertyDeclaration.type)) {
424
+ const type = this.checker.getTypeAtLocation(propertyDeclaration.type);
425
+ const symbol = type.getSymbol();
426
+ if (symbol && symbol.declarations) {
427
+ return symbol.declarations.some(decl => ts.isClassDeclaration(decl));
428
+ }
429
+ }
430
+ return false;
431
+ }
432
+ getDeclarationProperty(property) {
433
+ if (!property.originalProperty.type) {
434
+ return undefined;
435
+ }
436
+ if (ts.isArrayTypeNode(property.originalProperty.type) &&
437
+ ts.isTypeReferenceNode(property.originalProperty.type.elementType)) {
438
+ const type = this.checker.getTypeAtLocation(property.originalProperty.type.elementType);
439
+ const symbol = type.getSymbol();
440
+ if (symbol && symbol.declarations) {
441
+ return symbol.declarations[0];
442
+ }
443
+ }
444
+ else if (ts.isTypeReferenceNode(property.originalProperty.type)) {
445
+ const type = this.checker.getTypeAtLocation(property.originalProperty.type);
446
+ const symbol = type.getSymbol();
447
+ if (symbol && symbol.declarations) {
448
+ return symbol.declarations[0];
449
+ }
450
+ }
451
+ return undefined;
452
+ }
453
+ isArrayProperty(propertyDeclaration) {
454
+ if (!propertyDeclaration.type) {
455
+ return false;
456
+ }
457
+ return ts.isArrayTypeNode(propertyDeclaration.type);
458
+ }
459
+ getSchemaFromProperties({ properties, visitedClass, transformedSchema, }) {
460
+ let schema = {};
461
+ const required = [];
462
+ for (const property of properties) {
463
+ schema[property.name] = this.getSchemaFromProperty({
464
+ property,
465
+ visitedClass,
466
+ transformedSchema,
467
+ });
468
+ // this.applyDecorators(property, schema as SchemaType)
469
+ if (!property.isOptional) {
470
+ required.push(property.name);
1029
471
  }
1030
- // Determine if property should be required based on decorators and optional status
1031
- this.determineRequiredStatus(property, schema);
1032
472
  }
473
+ return {
474
+ type: 'object',
475
+ properties: schema,
476
+ required: required.length ? required : undefined,
477
+ };
478
+ }
479
+ getSchemaFromProperty({ property, visitedClass, transformedSchema, }) {
480
+ let schema = {};
481
+ if (property.isPrimitive) {
482
+ schema = this.getSchemaFromPrimitive(property);
483
+ }
484
+ else if (property.isClassType) {
485
+ schema = this.getSchemaFromClass({
486
+ property,
487
+ visitedClass,
488
+ transformedSchema,
489
+ });
490
+ }
491
+ else {
492
+ schema = { type: 'object', properties: {}, additionalProperties: true };
493
+ }
494
+ this.applyDecorators(property, schema);
1033
495
  return schema;
1034
496
  }
1035
- /**
1036
- * Maps TypeScript types to OpenAPI schema types and formats.
1037
- * Handles primitive types, arrays, and nested objects recursively.
1038
- *
1039
- * @param type - The TypeScript type string to map
1040
- * @param contextFilePath - Optional context file path for resolving class references
1041
- * @returns Object containing OpenAPI type, optional format, and nested schema
1042
- * @private
1043
- */
1044
- mapTypeToSchema(type, contextFilePath) {
1045
- // Handle arrays
1046
- if (type.endsWith('[]')) {
1047
- const elementType = type.slice(0, -2);
1048
- const elementSchema = this.mapTypeToSchema(elementType, contextFilePath);
1049
- const items = elementSchema.nestedSchema || {
1050
- type: elementSchema.type,
1051
- };
1052
- if (elementSchema.format)
1053
- items.format = elementSchema.format;
497
+ getSchemaFromClass({ property, transformedSchema = new Map(), visitedClass = new Set(), }) {
498
+ let schema = { type: 'object' };
499
+ const declaration = this.getDeclarationProperty(property);
500
+ if (!declaration ||
501
+ !ts.isClassDeclaration(declaration) ||
502
+ !declaration.name) {
503
+ return { type: 'object' };
504
+ }
505
+ if (visitedClass.has(declaration)) {
506
+ if (transformedSchema.has(declaration.name.text)) {
507
+ return transformedSchema.get(declaration.name.text);
508
+ }
1054
509
  return {
1055
- type: 'array',
1056
- nestedSchema: {
1057
- type: 'array',
1058
- items,
1059
- properties: {},
1060
- required: [],
1061
- },
510
+ $ref: `#/components/schemas/${declaration.name.text}`,
1062
511
  };
1063
512
  }
1064
- if (type.toLocaleLowerCase().includes('uploadfile'))
1065
- type = 'UploadFile';
1066
- // Handle primitives
1067
- switch (type.toLowerCase()) {
1068
- case constants.jsPrimitives.String.type.toLowerCase():
1069
- return { type: constants.jsPrimitives.String.value };
1070
- case constants.jsPrimitives.Number.type.toLowerCase():
1071
- return { type: constants.jsPrimitives.Number.value };
1072
- case constants.jsPrimitives.Boolean.type.toLowerCase():
1073
- return { type: constants.jsPrimitives.Boolean.value };
1074
- case constants.jsPrimitives.Date.type.toLowerCase():
1075
- return {
1076
- type: constants.jsPrimitives.Date.value,
1077
- format: constants.jsPrimitives.Date.format,
1078
- };
1079
- case constants.jsPrimitives.Buffer.type.toLowerCase():
1080
- case constants.jsPrimitives.Uint8Array.type.toLowerCase():
1081
- case constants.jsPrimitives.File.type.toLowerCase():
1082
- return {
1083
- type: constants.jsPrimitives.Buffer.value,
1084
- format: constants.jsPrimitives.Buffer.format,
1085
- };
1086
- case constants.jsPrimitives.UploadFile.type.toLowerCase():
1087
- return {
1088
- type: constants.jsPrimitives.UploadFile.value,
1089
- format: constants.jsPrimitives.UploadFile.format,
1090
- };
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
- }
1107
- // Handle nested objects
1108
- try {
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
- }
1117
- return {
1118
- type: constants.jsPrimitives.Object.value,
1119
- nestedSchema: nestedResult.schema,
1120
- };
1121
- }
1122
- catch {
1123
- return { type: constants.jsPrimitives.Object.value };
1124
- }
513
+ visitedClass.add(declaration);
514
+ const properties = this.getPropertiesByClassDeclaration(declaration);
515
+ let transformerProps = this.getSchemaFromProperties({
516
+ properties,
517
+ visitedClass,
518
+ transformedSchema: transformedSchema,
519
+ });
520
+ if (property.isArray) {
521
+ schema.type = 'array';
522
+ schema.items = {
523
+ type: transformerProps.type,
524
+ properties: transformerProps.properties,
525
+ required: transformerProps.required,
526
+ };
1125
527
  }
528
+ else {
529
+ schema.type = transformerProps.type;
530
+ schema.properties = transformerProps.properties;
531
+ schema.required = transformerProps.required;
532
+ }
533
+ transformedSchema.set(declaration.name.text, schema);
534
+ if (schema.properties && Object.keys(schema.properties).length === 0) {
535
+ schema = { type: 'object', properties: {}, additionalProperties: true };
536
+ }
537
+ return schema;
1126
538
  }
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
- }
1137
- /**
1138
- * Applies class-validator decorators to schema properties.
1139
- * Maps validation decorators to their corresponding OpenAPI schema constraints.
1140
- *
1141
- * @param decorators - Array of decorator information to apply
1142
- * @param schema - The schema object to modify
1143
- * @param propertyName - Name of the property being processed
1144
- * @private
1145
- */
1146
- applyDecorators(decorators, schema, propertyName) {
1147
- // Skip applying decorators to $ref schemas
1148
- if (this.isRefSchema(schema)) {
1149
- return;
539
+ getSchemaFromPrimitive(property) {
540
+ const propertySchema = { type: 'object' };
541
+ const propertyType = property.type.toLowerCase().replace('[]', '').trim();
542
+ switch (propertyType) {
543
+ case constants.jsPrimitives.String.value:
544
+ propertySchema.type = constants.jsPrimitives.String.value;
545
+ break;
546
+ case constants.jsPrimitives.Number.value:
547
+ propertySchema.type = constants.jsPrimitives.Number.value;
548
+ propertySchema.format = constants.jsPrimitives.Number.format;
549
+ break;
550
+ case constants.jsPrimitives.BigInt.type.toLocaleLowerCase():
551
+ propertySchema.type = constants.jsPrimitives.BigInt.value;
552
+ propertySchema.format = constants.jsPrimitives.BigInt.format;
553
+ break;
554
+ case constants.jsPrimitives.Date.type.toLocaleLowerCase():
555
+ propertySchema.type = constants.jsPrimitives.Date.value;
556
+ propertySchema.format = constants.jsPrimitives.Date.format;
557
+ break;
558
+ case constants.jsPrimitives.Buffer.value:
559
+ case constants.jsPrimitives.Uint8Array.value:
560
+ case constants.jsPrimitives.File.value:
561
+ case constants.jsPrimitives.UploadFile.value:
562
+ propertySchema.type = constants.jsPrimitives.UploadFile.value;
563
+ propertySchema.format = constants.jsPrimitives.UploadFile.format;
564
+ break;
565
+ case constants.jsPrimitives.Array.value:
566
+ propertySchema.type = constants.jsPrimitives.Array.value;
567
+ break;
568
+ case constants.jsPrimitives.Boolean.value:
569
+ propertySchema.type = constants.jsPrimitives.Boolean.value;
570
+ break;
571
+ case constants.jsPrimitives.Symbol.type.toLocaleLowerCase():
572
+ propertySchema.type = constants.jsPrimitives.Symbol.value;
573
+ break;
574
+ case constants.jsPrimitives.Object.value:
575
+ propertySchema.type = constants.jsPrimitives.Object.value;
576
+ break;
577
+ default:
578
+ propertySchema.type = constants.jsPrimitives.String.value;
1150
579
  }
1151
- const isArrayType = schema.properties[propertyName].type ===
1152
- constants.jsPrimitives.Array.value;
1153
- for (const decorator of decorators) {
580
+ if (property.isArray) {
581
+ propertySchema.type = `array`;
582
+ propertySchema.items = { type: propertyType };
583
+ }
584
+ return propertySchema;
585
+ }
586
+ //Todo: implement properly
587
+ applyEnumDecorator(decorator, schema) { }
588
+ applyDecorators(property, schema) {
589
+ for (const decorator of property.decorators) {
1154
590
  const decoratorName = decorator.name;
1155
591
  switch (decoratorName) {
1156
592
  case constants.validatorDecorators.IsString.name:
1157
- if (!isArrayType) {
1158
- schema.properties[propertyName].type =
1159
- constants.validatorDecorators.IsString.type;
593
+ if (!property.isArray) {
594
+ schema.type = constants.validatorDecorators.IsString.type;
1160
595
  }
1161
- else if (schema.properties[propertyName].items) {
1162
- schema.properties[propertyName].items.type =
1163
- constants.validatorDecorators.IsString.type;
596
+ else if (schema.items) {
597
+ schema.items.type = constants.validatorDecorators.IsString.type;
1164
598
  }
1165
599
  break;
1166
600
  case constants.validatorDecorators.IsInt.name:
1167
- if (!isArrayType) {
1168
- schema.properties[propertyName].type =
1169
- constants.validatorDecorators.IsInt.type;
1170
- schema.properties[propertyName].format =
1171
- constants.validatorDecorators.IsInt.format;
601
+ if (!property.isArray) {
602
+ schema.type = constants.validatorDecorators.IsInt.type;
603
+ schema.format = constants.validatorDecorators.IsInt.format;
1172
604
  }
1173
- else if (schema.properties[propertyName].items) {
1174
- schema.properties[propertyName].items.type =
1175
- constants.validatorDecorators.IsInt.type;
1176
- schema.properties[propertyName].items.format =
1177
- constants.validatorDecorators.IsInt.format;
605
+ else if (schema.items) {
606
+ schema.items.type = constants.validatorDecorators.IsInt.type;
607
+ schema.items.format = constants.validatorDecorators.IsInt.format;
1178
608
  }
1179
609
  break;
1180
610
  case constants.validatorDecorators.IsNumber.name:
1181
- if (!isArrayType) {
1182
- schema.properties[propertyName].type =
1183
- constants.validatorDecorators.IsNumber.type;
611
+ if (!property.isArray) {
612
+ schema.type = constants.validatorDecorators.IsNumber.type;
1184
613
  }
1185
- else if (schema.properties[propertyName].items) {
1186
- schema.properties[propertyName].items.type =
1187
- constants.validatorDecorators.IsNumber.type;
614
+ else if (schema.items) {
615
+ schema.items.type = constants.validatorDecorators.IsNumber.type;
1188
616
  }
1189
617
  break;
1190
618
  case constants.validatorDecorators.IsBoolean.name:
1191
- if (!isArrayType) {
1192
- schema.properties[propertyName].type =
1193
- constants.validatorDecorators.IsBoolean.type;
619
+ if (!property.isArray) {
620
+ schema.type = constants.validatorDecorators.IsBoolean.type;
1194
621
  }
1195
- else if (schema.properties[propertyName].items) {
1196
- schema.properties[propertyName].items.type =
1197
- constants.validatorDecorators.IsBoolean.type;
622
+ else if (schema.items) {
623
+ schema.items.type = constants.validatorDecorators.IsBoolean.type;
1198
624
  }
1199
625
  break;
1200
626
  case constants.validatorDecorators.IsEmail.name:
1201
- if (!isArrayType) {
1202
- schema.properties[propertyName].format =
1203
- constants.validatorDecorators.IsEmail.format;
627
+ if (!property.isArray) {
628
+ schema.format = constants.validatorDecorators.IsEmail.format;
1204
629
  }
1205
- else if (schema.properties[propertyName].items) {
1206
- schema.properties[propertyName].items.format =
1207
- constants.validatorDecorators.IsEmail.format;
630
+ else if (schema.items) {
631
+ schema.items.format = constants.validatorDecorators.IsEmail.format;
1208
632
  }
1209
633
  break;
1210
634
  case constants.validatorDecorators.IsDate.name:
1211
- if (!isArrayType) {
1212
- schema.properties[propertyName].type =
1213
- constants.validatorDecorators.IsDate.type;
1214
- schema.properties[propertyName].format =
1215
- constants.validatorDecorators.IsDate.format;
635
+ if (!property.isArray) {
636
+ schema.type = constants.validatorDecorators.IsDate.type;
637
+ schema.format = constants.validatorDecorators.IsDate.format;
1216
638
  }
1217
- else if (schema.properties[propertyName].items) {
1218
- schema.properties[propertyName].items.type =
1219
- constants.validatorDecorators.IsDate.type;
1220
- schema.properties[propertyName].items.format =
1221
- constants.validatorDecorators.IsDate.format;
639
+ else if (schema.items) {
640
+ schema.items.type = constants.validatorDecorators.IsDate.type;
641
+ schema.items.format = constants.validatorDecorators.IsDate.format;
1222
642
  }
1223
643
  break;
1224
644
  case constants.validatorDecorators.IsNotEmpty.name:
1225
- if (!schema.required.includes(propertyName)) {
1226
- schema.required.push(propertyName);
1227
- }
645
+ property.isOptional = false;
646
+ break;
647
+ case constants.validatorDecorators.IsOptional.name:
648
+ property.isOptional = true;
1228
649
  break;
1229
650
  case constants.validatorDecorators.MinLength.name:
1230
- schema.properties[propertyName].minLength = decorator.arguments[0];
651
+ schema.minLength = decorator.arguments[0];
1231
652
  break;
1232
653
  case constants.validatorDecorators.MaxLength.name:
1233
- schema.properties[propertyName].maxLength = decorator.arguments[0];
654
+ schema.maxLength = decorator.arguments[0];
1234
655
  break;
1235
656
  case constants.validatorDecorators.Length.name:
1236
- schema.properties[propertyName].minLength = decorator.arguments[0];
657
+ schema.minLength = decorator.arguments[0];
1237
658
  if (decorator.arguments[1]) {
1238
- schema.properties[propertyName].maxLength = decorator.arguments[1];
659
+ schema.maxLength = decorator.arguments[1];
1239
660
  }
1240
661
  break;
1241
662
  case constants.validatorDecorators.Min.name:
1242
- schema.properties[propertyName].minimum = decorator.arguments[0];
663
+ schema.minimum = decorator.arguments[0];
1243
664
  break;
1244
665
  case constants.validatorDecorators.Max.name:
1245
- schema.properties[propertyName].maximum = decorator.arguments[0];
666
+ schema.maximum = decorator.arguments[0];
1246
667
  break;
1247
668
  case constants.validatorDecorators.IsPositive.name:
1248
- schema.properties[propertyName].minimum = 0;
669
+ schema.minimum = 0;
1249
670
  break;
1250
671
  case constants.validatorDecorators.IsArray.name:
1251
- schema.properties[propertyName].type =
1252
- constants.jsPrimitives.Array.value;
672
+ schema.type = constants.jsPrimitives.Array.value;
1253
673
  break;
1254
674
  case constants.validatorDecorators.ArrayNotEmpty.name:
1255
- schema.properties[propertyName].minItems = 1;
1256
- if (!schema.required.includes(propertyName)) {
1257
- schema.required.push(propertyName);
1258
- }
675
+ schema.minItems = 1;
676
+ property.isOptional = false;
1259
677
  break;
1260
678
  case constants.validatorDecorators.ArrayMinSize.name:
1261
- schema.properties[propertyName].minItems = decorator.arguments[0];
679
+ schema.minItems = decorator.arguments[0];
1262
680
  break;
1263
681
  case constants.validatorDecorators.ArrayMaxSize.name:
1264
- schema.properties[propertyName].maxItems = decorator.arguments[0];
682
+ schema.maxItems = decorator.arguments[0];
1265
683
  break;
1266
684
  case constants.validatorDecorators.IsEnum.name:
1267
- this.applyEnumDecorator(decorator, schema, propertyName, isArrayType);
685
+ this.applyEnumDecorator(decorator, schema);
1268
686
  break;
1269
687
  }
1270
688
  }
1271
689
  }
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
- }
1450
- }
1451
- }
1452
- return values;
1453
- }
1454
- /**
1455
- * Applies sensible default behaviors for properties without class-validator decorators.
1456
- * This allows the schema generator to work with plain TypeScript classes.
1457
- *
1458
- * @param property - The property information
1459
- * @param schema - The schema object to modify
1460
- * @private
1461
- */
1462
- /**
1463
- * Applies OpenAPI format specifications based on TypeScript types.
1464
- * This method is called when no decorators are present to set appropriate
1465
- * format values for primitive types according to OpenAPI specification.
1466
- *
1467
- * @param property - The property information containing type details
1468
- * @param schema - The schema object to modify
1469
- * @private
1470
- */
1471
- applyTypeBasedFormats(property, schema) {
1472
- // Skip applying type-based formats to $ref schemas
1473
- if (this.isRefSchema(schema)) {
1474
- return;
1475
- }
1476
- const propertyName = property.name;
1477
- const propertyType = property.type.toLowerCase();
1478
- const propertySchema = schema.properties[propertyName];
1479
- switch (propertyType) {
1480
- case constants.jsPrimitives.Number.value:
1481
- propertySchema.format = constants.jsPrimitives.Number.format;
1482
- break;
1483
- case constants.jsPrimitives.BigInt.value:
1484
- propertySchema.format = constants.jsPrimitives.BigInt.format;
1485
- break;
1486
- case constants.jsPrimitives.Date.value:
1487
- propertySchema.format = constants.jsPrimitives.Date.format;
1488
- break;
1489
- case constants.jsPrimitives.Buffer.value:
1490
- case constants.jsPrimitives.Uint8Array.value:
1491
- case constants.jsPrimitives.File.value:
1492
- case constants.jsPrimitives.UploadFile.value:
1493
- propertySchema.format = constants.jsPrimitives.UploadFile.format;
1494
- break;
1495
- }
1496
- }
1497
- /**
1498
- * Determines if a property should be required based on decorators and optional status.
1499
- *
1500
- * Logic:
1501
- * - If property has IsNotEmpty or ArrayNotEmpty decorator, it's required (handled in applyDecorators)
1502
- * - Otherwise, the property is not required (preserving original behavior)
1503
- * - The isOptional information is stored for future use and documentation
1504
- *
1505
- * @param property - The property information
1506
- * @param schema - The schema object to modify
1507
- * @private
1508
- */
1509
- determineRequiredStatus(property, schema) {
1510
- // Skip determining required status for $ref schemas
1511
- if (this.isRefSchema(schema)) {
1512
- return;
1513
- }
1514
- const propertyName = property.name;
1515
- // Check if already marked as required by IsNotEmpty or ArrayNotEmpty decorator
1516
- const isAlreadyRequired = schema.required.includes(propertyName);
1517
- // If already required by decorators, don't change it
1518
- if (isAlreadyRequired) {
1519
- return;
1520
- }
1521
- // If property is optional (has ?), it should not be required unless explicitly marked
1522
- if (property.isOptional) {
1523
- return;
690
+ transform(cls, sourceOptions) {
691
+ let schema = { type: 'object', properties: {} };
692
+ const result = this.getSourceFileByClassName(cls.name, sourceOptions);
693
+ if (!result?.sourceFile) {
694
+ console.warn(`Class ${cls.name} not found in any source file.`);
695
+ return { name: cls.name, schema: {} };
1524
696
  }
1525
- // If property is not optional and not already required, make it required
1526
- schema.required.push(propertyName);
697
+ const properties = this.getPropertiesByClassDeclaration(result.node);
698
+ schema = this.getSchemaFromProperties({ properties });
699
+ return { name: cls.name, schema };
1527
700
  }
1528
701
  }
1529
- /**
1530
- * Convenience function to transform a class using the singleton instance.
1531
- *
1532
- * @param cls - The class constructor function to transform
1533
- * @param options - Optional configuration for memory management
1534
- * @returns Object containing the class name and its corresponding JSON schema
1535
- *
1536
- * @example
1537
- * ```typescript
1538
- * import { transform } from 'class-validator-to-open-api'
1539
- * import { User } from './entities/user.js'
1540
- *
1541
- * const schema = transform(User)
1542
- * console.log(schema)
1543
- * ```
1544
- *
1545
- * @example
1546
- * ```typescript
1547
- * // With memory optimization
1548
- * const schema = transform(User, { maxCacheSize: 50, autoCleanup: true })
1549
- * ```
1550
- *
1551
- * @public
1552
- */
1553
702
  function transform(cls, options) {
1554
- return SchemaTransformer.transformClass(cls, options);
703
+ // Use the singleton instance instead of creating a temporary one
704
+ const transformer = SchemaTransformer.getInstance(undefined, options);
705
+ return transformer.transform(cls, options?.sourceOptions);
1555
706
  }
1556
707
 
1557
- export { SchemaTransformer, transform };
708
+ export { transform };