ts-class-to-openapi 1.0.2 → 1.0.4

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/index.esm.js CHANGED
@@ -4,10 +4,10 @@ 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' },
7
+ Number: { type: 'Number', value: 'number', format: 'float' },
8
8
  Boolean: { type: 'Boolean', value: 'boolean' },
9
9
  Symbol: { type: 'Symbol', value: 'symbol' },
10
- BigInt: { type: 'BigInt', value: 'integer' },
10
+ BigInt: { type: 'BigInt', value: 'integer', format: 'int64' },
11
11
  null: { type: 'null', value: 'null' },
12
12
  Object: { type: 'Object', value: 'object' },
13
13
  Array: { type: 'Array', value: 'array' },
@@ -36,6 +36,7 @@ const validatorDecorators = {
36
36
  ArrayNotEmpty: { name: 'ArrayNotEmpty' },
37
37
  ArrayMaxSize: { name: 'ArrayMaxSize' },
38
38
  ArrayMinSize: { name: 'ArrayMinSize' },
39
+ IsEnum: { name: 'IsEnum', type: 'string' },
39
40
  };
40
41
  const constants = {
41
42
  TS_CONFIG_DEFAULT_PATH,
@@ -173,16 +174,38 @@ class SchemaTransformer {
173
174
  }
174
175
  /**
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.
176
178
  *
177
179
  * @param className - The name of the class to transform
178
180
  * @param filePath - Optional path to the file containing the class
181
+ * @param contextFile - Optional context file for resolving class ambiguity
179
182
  * @returns Object containing the class name and its corresponding JSON schema
180
183
  * @throws {Error} When the specified class cannot be found
181
184
  * @private
182
185
  */
183
- transformByName(className, filePath) {
186
+ transformByName(className, filePath, contextFile) {
184
187
  const sourceFiles = this.getRelevantSourceFiles(className, filePath);
185
- for (const sourceFile of sourceFiles) {
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) {
186
209
  const classNode = this.findClassByName(sourceFile, className);
187
210
  if (classNode && sourceFile?.fileName) {
188
211
  const cacheKey = this.getCacheKey(sourceFile.fileName, className);
@@ -190,7 +213,7 @@ class SchemaTransformer {
190
213
  if (this.classCache.has(cacheKey)) {
191
214
  return this.classCache.get(cacheKey);
192
215
  }
193
- const result = this.transformClass(classNode);
216
+ const result = this.transformClass(classNode, sourceFile);
194
217
  // Cache using fileName:className as key for uniqueness
195
218
  this.classCache.set(cacheKey, result);
196
219
  // Clean up cache if it gets too large
@@ -200,6 +223,38 @@ class SchemaTransformer {
200
223
  }
201
224
  throw new Error(`Class ${className} not found`);
202
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
+ }
203
258
  /**
204
259
  * Gets the singleton instance of SchemaTransformer.
205
260
  *
@@ -319,13 +374,14 @@ class SchemaTransformer {
319
374
  * Transforms a TypeScript class declaration into a schema object.
320
375
  *
321
376
  * @param classNode - The TypeScript class declaration node
377
+ * @param sourceFile - The source file containing the class (for context)
322
378
  * @returns Object containing class name and generated schema
323
379
  * @private
324
380
  */
325
- transformClass(classNode) {
381
+ transformClass(classNode, sourceFile) {
326
382
  const className = classNode.name?.text || 'Unknown';
327
383
  const properties = this.extractProperties(classNode);
328
- const schema = this.generateSchema(properties);
384
+ const schema = this.generateSchema(properties, sourceFile?.fileName);
329
385
  return { name: className, schema };
330
386
  }
331
387
  /**
@@ -369,6 +425,237 @@ class SchemaTransformer {
369
425
  const type = this.checker.getTypeAtLocation(property);
370
426
  return this.checker.typeToString(type);
371
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
+ }
498
+ }
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
+ }
372
659
  /**
373
660
  * Converts a TypeScript type node to its string representation.
374
661
  *
@@ -394,6 +681,7 @@ class SchemaTransformer {
394
681
  return firstTypeArg.typeName.text;
395
682
  }
396
683
  }
684
+ return this.resolveGenericType(typeNode);
397
685
  }
398
686
  return typeNode.typeName.text;
399
687
  }
@@ -492,17 +780,18 @@ class SchemaTransformer {
492
780
  * Generates an OpenAPI schema from extracted property information.
493
781
  *
494
782
  * @param properties - Array of property information to process
783
+ * @param contextFile - Optional context file path for resolving class references
495
784
  * @returns Complete OpenAPI schema object with properties and validation rules
496
785
  * @private
497
786
  */
498
- generateSchema(properties) {
787
+ generateSchema(properties, contextFile) {
499
788
  const schema = {
500
789
  type: 'object',
501
790
  properties: {},
502
791
  required: [],
503
792
  };
504
793
  for (const property of properties) {
505
- const { type, format, nestedSchema } = this.mapTypeToSchema(property.type);
794
+ const { type, format, nestedSchema } = this.mapTypeToSchema(property.type, contextFile);
506
795
  if (nestedSchema) {
507
796
  schema.properties[property.name] = nestedSchema;
508
797
  }
@@ -513,9 +802,9 @@ class SchemaTransformer {
513
802
  }
514
803
  // Apply decorators if present
515
804
  this.applyDecorators(property.decorators, schema, property.name);
516
- // If no decorators are present, apply sensible defaults based on TypeScript types
805
+ // If no decorators are present, apply type-based format specifications
517
806
  if (property.decorators.length === 0) {
518
- this.applySensibleDefaults(property, schema);
807
+ this.applyTypeBasedFormats(property, schema);
519
808
  }
520
809
  // Determine if property should be required based on decorators and optional status
521
810
  this.determineRequiredStatus(property, schema);
@@ -527,14 +816,15 @@ class SchemaTransformer {
527
816
  * Handles primitive types, arrays, and nested objects recursively.
528
817
  *
529
818
  * @param type - The TypeScript type string to map
819
+ * @param contextFile - Optional context file path for resolving class references
530
820
  * @returns Object containing OpenAPI type, optional format, and nested schema
531
821
  * @private
532
822
  */
533
- mapTypeToSchema(type) {
823
+ mapTypeToSchema(type, contextFile) {
534
824
  // Handle arrays
535
825
  if (type.endsWith('[]')) {
536
826
  const elementType = type.slice(0, -2);
537
- const elementSchema = this.mapTypeToSchema(elementType);
827
+ const elementSchema = this.mapTypeToSchema(elementType, contextFile);
538
828
  const items = elementSchema.nestedSchema || {
539
829
  type: elementSchema.type,
540
830
  };
@@ -578,9 +868,24 @@ class SchemaTransformer {
578
868
  format: constants.jsPrimitives.UploadFile.format,
579
869
  };
580
870
  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
+ }
581
886
  // Handle nested objects
582
887
  try {
583
- const nestedResult = this.transformByName(type);
888
+ const nestedResult = this.transformByName(type, undefined, contextFile);
584
889
  return {
585
890
  type: constants.jsPrimitives.Object.value,
586
891
  nestedSchema: nestedResult.schema,
@@ -716,68 +1021,232 @@ class SchemaTransformer {
716
1021
  case constants.validatorDecorators.ArrayMaxSize.name:
717
1022
  schema.properties[propertyName].maxItems = decorator.arguments[0];
718
1023
  break;
1024
+ case constants.validatorDecorators.IsEnum.name:
1025
+ this.applyEnumDecorator(decorator, schema, propertyName, isArrayType);
1026
+ break;
719
1027
  }
720
1028
  }
721
1029
  }
722
1030
  /**
723
- * Applies sensible default behaviors for properties without class-validator decorators.
724
- * This allows the schema generator to work with plain TypeScript classes.
1031
+ * Applies the @IsEnum decorator to a property, handling both primitive values and object enums.
1032
+ * Supports arrays of enum values as well.
725
1033
  *
726
- * @param property - The property information
1034
+ * @param decorator - The IsEnum decorator information
727
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
728
1038
  * @private
729
1039
  */
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;
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;
759
1057
  }
760
- else if (propertyName === 'description' || propertyName === 'bio') {
761
- propertySchema.maxLength = 500;
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';
762
1067
  }
763
- else if (propertyName === 'title') {
764
- propertySchema.minLength = 1;
765
- propertySchema.maxLength = 200;
1068
+ else if (schema.properties[propertyName].items) {
1069
+ schema.properties[propertyName].items.type = 'string';
766
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;
767
1092
  }
768
- if (propertySchema.type === 'integer' || propertySchema.type === 'number') {
769
- if (propertyName === 'age') {
770
- propertySchema.minimum = 0;
771
- propertySchema.maximum = 150;
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
+ }
772
1149
  }
773
- else if (propertyName === 'id') {
774
- propertySchema.minimum = 1;
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
+ }
775
1173
  }
776
- else if (propertyName.includes('count') ||
777
- propertyName.includes('quantity')) {
778
- propertySchema.minimum = 0;
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
+ }
779
1185
  }
780
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
+ }
781
1250
  }
782
1251
  /**
783
1252
  * Determines if a property should be required based on decorators and optional status.