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.js CHANGED
@@ -6,10 +6,10 @@ var path = require('path');
6
6
  const TS_CONFIG_DEFAULT_PATH = path.resolve(process.cwd(), 'tsconfig.json');
7
7
  const jsPrimitives = {
8
8
  String: { type: 'String', value: 'string' },
9
- Number: { type: 'Number', value: 'number' },
9
+ Number: { type: 'Number', value: 'number', format: 'float' },
10
10
  Boolean: { type: 'Boolean', value: 'boolean' },
11
11
  Symbol: { type: 'Symbol', value: 'symbol' },
12
- BigInt: { type: 'BigInt', value: 'integer' },
12
+ BigInt: { type: 'BigInt', value: 'integer', format: 'int64' },
13
13
  null: { type: 'null', value: 'null' },
14
14
  Object: { type: 'Object', value: 'object' },
15
15
  Array: { type: 'Array', value: 'array' },
@@ -38,6 +38,7 @@ const validatorDecorators = {
38
38
  ArrayNotEmpty: { name: 'ArrayNotEmpty' },
39
39
  ArrayMaxSize: { name: 'ArrayMaxSize' },
40
40
  ArrayMinSize: { name: 'ArrayMinSize' },
41
+ IsEnum: { name: 'IsEnum', type: 'string' },
41
42
  };
42
43
  const constants = {
43
44
  TS_CONFIG_DEFAULT_PATH,
@@ -175,16 +176,38 @@ class SchemaTransformer {
175
176
  }
176
177
  /**
177
178
  * Transforms a class by its name into an OpenAPI schema object.
179
+ * Now considers the context of the calling file to resolve ambiguous class names.
178
180
  *
179
181
  * @param className - The name of the class to transform
180
182
  * @param filePath - Optional path to the file containing the class
183
+ * @param contextFile - Optional context file for resolving class ambiguity
181
184
  * @returns Object containing the class name and its corresponding JSON schema
182
185
  * @throws {Error} When the specified class cannot be found
183
186
  * @private
184
187
  */
185
- transformByName(className, filePath) {
188
+ transformByName(className, filePath, contextFile) {
186
189
  const sourceFiles = this.getRelevantSourceFiles(className, filePath);
187
- for (const sourceFile of sourceFiles) {
190
+ // If we have a context file, try to find the class in that file first
191
+ if (contextFile) {
192
+ const contextSourceFile = this.program.getSourceFile(contextFile);
193
+ if (contextSourceFile) {
194
+ const classNode = this.findClassByName(contextSourceFile, className);
195
+ if (classNode) {
196
+ const cacheKey = this.getCacheKey(contextSourceFile.fileName, className);
197
+ // Check cache first
198
+ if (this.classCache.has(cacheKey)) {
199
+ return this.classCache.get(cacheKey);
200
+ }
201
+ const result = this.transformClass(classNode, contextSourceFile);
202
+ this.classCache.set(cacheKey, result);
203
+ this.cleanupCache();
204
+ return result;
205
+ }
206
+ }
207
+ }
208
+ // Fallback to searching all files, but prioritize files that are more likely to be relevant
209
+ const prioritizedFiles = this.prioritizeSourceFiles(sourceFiles, contextFile);
210
+ for (const sourceFile of prioritizedFiles) {
188
211
  const classNode = this.findClassByName(sourceFile, className);
189
212
  if (classNode && sourceFile?.fileName) {
190
213
  const cacheKey = this.getCacheKey(sourceFile.fileName, className);
@@ -192,7 +215,7 @@ class SchemaTransformer {
192
215
  if (this.classCache.has(cacheKey)) {
193
216
  return this.classCache.get(cacheKey);
194
217
  }
195
- const result = this.transformClass(classNode);
218
+ const result = this.transformClass(classNode, sourceFile);
196
219
  // Cache using fileName:className as key for uniqueness
197
220
  this.classCache.set(cacheKey, result);
198
221
  // Clean up cache if it gets too large
@@ -202,6 +225,38 @@ class SchemaTransformer {
202
225
  }
203
226
  throw new Error(`Class ${className} not found`);
204
227
  }
228
+ /**
229
+ * Prioritizes source files based on context to resolve class name conflicts.
230
+ * Gives priority to files in the same directory or with similar names.
231
+ *
232
+ * @param sourceFiles - Array of source files to prioritize
233
+ * @param contextFile - Optional context file for prioritization
234
+ * @returns Prioritized array of source files
235
+ * @private
236
+ */
237
+ prioritizeSourceFiles(sourceFiles, contextFile) {
238
+ if (!contextFile) {
239
+ return sourceFiles;
240
+ }
241
+ const contextDir = contextFile.substring(0, contextFile.lastIndexOf('/'));
242
+ return sourceFiles.sort((a, b) => {
243
+ const aDir = a.fileName.substring(0, a.fileName.lastIndexOf('/'));
244
+ const bDir = b.fileName.substring(0, b.fileName.lastIndexOf('/'));
245
+ // Prioritize files in the same directory as context
246
+ const aInSameDir = aDir === contextDir ? 1 : 0;
247
+ const bInSameDir = bDir === contextDir ? 1 : 0;
248
+ if (aInSameDir !== bInSameDir) {
249
+ return bInSameDir - aInSameDir; // Higher priority first
250
+ }
251
+ // Prioritize non-test files over test files
252
+ const aIsTest = a.fileName.includes('test') || a.fileName.includes('spec') ? 0 : 1;
253
+ const bIsTest = b.fileName.includes('test') || b.fileName.includes('spec') ? 0 : 1;
254
+ if (aIsTest !== bIsTest) {
255
+ return bIsTest - aIsTest; // Non-test files first
256
+ }
257
+ return 0;
258
+ });
259
+ }
205
260
  /**
206
261
  * Gets the singleton instance of SchemaTransformer.
207
262
  *
@@ -321,13 +376,14 @@ class SchemaTransformer {
321
376
  * Transforms a TypeScript class declaration into a schema object.
322
377
  *
323
378
  * @param classNode - The TypeScript class declaration node
379
+ * @param sourceFile - The source file containing the class (for context)
324
380
  * @returns Object containing class name and generated schema
325
381
  * @private
326
382
  */
327
- transformClass(classNode) {
383
+ transformClass(classNode, sourceFile) {
328
384
  const className = classNode.name?.text || 'Unknown';
329
385
  const properties = this.extractProperties(classNode);
330
- const schema = this.generateSchema(properties);
386
+ const schema = this.generateSchema(properties, sourceFile?.fileName);
331
387
  return { name: className, schema };
332
388
  }
333
389
  /**
@@ -371,6 +427,237 @@ class SchemaTransformer {
371
427
  const type = this.checker.getTypeAtLocation(property);
372
428
  return this.checker.typeToString(type);
373
429
  }
430
+ /**
431
+ * Resolves generic types by analyzing the type alias and its arguments.
432
+ * For example, User<Role> where User is a type alias will be resolved to its structure.
433
+ *
434
+ * @param typeNode - The TypeScript type reference node with generic arguments
435
+ * @returns String representation of the resolved type or schema
436
+ * @private
437
+ */
438
+ resolveGenericType(typeNode) {
439
+ const typeName = typeNode.typeName.text;
440
+ const typeArguments = typeNode.typeArguments;
441
+ if (!typeArguments || typeArguments.length === 0) {
442
+ return typeName;
443
+ }
444
+ // Try to resolve the type using the TypeScript type checker
445
+ const type = this.checker.getTypeAtLocation(typeNode);
446
+ const resolvedType = this.checker.typeToString(type);
447
+ // If we can resolve it to a meaningful structure, use that
448
+ if (resolvedType &&
449
+ resolvedType !== typeName &&
450
+ !resolvedType.includes('any')) {
451
+ // For type aliases like User<Role>, we want to create a synthetic type name
452
+ // that represents the resolved structure
453
+ const typeArgNames = typeArguments.map(arg => {
454
+ if (ts.isTypeReferenceNode(arg) && ts.isIdentifier(arg.typeName)) {
455
+ return arg.typeName.text;
456
+ }
457
+ return this.getTypeNodeToString(arg);
458
+ });
459
+ return `${typeName}_${typeArgNames.join('_')}`;
460
+ }
461
+ return typeName;
462
+ }
463
+ /**
464
+ * Checks if a type string represents a resolved generic type.
465
+ *
466
+ * @param type - The type string to check
467
+ * @returns True if it's a resolved generic type
468
+ * @private
469
+ */
470
+ isResolvedGenericType(type) {
471
+ // Simple heuristic: resolved generic types contain underscores and
472
+ // the parts after underscore should be known types
473
+ const parts = type.split('_');
474
+ return (parts.length > 1 &&
475
+ parts
476
+ .slice(1)
477
+ .every(part => this.isKnownType(part) || this.isPrimitiveType(part)));
478
+ }
479
+ /**
480
+ * Checks if a type is a known class or interface.
481
+ *
482
+ * @param typeName - The type name to check
483
+ * @returns True if it's a known type
484
+ * @private
485
+ */
486
+ isKnownType(typeName) {
487
+ // First check if it's a primitive type to avoid unnecessary lookups
488
+ if (this.isPrimitiveType(typeName)) {
489
+ return true;
490
+ }
491
+ try {
492
+ // Use a more conservative approach - check if we can find the class
493
+ // without actually transforming it to avoid side effects
494
+ const found = this.findClassInProject(typeName);
495
+ return found !== null;
496
+ }
497
+ catch {
498
+ return false;
499
+ }
500
+ }
501
+ /**
502
+ * Finds a class by name in the project without transforming it.
503
+ *
504
+ * @param className - The class name to find
505
+ * @returns True if found, false otherwise
506
+ * @private
507
+ */
508
+ findClassInProject(className) {
509
+ const sourceFiles = this.program.getSourceFiles().filter(sf => {
510
+ if (sf.isDeclarationFile)
511
+ return false;
512
+ if (sf.fileName.includes('.d.ts'))
513
+ return false;
514
+ if (sf.fileName.includes('node_modules'))
515
+ return false;
516
+ return true;
517
+ });
518
+ for (const sourceFile of sourceFiles) {
519
+ const found = this.findClassByName(sourceFile, className);
520
+ if (found)
521
+ return true;
522
+ }
523
+ return false;
524
+ }
525
+ /**
526
+ * Checks if a type is a primitive type.
527
+ *
528
+ * @param typeName - The type name to check
529
+ * @returns True if it's a primitive type
530
+ * @private
531
+ */
532
+ isPrimitiveType(typeName) {
533
+ const lowerTypeName = typeName.toLowerCase();
534
+ // Check against all primitive types from constants
535
+ const primitiveTypes = [
536
+ constants.jsPrimitives.String.type.toLowerCase(),
537
+ constants.jsPrimitives.Number.type.toLowerCase(),
538
+ constants.jsPrimitives.Boolean.type.toLowerCase(),
539
+ constants.jsPrimitives.Date.type.toLowerCase(),
540
+ constants.jsPrimitives.Buffer.type.toLowerCase(),
541
+ constants.jsPrimitives.Uint8Array.type.toLowerCase(),
542
+ constants.jsPrimitives.File.type.toLowerCase(),
543
+ constants.jsPrimitives.UploadFile.type.toLowerCase(),
544
+ constants.jsPrimitives.BigInt.type.toLowerCase(),
545
+ ];
546
+ return primitiveTypes.includes(lowerTypeName);
547
+ }
548
+ /**
549
+ * Resolves a generic type schema by analyzing the type alias structure.
550
+ *
551
+ * @param resolvedTypeName - The resolved generic type name (e.g., User_Role)
552
+ * @returns OpenAPI schema for the resolved generic type
553
+ * @private
554
+ */
555
+ resolveGenericTypeSchema(resolvedTypeName) {
556
+ const parts = resolvedTypeName.split('_');
557
+ const baseTypeName = parts[0];
558
+ const typeArgNames = parts.slice(1);
559
+ if (!baseTypeName) {
560
+ return null;
561
+ }
562
+ // Find the original type alias declaration
563
+ const typeAliasSymbol = this.findTypeAliasDeclaration(baseTypeName);
564
+ if (!typeAliasSymbol) {
565
+ return null;
566
+ }
567
+ // Create a schema based on the type alias structure, substituting type parameters
568
+ return this.createSchemaFromTypeAlias(typeAliasSymbol, typeArgNames);
569
+ }
570
+ /**
571
+ * Finds a type alias declaration by name.
572
+ *
573
+ * @param typeName - The type alias name to find
574
+ * @returns The type alias declaration node or null
575
+ * @private
576
+ */
577
+ findTypeAliasDeclaration(typeName) {
578
+ for (const sourceFile of this.program.getSourceFiles()) {
579
+ if (sourceFile.isDeclarationFile)
580
+ continue;
581
+ const findTypeAlias = (node) => {
582
+ if (ts.isTypeAliasDeclaration(node) && node.name.text === typeName) {
583
+ return node;
584
+ }
585
+ return ts.forEachChild(node, findTypeAlias) || null;
586
+ };
587
+ const result = findTypeAlias(sourceFile);
588
+ if (result)
589
+ return result;
590
+ }
591
+ return null;
592
+ }
593
+ /**
594
+ * Creates a schema from a type alias declaration, substituting type parameters.
595
+ *
596
+ * @param typeAlias - The type alias declaration
597
+ * @param typeArgNames - The concrete type arguments
598
+ * @returns OpenAPI schema for the type alias
599
+ * @private
600
+ */
601
+ createSchemaFromTypeAlias(typeAlias, typeArgNames) {
602
+ const typeNode = typeAlias.type;
603
+ if (ts.isTypeLiteralNode(typeNode)) {
604
+ const schema = {
605
+ type: 'object',
606
+ properties: {},
607
+ required: [],
608
+ };
609
+ for (const member of typeNode.members) {
610
+ if (ts.isPropertySignature(member) &&
611
+ member.name &&
612
+ ts.isIdentifier(member.name)) {
613
+ const propertyName = member.name.text;
614
+ const isOptional = !!member.questionToken;
615
+ if (member.type) {
616
+ const propertyType = this.resolveTypeParameterInTypeAlias(member.type, typeAlias.typeParameters, typeArgNames);
617
+ const { type, format, nestedSchema } = this.mapTypeToSchema(propertyType);
618
+ if (nestedSchema) {
619
+ schema.properties[propertyName] = nestedSchema;
620
+ }
621
+ else {
622
+ schema.properties[propertyName] = { type };
623
+ if (format)
624
+ schema.properties[propertyName].format = format;
625
+ }
626
+ if (!isOptional) {
627
+ schema.required.push(propertyName);
628
+ }
629
+ }
630
+ }
631
+ }
632
+ return schema;
633
+ }
634
+ return null;
635
+ }
636
+ /**
637
+ * Resolves type parameters in a type alias to concrete types.
638
+ *
639
+ * @param typeNode - The type node to resolve
640
+ * @param typeParameters - The type parameters of the type alias
641
+ * @param typeArgNames - The concrete type arguments
642
+ * @returns The resolved type string
643
+ * @private
644
+ */
645
+ resolveTypeParameterInTypeAlias(typeNode, typeParameters, typeArgNames) {
646
+ if (ts.isTypeReferenceNode(typeNode) &&
647
+ ts.isIdentifier(typeNode.typeName)) {
648
+ const typeName = typeNode.typeName.text;
649
+ // Check if this is a type parameter
650
+ if (typeParameters) {
651
+ const paramIndex = typeParameters.findIndex(param => param.name.text === typeName);
652
+ if (paramIndex !== -1 && paramIndex < typeArgNames.length) {
653
+ const resolvedType = typeArgNames[paramIndex];
654
+ return resolvedType || typeName;
655
+ }
656
+ }
657
+ return typeName;
658
+ }
659
+ return this.getTypeNodeToString(typeNode);
660
+ }
374
661
  /**
375
662
  * Converts a TypeScript type node to its string representation.
376
663
  *
@@ -396,6 +683,7 @@ class SchemaTransformer {
396
683
  return firstTypeArg.typeName.text;
397
684
  }
398
685
  }
686
+ return this.resolveGenericType(typeNode);
399
687
  }
400
688
  return typeNode.typeName.text;
401
689
  }
@@ -494,17 +782,18 @@ class SchemaTransformer {
494
782
  * Generates an OpenAPI schema from extracted property information.
495
783
  *
496
784
  * @param properties - Array of property information to process
785
+ * @param contextFile - Optional context file path for resolving class references
497
786
  * @returns Complete OpenAPI schema object with properties and validation rules
498
787
  * @private
499
788
  */
500
- generateSchema(properties) {
789
+ generateSchema(properties, contextFile) {
501
790
  const schema = {
502
791
  type: 'object',
503
792
  properties: {},
504
793
  required: [],
505
794
  };
506
795
  for (const property of properties) {
507
- const { type, format, nestedSchema } = this.mapTypeToSchema(property.type);
796
+ const { type, format, nestedSchema } = this.mapTypeToSchema(property.type, contextFile);
508
797
  if (nestedSchema) {
509
798
  schema.properties[property.name] = nestedSchema;
510
799
  }
@@ -515,9 +804,9 @@ class SchemaTransformer {
515
804
  }
516
805
  // Apply decorators if present
517
806
  this.applyDecorators(property.decorators, schema, property.name);
518
- // If no decorators are present, apply sensible defaults based on TypeScript types
807
+ // If no decorators are present, apply type-based format specifications
519
808
  if (property.decorators.length === 0) {
520
- this.applySensibleDefaults(property, schema);
809
+ this.applyTypeBasedFormats(property, schema);
521
810
  }
522
811
  // Determine if property should be required based on decorators and optional status
523
812
  this.determineRequiredStatus(property, schema);
@@ -529,14 +818,15 @@ class SchemaTransformer {
529
818
  * Handles primitive types, arrays, and nested objects recursively.
530
819
  *
531
820
  * @param type - The TypeScript type string to map
821
+ * @param contextFile - Optional context file path for resolving class references
532
822
  * @returns Object containing OpenAPI type, optional format, and nested schema
533
823
  * @private
534
824
  */
535
- mapTypeToSchema(type) {
825
+ mapTypeToSchema(type, contextFile) {
536
826
  // Handle arrays
537
827
  if (type.endsWith('[]')) {
538
828
  const elementType = type.slice(0, -2);
539
- const elementSchema = this.mapTypeToSchema(elementType);
829
+ const elementSchema = this.mapTypeToSchema(elementType, contextFile);
540
830
  const items = elementSchema.nestedSchema || {
541
831
  type: elementSchema.type,
542
832
  };
@@ -580,9 +870,24 @@ class SchemaTransformer {
580
870
  format: constants.jsPrimitives.UploadFile.format,
581
871
  };
582
872
  default:
873
+ // Check if it's a resolved generic type (e.g., User_Role)
874
+ if (type.includes('_') && this.isResolvedGenericType(type)) {
875
+ try {
876
+ const genericSchema = this.resolveGenericTypeSchema(type);
877
+ if (genericSchema) {
878
+ return {
879
+ type: constants.jsPrimitives.Object.value,
880
+ nestedSchema: genericSchema,
881
+ };
882
+ }
883
+ }
884
+ catch (error) {
885
+ console.warn(`Failed to resolve generic type ${type}:`, error);
886
+ }
887
+ }
583
888
  // Handle nested objects
584
889
  try {
585
- const nestedResult = this.transformByName(type);
890
+ const nestedResult = this.transformByName(type, undefined, contextFile);
586
891
  return {
587
892
  type: constants.jsPrimitives.Object.value,
588
893
  nestedSchema: nestedResult.schema,
@@ -718,68 +1023,232 @@ class SchemaTransformer {
718
1023
  case constants.validatorDecorators.ArrayMaxSize.name:
719
1024
  schema.properties[propertyName].maxItems = decorator.arguments[0];
720
1025
  break;
1026
+ case constants.validatorDecorators.IsEnum.name:
1027
+ this.applyEnumDecorator(decorator, schema, propertyName, isArrayType);
1028
+ break;
721
1029
  }
722
1030
  }
723
1031
  }
724
1032
  /**
725
- * Applies sensible default behaviors for properties without class-validator decorators.
726
- * This allows the schema generator to work with plain TypeScript classes.
1033
+ * Applies the @IsEnum decorator to a property, handling both primitive values and object enums.
1034
+ * Supports arrays of enum values as well.
727
1035
  *
728
- * @param property - The property information
1036
+ * @param decorator - The IsEnum decorator information
729
1037
  * @param schema - The schema object to modify
1038
+ * @param propertyName - The name of the property
1039
+ * @param isArrayType - Whether the property is an array type
730
1040
  * @private
731
1041
  */
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;
1042
+ applyEnumDecorator(decorator, schema, propertyName, isArrayType) {
1043
+ if (!decorator.arguments || decorator.arguments.length === 0) {
1044
+ return;
1045
+ }
1046
+ const enumArg = decorator.arguments[0];
1047
+ let enumValues = [];
1048
+ // Handle different enum argument types
1049
+ if (typeof enumArg === 'string') {
1050
+ // This is likely a reference to an enum type name
1051
+ // We need to try to resolve this to actual enum values
1052
+ enumValues = this.resolveEnumValues(enumArg);
1053
+ }
1054
+ else if (typeof enumArg === 'object' && enumArg !== null) {
1055
+ // Object enum - extract values
1056
+ if (Array.isArray(enumArg)) {
1057
+ // Already an array of values
1058
+ enumValues = enumArg;
761
1059
  }
762
- else if (propertyName === 'description' || propertyName === 'bio') {
763
- propertySchema.maxLength = 500;
1060
+ else {
1061
+ // Enum object - get all values
1062
+ enumValues = Object.values(enumArg);
1063
+ }
1064
+ }
1065
+ // If we couldn't resolve enum values, fall back to string type without enum constraint
1066
+ if (enumValues.length === 0) {
1067
+ if (!isArrayType) {
1068
+ schema.properties[propertyName].type = 'string';
764
1069
  }
765
- else if (propertyName === 'title') {
766
- propertySchema.minLength = 1;
767
- propertySchema.maxLength = 200;
1070
+ else if (schema.properties[propertyName].items) {
1071
+ schema.properties[propertyName].items.type = 'string';
768
1072
  }
1073
+ return;
1074
+ }
1075
+ // Determine the type based on enum values
1076
+ let enumType = 'string';
1077
+ if (enumValues.length > 0) {
1078
+ const firstValue = enumValues[0];
1079
+ if (typeof firstValue === 'number') {
1080
+ enumType = 'number';
1081
+ }
1082
+ else if (typeof firstValue === 'boolean') {
1083
+ enumType = 'boolean';
1084
+ }
1085
+ }
1086
+ // Apply enum to schema
1087
+ if (!isArrayType) {
1088
+ schema.properties[propertyName].type = enumType;
1089
+ schema.properties[propertyName].enum = enumValues;
1090
+ }
1091
+ else if (schema.properties[propertyName].items) {
1092
+ schema.properties[propertyName].items.type = enumType;
1093
+ schema.properties[propertyName].items.enum = enumValues;
769
1094
  }
770
- if (propertySchema.type === 'integer' || propertySchema.type === 'number') {
771
- if (propertyName === 'age') {
772
- propertySchema.minimum = 0;
773
- propertySchema.maximum = 150;
1095
+ }
1096
+ /**
1097
+ * Attempts to resolve enum values from an enum type name.
1098
+ * This searches through the TypeScript AST to find the enum declaration
1099
+ * and extract its values.
1100
+ *
1101
+ * @param enumTypeName - The name of the enum type
1102
+ * @returns Array of enum values if found, empty array otherwise
1103
+ * @private
1104
+ */
1105
+ resolveEnumValues(enumTypeName) {
1106
+ // Search for enum declarations in source files
1107
+ for (const sourceFile of this.program.getSourceFiles()) {
1108
+ if (sourceFile.isDeclarationFile)
1109
+ continue;
1110
+ if (sourceFile.fileName.includes('node_modules'))
1111
+ continue;
1112
+ const enumValues = this.findEnumValues(sourceFile, enumTypeName);
1113
+ if (enumValues.length > 0) {
1114
+ return enumValues;
1115
+ }
1116
+ }
1117
+ return [];
1118
+ }
1119
+ /**
1120
+ * Finds enum values in a specific source file.
1121
+ *
1122
+ * @param sourceFile - The source file to search
1123
+ * @param enumTypeName - The name of the enum to find
1124
+ * @returns Array of enum values if found, empty array otherwise
1125
+ * @private
1126
+ */
1127
+ findEnumValues(sourceFile, enumTypeName) {
1128
+ let enumValues = [];
1129
+ const visit = (node) => {
1130
+ // Handle TypeScript enum declarations
1131
+ if (ts.isEnumDeclaration(node) && node.name?.text === enumTypeName) {
1132
+ enumValues = this.extractEnumValues(node);
1133
+ return;
1134
+ }
1135
+ // Handle const object declarations (like const Status = { ... } as const)
1136
+ if (ts.isVariableStatement(node)) {
1137
+ for (const declaration of node.declarationList.declarations) {
1138
+ if (ts.isVariableDeclaration(declaration) &&
1139
+ ts.isIdentifier(declaration.name) &&
1140
+ declaration.name.text === enumTypeName &&
1141
+ declaration.initializer) {
1142
+ let initializer = declaration.initializer;
1143
+ // Handle "as const" assertions
1144
+ if (ts.isAsExpression(initializer) && initializer.expression) {
1145
+ initializer = initializer.expression;
1146
+ }
1147
+ enumValues = this.extractObjectEnumValues(initializer);
1148
+ return;
1149
+ }
1150
+ }
774
1151
  }
775
- else if (propertyName === 'id') {
776
- propertySchema.minimum = 1;
1152
+ ts.forEachChild(node, visit);
1153
+ };
1154
+ visit(sourceFile);
1155
+ return enumValues;
1156
+ }
1157
+ /**
1158
+ * Extracts values from a TypeScript enum declaration.
1159
+ *
1160
+ * @param enumNode - The enum declaration node
1161
+ * @returns Array of enum values
1162
+ * @private
1163
+ */
1164
+ extractEnumValues(enumNode) {
1165
+ const values = [];
1166
+ for (const member of enumNode.members) {
1167
+ if (member.initializer) {
1168
+ // Handle initialized enum members
1169
+ if (ts.isStringLiteral(member.initializer)) {
1170
+ values.push(member.initializer.text);
1171
+ }
1172
+ else if (ts.isNumericLiteral(member.initializer)) {
1173
+ values.push(Number(member.initializer.text));
1174
+ }
777
1175
  }
778
- else if (propertyName.includes('count') ||
779
- propertyName.includes('quantity')) {
780
- propertySchema.minimum = 0;
1176
+ else {
1177
+ // Handle auto-incremented numeric enums
1178
+ if (values.length === 0) {
1179
+ values.push(0);
1180
+ }
1181
+ else {
1182
+ const lastValue = values[values.length - 1];
1183
+ if (typeof lastValue === 'number') {
1184
+ values.push(lastValue + 1);
1185
+ }
1186
+ }
781
1187
  }
782
1188
  }
1189
+ return values;
1190
+ }
1191
+ /**
1192
+ * Extracts values from object literal enum (const object as const).
1193
+ *
1194
+ * @param initializer - The object literal initializer
1195
+ * @returns Array of enum values
1196
+ * @private
1197
+ */
1198
+ extractObjectEnumValues(initializer) {
1199
+ const values = [];
1200
+ if (ts.isObjectLiteralExpression(initializer)) {
1201
+ for (const property of initializer.properties) {
1202
+ if (ts.isPropertyAssignment(property) && property.initializer) {
1203
+ if (ts.isStringLiteral(property.initializer)) {
1204
+ values.push(property.initializer.text);
1205
+ }
1206
+ else if (ts.isNumericLiteral(property.initializer)) {
1207
+ values.push(Number(property.initializer.text));
1208
+ }
1209
+ }
1210
+ }
1211
+ }
1212
+ return values;
1213
+ }
1214
+ /**
1215
+ * Applies sensible default behaviors for properties without class-validator decorators.
1216
+ * This allows the schema generator to work with plain TypeScript classes.
1217
+ *
1218
+ * @param property - The property information
1219
+ * @param schema - The schema object to modify
1220
+ * @private
1221
+ */
1222
+ /**
1223
+ * Applies OpenAPI format specifications based on TypeScript types.
1224
+ * This method is called when no decorators are present to set appropriate
1225
+ * format values for primitive types according to OpenAPI specification.
1226
+ *
1227
+ * @param property - The property information containing type details
1228
+ * @param schema - The schema object to modify
1229
+ * @private
1230
+ */
1231
+ applyTypeBasedFormats(property, schema) {
1232
+ const propertyName = property.name;
1233
+ const propertyType = property.type.toLowerCase();
1234
+ const propertySchema = schema.properties[propertyName];
1235
+ switch (propertyType) {
1236
+ case constants.jsPrimitives.Number.value:
1237
+ propertySchema.format = constants.jsPrimitives.Number.format;
1238
+ break;
1239
+ case constants.jsPrimitives.BigInt.value:
1240
+ propertySchema.format = constants.jsPrimitives.BigInt.format;
1241
+ break;
1242
+ case constants.jsPrimitives.Date.value:
1243
+ propertySchema.format = constants.jsPrimitives.Date.format;
1244
+ break;
1245
+ case constants.jsPrimitives.Buffer.value:
1246
+ case constants.jsPrimitives.Uint8Array.value:
1247
+ case constants.jsPrimitives.File.value:
1248
+ case constants.jsPrimitives.UploadFile.value:
1249
+ propertySchema.format = constants.jsPrimitives.UploadFile.format;
1250
+ break;
1251
+ }
783
1252
  }
784
1253
  /**
785
1254
  * Determines if a property should be required based on decorators and optional status.