ts-class-to-openapi 1.2.0 → 1.2.2

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/README.md CHANGED
@@ -231,7 +231,7 @@ const schema = transform(User)
231
231
 
232
232
  ### 4. Enumerations and Special Types
233
233
 
234
- Full compatibility with TypeScript enumerations:
234
+ Full compatibility with TypeScript enumerations (both decorated and pure):
235
235
 
236
236
  ```typescript
237
237
  import { transform } from 'ts-class-to-openapi'
@@ -253,6 +253,9 @@ class Task {
253
253
  @IsEnum(UserType)
254
254
  assignedTo: UserType
255
255
 
256
+ // Pure TypeScript enum (automatically detected without decorator)
257
+ status: UserType
258
+
256
259
  @IsEnum(Priority)
257
260
  priority?: Priority
258
261
 
@@ -276,6 +279,10 @@ const schema = transform(Task)
276
279
  "type": "string",
277
280
  "enum": ["admin", "user", "moderator"]
278
281
  },
282
+ "status": {
283
+ "type": "string",
284
+ "enum": ["admin", "user", "moderator"]
285
+ },
279
286
  "priority": {
280
287
  "type": "number",
281
288
  "enum": [1, 2, 3]
@@ -287,7 +294,7 @@ const schema = transform(Task)
287
294
  "format": "date-time"
288
295
  }
289
296
  },
290
- "required": ["assignedTo", "title", "completed", "dueDate"]
297
+ "required": ["assignedTo", "status", "title", "completed", "dueDate"]
291
298
  }
292
299
  }
293
300
  ```
@@ -0,0 +1,30 @@
1
+ export declare enum UserRole {
2
+ ADMIN = "admin",
3
+ USER = "user",
4
+ GUEST = "guest"
5
+ }
6
+ export declare enum OrderStatus {
7
+ PENDING = 0,
8
+ PROCESSING = 1,
9
+ SHIPPED = 2,
10
+ DELIVERED = 3,
11
+ CANCELLED = 4
12
+ }
13
+ export declare enum MixedEnum {
14
+ YES = "yes",
15
+ NO = 0
16
+ }
17
+ export declare class EnumTestEntity {
18
+ role: UserRole;
19
+ status: OrderStatus;
20
+ mixed: MixedEnum;
21
+ }
22
+ export declare class ArrayEnumTestEntity {
23
+ roles: UserRole[];
24
+ statuses: OrderStatus[];
25
+ }
26
+ export declare class PureEnumTestEntity {
27
+ role: UserRole;
28
+ status: OrderStatus;
29
+ mixed: MixedEnum;
30
+ }
@@ -1,3 +1,4 @@
1
1
  import './testCases/pure-classes.test';
2
2
  import './testCases/decorated-classes.test';
3
3
  import './testCases/nested-classes.test';
4
+ import './testCases/enum-properties.test';
package/dist/index.esm.js CHANGED
@@ -110,7 +110,8 @@ class SchemaTransformer {
110
110
  const decorators = this.extractDecorators(member);
111
111
  const isOptional = !!member.questionToken;
112
112
  const isGeneric = this.isPropertyTypeGeneric(member);
113
- const isPrimitive = this.isPrimitiveType(type);
113
+ const isEnum = this.isEnum(member);
114
+ const isPrimitive = this.isPrimitiveType(type) || isEnum;
114
115
  const isClassType = this.isClassType(member);
115
116
  const isArray = this.isArrayProperty(member);
116
117
  const isTypeLiteral = this.isTypeLiteral(member);
@@ -124,6 +125,7 @@ class SchemaTransformer {
124
125
  isPrimitive,
125
126
  isClassType,
126
127
  isArray,
128
+ isEnum,
127
129
  isRef: false,
128
130
  isTypeLiteral: isTypeLiteral,
129
131
  };
@@ -275,9 +277,15 @@ class SchemaTransformer {
275
277
  return true;
276
278
  if (arg.kind === ts.SyntaxKind.FalseKeyword)
277
279
  return false;
278
- return arg.getText();
280
+ return arg;
279
281
  });
280
282
  }
283
+ getSafeDecoratorArgument(arg) {
284
+ if (arg && typeof arg === 'object' && 'kind' in arg) {
285
+ return arg.getText();
286
+ }
287
+ return arg;
288
+ }
281
289
  isPropertyTypeGeneric(property) {
282
290
  if (property.type && this.isGenericTypeFromNode(property.type)) {
283
291
  return true;
@@ -475,49 +483,135 @@ class SchemaTransformer {
475
483
  }
476
484
  }
477
485
  }
486
+ isEnum(propertyDeclaration) {
487
+ if (!propertyDeclaration.type) {
488
+ return false;
489
+ }
490
+ let typeNode = propertyDeclaration.type;
491
+ if (ts.isArrayTypeNode(typeNode)) {
492
+ typeNode = typeNode.elementType;
493
+ }
494
+ if (ts.isTypeReferenceNode(typeNode)) {
495
+ const type = this.checker.getTypeAtLocation(typeNode);
496
+ // console.log('isEnum check:', typeNode.getText(), type.flags)
497
+ return (!!(type.flags & ts.TypeFlags.Enum) ||
498
+ !!(type.flags & ts.TypeFlags.EnumLiteral));
499
+ }
500
+ return false;
501
+ }
478
502
  isClassType(propertyDeclaration) {
479
503
  // If there's no explicit type annotation, we can't determine reliably
480
504
  if (!propertyDeclaration.type) {
481
505
  return false;
482
506
  }
483
507
  // Check if the original property type is an array type
484
- if (this.isArrayProperty(propertyDeclaration) &&
485
- ts.isTypeReferenceNode(propertyDeclaration.type
486
- .elementType)) {
487
- const type = this.checker.getTypeAtLocation(propertyDeclaration.type.elementType);
508
+ if (this.isArrayProperty(propertyDeclaration)) {
509
+ const arrayType = propertyDeclaration.type;
510
+ const elementType = arrayType.elementType;
511
+ // Special handling for utility types with type arguments (e.g., PayloadEntity<Person>)
512
+ if (ts.isTypeReferenceNode(elementType) &&
513
+ elementType.typeArguments &&
514
+ elementType.typeArguments.length > 0) {
515
+ // Check the first type argument - it might be the actual class
516
+ const firstTypeArg = elementType.typeArguments[0];
517
+ if (firstTypeArg) {
518
+ const argType = this.checker.getTypeAtLocation(firstTypeArg);
519
+ const argSymbol = argType.getSymbol();
520
+ if (argSymbol && argSymbol.declarations) {
521
+ const hasClass = argSymbol.declarations.some(decl => ts.isClassDeclaration(decl));
522
+ if (hasClass)
523
+ return true;
524
+ }
525
+ }
526
+ }
527
+ // Get the type from the element, regardless of its syntaxkind
528
+ const type = this.checker.getTypeAtLocation(elementType);
488
529
  const symbol = type.getSymbol();
489
530
  if (symbol && symbol.declarations) {
490
531
  return symbol.declarations.some(decl => ts.isClassDeclaration(decl));
491
532
  }
533
+ return false;
492
534
  }
493
- else if (ts.isTypeReferenceNode(propertyDeclaration.type)) {
535
+ // Check non-array types
536
+ else {
537
+ // Special handling for utility types with type arguments (e.g., PayloadEntity<Branch>)
538
+ if (ts.isTypeReferenceNode(propertyDeclaration.type) &&
539
+ propertyDeclaration.type.typeArguments &&
540
+ propertyDeclaration.type.typeArguments.length > 0) {
541
+ // Check the first type argument - it might be the actual class
542
+ const firstTypeArg = propertyDeclaration.type.typeArguments[0];
543
+ if (firstTypeArg) {
544
+ const argType = this.checker.getTypeAtLocation(firstTypeArg);
545
+ const argSymbol = argType.getSymbol();
546
+ if (argSymbol && argSymbol.declarations) {
547
+ const hasClass = argSymbol.declarations.some(decl => ts.isClassDeclaration(decl));
548
+ if (hasClass)
549
+ return true;
550
+ }
551
+ }
552
+ }
494
553
  const type = this.checker.getTypeAtLocation(propertyDeclaration.type);
495
554
  const symbol = type.getSymbol();
496
555
  if (symbol && symbol.declarations) {
497
556
  return symbol.declarations.some(decl => ts.isClassDeclaration(decl));
498
557
  }
558
+ return false;
499
559
  }
500
- return false;
501
560
  }
502
561
  getDeclarationProperty(property) {
503
562
  if (!property.originalProperty.type) {
504
563
  return undefined;
505
564
  }
506
- if (ts.isArrayTypeNode(property.originalProperty.type) &&
507
- ts.isTypeReferenceNode(property.originalProperty.type.elementType)) {
508
- const type = this.checker.getTypeAtLocation(property.originalProperty.type.elementType);
565
+ // Handle array types - get the element type
566
+ if (ts.isArrayTypeNode(property.originalProperty.type)) {
567
+ const elementType = property.originalProperty.type.elementType;
568
+ // Check if it's a utility type with type arguments (e.g., PayloadEntity<Branch>[])
569
+ if (ts.isTypeReferenceNode(elementType) &&
570
+ elementType.typeArguments &&
571
+ elementType.typeArguments.length > 0) {
572
+ const firstTypeArg = elementType.typeArguments[0];
573
+ if (firstTypeArg) {
574
+ const argType = this.checker.getTypeAtLocation(firstTypeArg);
575
+ const argSymbol = argType.getSymbol();
576
+ if (argSymbol && argSymbol.declarations) {
577
+ const classDecl = argSymbol.declarations.find(decl => ts.isClassDeclaration(decl));
578
+ if (classDecl)
579
+ return classDecl;
580
+ }
581
+ }
582
+ }
583
+ const type = this.checker.getTypeAtLocation(elementType);
509
584
  const symbol = type.getSymbol();
510
585
  if (symbol && symbol.declarations) {
511
- return symbol.declarations[0];
586
+ // Return the first class declaration found
587
+ const classDecl = symbol.declarations.find(decl => ts.isClassDeclaration(decl));
588
+ return classDecl || symbol.declarations[0];
512
589
  }
590
+ return undefined;
513
591
  }
514
- else if (ts.isTypeReferenceNode(property.originalProperty.type)) {
515
- const type = this.checker.getTypeAtLocation(property.originalProperty.type);
516
- const symbol = type.getSymbol();
517
- if (symbol && symbol.declarations) {
518
- return symbol.declarations[0];
592
+ // Handle non-array types
593
+ // Check if it's a utility type with type arguments (e.g., PayloadEntity<Branch>)
594
+ if (ts.isTypeReferenceNode(property.originalProperty.type) &&
595
+ property.originalProperty.type.typeArguments &&
596
+ property.originalProperty.type.typeArguments.length > 0) {
597
+ const firstTypeArg = property.originalProperty.type.typeArguments[0];
598
+ if (firstTypeArg) {
599
+ const argType = this.checker.getTypeAtLocation(firstTypeArg);
600
+ const argSymbol = argType.getSymbol();
601
+ if (argSymbol && argSymbol.declarations) {
602
+ const classDecl = argSymbol.declarations.find(decl => ts.isClassDeclaration(decl));
603
+ if (classDecl)
604
+ return classDecl;
605
+ }
519
606
  }
520
607
  }
608
+ const type = this.checker.getTypeAtLocation(property.originalProperty.type);
609
+ const symbol = type.getSymbol();
610
+ if (symbol && symbol.declarations) {
611
+ // Return the first class declaration found
612
+ const classDecl = symbol.declarations.find(decl => ts.isClassDeclaration(decl));
613
+ return classDecl || symbol.declarations[0];
614
+ }
521
615
  return undefined;
522
616
  }
523
617
  isArrayProperty(propertyDeclaration) {
@@ -654,7 +748,58 @@ class SchemaTransformer {
654
748
  transformedSchema.set(declaration.name.text, schema);
655
749
  return schema;
656
750
  }
751
+ getSchemaFromEnum(property) {
752
+ let typeNode = property.originalProperty.type;
753
+ if (ts.isArrayTypeNode(typeNode)) {
754
+ typeNode = typeNode.elementType;
755
+ }
756
+ const type = this.checker.getTypeAtLocation(typeNode);
757
+ if (type.symbol && type.symbol.exports) {
758
+ const values = [];
759
+ type.symbol.exports.forEach(member => {
760
+ const declaration = member.valueDeclaration;
761
+ if (declaration && ts.isEnumMember(declaration)) {
762
+ const value = this.checker.getConstantValue(declaration);
763
+ if (value !== undefined) {
764
+ values.push(value);
765
+ }
766
+ }
767
+ });
768
+ if (values.length > 0) {
769
+ const propertySchema = { type: 'object' };
770
+ propertySchema.enum = values;
771
+ const isString = values.every(v => typeof v === 'string');
772
+ const isNumber = values.every(v => typeof v === 'number');
773
+ if (isString) {
774
+ propertySchema.type = 'string';
775
+ }
776
+ else if (isNumber) {
777
+ propertySchema.type = 'number';
778
+ }
779
+ else {
780
+ propertySchema.type = 'string';
781
+ }
782
+ if (property.isArray) {
783
+ const itemsSchema = { ...propertySchema };
784
+ propertySchema.type = 'array';
785
+ propertySchema.items = itemsSchema;
786
+ delete propertySchema.enum;
787
+ return propertySchema;
788
+ }
789
+ else {
790
+ return propertySchema;
791
+ }
792
+ }
793
+ }
794
+ return undefined;
795
+ }
657
796
  getSchemaFromPrimitive(property) {
797
+ if (property.isEnum) {
798
+ const enumSchema = this.getSchemaFromEnum(property);
799
+ if (enumSchema) {
800
+ return enumSchema;
801
+ }
802
+ }
658
803
  const propertySchema = { type: 'object' };
659
804
  const propertyType = property.type.toLowerCase().replace('[]', '').trim();
660
805
  let isFile = false;
@@ -742,8 +887,40 @@ class SchemaTransformer {
742
887
  ts.isTypeReferenceNode(typeNode) // Omit, Pick, Partial, etc.
743
888
  );
744
889
  }
745
- //Todo: implement properly
746
- applyEnumDecorator(decorator, schema) { }
890
+ applyEnumDecorator(decorator, schema) {
891
+ if (decorator.arguments.length === 0)
892
+ return;
893
+ const arg = decorator.arguments[0];
894
+ if (arg && typeof arg === 'object' && 'kind' in arg) {
895
+ const type = this.checker.getTypeAtLocation(arg);
896
+ if (type.symbol && type.symbol.exports) {
897
+ const values = [];
898
+ type.symbol.exports.forEach(member => {
899
+ const declaration = member.valueDeclaration;
900
+ if (declaration && ts.isEnumMember(declaration)) {
901
+ const value = this.checker.getConstantValue(declaration);
902
+ if (value !== undefined) {
903
+ values.push(value);
904
+ }
905
+ }
906
+ });
907
+ if (values.length > 0) {
908
+ schema.enum = values;
909
+ const isString = values.every(v => typeof v === 'string');
910
+ const isNumber = values.every(v => typeof v === 'number');
911
+ if (isString) {
912
+ schema.type = 'string';
913
+ }
914
+ else if (isNumber) {
915
+ schema.type = 'number';
916
+ }
917
+ else {
918
+ schema.type = 'string';
919
+ }
920
+ }
921
+ }
922
+ }
923
+ }
747
924
  applyDecorators(property, schema) {
748
925
  for (const decorator of property.decorators) {
749
926
  const decoratorName = decorator.name;
@@ -807,22 +984,22 @@ class SchemaTransformer {
807
984
  property.isOptional = true;
808
985
  break;
809
986
  case constants.validatorDecorators.MinLength.name:
810
- schema.minLength = decorator.arguments[0];
987
+ schema.minLength = this.getSafeDecoratorArgument(decorator.arguments[0]);
811
988
  break;
812
989
  case constants.validatorDecorators.MaxLength.name:
813
- schema.maxLength = decorator.arguments[0];
990
+ schema.maxLength = this.getSafeDecoratorArgument(decorator.arguments[0]);
814
991
  break;
815
992
  case constants.validatorDecorators.Length.name:
816
- schema.minLength = decorator.arguments[0];
993
+ schema.minLength = this.getSafeDecoratorArgument(decorator.arguments[0]);
817
994
  if (decorator.arguments[1]) {
818
- schema.maxLength = decorator.arguments[1];
995
+ schema.maxLength = this.getSafeDecoratorArgument(decorator.arguments[1]);
819
996
  }
820
997
  break;
821
998
  case constants.validatorDecorators.Min.name:
822
- schema.minimum = decorator.arguments[0];
999
+ schema.minimum = this.getSafeDecoratorArgument(decorator.arguments[0]);
823
1000
  break;
824
1001
  case constants.validatorDecorators.Max.name:
825
- schema.maximum = decorator.arguments[0];
1002
+ schema.maximum = this.getSafeDecoratorArgument(decorator.arguments[0]);
826
1003
  break;
827
1004
  case constants.validatorDecorators.IsPositive.name:
828
1005
  schema.minimum = 0;
@@ -835,13 +1012,18 @@ class SchemaTransformer {
835
1012
  property.isOptional = false;
836
1013
  break;
837
1014
  case constants.validatorDecorators.ArrayMinSize.name:
838
- schema.minItems = decorator.arguments[0];
1015
+ schema.minItems = this.getSafeDecoratorArgument(decorator.arguments[0]);
839
1016
  break;
840
1017
  case constants.validatorDecorators.ArrayMaxSize.name:
841
- schema.maxItems = decorator.arguments[0];
1018
+ schema.maxItems = this.getSafeDecoratorArgument(decorator.arguments[0]);
842
1019
  break;
843
1020
  case constants.validatorDecorators.IsEnum.name:
844
- this.applyEnumDecorator(decorator, schema);
1021
+ if (!property.isArray) {
1022
+ this.applyEnumDecorator(decorator, schema);
1023
+ }
1024
+ else if (schema.items) {
1025
+ this.applyEnumDecorator(decorator, schema.items);
1026
+ }
845
1027
  break;
846
1028
  }
847
1029
  }