ts-class-to-openapi 1.2.2 → 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/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 +1 -1
package/dist/__test__/index.d.ts
CHANGED
|
@@ -2,3 +2,6 @@ import './testCases/pure-classes.test';
|
|
|
2
2
|
import './testCases/decorated-classes.test';
|
|
3
3
|
import './testCases/nested-classes.test';
|
|
4
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,24 +89,42 @@ 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);
|
|
@@ -166,17 +184,21 @@ class SchemaTransformer {
|
|
|
166
184
|
}
|
|
167
185
|
return properties;
|
|
168
186
|
}
|
|
169
|
-
getPropertyType(property) {
|
|
187
|
+
getPropertyType(property, genericTypeMap = new Map()) {
|
|
170
188
|
if (property.type) {
|
|
171
|
-
return this.getTypeNodeToString(property.type);
|
|
189
|
+
return this.getTypeNodeToString(property.type, genericTypeMap);
|
|
172
190
|
}
|
|
173
191
|
const type = this.checker.getTypeAtLocation(property);
|
|
174
192
|
return this.getStringFromType(type);
|
|
175
193
|
}
|
|
176
|
-
getTypeNodeToString(typeNode) {
|
|
194
|
+
getTypeNodeToString(typeNode, genericTypeMap = new Map()) {
|
|
177
195
|
if (ts.isTypeReferenceNode(typeNode) &&
|
|
178
196
|
ts.isIdentifier(typeNode.typeName)) {
|
|
179
|
-
|
|
197
|
+
const typeName = typeNode.typeName.text;
|
|
198
|
+
if (genericTypeMap.has(typeName)) {
|
|
199
|
+
return genericTypeMap.get(typeName);
|
|
200
|
+
}
|
|
201
|
+
if (typeName.toLowerCase() === 'uploadfile') {
|
|
180
202
|
return 'UploadFile';
|
|
181
203
|
}
|
|
182
204
|
if (typeNode.typeArguments && typeNode.typeArguments.length > 0) {
|
|
@@ -201,11 +223,11 @@ class SchemaTransformer {
|
|
|
201
223
|
return constants.jsPrimitives.Boolean.type;
|
|
202
224
|
case ts.SyntaxKind.ArrayType:
|
|
203
225
|
const arrayType = typeNode;
|
|
204
|
-
return `${this.getTypeNodeToString(arrayType.elementType)}[]`;
|
|
226
|
+
return `${this.getTypeNodeToString(arrayType.elementType, genericTypeMap)}[]`;
|
|
205
227
|
case ts.SyntaxKind.UnionType:
|
|
206
228
|
// Handle union types like string | null
|
|
207
229
|
const unionType = typeNode;
|
|
208
|
-
const types = unionType.types.map(t => this.getTypeNodeToString(t));
|
|
230
|
+
const types = unionType.types.map(t => this.getTypeNodeToString(t, genericTypeMap));
|
|
209
231
|
// Filter out null and undefined, return the first meaningful type
|
|
210
232
|
const meaningfulTypes = types.filter(t => t !== 'null' && t !== 'undefined');
|
|
211
233
|
if (meaningfulTypes.length > 0 && meaningfulTypes[0]) {
|
|
@@ -217,6 +239,10 @@ class SchemaTransformer {
|
|
|
217
239
|
return 'object';
|
|
218
240
|
default:
|
|
219
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
|
+
}
|
|
220
246
|
// Handle some common TypeScript utility types
|
|
221
247
|
if (typeText.startsWith('Date'))
|
|
222
248
|
return constants.jsPrimitives.Date.type;
|
|
@@ -451,37 +477,159 @@ class SchemaTransformer {
|
|
|
451
477
|
}
|
|
452
478
|
return SchemaTransformer.instance;
|
|
453
479
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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
|
+
}
|
|
461
491
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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)
|
|
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) {
|
|
470
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]);
|
|
471
556
|
});
|
|
472
557
|
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
+
}
|
|
480
571
|
});
|
|
481
|
-
|
|
482
|
-
|
|
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;
|
|
483
609
|
}
|
|
484
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
|
+
});
|
|
485
633
|
}
|
|
486
634
|
isEnum(propertyDeclaration) {
|
|
487
635
|
if (!propertyDeclaration.type) {
|
|
@@ -1030,7 +1178,7 @@ class SchemaTransformer {
|
|
|
1030
1178
|
}
|
|
1031
1179
|
transform(cls, sourceOptions) {
|
|
1032
1180
|
let schema = { type: 'object', properties: {} };
|
|
1033
|
-
const result = this.
|
|
1181
|
+
const result = this.getSourceFileByClass(cls, sourceOptions);
|
|
1034
1182
|
if (!result?.sourceFile) {
|
|
1035
1183
|
console.warn(`Class ${cls.name} not found in any source file.`);
|
|
1036
1184
|
return { name: cls.name, schema: {} };
|