ts-class-to-openapi 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,838 @@
1
+ import ts from 'typescript';
2
+ import path from 'path';
3
+
4
+ const TS_CONFIG_DEFAULT_PATH = path.resolve(process.cwd(), 'tsconfig.json');
5
+ const jsPrimitives = {
6
+ String: { type: 'String', value: 'string' },
7
+ Number: { type: 'Number', value: 'number' },
8
+ Boolean: { type: 'Boolean', value: 'boolean' },
9
+ Symbol: { type: 'Symbol', value: 'symbol' },
10
+ BigInt: { type: 'BigInt', value: 'integer' },
11
+ null: { type: 'null', value: 'null' },
12
+ Object: { type: 'Object', value: 'object' },
13
+ Array: { type: 'Array', value: 'array' },
14
+ 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' },
18
+ UploadFile: { type: 'UploadFile', value: 'string', format: 'binary' },
19
+ File: { type: 'File', value: 'string', format: 'binary' },
20
+ };
21
+ const validatorDecorators = {
22
+ Length: { name: 'Length', type: 'string' },
23
+ MinLength: { name: 'MinLength', type: 'string' },
24
+ MaxLength: { name: 'MaxLength', type: 'string' },
25
+ 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' },
29
+ IsDate: { name: 'IsDate', type: 'string', format: 'date-time' },
30
+ IsEmail: { name: 'IsEmail', type: 'string', format: 'email' },
31
+ IsNotEmpty: { name: 'IsNotEmpty' },
32
+ IsBoolean: { name: 'IsBoolean', type: 'boolean' },
33
+ IsArray: { name: 'IsArray', type: 'array' },
34
+ Min: { name: 'Min' },
35
+ Max: { name: 'Max' },
36
+ ArrayNotEmpty: { name: 'ArrayNotEmpty' },
37
+ ArrayMaxSize: { name: 'ArrayMaxSize' },
38
+ ArrayMinSize: { name: 'ArrayMinSize' },
39
+ };
40
+ const constants = {
41
+ TS_CONFIG_DEFAULT_PATH,
42
+ jsPrimitives,
43
+ validatorDecorators,
44
+ };
45
+
46
+ /**
47
+ * Transforms class-validator decorated classes into OpenAPI schema objects.
48
+ * Analyzes TypeScript source files directly using the TypeScript compiler API.
49
+ * Implemented as a singleton for performance optimization.
50
+ *
51
+ * @example
52
+ * ```typescript
53
+ * const transformer = SchemaTransformer.getInstance();
54
+ * const schema = transformer.transform(User);
55
+ * console.log(schema);
56
+ * ```
57
+ *
58
+ * @public
59
+ */
60
+ class SchemaTransformer {
61
+ /**
62
+ * Singleton instance
63
+ * @private
64
+ */
65
+ static instance = null;
66
+ /**
67
+ * TypeScript program instance for analyzing source files.
68
+ * @private
69
+ */
70
+ program;
71
+ /**
72
+ * TypeScript type checker for resolving types.
73
+ * @private
74
+ */
75
+ checker;
76
+ /**
77
+ * Cache for storing transformed class schemas to avoid reprocessing.
78
+ * Key format: "fileName:className" for uniqueness across different files.
79
+ * @private
80
+ */
81
+ classCache = new Map();
82
+ /**
83
+ * Maximum number of entries to keep in cache before cleanup
84
+ * @private
85
+ */
86
+ maxCacheSize;
87
+ /**
88
+ * Whether to automatically clean up cache
89
+ * @private
90
+ */
91
+ autoCleanup;
92
+ /**
93
+ * Set of file paths that have been loaded to avoid redundant processing
94
+ * @private
95
+ */
96
+ loadedFiles = new Set();
97
+ /**
98
+ * Private constructor for singleton pattern.
99
+ *
100
+ * @param tsConfigPath - Optional path to a specific TypeScript config file
101
+ * @param options - Configuration options for memory management
102
+ * @throws {Error} When TypeScript configuration cannot be loaded
103
+ * @private
104
+ */
105
+ constructor(tsConfigPath = constants.TS_CONFIG_DEFAULT_PATH, options = {}) {
106
+ // Initialize configuration with defaults
107
+ this.maxCacheSize = options.maxCacheSize ?? 100;
108
+ this.autoCleanup = options.autoCleanup ?? true;
109
+ const { config, error } = ts.readConfigFile(tsConfigPath || 'tsconfig.json', ts.sys.readFile);
110
+ if (error) {
111
+ console.log(new Error(`Error reading tsconfig file: ${error.messageText}`).message);
112
+ throw new Error(`Error reading tsconfig file: ${error.messageText}`);
113
+ }
114
+ const { options: tsOptions, fileNames } = ts.parseJsonConfigFileContent(config, ts.sys, './');
115
+ this.program = ts.createProgram(fileNames, tsOptions);
116
+ this.checker = this.program.getTypeChecker();
117
+ }
118
+ /**
119
+ * Generates a unique cache key using file name and class name.
120
+ *
121
+ * @param fileName - The source file name
122
+ * @param className - The class name
123
+ * @returns Unique cache key in format "fileName:className"
124
+ * @private
125
+ */
126
+ getCacheKey(fileName, className) {
127
+ return `${fileName}:${className}`;
128
+ }
129
+ /**
130
+ * Cleans up cache when it exceeds maximum size to prevent memory leaks.
131
+ * Removes oldest entries using LRU strategy.
132
+ * @private
133
+ */
134
+ cleanupCache() {
135
+ if (!this.autoCleanup || this.classCache.size <= this.maxCacheSize) {
136
+ return;
137
+ }
138
+ const entries = Array.from(this.classCache.entries());
139
+ const toDelete = entries.slice(0, Math.floor(this.maxCacheSize / 2));
140
+ for (const [key] of toDelete) {
141
+ this.classCache.delete(key);
142
+ }
143
+ // Force garbage collection hint
144
+ if (global.gc) {
145
+ global.gc();
146
+ }
147
+ }
148
+ /**
149
+ * Gets relevant source files for a class, filtering out unnecessary files to save memory.
150
+ *
151
+ * @param className - The name of the class to find files for
152
+ * @param filePath - Optional specific file path
153
+ * @returns Array of relevant source files
154
+ * @private
155
+ */
156
+ getRelevantSourceFiles(className, filePath) {
157
+ if (filePath) {
158
+ const sourceFile = this.program.getSourceFile(filePath);
159
+ return sourceFile ? [sourceFile] : [];
160
+ }
161
+ // Only get source files that are not declaration files and not in node_modules
162
+ return this.program.getSourceFiles().filter(sf => {
163
+ if (sf.isDeclarationFile)
164
+ return false;
165
+ if (sf.fileName.includes('.d.ts'))
166
+ return false;
167
+ if (sf.fileName.includes('node_modules'))
168
+ return false;
169
+ // Mark file as loaded for memory tracking
170
+ this.loadedFiles.add(sf.fileName);
171
+ return true;
172
+ });
173
+ }
174
+ /**
175
+ * Transforms a class by its name into an OpenAPI schema object.
176
+ *
177
+ * @param className - The name of the class to transform
178
+ * @param filePath - Optional path to the file containing the class
179
+ * @returns Object containing the class name and its corresponding JSON schema
180
+ * @throws {Error} When the specified class cannot be found
181
+ * @private
182
+ */
183
+ transformByName(className, filePath) {
184
+ const sourceFiles = this.getRelevantSourceFiles(className, filePath);
185
+ for (const sourceFile of sourceFiles) {
186
+ const classNode = this.findClassByName(sourceFile, className);
187
+ if (classNode && sourceFile?.fileName) {
188
+ const cacheKey = this.getCacheKey(sourceFile.fileName, className);
189
+ // Check cache first using fileName:className as key
190
+ if (this.classCache.has(cacheKey)) {
191
+ return this.classCache.get(cacheKey);
192
+ }
193
+ const result = this.transformClass(classNode);
194
+ // Cache using fileName:className as key for uniqueness
195
+ this.classCache.set(cacheKey, result);
196
+ // Clean up cache if it gets too large
197
+ this.cleanupCache();
198
+ return result;
199
+ }
200
+ }
201
+ throw new Error(`Class ${className} not found`);
202
+ }
203
+ /**
204
+ * Gets the singleton instance of SchemaTransformer.
205
+ *
206
+ * @param tsConfigPath - Optional path to a specific TypeScript config file (only used on first call)
207
+ * @param options - Configuration options for memory management (only used on first call)
208
+ * @returns The singleton instance
209
+ *
210
+ * @example
211
+ * ```typescript
212
+ * const transformer = SchemaTransformer.getInstance();
213
+ * ```
214
+ *
215
+ * @example
216
+ * ```typescript
217
+ * // With memory optimization options
218
+ * const transformer = SchemaTransformer.getInstance('./tsconfig.json', {
219
+ * maxCacheSize: 50,
220
+ * autoCleanup: true
221
+ * });
222
+ * ```
223
+ *
224
+ * @public
225
+ */
226
+ /**
227
+ * Clears the current singleton instance. Useful for testing or when you need
228
+ * to create a new instance with different configuration.
229
+ */
230
+ static clearInstance() {
231
+ SchemaTransformer.instance = null;
232
+ }
233
+ static getInstance(tsConfigPath, options) {
234
+ if (!SchemaTransformer.instance) {
235
+ SchemaTransformer.instance = new SchemaTransformer(tsConfigPath, options);
236
+ }
237
+ return SchemaTransformer.instance;
238
+ }
239
+ /**
240
+ * Transforms a class constructor function into an OpenAPI schema object.
241
+ *
242
+ * @param cls - The class constructor function to transform
243
+ * @returns Object containing the class name and its corresponding JSON schema
244
+ *
245
+ * @example
246
+ * ```typescript
247
+ * import { User } from './entities/user.js';
248
+ * const transformer = SchemaTransformer.getInstance();
249
+ * const schema = transformer.transform(User);
250
+ * ```
251
+ *
252
+ * @public
253
+ */
254
+ transform(cls) {
255
+ return this.transformByName(cls.name);
256
+ }
257
+ /**
258
+ * Clears all cached schemas and loaded file references to free memory.
259
+ * Useful for long-running applications or when processing many different classes.
260
+ *
261
+ * @example
262
+ * ```typescript
263
+ * const transformer = SchemaTransformer.getInstance();
264
+ * // After processing many classes...
265
+ * transformer.clearCache();
266
+ * ```
267
+ *
268
+ * @public
269
+ */
270
+ clearCache() {
271
+ this.classCache.clear();
272
+ this.loadedFiles.clear();
273
+ // Force garbage collection hint if available
274
+ if (global.gc) {
275
+ global.gc();
276
+ }
277
+ }
278
+ /**
279
+ * Gets memory usage statistics for monitoring and debugging.
280
+ *
281
+ * @returns Object containing cache size and loaded files count
282
+ *
283
+ * @example
284
+ * ```typescript
285
+ * const transformer = SchemaTransformer.getInstance();
286
+ * const stats = transformer.getMemoryStats();
287
+ * console.log(`Cache entries: ${stats.cacheSize}, Files loaded: ${stats.loadedFiles}`);
288
+ * ```
289
+ *
290
+ * @public
291
+ */
292
+ getMemoryStats() {
293
+ return {
294
+ cacheSize: this.classCache.size,
295
+ loadedFiles: this.loadedFiles.size,
296
+ };
297
+ }
298
+ /**
299
+ * Finds a class declaration by name within a source file.
300
+ *
301
+ * @param sourceFile - The TypeScript source file to search in
302
+ * @param className - The name of the class to find
303
+ * @returns The class declaration node if found, undefined otherwise
304
+ * @private
305
+ */
306
+ findClassByName(sourceFile, className) {
307
+ let result;
308
+ const visit = (node) => {
309
+ if (ts.isClassDeclaration(node) && node.name?.text === className) {
310
+ result = node;
311
+ return;
312
+ }
313
+ ts.forEachChild(node, visit);
314
+ };
315
+ visit(sourceFile);
316
+ return result;
317
+ }
318
+ /**
319
+ * Transforms a TypeScript class declaration into a schema object.
320
+ *
321
+ * @param classNode - The TypeScript class declaration node
322
+ * @returns Object containing class name and generated schema
323
+ * @private
324
+ */
325
+ transformClass(classNode) {
326
+ const className = classNode.name?.text || 'Unknown';
327
+ const properties = this.extractProperties(classNode);
328
+ const schema = this.generateSchema(properties);
329
+ return { name: className, schema };
330
+ }
331
+ /**
332
+ * Extracts property information from a class declaration.
333
+ *
334
+ * @param classNode - The TypeScript class declaration node
335
+ * @returns Array of property information including names, types, decorators, and optional status
336
+ * @private
337
+ */
338
+ extractProperties(classNode) {
339
+ const properties = [];
340
+ for (const member of classNode.members) {
341
+ if (ts.isPropertyDeclaration(member) &&
342
+ member.name &&
343
+ ts.isIdentifier(member.name)) {
344
+ const propertyName = member.name.text;
345
+ const type = this.getPropertyType(member);
346
+ const decorators = this.extractDecorators(member);
347
+ const isOptional = !!member.questionToken;
348
+ properties.push({
349
+ name: propertyName,
350
+ type,
351
+ decorators,
352
+ isOptional,
353
+ });
354
+ }
355
+ }
356
+ return properties;
357
+ }
358
+ /**
359
+ * Gets the TypeScript type of a property as a string.
360
+ *
361
+ * @param property - The property declaration to analyze
362
+ * @returns String representation of the property's type
363
+ * @private
364
+ */
365
+ getPropertyType(property) {
366
+ if (property.type) {
367
+ return this.getTypeNodeToString(property.type);
368
+ }
369
+ const type = this.checker.getTypeAtLocation(property);
370
+ return this.checker.typeToString(type);
371
+ }
372
+ /**
373
+ * Converts a TypeScript type node to its string representation.
374
+ *
375
+ * @param typeNode - The TypeScript type node to convert
376
+ * @returns String representation of the type
377
+ * @private
378
+ */
379
+ getTypeNodeToString(typeNode) {
380
+ if (ts.isTypeReferenceNode(typeNode) &&
381
+ ts.isIdentifier(typeNode.typeName)) {
382
+ if (typeNode.typeName.text.toLowerCase().includes('uploadfile')) {
383
+ return 'UploadFile';
384
+ }
385
+ if (typeNode.typeArguments && typeNode.typeArguments.length > 0) {
386
+ const firstTypeArg = typeNode.typeArguments[0];
387
+ if (firstTypeArg &&
388
+ ts.isTypeReferenceNode(firstTypeArg) &&
389
+ ts.isIdentifier(firstTypeArg.typeName)) {
390
+ if (firstTypeArg.typeName.text.toLowerCase().includes('uploadfile')) {
391
+ return 'UploadFile';
392
+ }
393
+ if (typeNode.typeName.text === 'BaseDto') {
394
+ return firstTypeArg.typeName.text;
395
+ }
396
+ }
397
+ }
398
+ return typeNode.typeName.text;
399
+ }
400
+ switch (typeNode.kind) {
401
+ case ts.SyntaxKind.StringKeyword:
402
+ return constants.jsPrimitives.String.type;
403
+ case ts.SyntaxKind.NumberKeyword:
404
+ return constants.jsPrimitives.Number.type;
405
+ case ts.SyntaxKind.BooleanKeyword:
406
+ return constants.jsPrimitives.Boolean.type;
407
+ case ts.SyntaxKind.ArrayType:
408
+ const arrayType = typeNode;
409
+ return `${this.getTypeNodeToString(arrayType.elementType)}[]`;
410
+ case ts.SyntaxKind.UnionType:
411
+ // Handle union types like string | null
412
+ const unionType = typeNode;
413
+ const types = unionType.types.map(t => this.getTypeNodeToString(t));
414
+ // Filter out null and undefined, return the first meaningful type
415
+ const meaningfulTypes = types.filter(t => t !== 'null' && t !== 'undefined');
416
+ if (meaningfulTypes.length > 0 && meaningfulTypes[0]) {
417
+ return meaningfulTypes[0];
418
+ }
419
+ if (types.length > 0 && types[0]) {
420
+ return types[0];
421
+ }
422
+ return 'object';
423
+ default:
424
+ const typeText = typeNode.getText();
425
+ // Handle some common TypeScript utility types
426
+ if (typeText.startsWith('Date'))
427
+ return constants.jsPrimitives.Date.type;
428
+ if (typeText.includes('Buffer') || typeText.includes('Uint8Array'))
429
+ return constants.jsPrimitives.Buffer.type;
430
+ return typeText;
431
+ }
432
+ }
433
+ /**
434
+ * Extracts decorator information from a property declaration.
435
+ *
436
+ * @param member - The property declaration to analyze
437
+ * @returns Array of decorator information including names and arguments
438
+ * @private
439
+ */
440
+ extractDecorators(member) {
441
+ const decorators = [];
442
+ if (member.modifiers) {
443
+ for (const modifier of member.modifiers) {
444
+ if (ts.isDecorator(modifier) &&
445
+ ts.isCallExpression(modifier.expression)) {
446
+ const decoratorName = this.getDecoratorName(modifier.expression);
447
+ const args = this.getDecoratorArguments(modifier.expression);
448
+ decorators.push({ name: decoratorName, arguments: args });
449
+ }
450
+ else if (ts.isDecorator(modifier) &&
451
+ ts.isIdentifier(modifier.expression)) {
452
+ decorators.push({ name: modifier.expression.text, arguments: [] });
453
+ }
454
+ }
455
+ }
456
+ return decorators;
457
+ }
458
+ /**
459
+ * Gets the name of a decorator from a call expression.
460
+ *
461
+ * @param callExpression - The decorator call expression
462
+ * @returns The decorator name or "unknown" if not identifiable
463
+ * @private
464
+ */
465
+ getDecoratorName(callExpression) {
466
+ if (ts.isIdentifier(callExpression.expression)) {
467
+ return callExpression.expression.text;
468
+ }
469
+ return 'unknown';
470
+ }
471
+ /**
472
+ * Extracts arguments from a decorator call expression.
473
+ *
474
+ * @param callExpression - The decorator call expression
475
+ * @returns Array of parsed decorator arguments
476
+ * @private
477
+ */
478
+ getDecoratorArguments(callExpression) {
479
+ return callExpression.arguments.map(arg => {
480
+ if (ts.isNumericLiteral(arg))
481
+ return Number(arg.text);
482
+ if (ts.isStringLiteral(arg))
483
+ return arg.text;
484
+ if (arg.kind === ts.SyntaxKind.TrueKeyword)
485
+ return true;
486
+ if (arg.kind === ts.SyntaxKind.FalseKeyword)
487
+ return false;
488
+ return arg.getText();
489
+ });
490
+ }
491
+ /**
492
+ * Generates an OpenAPI schema from extracted property information.
493
+ *
494
+ * @param properties - Array of property information to process
495
+ * @returns Complete OpenAPI schema object with properties and validation rules
496
+ * @private
497
+ */
498
+ generateSchema(properties) {
499
+ const schema = {
500
+ type: 'object',
501
+ properties: {},
502
+ required: [],
503
+ };
504
+ for (const property of properties) {
505
+ const { type, format, nestedSchema } = this.mapTypeToSchema(property.type);
506
+ if (nestedSchema) {
507
+ schema.properties[property.name] = nestedSchema;
508
+ }
509
+ else {
510
+ schema.properties[property.name] = { type };
511
+ if (format)
512
+ schema.properties[property.name].format = format;
513
+ }
514
+ // Apply decorators if present
515
+ this.applyDecorators(property.decorators, schema, property.name);
516
+ // If no decorators are present, apply sensible defaults based on TypeScript types
517
+ if (property.decorators.length === 0) {
518
+ this.applySensibleDefaults(property, schema);
519
+ }
520
+ // Determine if property should be required based on decorators and optional status
521
+ this.determineRequiredStatus(property, schema);
522
+ }
523
+ return schema;
524
+ }
525
+ /**
526
+ * Maps TypeScript types to OpenAPI schema types and formats.
527
+ * Handles primitive types, arrays, and nested objects recursively.
528
+ *
529
+ * @param type - The TypeScript type string to map
530
+ * @returns Object containing OpenAPI type, optional format, and nested schema
531
+ * @private
532
+ */
533
+ mapTypeToSchema(type) {
534
+ // Handle arrays
535
+ if (type.endsWith('[]')) {
536
+ const elementType = type.slice(0, -2);
537
+ const elementSchema = this.mapTypeToSchema(elementType);
538
+ const items = elementSchema.nestedSchema || {
539
+ type: elementSchema.type,
540
+ };
541
+ if (elementSchema.format)
542
+ items.format = elementSchema.format;
543
+ return {
544
+ type: 'array',
545
+ nestedSchema: {
546
+ type: 'array',
547
+ items,
548
+ properties: {},
549
+ required: [],
550
+ },
551
+ };
552
+ }
553
+ if (type.toLocaleLowerCase().includes('uploadfile'))
554
+ type = 'UploadFile';
555
+ // Handle primitives
556
+ switch (type.toLowerCase()) {
557
+ case constants.jsPrimitives.String.type.toLowerCase():
558
+ return { type: constants.jsPrimitives.String.value };
559
+ case constants.jsPrimitives.Number.type.toLowerCase():
560
+ return { type: constants.jsPrimitives.Number.value };
561
+ case constants.jsPrimitives.Boolean.type.toLowerCase():
562
+ return { type: constants.jsPrimitives.Boolean.value };
563
+ case constants.jsPrimitives.Date.type.toLowerCase():
564
+ return {
565
+ type: constants.jsPrimitives.Date.value,
566
+ format: constants.jsPrimitives.Date.format,
567
+ };
568
+ case constants.jsPrimitives.Buffer.type.toLowerCase():
569
+ case constants.jsPrimitives.Uint8Array.type.toLowerCase():
570
+ case constants.jsPrimitives.File.type.toLowerCase():
571
+ return {
572
+ type: constants.jsPrimitives.Buffer.value,
573
+ format: constants.jsPrimitives.Buffer.format,
574
+ };
575
+ case constants.jsPrimitives.UploadFile.type.toLowerCase():
576
+ return {
577
+ type: constants.jsPrimitives.UploadFile.value,
578
+ format: constants.jsPrimitives.UploadFile.format,
579
+ };
580
+ default:
581
+ // Handle nested objects
582
+ try {
583
+ const nestedResult = this.transformByName(type);
584
+ return {
585
+ type: constants.jsPrimitives.Object.value,
586
+ nestedSchema: nestedResult.schema,
587
+ };
588
+ }
589
+ catch {
590
+ return { type: constants.jsPrimitives.Object.value };
591
+ }
592
+ }
593
+ }
594
+ /**
595
+ * Applies class-validator decorators to schema properties.
596
+ * Maps validation decorators to their corresponding OpenAPI schema constraints.
597
+ *
598
+ * @param decorators - Array of decorator information to apply
599
+ * @param schema - The schema object to modify
600
+ * @param propertyName - Name of the property being processed
601
+ * @private
602
+ */
603
+ applyDecorators(decorators, schema, propertyName) {
604
+ const isArrayType = schema.properties[propertyName].type ===
605
+ constants.jsPrimitives.Array.value;
606
+ for (const decorator of decorators) {
607
+ const decoratorName = decorator.name;
608
+ switch (decoratorName) {
609
+ case constants.validatorDecorators.IsString.name:
610
+ if (!isArrayType) {
611
+ schema.properties[propertyName].type =
612
+ constants.validatorDecorators.IsString.type;
613
+ }
614
+ else if (schema.properties[propertyName].items) {
615
+ schema.properties[propertyName].items.type =
616
+ constants.validatorDecorators.IsString.type;
617
+ }
618
+ break;
619
+ case constants.validatorDecorators.IsInt.name:
620
+ if (!isArrayType) {
621
+ schema.properties[propertyName].type =
622
+ constants.validatorDecorators.IsInt.type;
623
+ schema.properties[propertyName].format =
624
+ constants.validatorDecorators.IsInt.format;
625
+ }
626
+ else if (schema.properties[propertyName].items) {
627
+ schema.properties[propertyName].items.type =
628
+ constants.validatorDecorators.IsInt.type;
629
+ schema.properties[propertyName].items.format =
630
+ constants.validatorDecorators.IsInt.format;
631
+ }
632
+ break;
633
+ case constants.validatorDecorators.IsNumber.name:
634
+ if (!isArrayType) {
635
+ schema.properties[propertyName].type =
636
+ constants.validatorDecorators.IsNumber.type;
637
+ }
638
+ else if (schema.properties[propertyName].items) {
639
+ schema.properties[propertyName].items.type =
640
+ constants.validatorDecorators.IsNumber.type;
641
+ }
642
+ break;
643
+ case constants.validatorDecorators.IsBoolean.name:
644
+ if (!isArrayType) {
645
+ schema.properties[propertyName].type =
646
+ constants.validatorDecorators.IsBoolean.type;
647
+ }
648
+ else if (schema.properties[propertyName].items) {
649
+ schema.properties[propertyName].items.type =
650
+ constants.validatorDecorators.IsBoolean.type;
651
+ }
652
+ break;
653
+ case constants.validatorDecorators.IsEmail.name:
654
+ if (!isArrayType) {
655
+ schema.properties[propertyName].format =
656
+ constants.validatorDecorators.IsEmail.format;
657
+ }
658
+ else if (schema.properties[propertyName].items) {
659
+ schema.properties[propertyName].items.format =
660
+ constants.validatorDecorators.IsEmail.format;
661
+ }
662
+ break;
663
+ case constants.validatorDecorators.IsDate.name:
664
+ if (!isArrayType) {
665
+ schema.properties[propertyName].type =
666
+ constants.validatorDecorators.IsDate.type;
667
+ schema.properties[propertyName].format =
668
+ constants.validatorDecorators.IsDate.format;
669
+ }
670
+ else if (schema.properties[propertyName].items) {
671
+ schema.properties[propertyName].items.type =
672
+ constants.validatorDecorators.IsDate.type;
673
+ schema.properties[propertyName].items.format =
674
+ constants.validatorDecorators.IsDate.format;
675
+ }
676
+ break;
677
+ case constants.validatorDecorators.IsNotEmpty.name:
678
+ if (!schema.required.includes(propertyName)) {
679
+ schema.required.push(propertyName);
680
+ }
681
+ break;
682
+ case constants.validatorDecorators.MinLength.name:
683
+ schema.properties[propertyName].minLength = decorator.arguments[0];
684
+ break;
685
+ case constants.validatorDecorators.MaxLength.name:
686
+ schema.properties[propertyName].maxLength = decorator.arguments[0];
687
+ break;
688
+ case constants.validatorDecorators.Length.name:
689
+ schema.properties[propertyName].minLength = decorator.arguments[0];
690
+ if (decorator.arguments[1]) {
691
+ schema.properties[propertyName].maxLength = decorator.arguments[1];
692
+ }
693
+ break;
694
+ case constants.validatorDecorators.Min.name:
695
+ schema.properties[propertyName].minimum = decorator.arguments[0];
696
+ break;
697
+ case constants.validatorDecorators.Max.name:
698
+ schema.properties[propertyName].maximum = decorator.arguments[0];
699
+ break;
700
+ case constants.validatorDecorators.IsPositive.name:
701
+ schema.properties[propertyName].minimum = 0;
702
+ break;
703
+ case constants.validatorDecorators.IsArray.name:
704
+ schema.properties[propertyName].type =
705
+ constants.jsPrimitives.Array.value;
706
+ break;
707
+ case constants.validatorDecorators.ArrayNotEmpty.name:
708
+ schema.properties[propertyName].minItems = 1;
709
+ if (!schema.required.includes(propertyName)) {
710
+ schema.required.push(propertyName);
711
+ }
712
+ break;
713
+ case constants.validatorDecorators.ArrayMinSize.name:
714
+ schema.properties[propertyName].minItems = decorator.arguments[0];
715
+ break;
716
+ case constants.validatorDecorators.ArrayMaxSize.name:
717
+ schema.properties[propertyName].maxItems = decorator.arguments[0];
718
+ break;
719
+ }
720
+ }
721
+ }
722
+ /**
723
+ * Applies sensible default behaviors for properties without class-validator decorators.
724
+ * This allows the schema generator to work with plain TypeScript classes.
725
+ *
726
+ * @param property - The property information
727
+ * @param schema - The schema object to modify
728
+ * @private
729
+ */
730
+ applySensibleDefaults(property, schema) {
731
+ const propertyName = property.name;
732
+ property.type.toLowerCase();
733
+ // Add examples based on property names and types
734
+ const propertySchema = schema.properties[propertyName];
735
+ // Add common format hints based on property names
736
+ if (propertyName.includes('email') && propertySchema.type === 'string') {
737
+ propertySchema.format = 'email';
738
+ }
739
+ else if (propertyName.includes('password') &&
740
+ propertySchema.type === 'string') {
741
+ propertySchema.format = 'password';
742
+ propertySchema.minLength = 8;
743
+ }
744
+ else if (propertyName.includes('url') &&
745
+ propertySchema.type === 'string') {
746
+ propertySchema.format = 'uri';
747
+ }
748
+ else if (propertyName.includes('phone') &&
749
+ propertySchema.type === 'string') {
750
+ propertySchema.pattern = '^[+]?[1-9]\\d{1,14}$';
751
+ }
752
+ // Add reasonable constraints based on common property names
753
+ if (propertySchema.type === 'string') {
754
+ if (propertyName === 'name' ||
755
+ propertyName === 'firstName' ||
756
+ propertyName === 'lastName') {
757
+ propertySchema.minLength = 1;
758
+ propertySchema.maxLength = 100;
759
+ }
760
+ else if (propertyName === 'description' || propertyName === 'bio') {
761
+ propertySchema.maxLength = 500;
762
+ }
763
+ else if (propertyName === 'title') {
764
+ propertySchema.minLength = 1;
765
+ propertySchema.maxLength = 200;
766
+ }
767
+ }
768
+ if (propertySchema.type === 'integer' || propertySchema.type === 'number') {
769
+ if (propertyName === 'age') {
770
+ propertySchema.minimum = 0;
771
+ propertySchema.maximum = 150;
772
+ }
773
+ else if (propertyName === 'id') {
774
+ propertySchema.minimum = 1;
775
+ }
776
+ else if (propertyName.includes('count') ||
777
+ propertyName.includes('quantity')) {
778
+ propertySchema.minimum = 0;
779
+ }
780
+ }
781
+ }
782
+ /**
783
+ * Determines if a property should be required based on decorators and optional status.
784
+ *
785
+ * Logic:
786
+ * - If property has IsNotEmpty or ArrayNotEmpty decorator, it's required (handled in applyDecorators)
787
+ * - Otherwise, the property is not required (preserving original behavior)
788
+ * - The isOptional information is stored for future use and documentation
789
+ *
790
+ * @param property - The property information
791
+ * @param schema - The schema object to modify
792
+ * @private
793
+ */
794
+ determineRequiredStatus(property, schema) {
795
+ const propertyName = property.name;
796
+ // Check if already marked as required by IsNotEmpty or ArrayNotEmpty decorator
797
+ const isAlreadyRequired = schema.required.includes(propertyName);
798
+ // If already required by decorators, don't change it
799
+ if (isAlreadyRequired) {
800
+ return;
801
+ }
802
+ // If property is optional (has ?), it should not be required unless explicitly marked
803
+ if (property.isOptional) {
804
+ return;
805
+ }
806
+ // If property is not optional and not already required, make it required
807
+ schema.required.push(propertyName);
808
+ }
809
+ }
810
+ /**
811
+ * Convenience function to transform a class using the singleton instance.
812
+ *
813
+ * @param cls - The class constructor function to transform
814
+ * @param options - Optional configuration for memory management
815
+ * @returns Object containing the class name and its corresponding JSON schema
816
+ *
817
+ * @example
818
+ * ```typescript
819
+ * import { transform } from 'class-validator-to-open-api'
820
+ * import { User } from './entities/user.js'
821
+ *
822
+ * const schema = transform(User)
823
+ * console.log(schema)
824
+ * ```
825
+ *
826
+ * @example
827
+ * ```typescript
828
+ * // With memory optimization
829
+ * const schema = transform(User, { maxCacheSize: 50, autoCleanup: true })
830
+ * ```
831
+ *
832
+ * @public
833
+ */
834
+ function transform(cls, options) {
835
+ return SchemaTransformer.getInstance(undefined, options).transform(cls);
836
+ }
837
+
838
+ export { transform };