ts-class-to-openapi 1.2.2 → 1.2.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/__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/evaluation/generics.d.ts +6 -0
- package/dist/__test__/entities/evaluation/modifiers.d.ts +7 -0
- package/dist/__test__/index.d.ts +3 -0
- package/dist/__test__/testCases/collision-advanced.test.d.ts +1 -0
- package/dist/__test__/testCases/collision.test.d.ts +1 -0
- package/dist/__test__/testCases/generics-and-modifiers.test.d.ts +1 -0
- package/dist/index.esm.js +186 -38
- package/dist/index.js +186 -38
- package/dist/run.js +187 -39
- package/package.json +4 -6
package/dist/index.js
CHANGED
|
@@ -68,14 +68,14 @@ class SchemaTransformer {
|
|
|
68
68
|
this.program = ts.createProgram(fileNames, tsOptions);
|
|
69
69
|
this.checker = this.program.getTypeChecker();
|
|
70
70
|
}
|
|
71
|
-
getPropertiesByClassDeclaration(classNode, visitedDeclarations = new Set()) {
|
|
71
|
+
getPropertiesByClassDeclaration(classNode, visitedDeclarations = new Set(), genericTypeMap = new Map()) {
|
|
72
72
|
if (visitedDeclarations.has(classNode)) {
|
|
73
73
|
return [];
|
|
74
74
|
}
|
|
75
75
|
visitedDeclarations.add(classNode);
|
|
76
76
|
// if no heritage clauses, get properties directly from class
|
|
77
77
|
if (!classNode.heritageClauses) {
|
|
78
|
-
return this.getPropertiesByClassMembers(classNode.members, classNode);
|
|
78
|
+
return this.getPropertiesByClassMembers(classNode.members, classNode, genericTypeMap);
|
|
79
79
|
} // use heritage clauses to get properties from base classes
|
|
80
80
|
else {
|
|
81
81
|
const heritageClause = classNode.heritageClauses[0];
|
|
@@ -91,24 +91,42 @@ class SchemaTransformer {
|
|
|
91
91
|
return [];
|
|
92
92
|
const declaration = symbol.declarations?.[0];
|
|
93
93
|
if (declaration && ts.isClassDeclaration(declaration)) {
|
|
94
|
-
|
|
94
|
+
const newGenericTypeMap = new Map();
|
|
95
|
+
if (declaration.typeParameters && type.typeArguments) {
|
|
96
|
+
declaration.typeParameters.forEach((param, index) => {
|
|
97
|
+
const arg = type.typeArguments[index];
|
|
98
|
+
if (arg) {
|
|
99
|
+
const resolvedArg = this.getTypeNodeToString(arg, genericTypeMap);
|
|
100
|
+
newGenericTypeMap.set(param.name.text, resolvedArg);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
baseProperties = this.getPropertiesByClassDeclaration(declaration, visitedDeclarations, newGenericTypeMap);
|
|
95
105
|
}
|
|
96
|
-
properties = this.getPropertiesByClassMembers(classNode.members, classNode);
|
|
106
|
+
properties = this.getPropertiesByClassMembers(classNode.members, classNode, genericTypeMap);
|
|
97
107
|
return baseProperties.concat(properties);
|
|
98
108
|
}
|
|
99
109
|
else {
|
|
100
|
-
return this.getPropertiesByClassMembers(classNode.members, classNode);
|
|
110
|
+
return this.getPropertiesByClassMembers(classNode.members, classNode, genericTypeMap);
|
|
101
111
|
}
|
|
102
112
|
}
|
|
103
113
|
}
|
|
104
|
-
getPropertiesByClassMembers(members, parentClassNode) {
|
|
114
|
+
getPropertiesByClassMembers(members, parentClassNode, genericTypeMap = new Map()) {
|
|
105
115
|
const properties = [];
|
|
106
116
|
for (const member of members) {
|
|
107
117
|
if (ts.isPropertyDeclaration(member) &&
|
|
108
118
|
member.name &&
|
|
109
119
|
ts.isIdentifier(member.name)) {
|
|
120
|
+
// Skip static, private, and protected properties
|
|
121
|
+
if (member.modifiers) {
|
|
122
|
+
const hasExcludedModifier = member.modifiers.some(m => m.kind === ts.SyntaxKind.StaticKeyword ||
|
|
123
|
+
m.kind === ts.SyntaxKind.PrivateKeyword ||
|
|
124
|
+
m.kind === ts.SyntaxKind.ProtectedKeyword);
|
|
125
|
+
if (hasExcludedModifier)
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
110
128
|
const propertyName = member.name.text;
|
|
111
|
-
const type = this.getPropertyType(member);
|
|
129
|
+
const type = this.getPropertyType(member, genericTypeMap);
|
|
112
130
|
const decorators = this.extractDecorators(member);
|
|
113
131
|
const isOptional = !!member.questionToken;
|
|
114
132
|
const isGeneric = this.isPropertyTypeGeneric(member);
|
|
@@ -168,17 +186,21 @@ class SchemaTransformer {
|
|
|
168
186
|
}
|
|
169
187
|
return properties;
|
|
170
188
|
}
|
|
171
|
-
getPropertyType(property) {
|
|
189
|
+
getPropertyType(property, genericTypeMap = new Map()) {
|
|
172
190
|
if (property.type) {
|
|
173
|
-
return this.getTypeNodeToString(property.type);
|
|
191
|
+
return this.getTypeNodeToString(property.type, genericTypeMap);
|
|
174
192
|
}
|
|
175
193
|
const type = this.checker.getTypeAtLocation(property);
|
|
176
194
|
return this.getStringFromType(type);
|
|
177
195
|
}
|
|
178
|
-
getTypeNodeToString(typeNode) {
|
|
196
|
+
getTypeNodeToString(typeNode, genericTypeMap = new Map()) {
|
|
179
197
|
if (ts.isTypeReferenceNode(typeNode) &&
|
|
180
198
|
ts.isIdentifier(typeNode.typeName)) {
|
|
181
|
-
|
|
199
|
+
const typeName = typeNode.typeName.text;
|
|
200
|
+
if (genericTypeMap.has(typeName)) {
|
|
201
|
+
return genericTypeMap.get(typeName);
|
|
202
|
+
}
|
|
203
|
+
if (typeName.toLowerCase() === 'uploadfile') {
|
|
182
204
|
return 'UploadFile';
|
|
183
205
|
}
|
|
184
206
|
if (typeNode.typeArguments && typeNode.typeArguments.length > 0) {
|
|
@@ -203,11 +225,11 @@ class SchemaTransformer {
|
|
|
203
225
|
return constants.jsPrimitives.Boolean.type;
|
|
204
226
|
case ts.SyntaxKind.ArrayType:
|
|
205
227
|
const arrayType = typeNode;
|
|
206
|
-
return `${this.getTypeNodeToString(arrayType.elementType)}[]`;
|
|
228
|
+
return `${this.getTypeNodeToString(arrayType.elementType, genericTypeMap)}[]`;
|
|
207
229
|
case ts.SyntaxKind.UnionType:
|
|
208
230
|
// Handle union types like string | null
|
|
209
231
|
const unionType = typeNode;
|
|
210
|
-
const types = unionType.types.map(t => this.getTypeNodeToString(t));
|
|
232
|
+
const types = unionType.types.map(t => this.getTypeNodeToString(t, genericTypeMap));
|
|
211
233
|
// Filter out null and undefined, return the first meaningful type
|
|
212
234
|
const meaningfulTypes = types.filter(t => t !== 'null' && t !== 'undefined');
|
|
213
235
|
if (meaningfulTypes.length > 0 && meaningfulTypes[0]) {
|
|
@@ -219,6 +241,10 @@ class SchemaTransformer {
|
|
|
219
241
|
return 'object';
|
|
220
242
|
default:
|
|
221
243
|
const typeText = typeNode.getText();
|
|
244
|
+
// Check if this is a generic type parameter we can resolve
|
|
245
|
+
if (genericTypeMap && genericTypeMap.has(typeText)) {
|
|
246
|
+
return genericTypeMap.get(typeText);
|
|
247
|
+
}
|
|
222
248
|
// Handle some common TypeScript utility types
|
|
223
249
|
if (typeText.startsWith('Date'))
|
|
224
250
|
return constants.jsPrimitives.Date.type;
|
|
@@ -453,37 +479,159 @@ class SchemaTransformer {
|
|
|
453
479
|
}
|
|
454
480
|
return SchemaTransformer.instance;
|
|
455
481
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
482
|
+
getSourceFileByClass(cls, sourceOptions) {
|
|
483
|
+
const className = cls.name;
|
|
484
|
+
const sourceFiles = this.getFilteredSourceFiles(sourceOptions);
|
|
485
|
+
const matches = [];
|
|
486
|
+
for (const sourceFile of sourceFiles) {
|
|
487
|
+
const node = sourceFile.statements.find(stmt => ts.isClassDeclaration(stmt) &&
|
|
488
|
+
stmt.name &&
|
|
489
|
+
stmt.name.text === className);
|
|
490
|
+
if (node) {
|
|
491
|
+
matches.push({ sourceFile, node });
|
|
492
|
+
}
|
|
463
493
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
494
|
+
if (matches.length === 0) {
|
|
495
|
+
return undefined;
|
|
496
|
+
}
|
|
497
|
+
if (matches.length === 1) {
|
|
498
|
+
return matches[0];
|
|
499
|
+
}
|
|
500
|
+
if (matches.length > 1 && !sourceOptions?.filePath) {
|
|
501
|
+
const bestMatch = this.findBestMatch(cls, matches);
|
|
502
|
+
if (bestMatch) {
|
|
503
|
+
return bestMatch;
|
|
504
|
+
}
|
|
505
|
+
const firstMatch = matches[0];
|
|
506
|
+
if (firstMatch) {
|
|
507
|
+
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'.`);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return matches[0];
|
|
511
|
+
}
|
|
512
|
+
checkTypeMatch(value, typeNode) {
|
|
513
|
+
const runtimeType = typeof value;
|
|
514
|
+
if (runtimeType === 'string' &&
|
|
515
|
+
typeNode.kind === ts.SyntaxKind.StringKeyword)
|
|
516
|
+
return true;
|
|
517
|
+
if (runtimeType === 'number' &&
|
|
518
|
+
typeNode.kind === ts.SyntaxKind.NumberKeyword)
|
|
519
|
+
return true;
|
|
520
|
+
if (runtimeType === 'boolean' &&
|
|
521
|
+
typeNode.kind === ts.SyntaxKind.BooleanKeyword)
|
|
522
|
+
return true;
|
|
523
|
+
if (Array.isArray(value) && ts.isArrayTypeNode(typeNode)) {
|
|
524
|
+
if (value.length === 0)
|
|
525
|
+
return true;
|
|
526
|
+
const firstItem = value[0];
|
|
527
|
+
const elementType = typeNode.elementType;
|
|
528
|
+
return this.checkTypeMatch(firstItem, elementType);
|
|
529
|
+
}
|
|
530
|
+
if (runtimeType === 'object' && value !== null && !Array.isArray(value)) {
|
|
531
|
+
if (ts.isTypeReferenceNode(typeNode) ||
|
|
532
|
+
typeNode.kind === ts.SyntaxKind.ObjectKeyword) {
|
|
472
533
|
return true;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return false;
|
|
537
|
+
}
|
|
538
|
+
findBestMatch(cls, matches) {
|
|
539
|
+
const runtimeSource = cls.toString();
|
|
540
|
+
const runtimeProperties = new Map();
|
|
541
|
+
const regexProperties = new Set();
|
|
542
|
+
// Try to extract properties from runtime source (assignments in constructor)
|
|
543
|
+
try {
|
|
544
|
+
const regex = /this\.([a-zA-Z0-9_$]+)\s*=/g;
|
|
545
|
+
let match;
|
|
546
|
+
while ((match = regex.exec(runtimeSource)) !== null) {
|
|
547
|
+
regexProperties.add(match[1]);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
catch (e) {
|
|
551
|
+
// Ignore regex errors
|
|
552
|
+
}
|
|
553
|
+
// Try to instantiate the class to find properties
|
|
554
|
+
try {
|
|
555
|
+
const instance = new cls();
|
|
556
|
+
Object.keys(instance).forEach(key => {
|
|
557
|
+
runtimeProperties.set(key, instance[key]);
|
|
473
558
|
});
|
|
474
559
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
560
|
+
catch (e) {
|
|
561
|
+
// Ignore instantiation errors (e.g. required constructor arguments)
|
|
562
|
+
}
|
|
563
|
+
// Try to get properties from class-validator metadata
|
|
564
|
+
try {
|
|
565
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
566
|
+
const { getMetadataStorage } = require('class-validator');
|
|
567
|
+
const metadata = getMetadataStorage();
|
|
568
|
+
const targetMetadata = metadata.getTargetValidationMetadatas(cls, null, false, false);
|
|
569
|
+
targetMetadata.forEach((m) => {
|
|
570
|
+
if (m.propertyName && !runtimeProperties.has(m.propertyName)) {
|
|
571
|
+
runtimeProperties.set(m.propertyName, undefined);
|
|
572
|
+
}
|
|
482
573
|
});
|
|
483
|
-
|
|
484
|
-
|
|
574
|
+
}
|
|
575
|
+
catch (e) {
|
|
576
|
+
// Ignore if class-validator is not available or fails
|
|
577
|
+
}
|
|
578
|
+
// console.log(`[findBestMatch] Class: ${cls.name}, Runtime Props: ${Array.from(runtimeProperties.keys()).join(', ')}`)
|
|
579
|
+
const scores = matches.map(match => {
|
|
580
|
+
let score = 0;
|
|
581
|
+
for (const member of match.node.members) {
|
|
582
|
+
if (ts.isMethodDeclaration(member) && ts.isIdentifier(member.name)) {
|
|
583
|
+
if (runtimeSource.includes(member.name.text)) {
|
|
584
|
+
score += 2;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
else if (ts.isPropertyDeclaration(member) &&
|
|
588
|
+
ts.isIdentifier(member.name)) {
|
|
589
|
+
const propName = member.name.text;
|
|
590
|
+
if (runtimeProperties.has(propName)) {
|
|
591
|
+
score += 1;
|
|
592
|
+
const value = runtimeProperties.get(propName);
|
|
593
|
+
if (member.type && this.checkTypeMatch(value, member.type)) {
|
|
594
|
+
score += 5;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
else if (regexProperties.has(propName)) {
|
|
598
|
+
score += 1;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return { match, score };
|
|
603
|
+
});
|
|
604
|
+
scores.sort((a, b) => b.score - a.score);
|
|
605
|
+
const firstScore = scores[0];
|
|
606
|
+
const secondScore = scores[1];
|
|
607
|
+
if (firstScore && firstScore.score > 0) {
|
|
608
|
+
if (scores.length === 1 ||
|
|
609
|
+
(secondScore && firstScore.score > secondScore.score)) {
|
|
610
|
+
return firstScore.match;
|
|
485
611
|
}
|
|
486
612
|
}
|
|
613
|
+
return undefined;
|
|
614
|
+
}
|
|
615
|
+
getFilteredSourceFiles(sourceOptions) {
|
|
616
|
+
if (sourceOptions?.isExternal) {
|
|
617
|
+
return this.program.getSourceFiles().filter(sf => {
|
|
618
|
+
return (sf.fileName.includes(sourceOptions.packageName) &&
|
|
619
|
+
(!sourceOptions.filePath || sf.fileName === sourceOptions.filePath));
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
return this.program.getSourceFiles().filter(sf => {
|
|
623
|
+
if (sf.isDeclarationFile)
|
|
624
|
+
return false;
|
|
625
|
+
if (sf.fileName.includes('.d.ts'))
|
|
626
|
+
return false;
|
|
627
|
+
if (sf.fileName.includes('node_modules'))
|
|
628
|
+
return false;
|
|
629
|
+
if (sourceOptions?.filePath &&
|
|
630
|
+
!sf.fileName.includes(sourceOptions.filePath)) {
|
|
631
|
+
return false;
|
|
632
|
+
}
|
|
633
|
+
return true;
|
|
634
|
+
});
|
|
487
635
|
}
|
|
488
636
|
isEnum(propertyDeclaration) {
|
|
489
637
|
if (!propertyDeclaration.type) {
|
|
@@ -1032,7 +1180,7 @@ class SchemaTransformer {
|
|
|
1032
1180
|
}
|
|
1033
1181
|
transform(cls, sourceOptions) {
|
|
1034
1182
|
let schema = { type: 'object', properties: {} };
|
|
1035
|
-
const result = this.
|
|
1183
|
+
const result = this.getSourceFileByClass(cls, sourceOptions);
|
|
1036
1184
|
if (!result?.sourceFile) {
|
|
1037
1185
|
console.warn(`Class ${cls.name} not found in any source file.`);
|
|
1038
1186
|
return { name: cls.name, schema: {} };
|