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