ts-class-to-openapi 1.2.1 → 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,6 +483,22 @@ 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) {
@@ -724,7 +748,58 @@ class SchemaTransformer {
724
748
  transformedSchema.set(declaration.name.text, schema);
725
749
  return schema;
726
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
+ }
727
796
  getSchemaFromPrimitive(property) {
797
+ if (property.isEnum) {
798
+ const enumSchema = this.getSchemaFromEnum(property);
799
+ if (enumSchema) {
800
+ return enumSchema;
801
+ }
802
+ }
728
803
  const propertySchema = { type: 'object' };
729
804
  const propertyType = property.type.toLowerCase().replace('[]', '').trim();
730
805
  let isFile = false;
@@ -812,8 +887,40 @@ class SchemaTransformer {
812
887
  ts.isTypeReferenceNode(typeNode) // Omit, Pick, Partial, etc.
813
888
  );
814
889
  }
815
- //Todo: implement properly
816
- 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
+ }
817
924
  applyDecorators(property, schema) {
818
925
  for (const decorator of property.decorators) {
819
926
  const decoratorName = decorator.name;
@@ -877,22 +984,22 @@ class SchemaTransformer {
877
984
  property.isOptional = true;
878
985
  break;
879
986
  case constants.validatorDecorators.MinLength.name:
880
- schema.minLength = decorator.arguments[0];
987
+ schema.minLength = this.getSafeDecoratorArgument(decorator.arguments[0]);
881
988
  break;
882
989
  case constants.validatorDecorators.MaxLength.name:
883
- schema.maxLength = decorator.arguments[0];
990
+ schema.maxLength = this.getSafeDecoratorArgument(decorator.arguments[0]);
884
991
  break;
885
992
  case constants.validatorDecorators.Length.name:
886
- schema.minLength = decorator.arguments[0];
993
+ schema.minLength = this.getSafeDecoratorArgument(decorator.arguments[0]);
887
994
  if (decorator.arguments[1]) {
888
- schema.maxLength = decorator.arguments[1];
995
+ schema.maxLength = this.getSafeDecoratorArgument(decorator.arguments[1]);
889
996
  }
890
997
  break;
891
998
  case constants.validatorDecorators.Min.name:
892
- schema.minimum = decorator.arguments[0];
999
+ schema.minimum = this.getSafeDecoratorArgument(decorator.arguments[0]);
893
1000
  break;
894
1001
  case constants.validatorDecorators.Max.name:
895
- schema.maximum = decorator.arguments[0];
1002
+ schema.maximum = this.getSafeDecoratorArgument(decorator.arguments[0]);
896
1003
  break;
897
1004
  case constants.validatorDecorators.IsPositive.name:
898
1005
  schema.minimum = 0;
@@ -905,13 +1012,18 @@ class SchemaTransformer {
905
1012
  property.isOptional = false;
906
1013
  break;
907
1014
  case constants.validatorDecorators.ArrayMinSize.name:
908
- schema.minItems = decorator.arguments[0];
1015
+ schema.minItems = this.getSafeDecoratorArgument(decorator.arguments[0]);
909
1016
  break;
910
1017
  case constants.validatorDecorators.ArrayMaxSize.name:
911
- schema.maxItems = decorator.arguments[0];
1018
+ schema.maxItems = this.getSafeDecoratorArgument(decorator.arguments[0]);
912
1019
  break;
913
1020
  case constants.validatorDecorators.IsEnum.name:
914
- 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
+ }
915
1027
  break;
916
1028
  }
917
1029
  }
package/dist/index.js CHANGED
@@ -112,7 +112,8 @@ class SchemaTransformer {
112
112
  const decorators = this.extractDecorators(member);
113
113
  const isOptional = !!member.questionToken;
114
114
  const isGeneric = this.isPropertyTypeGeneric(member);
115
- const isPrimitive = this.isPrimitiveType(type);
115
+ const isEnum = this.isEnum(member);
116
+ const isPrimitive = this.isPrimitiveType(type) || isEnum;
116
117
  const isClassType = this.isClassType(member);
117
118
  const isArray = this.isArrayProperty(member);
118
119
  const isTypeLiteral = this.isTypeLiteral(member);
@@ -126,6 +127,7 @@ class SchemaTransformer {
126
127
  isPrimitive,
127
128
  isClassType,
128
129
  isArray,
130
+ isEnum,
129
131
  isRef: false,
130
132
  isTypeLiteral: isTypeLiteral,
131
133
  };
@@ -277,9 +279,15 @@ class SchemaTransformer {
277
279
  return true;
278
280
  if (arg.kind === ts.SyntaxKind.FalseKeyword)
279
281
  return false;
280
- return arg.getText();
282
+ return arg;
281
283
  });
282
284
  }
285
+ getSafeDecoratorArgument(arg) {
286
+ if (arg && typeof arg === 'object' && 'kind' in arg) {
287
+ return arg.getText();
288
+ }
289
+ return arg;
290
+ }
283
291
  isPropertyTypeGeneric(property) {
284
292
  if (property.type && this.isGenericTypeFromNode(property.type)) {
285
293
  return true;
@@ -477,6 +485,22 @@ class SchemaTransformer {
477
485
  }
478
486
  }
479
487
  }
488
+ isEnum(propertyDeclaration) {
489
+ if (!propertyDeclaration.type) {
490
+ return false;
491
+ }
492
+ let typeNode = propertyDeclaration.type;
493
+ if (ts.isArrayTypeNode(typeNode)) {
494
+ typeNode = typeNode.elementType;
495
+ }
496
+ if (ts.isTypeReferenceNode(typeNode)) {
497
+ const type = this.checker.getTypeAtLocation(typeNode);
498
+ // console.log('isEnum check:', typeNode.getText(), type.flags)
499
+ return (!!(type.flags & ts.TypeFlags.Enum) ||
500
+ !!(type.flags & ts.TypeFlags.EnumLiteral));
501
+ }
502
+ return false;
503
+ }
480
504
  isClassType(propertyDeclaration) {
481
505
  // If there's no explicit type annotation, we can't determine reliably
482
506
  if (!propertyDeclaration.type) {
@@ -726,7 +750,58 @@ class SchemaTransformer {
726
750
  transformedSchema.set(declaration.name.text, schema);
727
751
  return schema;
728
752
  }
753
+ getSchemaFromEnum(property) {
754
+ let typeNode = property.originalProperty.type;
755
+ if (ts.isArrayTypeNode(typeNode)) {
756
+ typeNode = typeNode.elementType;
757
+ }
758
+ const type = this.checker.getTypeAtLocation(typeNode);
759
+ if (type.symbol && type.symbol.exports) {
760
+ const values = [];
761
+ type.symbol.exports.forEach(member => {
762
+ const declaration = member.valueDeclaration;
763
+ if (declaration && ts.isEnumMember(declaration)) {
764
+ const value = this.checker.getConstantValue(declaration);
765
+ if (value !== undefined) {
766
+ values.push(value);
767
+ }
768
+ }
769
+ });
770
+ if (values.length > 0) {
771
+ const propertySchema = { type: 'object' };
772
+ propertySchema.enum = values;
773
+ const isString = values.every(v => typeof v === 'string');
774
+ const isNumber = values.every(v => typeof v === 'number');
775
+ if (isString) {
776
+ propertySchema.type = 'string';
777
+ }
778
+ else if (isNumber) {
779
+ propertySchema.type = 'number';
780
+ }
781
+ else {
782
+ propertySchema.type = 'string';
783
+ }
784
+ if (property.isArray) {
785
+ const itemsSchema = { ...propertySchema };
786
+ propertySchema.type = 'array';
787
+ propertySchema.items = itemsSchema;
788
+ delete propertySchema.enum;
789
+ return propertySchema;
790
+ }
791
+ else {
792
+ return propertySchema;
793
+ }
794
+ }
795
+ }
796
+ return undefined;
797
+ }
729
798
  getSchemaFromPrimitive(property) {
799
+ if (property.isEnum) {
800
+ const enumSchema = this.getSchemaFromEnum(property);
801
+ if (enumSchema) {
802
+ return enumSchema;
803
+ }
804
+ }
730
805
  const propertySchema = { type: 'object' };
731
806
  const propertyType = property.type.toLowerCase().replace('[]', '').trim();
732
807
  let isFile = false;
@@ -814,8 +889,40 @@ class SchemaTransformer {
814
889
  ts.isTypeReferenceNode(typeNode) // Omit, Pick, Partial, etc.
815
890
  );
816
891
  }
817
- //Todo: implement properly
818
- applyEnumDecorator(decorator, schema) { }
892
+ applyEnumDecorator(decorator, schema) {
893
+ if (decorator.arguments.length === 0)
894
+ return;
895
+ const arg = decorator.arguments[0];
896
+ if (arg && typeof arg === 'object' && 'kind' in arg) {
897
+ const type = this.checker.getTypeAtLocation(arg);
898
+ if (type.symbol && type.symbol.exports) {
899
+ const values = [];
900
+ type.symbol.exports.forEach(member => {
901
+ const declaration = member.valueDeclaration;
902
+ if (declaration && ts.isEnumMember(declaration)) {
903
+ const value = this.checker.getConstantValue(declaration);
904
+ if (value !== undefined) {
905
+ values.push(value);
906
+ }
907
+ }
908
+ });
909
+ if (values.length > 0) {
910
+ schema.enum = values;
911
+ const isString = values.every(v => typeof v === 'string');
912
+ const isNumber = values.every(v => typeof v === 'number');
913
+ if (isString) {
914
+ schema.type = 'string';
915
+ }
916
+ else if (isNumber) {
917
+ schema.type = 'number';
918
+ }
919
+ else {
920
+ schema.type = 'string';
921
+ }
922
+ }
923
+ }
924
+ }
925
+ }
819
926
  applyDecorators(property, schema) {
820
927
  for (const decorator of property.decorators) {
821
928
  const decoratorName = decorator.name;
@@ -879,22 +986,22 @@ class SchemaTransformer {
879
986
  property.isOptional = true;
880
987
  break;
881
988
  case constants.validatorDecorators.MinLength.name:
882
- schema.minLength = decorator.arguments[0];
989
+ schema.minLength = this.getSafeDecoratorArgument(decorator.arguments[0]);
883
990
  break;
884
991
  case constants.validatorDecorators.MaxLength.name:
885
- schema.maxLength = decorator.arguments[0];
992
+ schema.maxLength = this.getSafeDecoratorArgument(decorator.arguments[0]);
886
993
  break;
887
994
  case constants.validatorDecorators.Length.name:
888
- schema.minLength = decorator.arguments[0];
995
+ schema.minLength = this.getSafeDecoratorArgument(decorator.arguments[0]);
889
996
  if (decorator.arguments[1]) {
890
- schema.maxLength = decorator.arguments[1];
997
+ schema.maxLength = this.getSafeDecoratorArgument(decorator.arguments[1]);
891
998
  }
892
999
  break;
893
1000
  case constants.validatorDecorators.Min.name:
894
- schema.minimum = decorator.arguments[0];
1001
+ schema.minimum = this.getSafeDecoratorArgument(decorator.arguments[0]);
895
1002
  break;
896
1003
  case constants.validatorDecorators.Max.name:
897
- schema.maximum = decorator.arguments[0];
1004
+ schema.maximum = this.getSafeDecoratorArgument(decorator.arguments[0]);
898
1005
  break;
899
1006
  case constants.validatorDecorators.IsPositive.name:
900
1007
  schema.minimum = 0;
@@ -907,13 +1014,18 @@ class SchemaTransformer {
907
1014
  property.isOptional = false;
908
1015
  break;
909
1016
  case constants.validatorDecorators.ArrayMinSize.name:
910
- schema.minItems = decorator.arguments[0];
1017
+ schema.minItems = this.getSafeDecoratorArgument(decorator.arguments[0]);
911
1018
  break;
912
1019
  case constants.validatorDecorators.ArrayMaxSize.name:
913
- schema.maxItems = decorator.arguments[0];
1020
+ schema.maxItems = this.getSafeDecoratorArgument(decorator.arguments[0]);
914
1021
  break;
915
1022
  case constants.validatorDecorators.IsEnum.name:
916
- this.applyEnumDecorator(decorator, schema);
1023
+ if (!property.isArray) {
1024
+ this.applyEnumDecorator(decorator, schema);
1025
+ }
1026
+ else if (schema.items) {
1027
+ this.applyEnumDecorator(decorator, schema.items);
1028
+ }
917
1029
  break;
918
1030
  }
919
1031
  }
package/dist/run.d.ts CHANGED
@@ -1 +1 @@
1
- import './__test__/';
1
+ export {};