ts-class-to-openapi 1.3.2 → 1.3.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/index.cjs +985 -0
- package/dist/index.d.cts +56 -0
- package/dist/index.d.mts +58 -0
- package/dist/index.mjs +960 -0
- package/package.json +32 -28
- package/dist/__test__/entities/additional-test-classes.d.ts +0 -12
- package/dist/__test__/entities/circular-reference-cases.d.ts +0 -1
- package/dist/__test__/entities/circular-reference-classes.d.ts +0 -110
- package/dist/__test__/entities/collision/arrays/number-array.d.ts +0 -3
- package/dist/__test__/entities/collision/arrays/string-array.d.ts +0 -3
- package/dist/__test__/entities/collision/number-props/same-name.d.ts +0 -5
- package/dist/__test__/entities/collision/string-props/same-name.d.ts +0 -5
- package/dist/__test__/entities/collision/throwing/class-a.d.ts +0 -4
- package/dist/__test__/entities/collision/throwing/class-b.d.ts +0 -4
- package/dist/__test__/entities/complex-circular-dependencies.d.ts +0 -71
- package/dist/__test__/entities/decorated-classes.d.ts +0 -54
- package/dist/__test__/entities/deep-nested-classes.d.ts +0 -1
- package/dist/__test__/entities/enum-classes.d.ts +0 -38
- package/dist/__test__/entities/evaluation/generics.d.ts +0 -6
- package/dist/__test__/entities/evaluation/modifiers.d.ts +0 -7
- package/dist/__test__/entities/generic-circular-classes.d.ts +0 -57
- package/dist/__test__/entities/nested-classes.d.ts +0 -70
- package/dist/__test__/entities/nested-reuse-classes.d.ts +0 -43
- package/dist/__test__/entities/pure-classes.d.ts +0 -37
- package/dist/__test__/entities/schema-validation-classes.d.ts +0 -35
- package/dist/__test__/index.d.ts +0 -8
- package/dist/__test__/test.d.ts +0 -1
- package/dist/__test__/testCases/collision-advanced.test.d.ts +0 -1
- package/dist/__test__/testCases/collision.test.d.ts +0 -1
- package/dist/__test__/testCases/decorated-classes.test.d.ts +0 -1
- package/dist/__test__/testCases/edge-cases.test.d.ts +0 -1
- package/dist/__test__/testCases/enum-properties.test.d.ts +0 -1
- package/dist/__test__/testCases/generics-and-modifiers.test.d.ts +0 -1
- package/dist/__test__/testCases/nested-classes.test.d.ts +0 -1
- package/dist/__test__/testCases/nested-reuse.test.d.ts +0 -1
- package/dist/__test__/testCases/pure-classes.test.d.ts +0 -1
- package/dist/__test__/testCases/schema-validation.test.d.ts +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.esm.js +0 -1230
- package/dist/index.js +0 -1232
- package/dist/run.d.ts +0 -1
- package/dist/run.js +0 -1319
- package/dist/transformer.d.ts +0 -5
- package/dist/transformer.fixtures.d.ts +0 -175
- package/dist/types.d.ts +0 -97
package/dist/index.esm.js
DELETED
|
@@ -1,1230 +0,0 @@
|
|
|
1
|
-
import ts from 'typescript';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
|
|
4
|
-
const TS_CONFIG_DEFAULT_PATH = path.resolve(process.cwd(), 'tsconfig.json');
|
|
5
|
-
const jsPrimitives = {
|
|
6
|
-
String: { type: 'String', value: 'string' },
|
|
7
|
-
Any: { type: 'Any'},
|
|
8
|
-
Unknown: { type: 'Unknown'},
|
|
9
|
-
Number: { type: 'Number', value: 'number', format: 'double' },
|
|
10
|
-
Boolean: { type: 'Boolean', value: 'boolean' },
|
|
11
|
-
Symbol: { type: 'Symbol', value: 'string' },
|
|
12
|
-
BigInt: { type: 'BigInt', value: 'integer', format: 'int64' },
|
|
13
|
-
null: { type: 'null'},
|
|
14
|
-
Object: { type: 'Object', value: 'object' },
|
|
15
|
-
Array: { type: 'Array', value: 'array' },
|
|
16
|
-
Date: { type: 'Date', value: 'string', format: 'date-time' },
|
|
17
|
-
Buffer: { type: 'Buffer'},
|
|
18
|
-
Uint8Array: { type: 'Uint8Array'},
|
|
19
|
-
UploadFile: { type: 'UploadFile', value: 'string', format: 'binary' },
|
|
20
|
-
UploadFileDto: { type: 'UploadFileDto'},
|
|
21
|
-
File: { type: 'File'},
|
|
22
|
-
};
|
|
23
|
-
const validatorDecorators = {
|
|
24
|
-
Length: { name: 'Length'},
|
|
25
|
-
MinLength: { name: 'MinLength'},
|
|
26
|
-
MaxLength: { name: 'MaxLength'},
|
|
27
|
-
IsInt: { name: 'IsInt', type: 'integer', format: 'int32' },
|
|
28
|
-
IsNumber: { name: 'IsNumber', type: 'number'},
|
|
29
|
-
IsString: { name: 'IsString', type: 'string'},
|
|
30
|
-
IsPositive: { name: 'IsPositive'},
|
|
31
|
-
IsDate: { name: 'IsDate', type: 'string', format: 'date-time' },
|
|
32
|
-
IsEmail: { name: 'IsEmail', format: 'email' },
|
|
33
|
-
IsNotEmpty: { name: 'IsNotEmpty' },
|
|
34
|
-
IsOptional: { name: 'IsOptional' },
|
|
35
|
-
IsBoolean: { name: 'IsBoolean', type: 'boolean' },
|
|
36
|
-
IsArray: { name: 'IsArray'},
|
|
37
|
-
Min: { name: 'Min' },
|
|
38
|
-
Max: { name: 'Max' },
|
|
39
|
-
ArrayNotEmpty: { name: 'ArrayNotEmpty' },
|
|
40
|
-
ArrayMaxSize: { name: 'ArrayMaxSize' },
|
|
41
|
-
ArrayMinSize: { name: 'ArrayMinSize' },
|
|
42
|
-
IsEnum: { name: 'IsEnum'},
|
|
43
|
-
};
|
|
44
|
-
const constants = {
|
|
45
|
-
TS_CONFIG_DEFAULT_PATH,
|
|
46
|
-
jsPrimitives,
|
|
47
|
-
validatorDecorators};
|
|
48
|
-
|
|
49
|
-
class SchemaTransformer {
|
|
50
|
-
static instance = null;
|
|
51
|
-
program;
|
|
52
|
-
checker;
|
|
53
|
-
classCache = new Map();
|
|
54
|
-
maxCacheSize;
|
|
55
|
-
autoCleanup;
|
|
56
|
-
loadedFiles = new Set();
|
|
57
|
-
processingClasses = new Set();
|
|
58
|
-
sourceFiles;
|
|
59
|
-
classFileIndex = new Map();
|
|
60
|
-
constructor(tsConfigPath = constants.TS_CONFIG_DEFAULT_PATH, options = {}) {
|
|
61
|
-
this.maxCacheSize = options.maxCacheSize ?? 100;
|
|
62
|
-
this.autoCleanup = options.autoCleanup ?? true;
|
|
63
|
-
const { config, error } = ts.readConfigFile(tsConfigPath || 'tsconfig.json', ts.sys.readFile);
|
|
64
|
-
if (error) {
|
|
65
|
-
console.log(new Error(`Error reading tsconfig file: ${error.messageText}`).message);
|
|
66
|
-
throw new Error(`Error reading tsconfig file: ${error.messageText}`);
|
|
67
|
-
}
|
|
68
|
-
const { options: tsOptions, fileNames } = ts.parseJsonConfigFileContent(config, ts.sys, './');
|
|
69
|
-
this.program = ts.createProgram(fileNames, tsOptions);
|
|
70
|
-
this.checker = this.program.getTypeChecker();
|
|
71
|
-
this.sourceFiles = this.program.getSourceFiles().filter(sf => {
|
|
72
|
-
if (sf.isDeclarationFile)
|
|
73
|
-
return false;
|
|
74
|
-
if (sf.fileName.includes('.d.ts'))
|
|
75
|
-
return false;
|
|
76
|
-
if (sf.fileName.includes('node_modules'))
|
|
77
|
-
return false;
|
|
78
|
-
return true;
|
|
79
|
-
});
|
|
80
|
-
this.sourceFiles.forEach(sf => {
|
|
81
|
-
sf.statements.forEach(stmt => {
|
|
82
|
-
if (ts.isClassDeclaration(stmt) && stmt.name) {
|
|
83
|
-
const name = stmt.name.text;
|
|
84
|
-
const entry = this.classFileIndex.get(name) || [];
|
|
85
|
-
entry.push({ sourceFile: sf, node: stmt });
|
|
86
|
-
this.classFileIndex.set(name, entry);
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
getPropertiesByClassDeclaration(classNode, visitedDeclarations = new Set(), genericTypeMap = new Map()) {
|
|
92
|
-
if (visitedDeclarations.has(classNode)) {
|
|
93
|
-
return [];
|
|
94
|
-
}
|
|
95
|
-
visitedDeclarations.add(classNode);
|
|
96
|
-
// if no heritage clauses, get properties directly from class
|
|
97
|
-
if (!classNode.heritageClauses) {
|
|
98
|
-
return this.getPropertiesByClassMembers(classNode.members, classNode, genericTypeMap);
|
|
99
|
-
} // use heritage clauses to get properties from base classes
|
|
100
|
-
else {
|
|
101
|
-
const heritageClause = classNode.heritageClauses[0];
|
|
102
|
-
if (heritageClause &&
|
|
103
|
-
heritageClause.token === ts.SyntaxKind.ExtendsKeyword) {
|
|
104
|
-
const type = heritageClause.types[0];
|
|
105
|
-
let properties = [];
|
|
106
|
-
let baseProperties = [];
|
|
107
|
-
if (!type)
|
|
108
|
-
return [];
|
|
109
|
-
const symbol = this.checker.getSymbolAtLocation(type.expression);
|
|
110
|
-
if (!symbol)
|
|
111
|
-
return [];
|
|
112
|
-
const declaration = symbol.declarations?.[0];
|
|
113
|
-
if (declaration && ts.isClassDeclaration(declaration)) {
|
|
114
|
-
const newGenericTypeMap = new Map();
|
|
115
|
-
if (declaration.typeParameters && type.typeArguments) {
|
|
116
|
-
declaration.typeParameters.forEach((param, index) => {
|
|
117
|
-
const arg = type.typeArguments[index];
|
|
118
|
-
if (arg) {
|
|
119
|
-
const resolvedArg = this.getTypeNodeToString(arg, genericTypeMap);
|
|
120
|
-
newGenericTypeMap.set(param.name.text, resolvedArg);
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
baseProperties = this.getPropertiesByClassDeclaration(declaration, visitedDeclarations, newGenericTypeMap);
|
|
125
|
-
}
|
|
126
|
-
properties = this.getPropertiesByClassMembers(classNode.members, classNode, genericTypeMap);
|
|
127
|
-
return baseProperties.concat(properties);
|
|
128
|
-
}
|
|
129
|
-
else {
|
|
130
|
-
return this.getPropertiesByClassMembers(classNode.members, classNode, genericTypeMap);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
getPropertiesByClassMembers(members, parentClassNode, genericTypeMap = new Map()) {
|
|
135
|
-
const properties = [];
|
|
136
|
-
for (const member of members) {
|
|
137
|
-
if (ts.isPropertyDeclaration(member) &&
|
|
138
|
-
member.name &&
|
|
139
|
-
ts.isIdentifier(member.name)) {
|
|
140
|
-
// Skip static, private, and protected properties
|
|
141
|
-
if (member.modifiers) {
|
|
142
|
-
const hasExcludedModifier = member.modifiers.some(m => m.kind === ts.SyntaxKind.StaticKeyword ||
|
|
143
|
-
m.kind === ts.SyntaxKind.PrivateKeyword ||
|
|
144
|
-
m.kind === ts.SyntaxKind.ProtectedKeyword);
|
|
145
|
-
if (hasExcludedModifier)
|
|
146
|
-
continue;
|
|
147
|
-
}
|
|
148
|
-
const propertyName = member.name.text;
|
|
149
|
-
const type = this.getPropertyType(member, genericTypeMap);
|
|
150
|
-
const decorators = this.extractDecorators(member);
|
|
151
|
-
const isOptional = !!member.questionToken;
|
|
152
|
-
const isGeneric = this.isPropertyTypeGeneric(member);
|
|
153
|
-
const isEnum = this.isEnum(member);
|
|
154
|
-
const isPrimitive = this.isPrimitiveType(type) || isEnum;
|
|
155
|
-
const isClassType = this.isClassType(member);
|
|
156
|
-
const isArray = this.isArrayProperty(member);
|
|
157
|
-
const isTypeLiteral = this.isTypeLiteral(member);
|
|
158
|
-
const property = {
|
|
159
|
-
name: propertyName,
|
|
160
|
-
type,
|
|
161
|
-
decorators,
|
|
162
|
-
isOptional,
|
|
163
|
-
isGeneric,
|
|
164
|
-
originalProperty: member,
|
|
165
|
-
isPrimitive,
|
|
166
|
-
isClassType,
|
|
167
|
-
isArray,
|
|
168
|
-
isEnum,
|
|
169
|
-
isRef: false,
|
|
170
|
-
isTypeLiteral: isTypeLiteral,
|
|
171
|
-
};
|
|
172
|
-
// Check for self-referencing properties to mark as $ref
|
|
173
|
-
if (property.isClassType) {
|
|
174
|
-
const declaration = this.getDeclarationProperty(property);
|
|
175
|
-
if (parentClassNode) {
|
|
176
|
-
if (declaration &&
|
|
177
|
-
declaration.name &&
|
|
178
|
-
this.checker.getSymbolAtLocation(declaration.name) ===
|
|
179
|
-
this.checker.getSymbolAtLocation(parentClassNode.name)) {
|
|
180
|
-
property.isRef = true;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
if (property.isTypeLiteral &&
|
|
185
|
-
property.originalProperty.type &&
|
|
186
|
-
property.originalProperty.type
|
|
187
|
-
.typeArguments?.length === 1) {
|
|
188
|
-
const typeArguments = property.originalProperty.type.typeArguments;
|
|
189
|
-
if (typeArguments && typeArguments[0]) {
|
|
190
|
-
const firstTypeArg = typeArguments[0];
|
|
191
|
-
if (ts.isTypeReferenceNode(firstTypeArg)) {
|
|
192
|
-
const type = this.checker.getTypeAtLocation(firstTypeArg);
|
|
193
|
-
const symbol = type.getSymbol();
|
|
194
|
-
if (symbol && symbol.declarations) {
|
|
195
|
-
const classDeclaration = symbol.declarations.find(decl => ts.isClassDeclaration(decl));
|
|
196
|
-
if (classDeclaration &&
|
|
197
|
-
ts.isClassDeclaration(classDeclaration)) {
|
|
198
|
-
property.typeLiteralClassReference = classDeclaration;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
properties.push(property);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
return properties;
|
|
208
|
-
}
|
|
209
|
-
getPropertyType(property, genericTypeMap = new Map()) {
|
|
210
|
-
if (property.type) {
|
|
211
|
-
return this.getTypeNodeToString(property.type, genericTypeMap);
|
|
212
|
-
}
|
|
213
|
-
const type = this.checker.getTypeAtLocation(property);
|
|
214
|
-
return this.getStringFromType(type);
|
|
215
|
-
}
|
|
216
|
-
getTypeNodeToString(typeNode, genericTypeMap = new Map()) {
|
|
217
|
-
if (ts.isTypeReferenceNode(typeNode) &&
|
|
218
|
-
ts.isIdentifier(typeNode.typeName)) {
|
|
219
|
-
const typeName = typeNode.typeName.text;
|
|
220
|
-
if (genericTypeMap.has(typeName)) {
|
|
221
|
-
return genericTypeMap.get(typeName);
|
|
222
|
-
}
|
|
223
|
-
if (typeName.toLowerCase() === 'uploadfile') {
|
|
224
|
-
return 'UploadFile';
|
|
225
|
-
}
|
|
226
|
-
if (typeName.toLowerCase() === 'uploadfiledto') {
|
|
227
|
-
return 'UploadFileDto';
|
|
228
|
-
}
|
|
229
|
-
if (typeNode.typeArguments && typeNode.typeArguments.length > 0) {
|
|
230
|
-
const firstTypeArg = typeNode.typeArguments[0];
|
|
231
|
-
if (firstTypeArg &&
|
|
232
|
-
ts.isTypeReferenceNode(firstTypeArg) &&
|
|
233
|
-
ts.isIdentifier(firstTypeArg.typeName)) {
|
|
234
|
-
if (firstTypeArg.typeName.text.toLowerCase() === 'uploadfile') {
|
|
235
|
-
return 'UploadFile';
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
return this.resolveGenericType(typeNode);
|
|
239
|
-
}
|
|
240
|
-
return typeNode.typeName.text;
|
|
241
|
-
}
|
|
242
|
-
switch (typeNode.kind) {
|
|
243
|
-
case ts.SyntaxKind.StringKeyword:
|
|
244
|
-
return constants.jsPrimitives.String.type;
|
|
245
|
-
case ts.SyntaxKind.NumberKeyword:
|
|
246
|
-
return constants.jsPrimitives.Number.type;
|
|
247
|
-
case ts.SyntaxKind.BooleanKeyword:
|
|
248
|
-
return constants.jsPrimitives.Boolean.type;
|
|
249
|
-
case ts.SyntaxKind.ArrayType:
|
|
250
|
-
const arrayType = typeNode;
|
|
251
|
-
return `${this.getTypeNodeToString(arrayType.elementType, genericTypeMap)}[]`;
|
|
252
|
-
case ts.SyntaxKind.UnionType:
|
|
253
|
-
// Handle union types like string | null
|
|
254
|
-
const unionType = typeNode;
|
|
255
|
-
const types = unionType.types.map(t => this.getTypeNodeToString(t, genericTypeMap));
|
|
256
|
-
// Filter out null and undefined, return the first meaningful type
|
|
257
|
-
const meaningfulTypes = types.filter(t => t !== 'null' && t !== 'undefined');
|
|
258
|
-
if (meaningfulTypes.length > 0 && meaningfulTypes[0]) {
|
|
259
|
-
return meaningfulTypes[0];
|
|
260
|
-
}
|
|
261
|
-
if (types.length > 0 && types[0]) {
|
|
262
|
-
return types[0];
|
|
263
|
-
}
|
|
264
|
-
return 'object';
|
|
265
|
-
default:
|
|
266
|
-
const typeText = typeNode.getText();
|
|
267
|
-
// Check if this is a generic type parameter we can resolve
|
|
268
|
-
if (genericTypeMap && genericTypeMap.has(typeText)) {
|
|
269
|
-
return genericTypeMap.get(typeText);
|
|
270
|
-
}
|
|
271
|
-
// Handle some common TypeScript utility types
|
|
272
|
-
if (typeText.startsWith('Date'))
|
|
273
|
-
return constants.jsPrimitives.Date.type;
|
|
274
|
-
if (typeText.includes('Buffer') || typeText.includes('Uint8Array'))
|
|
275
|
-
return constants.jsPrimitives.Buffer.type;
|
|
276
|
-
return typeText;
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
resolveGenericType(typeNode) {
|
|
280
|
-
const typeName = typeNode.typeName.text;
|
|
281
|
-
const typeArguments = typeNode.typeArguments;
|
|
282
|
-
if (!typeArguments || typeArguments.length === 0) {
|
|
283
|
-
return typeName;
|
|
284
|
-
}
|
|
285
|
-
const type = this.checker.getTypeAtLocation(typeNode);
|
|
286
|
-
const resolvedType = this.getStringFromType(type);
|
|
287
|
-
if (resolvedType &&
|
|
288
|
-
resolvedType !== typeName &&
|
|
289
|
-
!resolvedType.includes('any')) {
|
|
290
|
-
return resolvedType;
|
|
291
|
-
}
|
|
292
|
-
return typeName;
|
|
293
|
-
}
|
|
294
|
-
getStringFromType(type) {
|
|
295
|
-
return this.checker.typeToString(type);
|
|
296
|
-
}
|
|
297
|
-
extractDecorators(member) {
|
|
298
|
-
const decorators = [];
|
|
299
|
-
if (member.modifiers) {
|
|
300
|
-
for (const modifier of member.modifiers) {
|
|
301
|
-
if (ts.isDecorator(modifier) &&
|
|
302
|
-
ts.isCallExpression(modifier.expression)) {
|
|
303
|
-
const decoratorName = this.getDecoratorName(modifier.expression);
|
|
304
|
-
const args = this.getDecoratorArguments(modifier.expression);
|
|
305
|
-
decorators.push({ name: decoratorName, arguments: args });
|
|
306
|
-
}
|
|
307
|
-
else if (ts.isDecorator(modifier) &&
|
|
308
|
-
ts.isIdentifier(modifier.expression)) {
|
|
309
|
-
decorators.push({ name: modifier.expression.text, arguments: [] });
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
return decorators;
|
|
314
|
-
}
|
|
315
|
-
getDecoratorName(callExpression) {
|
|
316
|
-
if (ts.isIdentifier(callExpression.expression)) {
|
|
317
|
-
return callExpression.expression.text;
|
|
318
|
-
}
|
|
319
|
-
return 'unknown';
|
|
320
|
-
}
|
|
321
|
-
getDecoratorArguments(callExpression) {
|
|
322
|
-
return callExpression.arguments.map(arg => {
|
|
323
|
-
if (ts.isNumericLiteral(arg))
|
|
324
|
-
return Number(arg.text);
|
|
325
|
-
if (ts.isStringLiteral(arg))
|
|
326
|
-
return arg.text;
|
|
327
|
-
if (arg.kind === ts.SyntaxKind.TrueKeyword)
|
|
328
|
-
return true;
|
|
329
|
-
if (arg.kind === ts.SyntaxKind.FalseKeyword)
|
|
330
|
-
return false;
|
|
331
|
-
return arg;
|
|
332
|
-
});
|
|
333
|
-
}
|
|
334
|
-
getSafeDecoratorArgument(arg) {
|
|
335
|
-
if (arg && typeof arg === 'object' && 'kind' in arg) {
|
|
336
|
-
return arg.getText();
|
|
337
|
-
}
|
|
338
|
-
return arg;
|
|
339
|
-
}
|
|
340
|
-
isPropertyTypeGeneric(property) {
|
|
341
|
-
if (property.type && this.isGenericTypeFromNode(property.type)) {
|
|
342
|
-
return true;
|
|
343
|
-
}
|
|
344
|
-
try {
|
|
345
|
-
const type = this.checker.getTypeAtLocation(property);
|
|
346
|
-
return this.isGenericTypeFromSymbol(type);
|
|
347
|
-
}
|
|
348
|
-
catch (error) {
|
|
349
|
-
console.warn('Error analyzing property type for generics:', error);
|
|
350
|
-
return false;
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
isGenericTypeFromNode(typeNode) {
|
|
354
|
-
if (ts.isTypeReferenceNode(typeNode) && typeNode.typeArguments) {
|
|
355
|
-
return typeNode.typeArguments.length > 0;
|
|
356
|
-
}
|
|
357
|
-
// Check for mapped types (e.g., { [K in keyof T]: T[K] })
|
|
358
|
-
if (ts.isMappedTypeNode(typeNode)) {
|
|
359
|
-
return true;
|
|
360
|
-
}
|
|
361
|
-
// Check for conditional types (e.g., T extends U ? X : Y)
|
|
362
|
-
if (ts.isConditionalTypeNode(typeNode)) {
|
|
363
|
-
return true;
|
|
364
|
-
}
|
|
365
|
-
// Check for indexed access types (e.g., T[K])
|
|
366
|
-
if (ts.isIndexedAccessTypeNode(typeNode)) {
|
|
367
|
-
return true;
|
|
368
|
-
}
|
|
369
|
-
// Check for type operators like keyof, typeof
|
|
370
|
-
if (ts.isTypeOperatorNode(typeNode)) {
|
|
371
|
-
return true;
|
|
372
|
-
}
|
|
373
|
-
return false;
|
|
374
|
-
}
|
|
375
|
-
isGenericTypeFromSymbol(type) {
|
|
376
|
-
// First check if it's a simple array type - these should NOT be considered generic
|
|
377
|
-
if (this.isSimpleArrayType(type)) {
|
|
378
|
-
return false;
|
|
379
|
-
}
|
|
380
|
-
// Check if the type has type parameters
|
|
381
|
-
if (type.aliasTypeArguments && type.aliasTypeArguments.length > 0) {
|
|
382
|
-
return true;
|
|
383
|
-
}
|
|
384
|
-
// Check if it's a type reference with type arguments
|
|
385
|
-
// But exclude simple arrays which internally use Array<T> representation
|
|
386
|
-
if (type.typeArguments && type.typeArguments.length > 0) {
|
|
387
|
-
const symbol = type.getSymbol();
|
|
388
|
-
if (symbol && symbol.getName() === 'Array') {
|
|
389
|
-
// This is Array<T> - only consider it generic if T itself is a utility type
|
|
390
|
-
const elementType = type.typeArguments[0];
|
|
391
|
-
if (elementType) {
|
|
392
|
-
return this.isUtilityTypeFromType(elementType);
|
|
393
|
-
}
|
|
394
|
-
return false;
|
|
395
|
-
}
|
|
396
|
-
const elementType = type.typeArguments[0];
|
|
397
|
-
return this.isUtilityTypeFromType(elementType);
|
|
398
|
-
}
|
|
399
|
-
// Check type flags for generic indicators
|
|
400
|
-
if (type.flags & ts.TypeFlags.TypeParameter) {
|
|
401
|
-
return true;
|
|
402
|
-
}
|
|
403
|
-
if (type.flags & ts.TypeFlags.Conditional) {
|
|
404
|
-
return true;
|
|
405
|
-
}
|
|
406
|
-
if (type.flags & ts.TypeFlags.Index) {
|
|
407
|
-
return true;
|
|
408
|
-
}
|
|
409
|
-
if (type.flags & ts.TypeFlags.IndexedAccess) {
|
|
410
|
-
return true;
|
|
411
|
-
}
|
|
412
|
-
// Check if the type symbol indicates a generic type
|
|
413
|
-
const symbol = type.getSymbol();
|
|
414
|
-
if (symbol && symbol.declarations) {
|
|
415
|
-
for (const declaration of symbol.declarations) {
|
|
416
|
-
// Check for type alias declarations with type parameters
|
|
417
|
-
if (ts.isTypeAliasDeclaration(declaration) &&
|
|
418
|
-
declaration.typeParameters) {
|
|
419
|
-
return true;
|
|
420
|
-
}
|
|
421
|
-
// Check for interface declarations with type parameters
|
|
422
|
-
if (ts.isInterfaceDeclaration(declaration) &&
|
|
423
|
-
declaration.typeParameters) {
|
|
424
|
-
return true;
|
|
425
|
-
}
|
|
426
|
-
// Check for class declarations with type parameters
|
|
427
|
-
if (ts.isClassDeclaration(declaration) && declaration.typeParameters) {
|
|
428
|
-
return true;
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
return false;
|
|
433
|
-
}
|
|
434
|
-
isUtilityTypeFromType(type) {
|
|
435
|
-
if (!type.aliasSymbol)
|
|
436
|
-
return false;
|
|
437
|
-
const aliasName = type.aliasSymbol.getName();
|
|
438
|
-
const utilityTypes = [
|
|
439
|
-
'Partial',
|
|
440
|
-
'Required',
|
|
441
|
-
'Readonly',
|
|
442
|
-
'Pick',
|
|
443
|
-
'Omit',
|
|
444
|
-
'Record',
|
|
445
|
-
'Exclude',
|
|
446
|
-
'Extract',
|
|
447
|
-
'NonNullable',
|
|
448
|
-
];
|
|
449
|
-
return utilityTypes.includes(aliasName);
|
|
450
|
-
}
|
|
451
|
-
isSimpleArrayType(type) {
|
|
452
|
-
const symbol = type.getSymbol();
|
|
453
|
-
if (!symbol || symbol.getName() !== 'Array') {
|
|
454
|
-
return false;
|
|
455
|
-
}
|
|
456
|
-
// Check if this is Array<T> where T is a simple, non-generic type
|
|
457
|
-
if (type.typeArguments &&
|
|
458
|
-
type.typeArguments.length === 1) {
|
|
459
|
-
const elementType = type.typeArguments[0];
|
|
460
|
-
if (!elementType)
|
|
461
|
-
return false;
|
|
462
|
-
// If the element type is a utility type, then this array should be considered generic
|
|
463
|
-
if (this.isUtilityTypeFromType(elementType)) {
|
|
464
|
-
return false;
|
|
465
|
-
}
|
|
466
|
-
// If the element type itself has generic parameters, this array is generic
|
|
467
|
-
if (elementType.typeArguments &&
|
|
468
|
-
elementType.typeArguments.length > 0) {
|
|
469
|
-
return false;
|
|
470
|
-
}
|
|
471
|
-
return true;
|
|
472
|
-
}
|
|
473
|
-
return false;
|
|
474
|
-
}
|
|
475
|
-
isPrimitiveType(typeName) {
|
|
476
|
-
const lowerTypeName = typeName.toLowerCase();
|
|
477
|
-
// Check against all primitive types from constants
|
|
478
|
-
const primitiveTypes = [
|
|
479
|
-
constants.jsPrimitives.String.type.toLowerCase(),
|
|
480
|
-
constants.jsPrimitives.Number.type.toLowerCase(),
|
|
481
|
-
constants.jsPrimitives.Boolean.type.toLowerCase(),
|
|
482
|
-
constants.jsPrimitives.Date.type.toLowerCase(),
|
|
483
|
-
constants.jsPrimitives.Buffer.type.toLowerCase(),
|
|
484
|
-
constants.jsPrimitives.Uint8Array.type.toLowerCase(),
|
|
485
|
-
constants.jsPrimitives.File.type.toLowerCase(),
|
|
486
|
-
constants.jsPrimitives.UploadFile.type.toLowerCase(),
|
|
487
|
-
constants.jsPrimitives.UploadFileDto.type.toLowerCase(),
|
|
488
|
-
constants.jsPrimitives.BigInt.type.toLowerCase(),
|
|
489
|
-
constants.jsPrimitives.Symbol.type.toLowerCase(),
|
|
490
|
-
constants.jsPrimitives.null.type.toLowerCase(),
|
|
491
|
-
constants.jsPrimitives.Object.type.toLowerCase(),
|
|
492
|
-
constants.jsPrimitives.Array.type.toLowerCase(),
|
|
493
|
-
constants.jsPrimitives.Any.type.toLowerCase(),
|
|
494
|
-
constants.jsPrimitives.Unknown.type.toLowerCase(),
|
|
495
|
-
];
|
|
496
|
-
const primitivesArray = primitiveTypes.map(t => t.concat('[]'));
|
|
497
|
-
return (primitiveTypes.includes(lowerTypeName) ||
|
|
498
|
-
primitivesArray.includes(lowerTypeName));
|
|
499
|
-
}
|
|
500
|
-
static getInstance(tsConfigPath, options) {
|
|
501
|
-
if (!SchemaTransformer.instance) {
|
|
502
|
-
SchemaTransformer.instance = new SchemaTransformer(tsConfigPath, options);
|
|
503
|
-
}
|
|
504
|
-
return SchemaTransformer.instance;
|
|
505
|
-
}
|
|
506
|
-
getSourceFileByClass(cls, sourceOptions) {
|
|
507
|
-
const className = cls.name;
|
|
508
|
-
let matches = [];
|
|
509
|
-
if (sourceOptions?.isExternal) {
|
|
510
|
-
const sourceFiles = this.getFilteredSourceFiles(sourceOptions);
|
|
511
|
-
for (const sourceFile of sourceFiles) {
|
|
512
|
-
const node = sourceFile.statements.find(stmt => ts.isClassDeclaration(stmt) &&
|
|
513
|
-
stmt.name &&
|
|
514
|
-
stmt.name.text === className);
|
|
515
|
-
if (node) {
|
|
516
|
-
matches.push({ sourceFile, node });
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
else {
|
|
521
|
-
matches = this.classFileIndex.get(className) || [];
|
|
522
|
-
if (sourceOptions?.filePath) {
|
|
523
|
-
matches = matches.filter(m => m.sourceFile.fileName.includes(sourceOptions.filePath));
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
if (matches.length === 0) {
|
|
527
|
-
return undefined;
|
|
528
|
-
}
|
|
529
|
-
if (matches.length === 1) {
|
|
530
|
-
return matches[0];
|
|
531
|
-
}
|
|
532
|
-
if (matches.length > 1 && !sourceOptions?.filePath) {
|
|
533
|
-
const bestMatch = this.findBestMatch(cls, matches);
|
|
534
|
-
if (bestMatch) {
|
|
535
|
-
return bestMatch;
|
|
536
|
-
}
|
|
537
|
-
const firstMatch = matches[0];
|
|
538
|
-
if (firstMatch) {
|
|
539
|
-
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'.`);
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
return matches[0];
|
|
543
|
-
}
|
|
544
|
-
checkTypeMatch(value, typeNode) {
|
|
545
|
-
const runtimeType = typeof value;
|
|
546
|
-
if (runtimeType === 'string' &&
|
|
547
|
-
typeNode.kind === ts.SyntaxKind.StringKeyword)
|
|
548
|
-
return true;
|
|
549
|
-
if (runtimeType === 'number' &&
|
|
550
|
-
typeNode.kind === ts.SyntaxKind.NumberKeyword)
|
|
551
|
-
return true;
|
|
552
|
-
if (runtimeType === 'boolean' &&
|
|
553
|
-
typeNode.kind === ts.SyntaxKind.BooleanKeyword)
|
|
554
|
-
return true;
|
|
555
|
-
if (Array.isArray(value) && ts.isArrayTypeNode(typeNode)) {
|
|
556
|
-
if (value.length === 0)
|
|
557
|
-
return true;
|
|
558
|
-
const firstItem = value[0];
|
|
559
|
-
const elementType = typeNode.elementType;
|
|
560
|
-
return this.checkTypeMatch(firstItem, elementType);
|
|
561
|
-
}
|
|
562
|
-
if (runtimeType === 'object' && value !== null && !Array.isArray(value)) {
|
|
563
|
-
if (ts.isTypeReferenceNode(typeNode) ||
|
|
564
|
-
typeNode.kind === ts.SyntaxKind.ObjectKeyword) {
|
|
565
|
-
return true;
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
return false;
|
|
569
|
-
}
|
|
570
|
-
findBestMatch(cls, matches) {
|
|
571
|
-
const runtimeSource = cls.toString();
|
|
572
|
-
const runtimeProperties = new Map();
|
|
573
|
-
const regexProperties = new Set();
|
|
574
|
-
// Try to extract properties from runtime source (assignments in constructor)
|
|
575
|
-
try {
|
|
576
|
-
const regex = /this\.([a-zA-Z0-9_$]+)\s*=/g;
|
|
577
|
-
let match;
|
|
578
|
-
while ((match = regex.exec(runtimeSource)) !== null) {
|
|
579
|
-
regexProperties.add(match[1]);
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
catch (e) {
|
|
583
|
-
// Ignore regex errors
|
|
584
|
-
}
|
|
585
|
-
// Try to instantiate the class to find properties
|
|
586
|
-
try {
|
|
587
|
-
const instance = new cls();
|
|
588
|
-
Object.keys(instance).forEach(key => {
|
|
589
|
-
runtimeProperties.set(key, instance[key]);
|
|
590
|
-
});
|
|
591
|
-
}
|
|
592
|
-
catch (e) {
|
|
593
|
-
// Ignore instantiation errors (e.g. required constructor arguments)
|
|
594
|
-
}
|
|
595
|
-
// Try to get properties from class-validator metadata
|
|
596
|
-
try {
|
|
597
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
598
|
-
const { getMetadataStorage } = require('class-validator');
|
|
599
|
-
const metadata = getMetadataStorage();
|
|
600
|
-
const targetMetadata = metadata.getTargetValidationMetadatas(cls, null, false, false);
|
|
601
|
-
targetMetadata.forEach((m) => {
|
|
602
|
-
if (m.propertyName && !runtimeProperties.has(m.propertyName)) {
|
|
603
|
-
runtimeProperties.set(m.propertyName, undefined);
|
|
604
|
-
}
|
|
605
|
-
});
|
|
606
|
-
}
|
|
607
|
-
catch (e) {
|
|
608
|
-
// Ignore if class-validator is not available or fails
|
|
609
|
-
}
|
|
610
|
-
// console.log(`[findBestMatch] Class: ${cls.name}, Runtime Props: ${Array.from(runtimeProperties.keys()).join(', ')}`)
|
|
611
|
-
const scores = matches.map(match => {
|
|
612
|
-
let score = 0;
|
|
613
|
-
for (const member of match.node.members) {
|
|
614
|
-
if (ts.isMethodDeclaration(member) && ts.isIdentifier(member.name)) {
|
|
615
|
-
if (runtimeSource.includes(member.name.text)) {
|
|
616
|
-
score += 2;
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
else if (ts.isPropertyDeclaration(member) &&
|
|
620
|
-
ts.isIdentifier(member.name)) {
|
|
621
|
-
const propName = member.name.text;
|
|
622
|
-
if (runtimeProperties.has(propName)) {
|
|
623
|
-
score += 1;
|
|
624
|
-
const value = runtimeProperties.get(propName);
|
|
625
|
-
if (member.type && this.checkTypeMatch(value, member.type)) {
|
|
626
|
-
score += 5;
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
else if (regexProperties.has(propName)) {
|
|
630
|
-
score += 1;
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
return { match, score };
|
|
635
|
-
});
|
|
636
|
-
scores.sort((a, b) => b.score - a.score);
|
|
637
|
-
const firstScore = scores[0];
|
|
638
|
-
const secondScore = scores[1];
|
|
639
|
-
if (firstScore && firstScore.score > 0) {
|
|
640
|
-
if (scores.length === 1 ||
|
|
641
|
-
(secondScore && firstScore.score > secondScore.score)) {
|
|
642
|
-
return firstScore.match;
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
return undefined;
|
|
646
|
-
}
|
|
647
|
-
getFilteredSourceFiles(sourceOptions) {
|
|
648
|
-
if (sourceOptions?.isExternal) {
|
|
649
|
-
return this.program.getSourceFiles().filter(sf => {
|
|
650
|
-
return (sf.fileName.includes(sourceOptions.packageName) &&
|
|
651
|
-
(!sourceOptions.filePath || sf.fileName === sourceOptions.filePath));
|
|
652
|
-
});
|
|
653
|
-
}
|
|
654
|
-
return this.sourceFiles.filter(sf => {
|
|
655
|
-
if (sourceOptions?.filePath &&
|
|
656
|
-
!sf.fileName.includes(sourceOptions.filePath)) {
|
|
657
|
-
return false;
|
|
658
|
-
}
|
|
659
|
-
return true;
|
|
660
|
-
});
|
|
661
|
-
}
|
|
662
|
-
isEnum(propertyDeclaration) {
|
|
663
|
-
if (!propertyDeclaration.type) {
|
|
664
|
-
return false;
|
|
665
|
-
}
|
|
666
|
-
let typeNode = propertyDeclaration.type;
|
|
667
|
-
if (ts.isArrayTypeNode(typeNode)) {
|
|
668
|
-
typeNode = typeNode.elementType;
|
|
669
|
-
}
|
|
670
|
-
if (ts.isTypeReferenceNode(typeNode)) {
|
|
671
|
-
const type = this.checker.getTypeAtLocation(typeNode);
|
|
672
|
-
// console.log('isEnum check:', typeNode.getText(), type.flags)
|
|
673
|
-
return (!!(type.flags & ts.TypeFlags.Enum) ||
|
|
674
|
-
!!(type.flags & ts.TypeFlags.EnumLiteral));
|
|
675
|
-
}
|
|
676
|
-
return false;
|
|
677
|
-
}
|
|
678
|
-
isClassType(propertyDeclaration) {
|
|
679
|
-
// If there's no explicit type annotation, we can't determine reliably
|
|
680
|
-
if (!propertyDeclaration.type) {
|
|
681
|
-
return false;
|
|
682
|
-
}
|
|
683
|
-
// Check if the original property type is an array type
|
|
684
|
-
if (this.isArrayProperty(propertyDeclaration)) {
|
|
685
|
-
const arrayType = propertyDeclaration.type;
|
|
686
|
-
const elementType = arrayType.elementType;
|
|
687
|
-
// Special handling for utility types with type arguments (e.g., PayloadEntity<Person>)
|
|
688
|
-
if (ts.isTypeReferenceNode(elementType) &&
|
|
689
|
-
elementType.typeArguments &&
|
|
690
|
-
elementType.typeArguments.length > 0) {
|
|
691
|
-
// Check the first type argument - it might be the actual class
|
|
692
|
-
const firstTypeArg = elementType.typeArguments[0];
|
|
693
|
-
if (firstTypeArg) {
|
|
694
|
-
const argType = this.checker.getTypeAtLocation(firstTypeArg);
|
|
695
|
-
const argSymbol = argType.getSymbol();
|
|
696
|
-
if (argSymbol && argSymbol.declarations) {
|
|
697
|
-
const hasClass = argSymbol.declarations.some(decl => ts.isClassDeclaration(decl));
|
|
698
|
-
if (hasClass)
|
|
699
|
-
return true;
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
// Get the type from the element, regardless of its syntaxkind
|
|
704
|
-
const type = this.checker.getTypeAtLocation(elementType);
|
|
705
|
-
const symbol = type.getSymbol();
|
|
706
|
-
if (symbol && symbol.declarations) {
|
|
707
|
-
return symbol.declarations.some(decl => ts.isClassDeclaration(decl));
|
|
708
|
-
}
|
|
709
|
-
return false;
|
|
710
|
-
}
|
|
711
|
-
// Check non-array types
|
|
712
|
-
else {
|
|
713
|
-
// Special handling for utility types with type arguments (e.g., PayloadEntity<Branch>)
|
|
714
|
-
if (ts.isTypeReferenceNode(propertyDeclaration.type) &&
|
|
715
|
-
propertyDeclaration.type.typeArguments &&
|
|
716
|
-
propertyDeclaration.type.typeArguments.length > 0) {
|
|
717
|
-
// Check the first type argument - it might be the actual class
|
|
718
|
-
const firstTypeArg = propertyDeclaration.type.typeArguments[0];
|
|
719
|
-
if (firstTypeArg) {
|
|
720
|
-
const argType = this.checker.getTypeAtLocation(firstTypeArg);
|
|
721
|
-
const argSymbol = argType.getSymbol();
|
|
722
|
-
if (argSymbol && argSymbol.declarations) {
|
|
723
|
-
const hasClass = argSymbol.declarations.some(decl => ts.isClassDeclaration(decl));
|
|
724
|
-
if (hasClass)
|
|
725
|
-
return true;
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
const type = this.checker.getTypeAtLocation(propertyDeclaration.type);
|
|
730
|
-
const symbol = type.getSymbol();
|
|
731
|
-
if (symbol && symbol.declarations) {
|
|
732
|
-
return symbol.declarations.some(decl => ts.isClassDeclaration(decl));
|
|
733
|
-
}
|
|
734
|
-
return false;
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
getDeclarationProperty(property) {
|
|
738
|
-
if (!property.originalProperty.type) {
|
|
739
|
-
return undefined;
|
|
740
|
-
}
|
|
741
|
-
// Handle array types - get the element type
|
|
742
|
-
if (ts.isArrayTypeNode(property.originalProperty.type)) {
|
|
743
|
-
const elementType = property.originalProperty.type.elementType;
|
|
744
|
-
// Check if it's a utility type with type arguments (e.g., PayloadEntity<Branch>[])
|
|
745
|
-
if (ts.isTypeReferenceNode(elementType) &&
|
|
746
|
-
elementType.typeArguments &&
|
|
747
|
-
elementType.typeArguments.length > 0) {
|
|
748
|
-
const firstTypeArg = elementType.typeArguments[0];
|
|
749
|
-
if (firstTypeArg) {
|
|
750
|
-
const argType = this.checker.getTypeAtLocation(firstTypeArg);
|
|
751
|
-
const argSymbol = argType.getSymbol();
|
|
752
|
-
if (argSymbol && argSymbol.declarations) {
|
|
753
|
-
const classDecl = argSymbol.declarations.find(decl => ts.isClassDeclaration(decl));
|
|
754
|
-
if (classDecl)
|
|
755
|
-
return classDecl;
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
const type = this.checker.getTypeAtLocation(elementType);
|
|
760
|
-
const symbol = type.getSymbol();
|
|
761
|
-
if (symbol && symbol.declarations) {
|
|
762
|
-
// Return the first class declaration found
|
|
763
|
-
const classDecl = symbol.declarations.find(decl => ts.isClassDeclaration(decl));
|
|
764
|
-
return classDecl || symbol.declarations[0];
|
|
765
|
-
}
|
|
766
|
-
return undefined;
|
|
767
|
-
}
|
|
768
|
-
// Handle non-array types
|
|
769
|
-
// Check if it's a utility type with type arguments (e.g., PayloadEntity<Branch>)
|
|
770
|
-
if (ts.isTypeReferenceNode(property.originalProperty.type) &&
|
|
771
|
-
property.originalProperty.type.typeArguments &&
|
|
772
|
-
property.originalProperty.type.typeArguments.length > 0) {
|
|
773
|
-
const firstTypeArg = property.originalProperty.type.typeArguments[0];
|
|
774
|
-
if (firstTypeArg) {
|
|
775
|
-
const argType = this.checker.getTypeAtLocation(firstTypeArg);
|
|
776
|
-
const argSymbol = argType.getSymbol();
|
|
777
|
-
if (argSymbol && argSymbol.declarations) {
|
|
778
|
-
const classDecl = argSymbol.declarations.find(decl => ts.isClassDeclaration(decl));
|
|
779
|
-
if (classDecl)
|
|
780
|
-
return classDecl;
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
const type = this.checker.getTypeAtLocation(property.originalProperty.type);
|
|
785
|
-
const symbol = type.getSymbol();
|
|
786
|
-
if (symbol && symbol.declarations) {
|
|
787
|
-
// Return the first class declaration found
|
|
788
|
-
const classDecl = symbol.declarations.find(decl => ts.isClassDeclaration(decl));
|
|
789
|
-
return classDecl || symbol.declarations[0];
|
|
790
|
-
}
|
|
791
|
-
return undefined;
|
|
792
|
-
}
|
|
793
|
-
isArrayProperty(propertyDeclaration) {
|
|
794
|
-
if (!propertyDeclaration.type) {
|
|
795
|
-
return false;
|
|
796
|
-
}
|
|
797
|
-
return ts.isArrayTypeNode(propertyDeclaration.type);
|
|
798
|
-
}
|
|
799
|
-
getSchemaFromProperties({ properties, visitedClass, transformedSchema, classDeclaration, }) {
|
|
800
|
-
let schema = {};
|
|
801
|
-
const required = [];
|
|
802
|
-
for (const property of properties) {
|
|
803
|
-
schema[property.name] = this.getSchemaFromProperty({
|
|
804
|
-
property,
|
|
805
|
-
visitedClass,
|
|
806
|
-
transformedSchema,
|
|
807
|
-
classDeclaration,
|
|
808
|
-
});
|
|
809
|
-
// this.applyDecorators(property, schema as SchemaType)
|
|
810
|
-
if (!property.isOptional) {
|
|
811
|
-
required.push(property.name);
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
return {
|
|
815
|
-
type: 'object',
|
|
816
|
-
properties: schema,
|
|
817
|
-
required: required.length ? required : undefined,
|
|
818
|
-
};
|
|
819
|
-
}
|
|
820
|
-
getSchemaFromProperty({ property, visitedClass, transformedSchema, classDeclaration, }) {
|
|
821
|
-
let schema = {};
|
|
822
|
-
if (property.isPrimitive) {
|
|
823
|
-
schema = this.getSchemaFromPrimitive(property);
|
|
824
|
-
}
|
|
825
|
-
else if (property.isClassType) {
|
|
826
|
-
schema = this.buildSchemaFromClass({
|
|
827
|
-
property,
|
|
828
|
-
classDeclaration,
|
|
829
|
-
visitedClass,
|
|
830
|
-
transformedSchema,
|
|
831
|
-
});
|
|
832
|
-
}
|
|
833
|
-
else if (property.isTypeLiteral && property.typeLiteralClassReference) {
|
|
834
|
-
schema = this.buildSchemaFromClass({
|
|
835
|
-
property,
|
|
836
|
-
classDeclaration: property.typeLiteralClassReference,
|
|
837
|
-
visitedClass,
|
|
838
|
-
transformedSchema,
|
|
839
|
-
});
|
|
840
|
-
}
|
|
841
|
-
else {
|
|
842
|
-
schema = { type: 'object', properties: {}, additionalProperties: true };
|
|
843
|
-
}
|
|
844
|
-
this.applyDecorators(property, schema);
|
|
845
|
-
return schema;
|
|
846
|
-
}
|
|
847
|
-
buildSchemaFromClass({ property, classDeclaration, visitedClass, transformedSchema, }) {
|
|
848
|
-
const declaration = this.getDeclarationProperty(property);
|
|
849
|
-
let schema = {};
|
|
850
|
-
if (property.isRef && classDeclaration.name) {
|
|
851
|
-
// Self-referencing property, handle as a reference to avoid infinite recursion
|
|
852
|
-
if (property.isArray) {
|
|
853
|
-
schema.type = 'array';
|
|
854
|
-
schema.items = {
|
|
855
|
-
$ref: `#/components/schemas/${classDeclaration.name.text}`,
|
|
856
|
-
};
|
|
857
|
-
}
|
|
858
|
-
else {
|
|
859
|
-
schema = {
|
|
860
|
-
$ref: `#/components/schemas/${classDeclaration.name.text}`,
|
|
861
|
-
};
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
else if (property.isTypeLiteral && property.typeLiteralClassReference) {
|
|
865
|
-
schema = this.getSchemaFromClass({
|
|
866
|
-
isArray: property.isArray,
|
|
867
|
-
visitedClass,
|
|
868
|
-
transformedSchema,
|
|
869
|
-
declaration: property.typeLiteralClassReference,
|
|
870
|
-
});
|
|
871
|
-
}
|
|
872
|
-
else {
|
|
873
|
-
schema = this.getSchemaFromClass({
|
|
874
|
-
isArray: property.isArray,
|
|
875
|
-
visitedClass,
|
|
876
|
-
transformedSchema,
|
|
877
|
-
declaration,
|
|
878
|
-
});
|
|
879
|
-
}
|
|
880
|
-
return schema;
|
|
881
|
-
}
|
|
882
|
-
getSchemaFromClass({ transformedSchema = new Map(), visitedClass = new Set(), declaration, isArray, }) {
|
|
883
|
-
let schema = { type: 'object' };
|
|
884
|
-
if (!declaration ||
|
|
885
|
-
!ts.isClassDeclaration(declaration) ||
|
|
886
|
-
!declaration.name) {
|
|
887
|
-
return { type: 'object' };
|
|
888
|
-
}
|
|
889
|
-
if (visitedClass.has(declaration)) {
|
|
890
|
-
if (isArray) {
|
|
891
|
-
schema.type = 'array';
|
|
892
|
-
schema.items = {
|
|
893
|
-
$ref: `#/components/schemas/${declaration.name.text}`,
|
|
894
|
-
};
|
|
895
|
-
}
|
|
896
|
-
else {
|
|
897
|
-
schema = {
|
|
898
|
-
$ref: `#/components/schemas/${declaration.name.text}`,
|
|
899
|
-
};
|
|
900
|
-
}
|
|
901
|
-
return schema;
|
|
902
|
-
}
|
|
903
|
-
visitedClass.add(declaration);
|
|
904
|
-
const properties = this.getPropertiesByClassDeclaration(declaration);
|
|
905
|
-
let transformerProps = this.getSchemaFromProperties({
|
|
906
|
-
properties,
|
|
907
|
-
visitedClass,
|
|
908
|
-
transformedSchema: transformedSchema,
|
|
909
|
-
classDeclaration: declaration,
|
|
910
|
-
});
|
|
911
|
-
if (isArray) {
|
|
912
|
-
schema.type = 'array';
|
|
913
|
-
schema.items = {
|
|
914
|
-
type: transformerProps.type,
|
|
915
|
-
properties: transformerProps.properties,
|
|
916
|
-
required: transformerProps.required,
|
|
917
|
-
};
|
|
918
|
-
}
|
|
919
|
-
else {
|
|
920
|
-
schema.type = transformerProps.type;
|
|
921
|
-
schema.properties = transformerProps.properties;
|
|
922
|
-
schema.required = transformerProps.required;
|
|
923
|
-
}
|
|
924
|
-
transformedSchema.set(declaration.name.text, schema);
|
|
925
|
-
visitedClass.delete(declaration);
|
|
926
|
-
return schema;
|
|
927
|
-
}
|
|
928
|
-
getSchemaFromEnum(property) {
|
|
929
|
-
let typeNode = property.originalProperty.type;
|
|
930
|
-
if (ts.isArrayTypeNode(typeNode)) {
|
|
931
|
-
typeNode = typeNode.elementType;
|
|
932
|
-
}
|
|
933
|
-
const type = this.checker.getTypeAtLocation(typeNode);
|
|
934
|
-
if (type.symbol && type.symbol.exports) {
|
|
935
|
-
const values = [];
|
|
936
|
-
type.symbol.exports.forEach(member => {
|
|
937
|
-
const declaration = member.valueDeclaration;
|
|
938
|
-
if (declaration && ts.isEnumMember(declaration)) {
|
|
939
|
-
const value = this.checker.getConstantValue(declaration);
|
|
940
|
-
if (value !== undefined) {
|
|
941
|
-
values.push(value);
|
|
942
|
-
}
|
|
943
|
-
}
|
|
944
|
-
});
|
|
945
|
-
if (values.length > 0) {
|
|
946
|
-
const propertySchema = { type: 'object' };
|
|
947
|
-
propertySchema.enum = values;
|
|
948
|
-
const isString = values.every(v => typeof v === 'string');
|
|
949
|
-
const isNumber = values.every(v => typeof v === 'number');
|
|
950
|
-
if (isString) {
|
|
951
|
-
propertySchema.type = 'string';
|
|
952
|
-
}
|
|
953
|
-
else if (isNumber) {
|
|
954
|
-
propertySchema.type = 'number';
|
|
955
|
-
}
|
|
956
|
-
else {
|
|
957
|
-
propertySchema.type = 'string';
|
|
958
|
-
}
|
|
959
|
-
if (property.isArray) {
|
|
960
|
-
const itemsSchema = { ...propertySchema };
|
|
961
|
-
propertySchema.type = 'array';
|
|
962
|
-
propertySchema.items = itemsSchema;
|
|
963
|
-
delete propertySchema.enum;
|
|
964
|
-
return propertySchema;
|
|
965
|
-
}
|
|
966
|
-
else {
|
|
967
|
-
return propertySchema;
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
}
|
|
971
|
-
return undefined;
|
|
972
|
-
}
|
|
973
|
-
getSchemaFromPrimitive(property) {
|
|
974
|
-
if (property.isEnum) {
|
|
975
|
-
const enumSchema = this.getSchemaFromEnum(property);
|
|
976
|
-
if (enumSchema) {
|
|
977
|
-
return enumSchema;
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
const propertySchema = { type: 'object' };
|
|
981
|
-
const propertyType = property.type.toLowerCase().replace('[]', '').trim();
|
|
982
|
-
let isFile = false;
|
|
983
|
-
switch (propertyType) {
|
|
984
|
-
case constants.jsPrimitives.String.value:
|
|
985
|
-
propertySchema.type = constants.jsPrimitives.String.value;
|
|
986
|
-
break;
|
|
987
|
-
case constants.jsPrimitives.Number.value:
|
|
988
|
-
propertySchema.type = constants.jsPrimitives.Number.value;
|
|
989
|
-
propertySchema.format = constants.jsPrimitives.Number.format;
|
|
990
|
-
break;
|
|
991
|
-
case constants.jsPrimitives.BigInt.type.toLocaleLowerCase():
|
|
992
|
-
propertySchema.type = constants.jsPrimitives.BigInt.value;
|
|
993
|
-
propertySchema.format = constants.jsPrimitives.BigInt.format;
|
|
994
|
-
break;
|
|
995
|
-
case constants.jsPrimitives.Date.type.toLocaleLowerCase():
|
|
996
|
-
propertySchema.type = constants.jsPrimitives.Date.value;
|
|
997
|
-
propertySchema.format = constants.jsPrimitives.Date.format;
|
|
998
|
-
break;
|
|
999
|
-
case constants.jsPrimitives.Buffer.type.toLocaleLowerCase():
|
|
1000
|
-
case constants.jsPrimitives.Uint8Array.type.toLocaleLowerCase():
|
|
1001
|
-
case constants.jsPrimitives.File.type.toLocaleLowerCase():
|
|
1002
|
-
case constants.jsPrimitives.UploadFile.type.toLocaleLowerCase():
|
|
1003
|
-
case constants.jsPrimitives.UploadFileDto.type.toLocaleLowerCase():
|
|
1004
|
-
propertySchema.type = constants.jsPrimitives.UploadFile.value;
|
|
1005
|
-
propertySchema.format = constants.jsPrimitives.UploadFile.format;
|
|
1006
|
-
isFile = true;
|
|
1007
|
-
break;
|
|
1008
|
-
case constants.jsPrimitives.Array.value:
|
|
1009
|
-
propertySchema.type = constants.jsPrimitives.Array.value;
|
|
1010
|
-
break;
|
|
1011
|
-
case constants.jsPrimitives.Boolean.value:
|
|
1012
|
-
propertySchema.type = constants.jsPrimitives.Boolean.value;
|
|
1013
|
-
break;
|
|
1014
|
-
case constants.jsPrimitives.Symbol.type.toLocaleLowerCase():
|
|
1015
|
-
propertySchema.type = constants.jsPrimitives.Symbol.value;
|
|
1016
|
-
break;
|
|
1017
|
-
case constants.jsPrimitives.Object.value:
|
|
1018
|
-
propertySchema.type = constants.jsPrimitives.Object.value;
|
|
1019
|
-
break;
|
|
1020
|
-
default:
|
|
1021
|
-
propertySchema.type = constants.jsPrimitives.String.value;
|
|
1022
|
-
}
|
|
1023
|
-
if (property.isArray) {
|
|
1024
|
-
delete propertySchema.format;
|
|
1025
|
-
propertySchema.type = `array`;
|
|
1026
|
-
propertySchema.items = {
|
|
1027
|
-
type: isFile ? constants.jsPrimitives.UploadFile.value : propertyType,
|
|
1028
|
-
format: isFile
|
|
1029
|
-
? constants.jsPrimitives.UploadFile.format
|
|
1030
|
-
: propertySchema.format,
|
|
1031
|
-
};
|
|
1032
|
-
}
|
|
1033
|
-
return propertySchema;
|
|
1034
|
-
}
|
|
1035
|
-
isTypeLiteral(property) {
|
|
1036
|
-
if (!property.type)
|
|
1037
|
-
return false;
|
|
1038
|
-
if (ts.isTypeReferenceNode(property.type)) {
|
|
1039
|
-
const symbol = this.checker.getSymbolAtLocation(property.type.typeName);
|
|
1040
|
-
if (symbol) {
|
|
1041
|
-
const declarations = symbol.getDeclarations();
|
|
1042
|
-
if (declarations && declarations.length > 0) {
|
|
1043
|
-
const typeAliasDecl = declarations.find(decl => ts.isTypeAliasDeclaration(decl));
|
|
1044
|
-
if (typeAliasDecl && typeAliasDecl.type) {
|
|
1045
|
-
return this.isLiteralTypeNode(typeAliasDecl.type);
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
return false;
|
|
1051
|
-
}
|
|
1052
|
-
/**
|
|
1053
|
-
*
|
|
1054
|
-
* @param typeNode
|
|
1055
|
-
* @returns boolean - true si el typeNode representa un tipo literal complejo
|
|
1056
|
-
*/
|
|
1057
|
-
isLiteralTypeNode(typeNode) {
|
|
1058
|
-
return (ts.isIntersectionTypeNode(typeNode) || // {} & Omit<T, ...>
|
|
1059
|
-
ts.isUnionTypeNode(typeNode) || // string | number
|
|
1060
|
-
ts.isMappedTypeNode(typeNode) || // { [K in keyof T]: ... }
|
|
1061
|
-
ts.isTypeLiteralNode(typeNode) || // { foo: string }
|
|
1062
|
-
ts.isConditionalTypeNode(typeNode) || // T extends U ? X : Y
|
|
1063
|
-
ts.isIndexedAccessTypeNode(typeNode) || // T['key']
|
|
1064
|
-
ts.isTypeOperatorNode(typeNode) || // keyof T, readonly T
|
|
1065
|
-
ts.isTypeReferenceNode(typeNode) // Omit, Pick, Partial, etc.
|
|
1066
|
-
);
|
|
1067
|
-
}
|
|
1068
|
-
applyEnumDecorator(decorator, schema) {
|
|
1069
|
-
if (decorator.arguments.length === 0)
|
|
1070
|
-
return;
|
|
1071
|
-
const arg = decorator.arguments[0];
|
|
1072
|
-
if (arg && typeof arg === 'object' && 'kind' in arg) {
|
|
1073
|
-
const type = this.checker.getTypeAtLocation(arg);
|
|
1074
|
-
if (type.symbol && type.symbol.exports) {
|
|
1075
|
-
const values = [];
|
|
1076
|
-
type.symbol.exports.forEach(member => {
|
|
1077
|
-
const declaration = member.valueDeclaration;
|
|
1078
|
-
if (declaration && ts.isEnumMember(declaration)) {
|
|
1079
|
-
const value = this.checker.getConstantValue(declaration);
|
|
1080
|
-
if (value !== undefined) {
|
|
1081
|
-
values.push(value);
|
|
1082
|
-
}
|
|
1083
|
-
}
|
|
1084
|
-
});
|
|
1085
|
-
if (values.length > 0) {
|
|
1086
|
-
schema.enum = values;
|
|
1087
|
-
const isString = values.every(v => typeof v === 'string');
|
|
1088
|
-
const isNumber = values.every(v => typeof v === 'number');
|
|
1089
|
-
if (isString) {
|
|
1090
|
-
schema.type = 'string';
|
|
1091
|
-
}
|
|
1092
|
-
else if (isNumber) {
|
|
1093
|
-
schema.type = 'number';
|
|
1094
|
-
}
|
|
1095
|
-
else {
|
|
1096
|
-
schema.type = 'string';
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
}
|
|
1102
|
-
applyDecorators(property, schema) {
|
|
1103
|
-
for (const decorator of property.decorators) {
|
|
1104
|
-
const decoratorName = decorator.name;
|
|
1105
|
-
switch (decoratorName) {
|
|
1106
|
-
case constants.validatorDecorators.IsString.name:
|
|
1107
|
-
if (!property.isArray) {
|
|
1108
|
-
schema.type = constants.validatorDecorators.IsString.type;
|
|
1109
|
-
}
|
|
1110
|
-
else if (schema.items) {
|
|
1111
|
-
schema.items.type = constants.validatorDecorators.IsString.type;
|
|
1112
|
-
}
|
|
1113
|
-
break;
|
|
1114
|
-
case constants.validatorDecorators.IsInt.name:
|
|
1115
|
-
if (!property.isArray) {
|
|
1116
|
-
schema.type = constants.validatorDecorators.IsInt.type;
|
|
1117
|
-
schema.format = constants.validatorDecorators.IsInt.format;
|
|
1118
|
-
}
|
|
1119
|
-
else if (schema.items) {
|
|
1120
|
-
schema.items.type = constants.validatorDecorators.IsInt.type;
|
|
1121
|
-
schema.items.format = constants.validatorDecorators.IsInt.format;
|
|
1122
|
-
}
|
|
1123
|
-
break;
|
|
1124
|
-
case constants.validatorDecorators.IsNumber.name:
|
|
1125
|
-
if (!property.isArray) {
|
|
1126
|
-
schema.type = constants.validatorDecorators.IsNumber.type;
|
|
1127
|
-
}
|
|
1128
|
-
else if (schema.items) {
|
|
1129
|
-
schema.items.type = constants.validatorDecorators.IsNumber.type;
|
|
1130
|
-
}
|
|
1131
|
-
break;
|
|
1132
|
-
case constants.validatorDecorators.IsBoolean.name:
|
|
1133
|
-
if (!property.isArray) {
|
|
1134
|
-
schema.type = constants.validatorDecorators.IsBoolean.type;
|
|
1135
|
-
}
|
|
1136
|
-
else if (schema.items) {
|
|
1137
|
-
schema.items.type = constants.validatorDecorators.IsBoolean.type;
|
|
1138
|
-
}
|
|
1139
|
-
break;
|
|
1140
|
-
case constants.validatorDecorators.IsEmail.name:
|
|
1141
|
-
if (!property.isArray) {
|
|
1142
|
-
schema.format = constants.validatorDecorators.IsEmail.format;
|
|
1143
|
-
}
|
|
1144
|
-
else if (schema.items) {
|
|
1145
|
-
schema.items.format = constants.validatorDecorators.IsEmail.format;
|
|
1146
|
-
}
|
|
1147
|
-
break;
|
|
1148
|
-
case constants.validatorDecorators.IsDate.name:
|
|
1149
|
-
if (!property.isArray) {
|
|
1150
|
-
schema.type = constants.validatorDecorators.IsDate.type;
|
|
1151
|
-
schema.format = constants.validatorDecorators.IsDate.format;
|
|
1152
|
-
}
|
|
1153
|
-
else if (schema.items) {
|
|
1154
|
-
schema.items.type = constants.validatorDecorators.IsDate.type;
|
|
1155
|
-
schema.items.format = constants.validatorDecorators.IsDate.format;
|
|
1156
|
-
}
|
|
1157
|
-
break;
|
|
1158
|
-
case constants.validatorDecorators.IsNotEmpty.name:
|
|
1159
|
-
property.isOptional = false;
|
|
1160
|
-
break;
|
|
1161
|
-
case constants.validatorDecorators.IsOptional.name:
|
|
1162
|
-
property.isOptional = true;
|
|
1163
|
-
break;
|
|
1164
|
-
case constants.validatorDecorators.MinLength.name:
|
|
1165
|
-
schema.minLength = this.getSafeDecoratorArgument(decorator.arguments[0]);
|
|
1166
|
-
break;
|
|
1167
|
-
case constants.validatorDecorators.MaxLength.name:
|
|
1168
|
-
schema.maxLength = this.getSafeDecoratorArgument(decorator.arguments[0]);
|
|
1169
|
-
break;
|
|
1170
|
-
case constants.validatorDecorators.Length.name:
|
|
1171
|
-
schema.minLength = this.getSafeDecoratorArgument(decorator.arguments[0]);
|
|
1172
|
-
if (decorator.arguments[1]) {
|
|
1173
|
-
schema.maxLength = this.getSafeDecoratorArgument(decorator.arguments[1]);
|
|
1174
|
-
}
|
|
1175
|
-
break;
|
|
1176
|
-
case constants.validatorDecorators.Min.name:
|
|
1177
|
-
schema.minimum = this.getSafeDecoratorArgument(decorator.arguments[0]);
|
|
1178
|
-
break;
|
|
1179
|
-
case constants.validatorDecorators.Max.name:
|
|
1180
|
-
schema.maximum = this.getSafeDecoratorArgument(decorator.arguments[0]);
|
|
1181
|
-
break;
|
|
1182
|
-
case constants.validatorDecorators.IsPositive.name:
|
|
1183
|
-
schema.minimum = 0;
|
|
1184
|
-
break;
|
|
1185
|
-
case constants.validatorDecorators.IsArray.name:
|
|
1186
|
-
schema.type = constants.jsPrimitives.Array.value;
|
|
1187
|
-
break;
|
|
1188
|
-
case constants.validatorDecorators.ArrayNotEmpty.name:
|
|
1189
|
-
schema.minItems = 1;
|
|
1190
|
-
property.isOptional = false;
|
|
1191
|
-
break;
|
|
1192
|
-
case constants.validatorDecorators.ArrayMinSize.name:
|
|
1193
|
-
schema.minItems = this.getSafeDecoratorArgument(decorator.arguments[0]);
|
|
1194
|
-
break;
|
|
1195
|
-
case constants.validatorDecorators.ArrayMaxSize.name:
|
|
1196
|
-
schema.maxItems = this.getSafeDecoratorArgument(decorator.arguments[0]);
|
|
1197
|
-
break;
|
|
1198
|
-
case constants.validatorDecorators.IsEnum.name:
|
|
1199
|
-
if (!property.isArray) {
|
|
1200
|
-
this.applyEnumDecorator(decorator, schema);
|
|
1201
|
-
}
|
|
1202
|
-
else if (schema.items) {
|
|
1203
|
-
this.applyEnumDecorator(decorator, schema.items);
|
|
1204
|
-
}
|
|
1205
|
-
break;
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
transform(cls, sourceOptions) {
|
|
1210
|
-
let schema = { type: 'object', properties: {} };
|
|
1211
|
-
const result = this.getSourceFileByClass(cls, sourceOptions);
|
|
1212
|
-
if (!result?.sourceFile) {
|
|
1213
|
-
console.warn(`Class ${cls.name} not found in any source file.`);
|
|
1214
|
-
return { name: cls.name, schema: {} };
|
|
1215
|
-
}
|
|
1216
|
-
const properties = this.getPropertiesByClassDeclaration(result.node);
|
|
1217
|
-
schema = this.getSchemaFromProperties({
|
|
1218
|
-
properties,
|
|
1219
|
-
classDeclaration: result.node,
|
|
1220
|
-
});
|
|
1221
|
-
return { name: cls.name, schema };
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
function transform(cls, options) {
|
|
1225
|
-
// Use the singleton instance instead of creating a temporary one
|
|
1226
|
-
const transformer = SchemaTransformer.getInstance(undefined, options);
|
|
1227
|
-
return transformer.transform(cls, options?.sourceOptions);
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
|
-
export { transform };
|