ts-class-to-openapi 1.2.1 → 1.2.3
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/collision/arrays/number-array.d.ts +3 -0
- package/dist/__test__/entities/collision/arrays/string-array.d.ts +3 -0
- package/dist/__test__/entities/collision/number-props/same-name.d.ts +5 -0
- package/dist/__test__/entities/collision/string-props/same-name.d.ts +5 -0
- package/dist/__test__/entities/collision/throwing/class-a.d.ts +4 -0
- package/dist/__test__/entities/collision/throwing/class-b.d.ts +4 -0
- package/dist/__test__/entities/enum-classes.d.ts +30 -0
- package/dist/__test__/entities/evaluation/generics.d.ts +6 -0
- package/dist/__test__/entities/evaluation/modifiers.d.ts +7 -0
- package/dist/__test__/index.d.ts +4 -0
- package/dist/__test__/testCases/collision.test.d.ts +1 -0
- package/dist/__test__/testCases/enum-properties.test.d.ts +1 -0
- package/dist/__test__/testCases/generics-and-modifiers.test.d.ts +1 -0
- package/dist/index.esm.js +311 -51
- package/dist/index.js +311 -51
- package/dist/run.d.ts +1 -1
- package/dist/run.js +380 -672
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
- /package/dist/__test__/testCases/{debug.test.d.ts → collision-advanced.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
|
@@ -1,3 +1,7 @@
|
|
|
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';
|
|
5
|
+
import './testCases/collision.test';
|
|
6
|
+
import './testCases/collision-advanced.test';
|
|
7
|
+
import './testCases/generics-and-modifiers.test';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.esm.js
CHANGED
|
@@ -66,14 +66,14 @@ class SchemaTransformer {
|
|
|
66
66
|
this.program = ts.createProgram(fileNames, tsOptions);
|
|
67
67
|
this.checker = this.program.getTypeChecker();
|
|
68
68
|
}
|
|
69
|
-
getPropertiesByClassDeclaration(classNode, visitedDeclarations = new Set()) {
|
|
69
|
+
getPropertiesByClassDeclaration(classNode, visitedDeclarations = new Set(), genericTypeMap = new Map()) {
|
|
70
70
|
if (visitedDeclarations.has(classNode)) {
|
|
71
71
|
return [];
|
|
72
72
|
}
|
|
73
73
|
visitedDeclarations.add(classNode);
|
|
74
74
|
// if no heritage clauses, get properties directly from class
|
|
75
75
|
if (!classNode.heritageClauses) {
|
|
76
|
-
return this.getPropertiesByClassMembers(classNode.members, classNode);
|
|
76
|
+
return this.getPropertiesByClassMembers(classNode.members, classNode, genericTypeMap);
|
|
77
77
|
} // use heritage clauses to get properties from base classes
|
|
78
78
|
else {
|
|
79
79
|
const heritageClause = classNode.heritageClauses[0];
|
|
@@ -89,28 +89,47 @@ class SchemaTransformer {
|
|
|
89
89
|
return [];
|
|
90
90
|
const declaration = symbol.declarations?.[0];
|
|
91
91
|
if (declaration && ts.isClassDeclaration(declaration)) {
|
|
92
|
-
|
|
92
|
+
const newGenericTypeMap = new Map();
|
|
93
|
+
if (declaration.typeParameters && type.typeArguments) {
|
|
94
|
+
declaration.typeParameters.forEach((param, index) => {
|
|
95
|
+
const arg = type.typeArguments[index];
|
|
96
|
+
if (arg) {
|
|
97
|
+
const resolvedArg = this.getTypeNodeToString(arg, genericTypeMap);
|
|
98
|
+
newGenericTypeMap.set(param.name.text, resolvedArg);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
baseProperties = this.getPropertiesByClassDeclaration(declaration, visitedDeclarations, newGenericTypeMap);
|
|
93
103
|
}
|
|
94
|
-
properties = this.getPropertiesByClassMembers(classNode.members, classNode);
|
|
104
|
+
properties = this.getPropertiesByClassMembers(classNode.members, classNode, genericTypeMap);
|
|
95
105
|
return baseProperties.concat(properties);
|
|
96
106
|
}
|
|
97
107
|
else {
|
|
98
|
-
return this.getPropertiesByClassMembers(classNode.members, classNode);
|
|
108
|
+
return this.getPropertiesByClassMembers(classNode.members, classNode, genericTypeMap);
|
|
99
109
|
}
|
|
100
110
|
}
|
|
101
111
|
}
|
|
102
|
-
getPropertiesByClassMembers(members, parentClassNode) {
|
|
112
|
+
getPropertiesByClassMembers(members, parentClassNode, genericTypeMap = new Map()) {
|
|
103
113
|
const properties = [];
|
|
104
114
|
for (const member of members) {
|
|
105
115
|
if (ts.isPropertyDeclaration(member) &&
|
|
106
116
|
member.name &&
|
|
107
117
|
ts.isIdentifier(member.name)) {
|
|
118
|
+
// Skip static, private, and protected properties
|
|
119
|
+
if (member.modifiers) {
|
|
120
|
+
const hasExcludedModifier = member.modifiers.some(m => m.kind === ts.SyntaxKind.StaticKeyword ||
|
|
121
|
+
m.kind === ts.SyntaxKind.PrivateKeyword ||
|
|
122
|
+
m.kind === ts.SyntaxKind.ProtectedKeyword);
|
|
123
|
+
if (hasExcludedModifier)
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
108
126
|
const propertyName = member.name.text;
|
|
109
|
-
const type = this.getPropertyType(member);
|
|
127
|
+
const type = this.getPropertyType(member, genericTypeMap);
|
|
110
128
|
const decorators = this.extractDecorators(member);
|
|
111
129
|
const isOptional = !!member.questionToken;
|
|
112
130
|
const isGeneric = this.isPropertyTypeGeneric(member);
|
|
113
|
-
const
|
|
131
|
+
const isEnum = this.isEnum(member);
|
|
132
|
+
const isPrimitive = this.isPrimitiveType(type) || isEnum;
|
|
114
133
|
const isClassType = this.isClassType(member);
|
|
115
134
|
const isArray = this.isArrayProperty(member);
|
|
116
135
|
const isTypeLiteral = this.isTypeLiteral(member);
|
|
@@ -124,6 +143,7 @@ class SchemaTransformer {
|
|
|
124
143
|
isPrimitive,
|
|
125
144
|
isClassType,
|
|
126
145
|
isArray,
|
|
146
|
+
isEnum,
|
|
127
147
|
isRef: false,
|
|
128
148
|
isTypeLiteral: isTypeLiteral,
|
|
129
149
|
};
|
|
@@ -164,17 +184,21 @@ class SchemaTransformer {
|
|
|
164
184
|
}
|
|
165
185
|
return properties;
|
|
166
186
|
}
|
|
167
|
-
getPropertyType(property) {
|
|
187
|
+
getPropertyType(property, genericTypeMap = new Map()) {
|
|
168
188
|
if (property.type) {
|
|
169
|
-
return this.getTypeNodeToString(property.type);
|
|
189
|
+
return this.getTypeNodeToString(property.type, genericTypeMap);
|
|
170
190
|
}
|
|
171
191
|
const type = this.checker.getTypeAtLocation(property);
|
|
172
192
|
return this.getStringFromType(type);
|
|
173
193
|
}
|
|
174
|
-
getTypeNodeToString(typeNode) {
|
|
194
|
+
getTypeNodeToString(typeNode, genericTypeMap = new Map()) {
|
|
175
195
|
if (ts.isTypeReferenceNode(typeNode) &&
|
|
176
196
|
ts.isIdentifier(typeNode.typeName)) {
|
|
177
|
-
|
|
197
|
+
const typeName = typeNode.typeName.text;
|
|
198
|
+
if (genericTypeMap.has(typeName)) {
|
|
199
|
+
return genericTypeMap.get(typeName);
|
|
200
|
+
}
|
|
201
|
+
if (typeName.toLowerCase() === 'uploadfile') {
|
|
178
202
|
return 'UploadFile';
|
|
179
203
|
}
|
|
180
204
|
if (typeNode.typeArguments && typeNode.typeArguments.length > 0) {
|
|
@@ -199,11 +223,11 @@ class SchemaTransformer {
|
|
|
199
223
|
return constants.jsPrimitives.Boolean.type;
|
|
200
224
|
case ts.SyntaxKind.ArrayType:
|
|
201
225
|
const arrayType = typeNode;
|
|
202
|
-
return `${this.getTypeNodeToString(arrayType.elementType)}[]`;
|
|
226
|
+
return `${this.getTypeNodeToString(arrayType.elementType, genericTypeMap)}[]`;
|
|
203
227
|
case ts.SyntaxKind.UnionType:
|
|
204
228
|
// Handle union types like string | null
|
|
205
229
|
const unionType = typeNode;
|
|
206
|
-
const types = unionType.types.map(t => this.getTypeNodeToString(t));
|
|
230
|
+
const types = unionType.types.map(t => this.getTypeNodeToString(t, genericTypeMap));
|
|
207
231
|
// Filter out null and undefined, return the first meaningful type
|
|
208
232
|
const meaningfulTypes = types.filter(t => t !== 'null' && t !== 'undefined');
|
|
209
233
|
if (meaningfulTypes.length > 0 && meaningfulTypes[0]) {
|
|
@@ -215,6 +239,10 @@ class SchemaTransformer {
|
|
|
215
239
|
return 'object';
|
|
216
240
|
default:
|
|
217
241
|
const typeText = typeNode.getText();
|
|
242
|
+
// Check if this is a generic type parameter we can resolve
|
|
243
|
+
if (genericTypeMap && genericTypeMap.has(typeText)) {
|
|
244
|
+
return genericTypeMap.get(typeText);
|
|
245
|
+
}
|
|
218
246
|
// Handle some common TypeScript utility types
|
|
219
247
|
if (typeText.startsWith('Date'))
|
|
220
248
|
return constants.jsPrimitives.Date.type;
|
|
@@ -275,9 +303,15 @@ class SchemaTransformer {
|
|
|
275
303
|
return true;
|
|
276
304
|
if (arg.kind === ts.SyntaxKind.FalseKeyword)
|
|
277
305
|
return false;
|
|
278
|
-
return arg
|
|
306
|
+
return arg;
|
|
279
307
|
});
|
|
280
308
|
}
|
|
309
|
+
getSafeDecoratorArgument(arg) {
|
|
310
|
+
if (arg && typeof arg === 'object' && 'kind' in arg) {
|
|
311
|
+
return arg.getText();
|
|
312
|
+
}
|
|
313
|
+
return arg;
|
|
314
|
+
}
|
|
281
315
|
isPropertyTypeGeneric(property) {
|
|
282
316
|
if (property.type && this.isGenericTypeFromNode(property.type)) {
|
|
283
317
|
return true;
|
|
@@ -443,37 +477,175 @@ class SchemaTransformer {
|
|
|
443
477
|
}
|
|
444
478
|
return SchemaTransformer.instance;
|
|
445
479
|
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
480
|
+
getSourceFileByClass(cls, sourceOptions) {
|
|
481
|
+
const className = cls.name;
|
|
482
|
+
const sourceFiles = this.getFilteredSourceFiles(sourceOptions);
|
|
483
|
+
const matches = [];
|
|
484
|
+
for (const sourceFile of sourceFiles) {
|
|
485
|
+
const node = sourceFile.statements.find(stmt => ts.isClassDeclaration(stmt) &&
|
|
486
|
+
stmt.name &&
|
|
487
|
+
stmt.name.text === className);
|
|
488
|
+
if (node) {
|
|
489
|
+
matches.push({ sourceFile, node });
|
|
490
|
+
}
|
|
453
491
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
492
|
+
if (matches.length === 0) {
|
|
493
|
+
return undefined;
|
|
494
|
+
}
|
|
495
|
+
if (matches.length === 1) {
|
|
496
|
+
return matches[0];
|
|
497
|
+
}
|
|
498
|
+
if (matches.length > 1 && !sourceOptions?.filePath) {
|
|
499
|
+
const bestMatch = this.findBestMatch(cls, matches);
|
|
500
|
+
if (bestMatch) {
|
|
501
|
+
return bestMatch;
|
|
502
|
+
}
|
|
503
|
+
const firstMatch = matches[0];
|
|
504
|
+
if (firstMatch) {
|
|
505
|
+
console.warn(`[ts-class-to-openapi] Warning: Found multiple classes with name '${className}'. Using the first one found in '${firstMatch.sourceFile.fileName}'. To resolve this collision, provide 'sourceOptions.filePath'.`);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return matches[0];
|
|
509
|
+
}
|
|
510
|
+
checkTypeMatch(value, typeNode) {
|
|
511
|
+
const runtimeType = typeof value;
|
|
512
|
+
if (runtimeType === 'string' &&
|
|
513
|
+
typeNode.kind === ts.SyntaxKind.StringKeyword)
|
|
514
|
+
return true;
|
|
515
|
+
if (runtimeType === 'number' &&
|
|
516
|
+
typeNode.kind === ts.SyntaxKind.NumberKeyword)
|
|
517
|
+
return true;
|
|
518
|
+
if (runtimeType === 'boolean' &&
|
|
519
|
+
typeNode.kind === ts.SyntaxKind.BooleanKeyword)
|
|
520
|
+
return true;
|
|
521
|
+
if (Array.isArray(value) && ts.isArrayTypeNode(typeNode)) {
|
|
522
|
+
if (value.length === 0)
|
|
462
523
|
return true;
|
|
524
|
+
const firstItem = value[0];
|
|
525
|
+
const elementType = typeNode.elementType;
|
|
526
|
+
return this.checkTypeMatch(firstItem, elementType);
|
|
527
|
+
}
|
|
528
|
+
if (runtimeType === 'object' && value !== null && !Array.isArray(value)) {
|
|
529
|
+
if (ts.isTypeReferenceNode(typeNode) ||
|
|
530
|
+
typeNode.kind === ts.SyntaxKind.ObjectKeyword) {
|
|
531
|
+
return true;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
findBestMatch(cls, matches) {
|
|
537
|
+
const runtimeSource = cls.toString();
|
|
538
|
+
const runtimeProperties = new Map();
|
|
539
|
+
const regexProperties = new Set();
|
|
540
|
+
// Try to extract properties from runtime source (assignments in constructor)
|
|
541
|
+
try {
|
|
542
|
+
const regex = /this\.([a-zA-Z0-9_$]+)\s*=/g;
|
|
543
|
+
let match;
|
|
544
|
+
while ((match = regex.exec(runtimeSource)) !== null) {
|
|
545
|
+
regexProperties.add(match[1]);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
catch (e) {
|
|
549
|
+
// Ignore regex errors
|
|
550
|
+
}
|
|
551
|
+
// Try to instantiate the class to find properties
|
|
552
|
+
try {
|
|
553
|
+
const instance = new cls();
|
|
554
|
+
Object.keys(instance).forEach(key => {
|
|
555
|
+
runtimeProperties.set(key, instance[key]);
|
|
463
556
|
});
|
|
464
557
|
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
558
|
+
catch (e) {
|
|
559
|
+
// Ignore instantiation errors (e.g. required constructor arguments)
|
|
560
|
+
}
|
|
561
|
+
// Try to get properties from class-validator metadata
|
|
562
|
+
try {
|
|
563
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
564
|
+
const { getMetadataStorage } = require('class-validator');
|
|
565
|
+
const metadata = getMetadataStorage();
|
|
566
|
+
const targetMetadata = metadata.getTargetValidationMetadatas(cls, null, false, false);
|
|
567
|
+
targetMetadata.forEach((m) => {
|
|
568
|
+
if (m.propertyName && !runtimeProperties.has(m.propertyName)) {
|
|
569
|
+
runtimeProperties.set(m.propertyName, undefined);
|
|
570
|
+
}
|
|
472
571
|
});
|
|
473
|
-
|
|
474
|
-
|
|
572
|
+
}
|
|
573
|
+
catch (e) {
|
|
574
|
+
// Ignore if class-validator is not available or fails
|
|
575
|
+
}
|
|
576
|
+
// console.log(`[findBestMatch] Class: ${cls.name}, Runtime Props: ${Array.from(runtimeProperties.keys()).join(', ')}`)
|
|
577
|
+
const scores = matches.map(match => {
|
|
578
|
+
let score = 0;
|
|
579
|
+
for (const member of match.node.members) {
|
|
580
|
+
if (ts.isMethodDeclaration(member) && ts.isIdentifier(member.name)) {
|
|
581
|
+
if (runtimeSource.includes(member.name.text)) {
|
|
582
|
+
score += 2;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
else if (ts.isPropertyDeclaration(member) &&
|
|
586
|
+
ts.isIdentifier(member.name)) {
|
|
587
|
+
const propName = member.name.text;
|
|
588
|
+
if (runtimeProperties.has(propName)) {
|
|
589
|
+
score += 1;
|
|
590
|
+
const value = runtimeProperties.get(propName);
|
|
591
|
+
if (member.type && this.checkTypeMatch(value, member.type)) {
|
|
592
|
+
score += 5;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
else if (regexProperties.has(propName)) {
|
|
596
|
+
score += 1;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
return { match, score };
|
|
601
|
+
});
|
|
602
|
+
scores.sort((a, b) => b.score - a.score);
|
|
603
|
+
const firstScore = scores[0];
|
|
604
|
+
const secondScore = scores[1];
|
|
605
|
+
if (firstScore && firstScore.score > 0) {
|
|
606
|
+
if (scores.length === 1 ||
|
|
607
|
+
(secondScore && firstScore.score > secondScore.score)) {
|
|
608
|
+
return firstScore.match;
|
|
475
609
|
}
|
|
476
610
|
}
|
|
611
|
+
return undefined;
|
|
612
|
+
}
|
|
613
|
+
getFilteredSourceFiles(sourceOptions) {
|
|
614
|
+
if (sourceOptions?.isExternal) {
|
|
615
|
+
return this.program.getSourceFiles().filter(sf => {
|
|
616
|
+
return (sf.fileName.includes(sourceOptions.packageName) &&
|
|
617
|
+
(!sourceOptions.filePath || sf.fileName === sourceOptions.filePath));
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
return this.program.getSourceFiles().filter(sf => {
|
|
621
|
+
if (sf.isDeclarationFile)
|
|
622
|
+
return false;
|
|
623
|
+
if (sf.fileName.includes('.d.ts'))
|
|
624
|
+
return false;
|
|
625
|
+
if (sf.fileName.includes('node_modules'))
|
|
626
|
+
return false;
|
|
627
|
+
if (sourceOptions?.filePath &&
|
|
628
|
+
!sf.fileName.includes(sourceOptions.filePath)) {
|
|
629
|
+
return false;
|
|
630
|
+
}
|
|
631
|
+
return true;
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
isEnum(propertyDeclaration) {
|
|
635
|
+
if (!propertyDeclaration.type) {
|
|
636
|
+
return false;
|
|
637
|
+
}
|
|
638
|
+
let typeNode = propertyDeclaration.type;
|
|
639
|
+
if (ts.isArrayTypeNode(typeNode)) {
|
|
640
|
+
typeNode = typeNode.elementType;
|
|
641
|
+
}
|
|
642
|
+
if (ts.isTypeReferenceNode(typeNode)) {
|
|
643
|
+
const type = this.checker.getTypeAtLocation(typeNode);
|
|
644
|
+
// console.log('isEnum check:', typeNode.getText(), type.flags)
|
|
645
|
+
return (!!(type.flags & ts.TypeFlags.Enum) ||
|
|
646
|
+
!!(type.flags & ts.TypeFlags.EnumLiteral));
|
|
647
|
+
}
|
|
648
|
+
return false;
|
|
477
649
|
}
|
|
478
650
|
isClassType(propertyDeclaration) {
|
|
479
651
|
// If there's no explicit type annotation, we can't determine reliably
|
|
@@ -724,7 +896,58 @@ class SchemaTransformer {
|
|
|
724
896
|
transformedSchema.set(declaration.name.text, schema);
|
|
725
897
|
return schema;
|
|
726
898
|
}
|
|
899
|
+
getSchemaFromEnum(property) {
|
|
900
|
+
let typeNode = property.originalProperty.type;
|
|
901
|
+
if (ts.isArrayTypeNode(typeNode)) {
|
|
902
|
+
typeNode = typeNode.elementType;
|
|
903
|
+
}
|
|
904
|
+
const type = this.checker.getTypeAtLocation(typeNode);
|
|
905
|
+
if (type.symbol && type.symbol.exports) {
|
|
906
|
+
const values = [];
|
|
907
|
+
type.symbol.exports.forEach(member => {
|
|
908
|
+
const declaration = member.valueDeclaration;
|
|
909
|
+
if (declaration && ts.isEnumMember(declaration)) {
|
|
910
|
+
const value = this.checker.getConstantValue(declaration);
|
|
911
|
+
if (value !== undefined) {
|
|
912
|
+
values.push(value);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
});
|
|
916
|
+
if (values.length > 0) {
|
|
917
|
+
const propertySchema = { type: 'object' };
|
|
918
|
+
propertySchema.enum = values;
|
|
919
|
+
const isString = values.every(v => typeof v === 'string');
|
|
920
|
+
const isNumber = values.every(v => typeof v === 'number');
|
|
921
|
+
if (isString) {
|
|
922
|
+
propertySchema.type = 'string';
|
|
923
|
+
}
|
|
924
|
+
else if (isNumber) {
|
|
925
|
+
propertySchema.type = 'number';
|
|
926
|
+
}
|
|
927
|
+
else {
|
|
928
|
+
propertySchema.type = 'string';
|
|
929
|
+
}
|
|
930
|
+
if (property.isArray) {
|
|
931
|
+
const itemsSchema = { ...propertySchema };
|
|
932
|
+
propertySchema.type = 'array';
|
|
933
|
+
propertySchema.items = itemsSchema;
|
|
934
|
+
delete propertySchema.enum;
|
|
935
|
+
return propertySchema;
|
|
936
|
+
}
|
|
937
|
+
else {
|
|
938
|
+
return propertySchema;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
return undefined;
|
|
943
|
+
}
|
|
727
944
|
getSchemaFromPrimitive(property) {
|
|
945
|
+
if (property.isEnum) {
|
|
946
|
+
const enumSchema = this.getSchemaFromEnum(property);
|
|
947
|
+
if (enumSchema) {
|
|
948
|
+
return enumSchema;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
728
951
|
const propertySchema = { type: 'object' };
|
|
729
952
|
const propertyType = property.type.toLowerCase().replace('[]', '').trim();
|
|
730
953
|
let isFile = false;
|
|
@@ -812,8 +1035,40 @@ class SchemaTransformer {
|
|
|
812
1035
|
ts.isTypeReferenceNode(typeNode) // Omit, Pick, Partial, etc.
|
|
813
1036
|
);
|
|
814
1037
|
}
|
|
815
|
-
|
|
816
|
-
|
|
1038
|
+
applyEnumDecorator(decorator, schema) {
|
|
1039
|
+
if (decorator.arguments.length === 0)
|
|
1040
|
+
return;
|
|
1041
|
+
const arg = decorator.arguments[0];
|
|
1042
|
+
if (arg && typeof arg === 'object' && 'kind' in arg) {
|
|
1043
|
+
const type = this.checker.getTypeAtLocation(arg);
|
|
1044
|
+
if (type.symbol && type.symbol.exports) {
|
|
1045
|
+
const values = [];
|
|
1046
|
+
type.symbol.exports.forEach(member => {
|
|
1047
|
+
const declaration = member.valueDeclaration;
|
|
1048
|
+
if (declaration && ts.isEnumMember(declaration)) {
|
|
1049
|
+
const value = this.checker.getConstantValue(declaration);
|
|
1050
|
+
if (value !== undefined) {
|
|
1051
|
+
values.push(value);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
});
|
|
1055
|
+
if (values.length > 0) {
|
|
1056
|
+
schema.enum = values;
|
|
1057
|
+
const isString = values.every(v => typeof v === 'string');
|
|
1058
|
+
const isNumber = values.every(v => typeof v === 'number');
|
|
1059
|
+
if (isString) {
|
|
1060
|
+
schema.type = 'string';
|
|
1061
|
+
}
|
|
1062
|
+
else if (isNumber) {
|
|
1063
|
+
schema.type = 'number';
|
|
1064
|
+
}
|
|
1065
|
+
else {
|
|
1066
|
+
schema.type = 'string';
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
817
1072
|
applyDecorators(property, schema) {
|
|
818
1073
|
for (const decorator of property.decorators) {
|
|
819
1074
|
const decoratorName = decorator.name;
|
|
@@ -877,22 +1132,22 @@ class SchemaTransformer {
|
|
|
877
1132
|
property.isOptional = true;
|
|
878
1133
|
break;
|
|
879
1134
|
case constants.validatorDecorators.MinLength.name:
|
|
880
|
-
schema.minLength = decorator.arguments[0];
|
|
1135
|
+
schema.minLength = this.getSafeDecoratorArgument(decorator.arguments[0]);
|
|
881
1136
|
break;
|
|
882
1137
|
case constants.validatorDecorators.MaxLength.name:
|
|
883
|
-
schema.maxLength = decorator.arguments[0];
|
|
1138
|
+
schema.maxLength = this.getSafeDecoratorArgument(decorator.arguments[0]);
|
|
884
1139
|
break;
|
|
885
1140
|
case constants.validatorDecorators.Length.name:
|
|
886
|
-
schema.minLength = decorator.arguments[0];
|
|
1141
|
+
schema.minLength = this.getSafeDecoratorArgument(decorator.arguments[0]);
|
|
887
1142
|
if (decorator.arguments[1]) {
|
|
888
|
-
schema.maxLength = decorator.arguments[1];
|
|
1143
|
+
schema.maxLength = this.getSafeDecoratorArgument(decorator.arguments[1]);
|
|
889
1144
|
}
|
|
890
1145
|
break;
|
|
891
1146
|
case constants.validatorDecorators.Min.name:
|
|
892
|
-
schema.minimum = decorator.arguments[0];
|
|
1147
|
+
schema.minimum = this.getSafeDecoratorArgument(decorator.arguments[0]);
|
|
893
1148
|
break;
|
|
894
1149
|
case constants.validatorDecorators.Max.name:
|
|
895
|
-
schema.maximum = decorator.arguments[0];
|
|
1150
|
+
schema.maximum = this.getSafeDecoratorArgument(decorator.arguments[0]);
|
|
896
1151
|
break;
|
|
897
1152
|
case constants.validatorDecorators.IsPositive.name:
|
|
898
1153
|
schema.minimum = 0;
|
|
@@ -905,20 +1160,25 @@ class SchemaTransformer {
|
|
|
905
1160
|
property.isOptional = false;
|
|
906
1161
|
break;
|
|
907
1162
|
case constants.validatorDecorators.ArrayMinSize.name:
|
|
908
|
-
schema.minItems = decorator.arguments[0];
|
|
1163
|
+
schema.minItems = this.getSafeDecoratorArgument(decorator.arguments[0]);
|
|
909
1164
|
break;
|
|
910
1165
|
case constants.validatorDecorators.ArrayMaxSize.name:
|
|
911
|
-
schema.maxItems = decorator.arguments[0];
|
|
1166
|
+
schema.maxItems = this.getSafeDecoratorArgument(decorator.arguments[0]);
|
|
912
1167
|
break;
|
|
913
1168
|
case constants.validatorDecorators.IsEnum.name:
|
|
914
|
-
|
|
1169
|
+
if (!property.isArray) {
|
|
1170
|
+
this.applyEnumDecorator(decorator, schema);
|
|
1171
|
+
}
|
|
1172
|
+
else if (schema.items) {
|
|
1173
|
+
this.applyEnumDecorator(decorator, schema.items);
|
|
1174
|
+
}
|
|
915
1175
|
break;
|
|
916
1176
|
}
|
|
917
1177
|
}
|
|
918
1178
|
}
|
|
919
1179
|
transform(cls, sourceOptions) {
|
|
920
1180
|
let schema = { type: 'object', properties: {} };
|
|
921
|
-
const result = this.
|
|
1181
|
+
const result = this.getSourceFileByClass(cls, sourceOptions);
|
|
922
1182
|
if (!result?.sourceFile) {
|
|
923
1183
|
console.warn(`Class ${cls.name} not found in any source file.`);
|
|
924
1184
|
return { name: cls.name, schema: {} };
|