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 +9 -2
- package/dist/__test__/entities/enum-classes.d.ts +30 -0
- package/dist/__test__/index.d.ts +1 -0
- package/dist/index.esm.js +210 -28
- package/dist/index.js +210 -28
- package/dist/run.d.ts +1 -1
- package/dist/run.js +279 -649
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
- /package/dist/__test__/testCases/{debug.test.d.ts → enum-properties.test.d.ts} +0 -0
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
|
+
}
|
package/dist/__test__/index.d.ts
CHANGED
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
|
|
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
|
|
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
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
-
|
|
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
|
-
|
|
507
|
-
|
|
508
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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
|
-
|
|
746
|
-
|
|
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
|
-
|
|
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
|
}
|