ts-class-to-openapi 1.0.4 → 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 (38) hide show
  1. package/README.md +369 -875
  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 -6
  8. package/dist/__test__/test.d.ts +4 -0
  9. package/dist/index.d.ts +2 -2
  10. package/dist/index.esm.js +476 -1075
  11. package/dist/index.js +476 -1075
  12. package/dist/run.d.ts +1 -1
  13. package/dist/run.js +1064 -1101
  14. package/dist/transformer.d.ts +1 -471
  15. package/dist/transformer.fixtures.d.ts +21 -0
  16. package/dist/types.d.ts +55 -6
  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/complete.entity.d.ts +0 -16
  22. package/dist/__test__/entities/complex-generics.entity.d.ts +0 -33
  23. package/dist/__test__/entities/comprehensive-enum.entity.d.ts +0 -23
  24. package/dist/__test__/entities/enum.entity.d.ts +0 -29
  25. package/dist/__test__/entities/generic.entity.d.ts +0 -11
  26. package/dist/__test__/entities/optional-properties.entity.d.ts +0 -11
  27. package/dist/__test__/entities/plain.entity.d.ts +0 -19
  28. package/dist/__test__/entities/simple.entity.d.ts +0 -5
  29. package/dist/__test__/entities/upload.entity.d.ts +0 -8
  30. package/dist/__test__/entities/user-role-generic.entity.d.ts +0 -13
  31. package/dist/__test__/test-entities/duplicate-name.entity.d.ts +0 -5
  32. package/dist/__test__/test-entities/generic.entity.d.ts +0 -11
  33. /package/dist/__test__/{enum.test.d.ts → testCases/debug.test.d.ts} +0 -0
  34. /package/dist/__test__/{generic-types.test.d.ts → testCases/decorated-classes.test.d.ts} +0 -0
  35. /package/dist/__test__/{integration.test.d.ts → testCases/edge-cases.test.d.ts} +0 -0
  36. /package/dist/__test__/{main.test.d.ts → testCases/nested-classes.test.d.ts} +0 -0
  37. /package/dist/__test__/{optional-properties.test.d.ts → testCases/pure-classes.test.d.ts} +0 -0
  38. /package/dist/__test__/{plain.test.d.ts → testCases/schema-validation.test.d.ts} +0 -0
package/dist/index.esm.js CHANGED
@@ -4,107 +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
- * Private constructor for singleton pattern.
100
- *
101
- * @param tsConfigPath - Optional path to a specific TypeScript config file
102
- * @param options - Configuration options for memory management
103
- * @throws {Error} When TypeScript configuration cannot be loaded
104
- * @private
105
- */
56
+ processingClasses = new Set();
106
57
  constructor(tsConfigPath = constants.TS_CONFIG_DEFAULT_PATH, options = {}) {
107
- // Initialize configuration with defaults
108
58
  this.maxCacheSize = options.maxCacheSize ?? 100;
109
59
  this.autoCleanup = options.autoCleanup ?? true;
110
60
  const { config, error } = ts.readConfigFile(tsConfigPath || 'tsconfig.json', ts.sys.readFile);
@@ -116,282 +66,7 @@ class SchemaTransformer {
116
66
  this.program = ts.createProgram(fileNames, tsOptions);
117
67
  this.checker = this.program.getTypeChecker();
118
68
  }
119
- /**
120
- * Generates a unique cache key using file name and class name.
121
- *
122
- * @param fileName - The source file name
123
- * @param className - The class name
124
- * @returns Unique cache key in format "fileName:className"
125
- * @private
126
- */
127
- getCacheKey(fileName, className) {
128
- return `${fileName}:${className}`;
129
- }
130
- /**
131
- * Cleans up cache when it exceeds maximum size to prevent memory leaks.
132
- * Removes oldest entries using LRU strategy.
133
- * @private
134
- */
135
- cleanupCache() {
136
- if (!this.autoCleanup || this.classCache.size <= this.maxCacheSize) {
137
- return;
138
- }
139
- const entries = Array.from(this.classCache.entries());
140
- const toDelete = entries.slice(0, Math.floor(this.maxCacheSize / 2));
141
- for (const [key] of toDelete) {
142
- this.classCache.delete(key);
143
- }
144
- // Force garbage collection hint
145
- if (global.gc) {
146
- global.gc();
147
- }
148
- }
149
- /**
150
- * Gets relevant source files for a class, filtering out unnecessary files to save memory.
151
- *
152
- * @param className - The name of the class to find files for
153
- * @param filePath - Optional specific file path
154
- * @returns Array of relevant source files
155
- * @private
156
- */
157
- getRelevantSourceFiles(className, filePath) {
158
- if (filePath) {
159
- const sourceFile = this.program.getSourceFile(filePath);
160
- return sourceFile ? [sourceFile] : [];
161
- }
162
- // Only get source files that are not declaration files and not in node_modules
163
- return this.program.getSourceFiles().filter(sf => {
164
- if (sf.isDeclarationFile)
165
- return false;
166
- if (sf.fileName.includes('.d.ts'))
167
- return false;
168
- if (sf.fileName.includes('node_modules'))
169
- return false;
170
- // Mark file as loaded for memory tracking
171
- this.loadedFiles.add(sf.fileName);
172
- return true;
173
- });
174
- }
175
- /**
176
- * Transforms a class by its name into an OpenAPI schema object.
177
- * Now considers the context of the calling file to resolve ambiguous class names.
178
- *
179
- * @param className - The name of the class to transform
180
- * @param filePath - Optional path to the file containing the class
181
- * @param contextFile - Optional context file for resolving class ambiguity
182
- * @returns Object containing the class name and its corresponding JSON schema
183
- * @throws {Error} When the specified class cannot be found
184
- * @private
185
- */
186
- transformByName(className, filePath, contextFile) {
187
- const sourceFiles = this.getRelevantSourceFiles(className, filePath);
188
- // If we have a context file, try to find the class in that file first
189
- if (contextFile) {
190
- const contextSourceFile = this.program.getSourceFile(contextFile);
191
- if (contextSourceFile) {
192
- const classNode = this.findClassByName(contextSourceFile, className);
193
- if (classNode) {
194
- const cacheKey = this.getCacheKey(contextSourceFile.fileName, className);
195
- // Check cache first
196
- if (this.classCache.has(cacheKey)) {
197
- return this.classCache.get(cacheKey);
198
- }
199
- const result = this.transformClass(classNode, contextSourceFile);
200
- this.classCache.set(cacheKey, result);
201
- this.cleanupCache();
202
- return result;
203
- }
204
- }
205
- }
206
- // Fallback to searching all files, but prioritize files that are more likely to be relevant
207
- const prioritizedFiles = this.prioritizeSourceFiles(sourceFiles, contextFile);
208
- for (const sourceFile of prioritizedFiles) {
209
- const classNode = this.findClassByName(sourceFile, className);
210
- if (classNode && sourceFile?.fileName) {
211
- const cacheKey = this.getCacheKey(sourceFile.fileName, className);
212
- // Check cache first using fileName:className as key
213
- if (this.classCache.has(cacheKey)) {
214
- return this.classCache.get(cacheKey);
215
- }
216
- const result = this.transformClass(classNode, sourceFile);
217
- // Cache using fileName:className as key for uniqueness
218
- this.classCache.set(cacheKey, result);
219
- // Clean up cache if it gets too large
220
- this.cleanupCache();
221
- return result;
222
- }
223
- }
224
- throw new Error(`Class ${className} not found`);
225
- }
226
- /**
227
- * Prioritizes source files based on context to resolve class name conflicts.
228
- * Gives priority to files in the same directory or with similar names.
229
- *
230
- * @param sourceFiles - Array of source files to prioritize
231
- * @param contextFile - Optional context file for prioritization
232
- * @returns Prioritized array of source files
233
- * @private
234
- */
235
- prioritizeSourceFiles(sourceFiles, contextFile) {
236
- if (!contextFile) {
237
- return sourceFiles;
238
- }
239
- const contextDir = contextFile.substring(0, contextFile.lastIndexOf('/'));
240
- return sourceFiles.sort((a, b) => {
241
- const aDir = a.fileName.substring(0, a.fileName.lastIndexOf('/'));
242
- const bDir = b.fileName.substring(0, b.fileName.lastIndexOf('/'));
243
- // Prioritize files in the same directory as context
244
- const aInSameDir = aDir === contextDir ? 1 : 0;
245
- const bInSameDir = bDir === contextDir ? 1 : 0;
246
- if (aInSameDir !== bInSameDir) {
247
- return bInSameDir - aInSameDir; // Higher priority first
248
- }
249
- // Prioritize non-test files over test files
250
- const aIsTest = a.fileName.includes('test') || a.fileName.includes('spec') ? 0 : 1;
251
- const bIsTest = b.fileName.includes('test') || b.fileName.includes('spec') ? 0 : 1;
252
- if (aIsTest !== bIsTest) {
253
- return bIsTest - aIsTest; // Non-test files first
254
- }
255
- return 0;
256
- });
257
- }
258
- /**
259
- * Gets the singleton instance of SchemaTransformer.
260
- *
261
- * @param tsConfigPath - Optional path to a specific TypeScript config file (only used on first call)
262
- * @param options - Configuration options for memory management (only used on first call)
263
- * @returns The singleton instance
264
- *
265
- * @example
266
- * ```typescript
267
- * const transformer = SchemaTransformer.getInstance();
268
- * ```
269
- *
270
- * @example
271
- * ```typescript
272
- * // With memory optimization options
273
- * const transformer = SchemaTransformer.getInstance('./tsconfig.json', {
274
- * maxCacheSize: 50,
275
- * autoCleanup: true
276
- * });
277
- * ```
278
- *
279
- * @public
280
- */
281
- /**
282
- * Clears the current singleton instance. Useful for testing or when you need
283
- * to create a new instance with different configuration.
284
- */
285
- static clearInstance() {
286
- SchemaTransformer.instance = null;
287
- }
288
- static getInstance(tsConfigPath, options) {
289
- if (!SchemaTransformer.instance) {
290
- SchemaTransformer.instance = new SchemaTransformer(tsConfigPath, options);
291
- }
292
- return SchemaTransformer.instance;
293
- }
294
- /**
295
- * Transforms a class constructor function into an OpenAPI schema object.
296
- *
297
- * @param cls - The class constructor function to transform
298
- * @returns Object containing the class name and its corresponding JSON schema
299
- *
300
- * @example
301
- * ```typescript
302
- * import { User } from './entities/user.js';
303
- * const transformer = SchemaTransformer.getInstance();
304
- * const schema = transformer.transform(User);
305
- * ```
306
- *
307
- * @public
308
- */
309
- transform(cls) {
310
- return this.transformByName(cls.name);
311
- }
312
- /**
313
- * Clears all cached schemas and loaded file references to free memory.
314
- * Useful for long-running applications or when processing many different classes.
315
- *
316
- * @example
317
- * ```typescript
318
- * const transformer = SchemaTransformer.getInstance();
319
- * // After processing many classes...
320
- * transformer.clearCache();
321
- * ```
322
- *
323
- * @public
324
- */
325
- clearCache() {
326
- this.classCache.clear();
327
- this.loadedFiles.clear();
328
- // Force garbage collection hint if available
329
- if (global.gc) {
330
- global.gc();
331
- }
332
- }
333
- /**
334
- * Gets memory usage statistics for monitoring and debugging.
335
- *
336
- * @returns Object containing cache size and loaded files count
337
- *
338
- * @example
339
- * ```typescript
340
- * const transformer = SchemaTransformer.getInstance();
341
- * const stats = transformer.getMemoryStats();
342
- * console.log(`Cache entries: ${stats.cacheSize}, Files loaded: ${stats.loadedFiles}`);
343
- * ```
344
- *
345
- * @public
346
- */
347
- getMemoryStats() {
348
- return {
349
- cacheSize: this.classCache.size,
350
- loadedFiles: this.loadedFiles.size,
351
- };
352
- }
353
- /**
354
- * Finds a class declaration by name within a source file.
355
- *
356
- * @param sourceFile - The TypeScript source file to search in
357
- * @param className - The name of the class to find
358
- * @returns The class declaration node if found, undefined otherwise
359
- * @private
360
- */
361
- findClassByName(sourceFile, className) {
362
- let result;
363
- const visit = (node) => {
364
- if (ts.isClassDeclaration(node) && node.name?.text === className) {
365
- result = node;
366
- return;
367
- }
368
- ts.forEachChild(node, visit);
369
- };
370
- visit(sourceFile);
371
- return result;
372
- }
373
- /**
374
- * Transforms a TypeScript class declaration into a schema object.
375
- *
376
- * @param classNode - The TypeScript class declaration node
377
- * @param sourceFile - The source file containing the class (for context)
378
- * @returns Object containing class name and generated schema
379
- * @private
380
- */
381
- transformClass(classNode, sourceFile) {
382
- const className = classNode.name?.text || 'Unknown';
383
- const properties = this.extractProperties(classNode);
384
- const schema = this.generateSchema(properties, sourceFile?.fileName);
385
- return { name: className, schema };
386
- }
387
- /**
388
- * Extracts property information from a class declaration.
389
- *
390
- * @param classNode - The TypeScript class declaration node
391
- * @returns Array of property information including names, types, decorators, and optional status
392
- * @private
393
- */
394
- extractProperties(classNode) {
69
+ getPropertiesByClassDeclaration(classNode) {
395
70
  const properties = [];
396
71
  for (const member of classNode.members) {
397
72
  if (ts.isPropertyDeclaration(member) &&
@@ -401,272 +76,35 @@ class SchemaTransformer {
401
76
  const type = this.getPropertyType(member);
402
77
  const decorators = this.extractDecorators(member);
403
78
  const isOptional = !!member.questionToken;
404
- properties.push({
79
+ const isGeneric = this.isPropertyTypeGeneric(member);
80
+ const isPrimitive = this.isPrimitiveType(type);
81
+ const property = {
405
82
  name: propertyName,
406
83
  type,
407
84
  decorators,
408
85
  isOptional,
409
- });
86
+ isGeneric,
87
+ originalProperty: member,
88
+ isPrimitive,
89
+ isClassType: this.isClassType(member),
90
+ isArray: this.isArrayProperty(member),
91
+ };
92
+ properties.push(property);
410
93
  }
411
94
  }
412
95
  return properties;
413
96
  }
414
- /**
415
- * Gets the TypeScript type of a property as a string.
416
- *
417
- * @param property - The property declaration to analyze
418
- * @returns String representation of the property's type
419
- * @private
420
- */
421
97
  getPropertyType(property) {
422
98
  if (property.type) {
423
99
  return this.getTypeNodeToString(property.type);
424
100
  }
425
101
  const type = this.checker.getTypeAtLocation(property);
426
- return this.checker.typeToString(type);
427
- }
428
- /**
429
- * Resolves generic types by analyzing the type alias and its arguments.
430
- * For example, User<Role> where User is a type alias will be resolved to its structure.
431
- *
432
- * @param typeNode - The TypeScript type reference node with generic arguments
433
- * @returns String representation of the resolved type or schema
434
- * @private
435
- */
436
- resolveGenericType(typeNode) {
437
- const typeName = typeNode.typeName.text;
438
- const typeArguments = typeNode.typeArguments;
439
- if (!typeArguments || typeArguments.length === 0) {
440
- return typeName;
441
- }
442
- // Try to resolve the type using the TypeScript type checker
443
- const type = this.checker.getTypeAtLocation(typeNode);
444
- const resolvedType = this.checker.typeToString(type);
445
- // If we can resolve it to a meaningful structure, use that
446
- if (resolvedType &&
447
- resolvedType !== typeName &&
448
- !resolvedType.includes('any')) {
449
- // For type aliases like User<Role>, we want to create a synthetic type name
450
- // that represents the resolved structure
451
- const typeArgNames = typeArguments.map(arg => {
452
- if (ts.isTypeReferenceNode(arg) && ts.isIdentifier(arg.typeName)) {
453
- return arg.typeName.text;
454
- }
455
- return this.getTypeNodeToString(arg);
456
- });
457
- return `${typeName}_${typeArgNames.join('_')}`;
458
- }
459
- return typeName;
460
- }
461
- /**
462
- * Checks if a type string represents a resolved generic type.
463
- *
464
- * @param type - The type string to check
465
- * @returns True if it's a resolved generic type
466
- * @private
467
- */
468
- isResolvedGenericType(type) {
469
- // Simple heuristic: resolved generic types contain underscores and
470
- // the parts after underscore should be known types
471
- const parts = type.split('_');
472
- return (parts.length > 1 &&
473
- parts
474
- .slice(1)
475
- .every(part => this.isKnownType(part) || this.isPrimitiveType(part)));
476
- }
477
- /**
478
- * Checks if a type is a known class or interface.
479
- *
480
- * @param typeName - The type name to check
481
- * @returns True if it's a known type
482
- * @private
483
- */
484
- isKnownType(typeName) {
485
- // First check if it's a primitive type to avoid unnecessary lookups
486
- if (this.isPrimitiveType(typeName)) {
487
- return true;
488
- }
489
- try {
490
- // Use a more conservative approach - check if we can find the class
491
- // without actually transforming it to avoid side effects
492
- const found = this.findClassInProject(typeName);
493
- return found !== null;
494
- }
495
- catch {
496
- return false;
497
- }
102
+ return this.getStringFromType(type);
498
103
  }
499
- /**
500
- * Finds a class by name in the project without transforming it.
501
- *
502
- * @param className - The class name to find
503
- * @returns True if found, false otherwise
504
- * @private
505
- */
506
- findClassInProject(className) {
507
- const sourceFiles = this.program.getSourceFiles().filter(sf => {
508
- if (sf.isDeclarationFile)
509
- return false;
510
- if (sf.fileName.includes('.d.ts'))
511
- return false;
512
- if (sf.fileName.includes('node_modules'))
513
- return false;
514
- return true;
515
- });
516
- for (const sourceFile of sourceFiles) {
517
- const found = this.findClassByName(sourceFile, className);
518
- if (found)
519
- return true;
520
- }
521
- return false;
522
- }
523
- /**
524
- * Checks if a type is a primitive type.
525
- *
526
- * @param typeName - The type name to check
527
- * @returns True if it's a primitive type
528
- * @private
529
- */
530
- isPrimitiveType(typeName) {
531
- const lowerTypeName = typeName.toLowerCase();
532
- // Check against all primitive types from constants
533
- const primitiveTypes = [
534
- constants.jsPrimitives.String.type.toLowerCase(),
535
- constants.jsPrimitives.Number.type.toLowerCase(),
536
- constants.jsPrimitives.Boolean.type.toLowerCase(),
537
- constants.jsPrimitives.Date.type.toLowerCase(),
538
- constants.jsPrimitives.Buffer.type.toLowerCase(),
539
- constants.jsPrimitives.Uint8Array.type.toLowerCase(),
540
- constants.jsPrimitives.File.type.toLowerCase(),
541
- constants.jsPrimitives.UploadFile.type.toLowerCase(),
542
- constants.jsPrimitives.BigInt.type.toLowerCase(),
543
- ];
544
- return primitiveTypes.includes(lowerTypeName);
545
- }
546
- /**
547
- * Resolves a generic type schema by analyzing the type alias structure.
548
- *
549
- * @param resolvedTypeName - The resolved generic type name (e.g., User_Role)
550
- * @returns OpenAPI schema for the resolved generic type
551
- * @private
552
- */
553
- resolveGenericTypeSchema(resolvedTypeName) {
554
- const parts = resolvedTypeName.split('_');
555
- const baseTypeName = parts[0];
556
- const typeArgNames = parts.slice(1);
557
- if (!baseTypeName) {
558
- return null;
559
- }
560
- // Find the original type alias declaration
561
- const typeAliasSymbol = this.findTypeAliasDeclaration(baseTypeName);
562
- if (!typeAliasSymbol) {
563
- return null;
564
- }
565
- // Create a schema based on the type alias structure, substituting type parameters
566
- return this.createSchemaFromTypeAlias(typeAliasSymbol, typeArgNames);
567
- }
568
- /**
569
- * Finds a type alias declaration by name.
570
- *
571
- * @param typeName - The type alias name to find
572
- * @returns The type alias declaration node or null
573
- * @private
574
- */
575
- findTypeAliasDeclaration(typeName) {
576
- for (const sourceFile of this.program.getSourceFiles()) {
577
- if (sourceFile.isDeclarationFile)
578
- continue;
579
- const findTypeAlias = (node) => {
580
- if (ts.isTypeAliasDeclaration(node) && node.name.text === typeName) {
581
- return node;
582
- }
583
- return ts.forEachChild(node, findTypeAlias) || null;
584
- };
585
- const result = findTypeAlias(sourceFile);
586
- if (result)
587
- return result;
588
- }
589
- return null;
590
- }
591
- /**
592
- * Creates a schema from a type alias declaration, substituting type parameters.
593
- *
594
- * @param typeAlias - The type alias declaration
595
- * @param typeArgNames - The concrete type arguments
596
- * @returns OpenAPI schema for the type alias
597
- * @private
598
- */
599
- createSchemaFromTypeAlias(typeAlias, typeArgNames) {
600
- const typeNode = typeAlias.type;
601
- if (ts.isTypeLiteralNode(typeNode)) {
602
- const schema = {
603
- type: 'object',
604
- properties: {},
605
- required: [],
606
- };
607
- for (const member of typeNode.members) {
608
- if (ts.isPropertySignature(member) &&
609
- member.name &&
610
- ts.isIdentifier(member.name)) {
611
- const propertyName = member.name.text;
612
- const isOptional = !!member.questionToken;
613
- if (member.type) {
614
- const propertyType = this.resolveTypeParameterInTypeAlias(member.type, typeAlias.typeParameters, typeArgNames);
615
- const { type, format, nestedSchema } = this.mapTypeToSchema(propertyType);
616
- if (nestedSchema) {
617
- schema.properties[propertyName] = nestedSchema;
618
- }
619
- else {
620
- schema.properties[propertyName] = { type };
621
- if (format)
622
- schema.properties[propertyName].format = format;
623
- }
624
- if (!isOptional) {
625
- schema.required.push(propertyName);
626
- }
627
- }
628
- }
629
- }
630
- return schema;
631
- }
632
- return null;
633
- }
634
- /**
635
- * Resolves type parameters in a type alias to concrete types.
636
- *
637
- * @param typeNode - The type node to resolve
638
- * @param typeParameters - The type parameters of the type alias
639
- * @param typeArgNames - The concrete type arguments
640
- * @returns The resolved type string
641
- * @private
642
- */
643
- resolveTypeParameterInTypeAlias(typeNode, typeParameters, typeArgNames) {
644
- if (ts.isTypeReferenceNode(typeNode) &&
645
- ts.isIdentifier(typeNode.typeName)) {
646
- const typeName = typeNode.typeName.text;
647
- // Check if this is a type parameter
648
- if (typeParameters) {
649
- const paramIndex = typeParameters.findIndex(param => param.name.text === typeName);
650
- if (paramIndex !== -1 && paramIndex < typeArgNames.length) {
651
- const resolvedType = typeArgNames[paramIndex];
652
- return resolvedType || typeName;
653
- }
654
- }
655
- return typeName;
656
- }
657
- return this.getTypeNodeToString(typeNode);
658
- }
659
- /**
660
- * Converts a TypeScript type node to its string representation.
661
- *
662
- * @param typeNode - The TypeScript type node to convert
663
- * @returns String representation of the type
664
- * @private
665
- */
666
104
  getTypeNodeToString(typeNode) {
667
105
  if (ts.isTypeReferenceNode(typeNode) &&
668
106
  ts.isIdentifier(typeNode.typeName)) {
669
- if (typeNode.typeName.text.toLowerCase().includes('uploadfile')) {
107
+ if (typeNode.typeName.text.toLowerCase() === 'uploadfile') {
670
108
  return 'UploadFile';
671
109
  }
672
110
  if (typeNode.typeArguments && typeNode.typeArguments.length > 0) {
@@ -674,12 +112,9 @@ class SchemaTransformer {
674
112
  if (firstTypeArg &&
675
113
  ts.isTypeReferenceNode(firstTypeArg) &&
676
114
  ts.isIdentifier(firstTypeArg.typeName)) {
677
- if (firstTypeArg.typeName.text.toLowerCase().includes('uploadfile')) {
115
+ if (firstTypeArg.typeName.text.toLowerCase() === 'uploadfile') {
678
116
  return 'UploadFile';
679
117
  }
680
- if (typeNode.typeName.text === 'BaseDto') {
681
- return firstTypeArg.typeName.text;
682
- }
683
118
  }
684
119
  return this.resolveGenericType(typeNode);
685
120
  }
@@ -718,13 +153,24 @@ class SchemaTransformer {
718
153
  return typeText;
719
154
  }
720
155
  }
721
- /**
722
- * Extracts decorator information from a property declaration.
723
- *
724
- * @param member - The property declaration to analyze
725
- * @returns Array of decorator information including names and arguments
726
- * @private
727
- */
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
+ }
728
174
  extractDecorators(member) {
729
175
  const decorators = [];
730
176
  if (member.modifiers) {
@@ -743,26 +189,12 @@ class SchemaTransformer {
743
189
  }
744
190
  return decorators;
745
191
  }
746
- /**
747
- * Gets the name of a decorator from a call expression.
748
- *
749
- * @param callExpression - The decorator call expression
750
- * @returns The decorator name or "unknown" if not identifiable
751
- * @private
752
- */
753
192
  getDecoratorName(callExpression) {
754
193
  if (ts.isIdentifier(callExpression.expression)) {
755
194
  return callExpression.expression.text;
756
195
  }
757
196
  return 'unknown';
758
197
  }
759
- /**
760
- * Extracts arguments from a decorator call expression.
761
- *
762
- * @param callExpression - The decorator call expression
763
- * @returns Array of parsed decorator arguments
764
- * @private
765
- */
766
198
  getDecoratorArguments(callExpression) {
767
199
  return callExpression.arguments.map(arg => {
768
200
  if (ts.isNumericLiteral(arg))
@@ -776,532 +208,501 @@ class SchemaTransformer {
776
208
  return arg.getText();
777
209
  });
778
210
  }
779
- /**
780
- * Generates an OpenAPI schema from extracted property information.
781
- *
782
- * @param properties - Array of property information to process
783
- * @param contextFile - Optional context file path for resolving class references
784
- * @returns Complete OpenAPI schema object with properties and validation rules
785
- * @private
786
- */
787
- generateSchema(properties, contextFile) {
788
- const schema = {
789
- type: 'object',
790
- properties: {},
791
- required: [],
792
- };
793
- for (const property of properties) {
794
- const { type, format, nestedSchema } = this.mapTypeToSchema(property.type, contextFile);
795
- if (nestedSchema) {
796
- schema.properties[property.name] = nestedSchema;
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);
264
+ }
265
+ return false;
266
+ }
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
+ }
797
301
  }
798
- else {
799
- schema.properties[property.name] = { type };
800
- if (format)
801
- schema.properties[property.name].format = format;
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;
802
336
  }
803
- // Apply decorators if present
804
- this.applyDecorators(property.decorators, schema, property.name);
805
- // If no decorators are present, apply type-based format specifications
806
- if (property.decorators.length === 0) {
807
- this.applyTypeBasedFormats(property, schema);
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;
808
341
  }
809
- // Determine if property should be required based on decorators and optional status
810
- this.determineRequiredStatus(property, schema);
342
+ return true;
811
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 };
405
+ }
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);
471
+ }
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);
812
495
  return schema;
813
496
  }
814
- /**
815
- * Maps TypeScript types to OpenAPI schema types and formats.
816
- * Handles primitive types, arrays, and nested objects recursively.
817
- *
818
- * @param type - The TypeScript type string to map
819
- * @param contextFile - Optional context file path for resolving class references
820
- * @returns Object containing OpenAPI type, optional format, and nested schema
821
- * @private
822
- */
823
- mapTypeToSchema(type, contextFile) {
824
- // Handle arrays
825
- if (type.endsWith('[]')) {
826
- const elementType = type.slice(0, -2);
827
- const elementSchema = this.mapTypeToSchema(elementType, contextFile);
828
- const items = elementSchema.nestedSchema || {
829
- type: elementSchema.type,
830
- };
831
- if (elementSchema.format)
832
- 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
+ }
833
509
  return {
834
- type: 'array',
835
- nestedSchema: {
836
- type: 'array',
837
- items,
838
- properties: {},
839
- required: [],
840
- },
510
+ $ref: `#/components/schemas/${declaration.name.text}`,
841
511
  };
842
512
  }
843
- if (type.toLocaleLowerCase().includes('uploadfile'))
844
- type = 'UploadFile';
845
- // Handle primitives
846
- switch (type.toLowerCase()) {
847
- case constants.jsPrimitives.String.type.toLowerCase():
848
- return { type: constants.jsPrimitives.String.value };
849
- case constants.jsPrimitives.Number.type.toLowerCase():
850
- return { type: constants.jsPrimitives.Number.value };
851
- case constants.jsPrimitives.Boolean.type.toLowerCase():
852
- return { type: constants.jsPrimitives.Boolean.value };
853
- case constants.jsPrimitives.Date.type.toLowerCase():
854
- return {
855
- type: constants.jsPrimitives.Date.value,
856
- format: constants.jsPrimitives.Date.format,
857
- };
858
- case constants.jsPrimitives.Buffer.type.toLowerCase():
859
- case constants.jsPrimitives.Uint8Array.type.toLowerCase():
860
- case constants.jsPrimitives.File.type.toLowerCase():
861
- return {
862
- type: constants.jsPrimitives.Buffer.value,
863
- format: constants.jsPrimitives.Buffer.format,
864
- };
865
- case constants.jsPrimitives.UploadFile.type.toLowerCase():
866
- return {
867
- type: constants.jsPrimitives.UploadFile.value,
868
- format: constants.jsPrimitives.UploadFile.format,
869
- };
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
+ };
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;
538
+ }
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;
870
577
  default:
871
- // Check if it's a resolved generic type (e.g., User_Role)
872
- if (type.includes('_') && this.isResolvedGenericType(type)) {
873
- try {
874
- const genericSchema = this.resolveGenericTypeSchema(type);
875
- if (genericSchema) {
876
- return {
877
- type: constants.jsPrimitives.Object.value,
878
- nestedSchema: genericSchema,
879
- };
880
- }
881
- }
882
- catch (error) {
883
- console.warn(`Failed to resolve generic type ${type}:`, error);
884
- }
885
- }
886
- // Handle nested objects
887
- try {
888
- const nestedResult = this.transformByName(type, undefined, contextFile);
889
- return {
890
- type: constants.jsPrimitives.Object.value,
891
- nestedSchema: nestedResult.schema,
892
- };
893
- }
894
- catch {
895
- return { type: constants.jsPrimitives.Object.value };
896
- }
578
+ propertySchema.type = constants.jsPrimitives.String.value;
579
+ }
580
+ if (property.isArray) {
581
+ propertySchema.type = `array`;
582
+ propertySchema.items = { type: propertyType };
897
583
  }
584
+ return propertySchema;
898
585
  }
899
- /**
900
- * Applies class-validator decorators to schema properties.
901
- * Maps validation decorators to their corresponding OpenAPI schema constraints.
902
- *
903
- * @param decorators - Array of decorator information to apply
904
- * @param schema - The schema object to modify
905
- * @param propertyName - Name of the property being processed
906
- * @private
907
- */
908
- applyDecorators(decorators, schema, propertyName) {
909
- const isArrayType = schema.properties[propertyName].type ===
910
- constants.jsPrimitives.Array.value;
911
- for (const decorator of decorators) {
586
+ //Todo: implement properly
587
+ applyEnumDecorator(decorator, schema) { }
588
+ applyDecorators(property, schema) {
589
+ for (const decorator of property.decorators) {
912
590
  const decoratorName = decorator.name;
913
591
  switch (decoratorName) {
914
592
  case constants.validatorDecorators.IsString.name:
915
- if (!isArrayType) {
916
- schema.properties[propertyName].type =
917
- constants.validatorDecorators.IsString.type;
593
+ if (!property.isArray) {
594
+ schema.type = constants.validatorDecorators.IsString.type;
918
595
  }
919
- else if (schema.properties[propertyName].items) {
920
- schema.properties[propertyName].items.type =
921
- constants.validatorDecorators.IsString.type;
596
+ else if (schema.items) {
597
+ schema.items.type = constants.validatorDecorators.IsString.type;
922
598
  }
923
599
  break;
924
600
  case constants.validatorDecorators.IsInt.name:
925
- if (!isArrayType) {
926
- schema.properties[propertyName].type =
927
- constants.validatorDecorators.IsInt.type;
928
- schema.properties[propertyName].format =
929
- constants.validatorDecorators.IsInt.format;
601
+ if (!property.isArray) {
602
+ schema.type = constants.validatorDecorators.IsInt.type;
603
+ schema.format = constants.validatorDecorators.IsInt.format;
930
604
  }
931
- else if (schema.properties[propertyName].items) {
932
- schema.properties[propertyName].items.type =
933
- constants.validatorDecorators.IsInt.type;
934
- schema.properties[propertyName].items.format =
935
- 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;
936
608
  }
937
609
  break;
938
610
  case constants.validatorDecorators.IsNumber.name:
939
- if (!isArrayType) {
940
- schema.properties[propertyName].type =
941
- constants.validatorDecorators.IsNumber.type;
611
+ if (!property.isArray) {
612
+ schema.type = constants.validatorDecorators.IsNumber.type;
942
613
  }
943
- else if (schema.properties[propertyName].items) {
944
- schema.properties[propertyName].items.type =
945
- constants.validatorDecorators.IsNumber.type;
614
+ else if (schema.items) {
615
+ schema.items.type = constants.validatorDecorators.IsNumber.type;
946
616
  }
947
617
  break;
948
618
  case constants.validatorDecorators.IsBoolean.name:
949
- if (!isArrayType) {
950
- schema.properties[propertyName].type =
951
- constants.validatorDecorators.IsBoolean.type;
619
+ if (!property.isArray) {
620
+ schema.type = constants.validatorDecorators.IsBoolean.type;
952
621
  }
953
- else if (schema.properties[propertyName].items) {
954
- schema.properties[propertyName].items.type =
955
- constants.validatorDecorators.IsBoolean.type;
622
+ else if (schema.items) {
623
+ schema.items.type = constants.validatorDecorators.IsBoolean.type;
956
624
  }
957
625
  break;
958
626
  case constants.validatorDecorators.IsEmail.name:
959
- if (!isArrayType) {
960
- schema.properties[propertyName].format =
961
- constants.validatorDecorators.IsEmail.format;
627
+ if (!property.isArray) {
628
+ schema.format = constants.validatorDecorators.IsEmail.format;
962
629
  }
963
- else if (schema.properties[propertyName].items) {
964
- schema.properties[propertyName].items.format =
965
- constants.validatorDecorators.IsEmail.format;
630
+ else if (schema.items) {
631
+ schema.items.format = constants.validatorDecorators.IsEmail.format;
966
632
  }
967
633
  break;
968
634
  case constants.validatorDecorators.IsDate.name:
969
- if (!isArrayType) {
970
- schema.properties[propertyName].type =
971
- constants.validatorDecorators.IsDate.type;
972
- schema.properties[propertyName].format =
973
- constants.validatorDecorators.IsDate.format;
635
+ if (!property.isArray) {
636
+ schema.type = constants.validatorDecorators.IsDate.type;
637
+ schema.format = constants.validatorDecorators.IsDate.format;
974
638
  }
975
- else if (schema.properties[propertyName].items) {
976
- schema.properties[propertyName].items.type =
977
- constants.validatorDecorators.IsDate.type;
978
- schema.properties[propertyName].items.format =
979
- 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;
980
642
  }
981
643
  break;
982
644
  case constants.validatorDecorators.IsNotEmpty.name:
983
- if (!schema.required.includes(propertyName)) {
984
- schema.required.push(propertyName);
985
- }
645
+ property.isOptional = false;
646
+ break;
647
+ case constants.validatorDecorators.IsOptional.name:
648
+ property.isOptional = true;
986
649
  break;
987
650
  case constants.validatorDecorators.MinLength.name:
988
- schema.properties[propertyName].minLength = decorator.arguments[0];
651
+ schema.minLength = decorator.arguments[0];
989
652
  break;
990
653
  case constants.validatorDecorators.MaxLength.name:
991
- schema.properties[propertyName].maxLength = decorator.arguments[0];
654
+ schema.maxLength = decorator.arguments[0];
992
655
  break;
993
656
  case constants.validatorDecorators.Length.name:
994
- schema.properties[propertyName].minLength = decorator.arguments[0];
657
+ schema.minLength = decorator.arguments[0];
995
658
  if (decorator.arguments[1]) {
996
- schema.properties[propertyName].maxLength = decorator.arguments[1];
659
+ schema.maxLength = decorator.arguments[1];
997
660
  }
998
661
  break;
999
662
  case constants.validatorDecorators.Min.name:
1000
- schema.properties[propertyName].minimum = decorator.arguments[0];
663
+ schema.minimum = decorator.arguments[0];
1001
664
  break;
1002
665
  case constants.validatorDecorators.Max.name:
1003
- schema.properties[propertyName].maximum = decorator.arguments[0];
666
+ schema.maximum = decorator.arguments[0];
1004
667
  break;
1005
668
  case constants.validatorDecorators.IsPositive.name:
1006
- schema.properties[propertyName].minimum = 0;
669
+ schema.minimum = 0;
1007
670
  break;
1008
671
  case constants.validatorDecorators.IsArray.name:
1009
- schema.properties[propertyName].type =
1010
- constants.jsPrimitives.Array.value;
672
+ schema.type = constants.jsPrimitives.Array.value;
1011
673
  break;
1012
674
  case constants.validatorDecorators.ArrayNotEmpty.name:
1013
- schema.properties[propertyName].minItems = 1;
1014
- if (!schema.required.includes(propertyName)) {
1015
- schema.required.push(propertyName);
1016
- }
675
+ schema.minItems = 1;
676
+ property.isOptional = false;
1017
677
  break;
1018
678
  case constants.validatorDecorators.ArrayMinSize.name:
1019
- schema.properties[propertyName].minItems = decorator.arguments[0];
679
+ schema.minItems = decorator.arguments[0];
1020
680
  break;
1021
681
  case constants.validatorDecorators.ArrayMaxSize.name:
1022
- schema.properties[propertyName].maxItems = decorator.arguments[0];
682
+ schema.maxItems = decorator.arguments[0];
1023
683
  break;
1024
684
  case constants.validatorDecorators.IsEnum.name:
1025
- this.applyEnumDecorator(decorator, schema, propertyName, isArrayType);
685
+ this.applyEnumDecorator(decorator, schema);
1026
686
  break;
1027
687
  }
1028
688
  }
1029
689
  }
1030
- /**
1031
- * Applies the @IsEnum decorator to a property, handling both primitive values and object enums.
1032
- * Supports arrays of enum values as well.
1033
- *
1034
- * @param decorator - The IsEnum decorator information
1035
- * @param schema - The schema object to modify
1036
- * @param propertyName - The name of the property
1037
- * @param isArrayType - Whether the property is an array type
1038
- * @private
1039
- */
1040
- applyEnumDecorator(decorator, schema, propertyName, isArrayType) {
1041
- if (!decorator.arguments || decorator.arguments.length === 0) {
1042
- return;
1043
- }
1044
- const enumArg = decorator.arguments[0];
1045
- let enumValues = [];
1046
- // Handle different enum argument types
1047
- if (typeof enumArg === 'string') {
1048
- // This is likely a reference to an enum type name
1049
- // We need to try to resolve this to actual enum values
1050
- enumValues = this.resolveEnumValues(enumArg);
1051
- }
1052
- else if (typeof enumArg === 'object' && enumArg !== null) {
1053
- // Object enum - extract values
1054
- if (Array.isArray(enumArg)) {
1055
- // Already an array of values
1056
- enumValues = enumArg;
1057
- }
1058
- else {
1059
- // Enum object - get all values
1060
- enumValues = Object.values(enumArg);
1061
- }
1062
- }
1063
- // If we couldn't resolve enum values, fall back to string type without enum constraint
1064
- if (enumValues.length === 0) {
1065
- if (!isArrayType) {
1066
- schema.properties[propertyName].type = 'string';
1067
- }
1068
- else if (schema.properties[propertyName].items) {
1069
- schema.properties[propertyName].items.type = 'string';
1070
- }
1071
- return;
1072
- }
1073
- // Determine the type based on enum values
1074
- let enumType = 'string';
1075
- if (enumValues.length > 0) {
1076
- const firstValue = enumValues[0];
1077
- if (typeof firstValue === 'number') {
1078
- enumType = 'number';
1079
- }
1080
- else if (typeof firstValue === 'boolean') {
1081
- enumType = 'boolean';
1082
- }
1083
- }
1084
- // Apply enum to schema
1085
- if (!isArrayType) {
1086
- schema.properties[propertyName].type = enumType;
1087
- schema.properties[propertyName].enum = enumValues;
1088
- }
1089
- else if (schema.properties[propertyName].items) {
1090
- schema.properties[propertyName].items.type = enumType;
1091
- schema.properties[propertyName].items.enum = enumValues;
1092
- }
1093
- }
1094
- /**
1095
- * Attempts to resolve enum values from an enum type name.
1096
- * This searches through the TypeScript AST to find the enum declaration
1097
- * and extract its values.
1098
- *
1099
- * @param enumTypeName - The name of the enum type
1100
- * @returns Array of enum values if found, empty array otherwise
1101
- * @private
1102
- */
1103
- resolveEnumValues(enumTypeName) {
1104
- // Search for enum declarations in source files
1105
- for (const sourceFile of this.program.getSourceFiles()) {
1106
- if (sourceFile.isDeclarationFile)
1107
- continue;
1108
- if (sourceFile.fileName.includes('node_modules'))
1109
- continue;
1110
- const enumValues = this.findEnumValues(sourceFile, enumTypeName);
1111
- if (enumValues.length > 0) {
1112
- return enumValues;
1113
- }
1114
- }
1115
- return [];
1116
- }
1117
- /**
1118
- * Finds enum values in a specific source file.
1119
- *
1120
- * @param sourceFile - The source file to search
1121
- * @param enumTypeName - The name of the enum to find
1122
- * @returns Array of enum values if found, empty array otherwise
1123
- * @private
1124
- */
1125
- findEnumValues(sourceFile, enumTypeName) {
1126
- let enumValues = [];
1127
- const visit = (node) => {
1128
- // Handle TypeScript enum declarations
1129
- if (ts.isEnumDeclaration(node) && node.name?.text === enumTypeName) {
1130
- enumValues = this.extractEnumValues(node);
1131
- return;
1132
- }
1133
- // Handle const object declarations (like const Status = { ... } as const)
1134
- if (ts.isVariableStatement(node)) {
1135
- for (const declaration of node.declarationList.declarations) {
1136
- if (ts.isVariableDeclaration(declaration) &&
1137
- ts.isIdentifier(declaration.name) &&
1138
- declaration.name.text === enumTypeName &&
1139
- declaration.initializer) {
1140
- let initializer = declaration.initializer;
1141
- // Handle "as const" assertions
1142
- if (ts.isAsExpression(initializer) && initializer.expression) {
1143
- initializer = initializer.expression;
1144
- }
1145
- enumValues = this.extractObjectEnumValues(initializer);
1146
- return;
1147
- }
1148
- }
1149
- }
1150
- ts.forEachChild(node, visit);
1151
- };
1152
- visit(sourceFile);
1153
- return enumValues;
1154
- }
1155
- /**
1156
- * Extracts values from a TypeScript enum declaration.
1157
- *
1158
- * @param enumNode - The enum declaration node
1159
- * @returns Array of enum values
1160
- * @private
1161
- */
1162
- extractEnumValues(enumNode) {
1163
- const values = [];
1164
- for (const member of enumNode.members) {
1165
- if (member.initializer) {
1166
- // Handle initialized enum members
1167
- if (ts.isStringLiteral(member.initializer)) {
1168
- values.push(member.initializer.text);
1169
- }
1170
- else if (ts.isNumericLiteral(member.initializer)) {
1171
- values.push(Number(member.initializer.text));
1172
- }
1173
- }
1174
- else {
1175
- // Handle auto-incremented numeric enums
1176
- if (values.length === 0) {
1177
- values.push(0);
1178
- }
1179
- else {
1180
- const lastValue = values[values.length - 1];
1181
- if (typeof lastValue === 'number') {
1182
- values.push(lastValue + 1);
1183
- }
1184
- }
1185
- }
1186
- }
1187
- return values;
1188
- }
1189
- /**
1190
- * Extracts values from object literal enum (const object as const).
1191
- *
1192
- * @param initializer - The object literal initializer
1193
- * @returns Array of enum values
1194
- * @private
1195
- */
1196
- extractObjectEnumValues(initializer) {
1197
- const values = [];
1198
- if (ts.isObjectLiteralExpression(initializer)) {
1199
- for (const property of initializer.properties) {
1200
- if (ts.isPropertyAssignment(property) && property.initializer) {
1201
- if (ts.isStringLiteral(property.initializer)) {
1202
- values.push(property.initializer.text);
1203
- }
1204
- else if (ts.isNumericLiteral(property.initializer)) {
1205
- values.push(Number(property.initializer.text));
1206
- }
1207
- }
1208
- }
1209
- }
1210
- return values;
1211
- }
1212
- /**
1213
- * Applies sensible default behaviors for properties without class-validator decorators.
1214
- * This allows the schema generator to work with plain TypeScript classes.
1215
- *
1216
- * @param property - The property information
1217
- * @param schema - The schema object to modify
1218
- * @private
1219
- */
1220
- /**
1221
- * Applies OpenAPI format specifications based on TypeScript types.
1222
- * This method is called when no decorators are present to set appropriate
1223
- * format values for primitive types according to OpenAPI specification.
1224
- *
1225
- * @param property - The property information containing type details
1226
- * @param schema - The schema object to modify
1227
- * @private
1228
- */
1229
- applyTypeBasedFormats(property, schema) {
1230
- const propertyName = property.name;
1231
- const propertyType = property.type.toLowerCase();
1232
- const propertySchema = schema.properties[propertyName];
1233
- switch (propertyType) {
1234
- case constants.jsPrimitives.Number.value:
1235
- propertySchema.format = constants.jsPrimitives.Number.format;
1236
- break;
1237
- case constants.jsPrimitives.BigInt.value:
1238
- propertySchema.format = constants.jsPrimitives.BigInt.format;
1239
- break;
1240
- case constants.jsPrimitives.Date.value:
1241
- propertySchema.format = constants.jsPrimitives.Date.format;
1242
- break;
1243
- case constants.jsPrimitives.Buffer.value:
1244
- case constants.jsPrimitives.Uint8Array.value:
1245
- case constants.jsPrimitives.File.value:
1246
- case constants.jsPrimitives.UploadFile.value:
1247
- propertySchema.format = constants.jsPrimitives.UploadFile.format;
1248
- break;
1249
- }
1250
- }
1251
- /**
1252
- * Determines if a property should be required based on decorators and optional status.
1253
- *
1254
- * Logic:
1255
- * - If property has IsNotEmpty or ArrayNotEmpty decorator, it's required (handled in applyDecorators)
1256
- * - Otherwise, the property is not required (preserving original behavior)
1257
- * - The isOptional information is stored for future use and documentation
1258
- *
1259
- * @param property - The property information
1260
- * @param schema - The schema object to modify
1261
- * @private
1262
- */
1263
- determineRequiredStatus(property, schema) {
1264
- const propertyName = property.name;
1265
- // Check if already marked as required by IsNotEmpty or ArrayNotEmpty decorator
1266
- const isAlreadyRequired = schema.required.includes(propertyName);
1267
- // If already required by decorators, don't change it
1268
- if (isAlreadyRequired) {
1269
- return;
1270
- }
1271
- // If property is optional (has ?), it should not be required unless explicitly marked
1272
- if (property.isOptional) {
1273
- 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: {} };
1274
696
  }
1275
- // If property is not optional and not already required, make it required
1276
- schema.required.push(propertyName);
697
+ const properties = this.getPropertiesByClassDeclaration(result.node);
698
+ schema = this.getSchemaFromProperties({ properties });
699
+ return { name: cls.name, schema };
1277
700
  }
1278
701
  }
1279
- /**
1280
- * Convenience function to transform a class using the singleton instance.
1281
- *
1282
- * @param cls - The class constructor function to transform
1283
- * @param options - Optional configuration for memory management
1284
- * @returns Object containing the class name and its corresponding JSON schema
1285
- *
1286
- * @example
1287
- * ```typescript
1288
- * import { transform } from 'class-validator-to-open-api'
1289
- * import { User } from './entities/user.js'
1290
- *
1291
- * const schema = transform(User)
1292
- * console.log(schema)
1293
- * ```
1294
- *
1295
- * @example
1296
- * ```typescript
1297
- * // With memory optimization
1298
- * const schema = transform(User, { maxCacheSize: 50, autoCleanup: true })
1299
- * ```
1300
- *
1301
- * @public
1302
- */
1303
702
  function transform(cls, options) {
1304
- return SchemaTransformer.getInstance(undefined, options).transform(cls);
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);
1305
706
  }
1306
707
 
1307
708
  export { transform };