ts-class-to-openapi 1.0.5 → 1.1.0
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 +368 -882
- package/dist/__test__/entities/additional-test-classes.d.ts +12 -0
- package/dist/__test__/entities/circular-reference-classes.d.ts +110 -0
- package/dist/__test__/entities/complex-circular-dependencies.d.ts +71 -0
- package/dist/__test__/entities/decorated-classes.d.ts +54 -0
- package/dist/__test__/entities/generic-circular-classes.d.ts +57 -0
- package/dist/__test__/entities/nested-classes.d.ts +70 -0
- package/dist/__test__/entities/pure-classes.d.ts +37 -0
- package/dist/__test__/entities/schema-validation-classes.d.ts +35 -0
- package/dist/__test__/index.d.ts +3 -9
- package/dist/__test__/testCases/schema-validation.test.d.ts +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.esm.js +546 -1319
- package/dist/index.js +545 -1319
- package/dist/run.d.ts +1 -1
- package/dist/run.js +1131 -1343
- package/dist/transformer.d.ts +1 -575
- package/dist/transformer.fixtures.d.ts +21 -0
- package/dist/types.d.ts +40 -3
- package/package.json +16 -15
- package/dist/__test__/entities/address.entity.d.ts +0 -5
- package/dist/__test__/entities/array.entity.d.ts +0 -7
- package/dist/__test__/entities/broken.entity.d.ts +0 -7
- package/dist/__test__/entities/circular.entity.d.ts +0 -59
- package/dist/__test__/entities/complete.entity.d.ts +0 -16
- package/dist/__test__/entities/complex-generics.entity.d.ts +0 -33
- package/dist/__test__/entities/comprehensive-enum.entity.d.ts +0 -23
- package/dist/__test__/entities/enum.entity.d.ts +0 -29
- package/dist/__test__/entities/generic.entity.d.ts +0 -11
- package/dist/__test__/entities/optional-properties.entity.d.ts +0 -11
- package/dist/__test__/entities/plain.entity.d.ts +0 -19
- package/dist/__test__/entities/simple.entity.d.ts +0 -5
- package/dist/__test__/entities/upload.entity.d.ts +0 -8
- package/dist/__test__/entities/user-role-generic.entity.d.ts +0 -13
- package/dist/__test__/test-entities/duplicate-name.entity.d.ts +0 -5
- package/dist/__test__/test-entities/generic.entity.d.ts +0 -11
- /package/dist/__test__/{circular-reference.test.d.ts → entities/circular-reference-cases.d.ts} +0 -0
- /package/dist/__test__/{enum.test.d.ts → entities/deep-nested-classes.d.ts} +0 -0
- /package/dist/__test__/{generic-types.test.d.ts → test.d.ts} +0 -0
- /package/dist/__test__/{integration.test.d.ts → testCases/circular-references.test.d.ts} +0 -0
- /package/dist/__test__/{main.test.d.ts → testCases/debug.test.d.ts} +0 -0
- /package/dist/__test__/{optional-properties.test.d.ts → testCases/decorated-classes.test.d.ts} +0 -0
- /package/dist/__test__/{plain.test.d.ts → testCases/edge-cases.test.d.ts} +0 -0
- /package/dist/__test__/{ref-pattern.test.d.ts → testCases/nested-classes.test.d.ts} +0 -0
- /package/dist/__test__/{singleton-behavior.test.d.ts → testCases/pure-classes.test.d.ts} +0 -0
package/dist/index.js
CHANGED
|
@@ -6,113 +6,57 @@ var path = require('path');
|
|
|
6
6
|
const TS_CONFIG_DEFAULT_PATH = path.resolve(process.cwd(), 'tsconfig.json');
|
|
7
7
|
const jsPrimitives = {
|
|
8
8
|
String: { type: 'String', value: 'string' },
|
|
9
|
-
|
|
9
|
+
Any: { type: 'Any'},
|
|
10
|
+
Unknown: { type: 'Unknown'},
|
|
11
|
+
Number: { type: 'Number', value: 'number', format: 'double' },
|
|
10
12
|
Boolean: { type: 'Boolean', value: 'boolean' },
|
|
11
|
-
Symbol: { type: 'Symbol', value: '
|
|
13
|
+
Symbol: { type: 'Symbol', value: 'string' },
|
|
12
14
|
BigInt: { type: 'BigInt', value: 'integer', format: 'int64' },
|
|
13
|
-
null: { type: 'null'
|
|
15
|
+
null: { type: 'null'},
|
|
14
16
|
Object: { type: 'Object', value: 'object' },
|
|
15
17
|
Array: { type: 'Array', value: 'array' },
|
|
16
18
|
Date: { type: 'Date', value: 'string', format: 'date-time' },
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
Uint8Array: { type: 'Uint8Array', value: 'string', format: 'binary' },
|
|
19
|
+
Buffer: { type: 'Buffer', value: 'string'},
|
|
20
|
+
Uint8Array: { type: 'Uint8Array', value: 'string'},
|
|
20
21
|
UploadFile: { type: 'UploadFile', value: 'string', format: 'binary' },
|
|
21
|
-
File: { type: 'File', value: 'string'
|
|
22
|
+
File: { type: 'File', value: 'string'},
|
|
22
23
|
};
|
|
23
24
|
const validatorDecorators = {
|
|
24
|
-
Length: { name: 'Length'
|
|
25
|
-
MinLength: { name: 'MinLength'
|
|
26
|
-
MaxLength: { name: 'MaxLength'
|
|
25
|
+
Length: { name: 'Length'},
|
|
26
|
+
MinLength: { name: 'MinLength'},
|
|
27
|
+
MaxLength: { name: 'MaxLength'},
|
|
27
28
|
IsInt: { name: 'IsInt', type: 'integer', format: 'int32' },
|
|
28
|
-
IsNumber: { name: 'IsNumber', type: 'number'
|
|
29
|
-
IsString: { name: 'IsString', type: 'string'
|
|
30
|
-
IsPositive: { name: 'IsPositive'
|
|
29
|
+
IsNumber: { name: 'IsNumber', type: 'number'},
|
|
30
|
+
IsString: { name: 'IsString', type: 'string'},
|
|
31
|
+
IsPositive: { name: 'IsPositive'},
|
|
31
32
|
IsDate: { name: 'IsDate', type: 'string', format: 'date-time' },
|
|
32
|
-
IsEmail: { name: 'IsEmail',
|
|
33
|
+
IsEmail: { name: 'IsEmail', format: 'email' },
|
|
33
34
|
IsNotEmpty: { name: 'IsNotEmpty' },
|
|
35
|
+
IsOptional: { name: 'IsOptional' },
|
|
34
36
|
IsBoolean: { name: 'IsBoolean', type: 'boolean' },
|
|
35
|
-
IsArray: { name: 'IsArray'
|
|
37
|
+
IsArray: { name: 'IsArray'},
|
|
36
38
|
Min: { name: 'Min' },
|
|
37
39
|
Max: { name: 'Max' },
|
|
38
40
|
ArrayNotEmpty: { name: 'ArrayNotEmpty' },
|
|
39
41
|
ArrayMaxSize: { name: 'ArrayMaxSize' },
|
|
40
42
|
ArrayMinSize: { name: 'ArrayMinSize' },
|
|
41
|
-
IsEnum: { name: 'IsEnum'
|
|
43
|
+
IsEnum: { name: 'IsEnum'},
|
|
42
44
|
};
|
|
43
45
|
const constants = {
|
|
44
46
|
TS_CONFIG_DEFAULT_PATH,
|
|
45
47
|
jsPrimitives,
|
|
46
|
-
validatorDecorators
|
|
47
|
-
};
|
|
48
|
+
validatorDecorators};
|
|
48
49
|
|
|
49
|
-
/**
|
|
50
|
-
* Transforms class-validator decorated classes into OpenAPI schema objects.
|
|
51
|
-
* Analyzes TypeScript source files directly using the TypeScript compiler API.
|
|
52
|
-
* Implemented as a singleton for performance optimization.
|
|
53
|
-
*
|
|
54
|
-
* @example
|
|
55
|
-
* ```typescript
|
|
56
|
-
* const transformer = SchemaTransformer.getInstance();
|
|
57
|
-
* const schema = transformer.transform(User);
|
|
58
|
-
* console.log(schema);
|
|
59
|
-
* ```
|
|
60
|
-
*
|
|
61
|
-
* @public
|
|
62
|
-
*/
|
|
63
50
|
class SchemaTransformer {
|
|
64
|
-
/**
|
|
65
|
-
* Singleton instance
|
|
66
|
-
* @private
|
|
67
|
-
*/
|
|
68
51
|
static instance = null;
|
|
69
|
-
/**
|
|
70
|
-
* TypeScript program instance for analyzing source files.
|
|
71
|
-
* @private
|
|
72
|
-
*/
|
|
73
52
|
program;
|
|
74
|
-
/**
|
|
75
|
-
* TypeScript type checker for resolving types.
|
|
76
|
-
* @private
|
|
77
|
-
*/
|
|
78
53
|
checker;
|
|
79
|
-
/**
|
|
80
|
-
* Cache for storing transformed class schemas to avoid reprocessing.
|
|
81
|
-
* Key format: "fileName:className" for uniqueness across different files.
|
|
82
|
-
* @private
|
|
83
|
-
*/
|
|
84
54
|
classCache = new Map();
|
|
85
|
-
/**
|
|
86
|
-
* Maximum number of entries to keep in cache before cleanup
|
|
87
|
-
* @private
|
|
88
|
-
*/
|
|
89
55
|
maxCacheSize;
|
|
90
|
-
/**
|
|
91
|
-
* Whether to automatically clean up cache
|
|
92
|
-
* @private
|
|
93
|
-
*/
|
|
94
56
|
autoCleanup;
|
|
95
|
-
/**
|
|
96
|
-
* Set of file paths that have been loaded to avoid redundant processing
|
|
97
|
-
* @private
|
|
98
|
-
*/
|
|
99
57
|
loadedFiles = new Set();
|
|
100
|
-
/**
|
|
101
|
-
* Set of class names currently being processed to prevent circular references
|
|
102
|
-
* Key format: "fileName:className" for uniqueness across different files
|
|
103
|
-
* @private
|
|
104
|
-
*/
|
|
105
58
|
processingClasses = new Set();
|
|
106
|
-
/**
|
|
107
|
-
* Private constructor for singleton pattern.
|
|
108
|
-
*
|
|
109
|
-
* @param tsConfigPath - Optional path to a specific TypeScript config file
|
|
110
|
-
* @param options - Configuration options for memory management
|
|
111
|
-
* @throws {Error} When TypeScript configuration cannot be loaded
|
|
112
|
-
* @private
|
|
113
|
-
*/
|
|
114
59
|
constructor(tsConfigPath = constants.TS_CONFIG_DEFAULT_PATH, options = {}) {
|
|
115
|
-
// Initialize configuration with defaults
|
|
116
60
|
this.maxCacheSize = options.maxCacheSize ?? 100;
|
|
117
61
|
this.autoCleanup = options.autoCleanup ?? true;
|
|
118
62
|
const { config, error } = ts.readConfigFile(tsConfigPath || 'tsconfig.json', ts.sys.readFile);
|
|
@@ -124,495 +68,42 @@ class SchemaTransformer {
|
|
|
124
68
|
this.program = ts.createProgram(fileNames, tsOptions);
|
|
125
69
|
this.checker = this.program.getTypeChecker();
|
|
126
70
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
// Force garbage collection hint
|
|
153
|
-
if (global.gc) {
|
|
154
|
-
global.gc();
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
/**
|
|
158
|
-
* Transforms a class by its name into an OpenAPI schema object.
|
|
159
|
-
* Considers the context of the calling file to resolve ambiguous class names.
|
|
160
|
-
* Includes circular reference detection to prevent infinite recursion.
|
|
161
|
-
*
|
|
162
|
-
* @param className - The name of the class to transform
|
|
163
|
-
* @param contextFilePath - Optional path to context file for resolving class ambiguity
|
|
164
|
-
* @returns Object containing the class name and its corresponding JSON schema
|
|
165
|
-
* @throws {Error} When the specified class cannot be found
|
|
166
|
-
* @private
|
|
167
|
-
*/
|
|
168
|
-
transformByName(className, contextFilePath) {
|
|
169
|
-
// Get all relevant source files (not declaration files and not in node_modules)
|
|
170
|
-
const sourceFiles = this.program.getSourceFiles().filter(sf => {
|
|
171
|
-
if (sf.isDeclarationFile)
|
|
172
|
-
return false;
|
|
173
|
-
if (sf.fileName.includes('.d.ts'))
|
|
174
|
-
return false;
|
|
175
|
-
if (sf.fileName.includes('node_modules'))
|
|
176
|
-
return false;
|
|
177
|
-
// Mark file as loaded for memory tracking
|
|
178
|
-
this.loadedFiles.add(sf.fileName);
|
|
179
|
-
return true;
|
|
180
|
-
});
|
|
181
|
-
// If we have a context file, try to find the class in that file first
|
|
182
|
-
if (contextFilePath) {
|
|
183
|
-
const contextSourceFile = this.program.getSourceFile(contextFilePath);
|
|
184
|
-
if (contextSourceFile) {
|
|
185
|
-
const classNode = this.findClassByName(contextSourceFile, className);
|
|
186
|
-
if (classNode) {
|
|
187
|
-
const cacheKey = this.getCacheKey(contextSourceFile.fileName, className);
|
|
188
|
-
// Check cache first
|
|
189
|
-
if (this.classCache.has(cacheKey)) {
|
|
190
|
-
return this.classCache.get(cacheKey);
|
|
191
|
-
}
|
|
192
|
-
// Check for circular reference before processing
|
|
193
|
-
if (this.processingClasses.has(cacheKey)) {
|
|
194
|
-
// Return a $ref reference to break circular dependency (OpenAPI 3.1 style)
|
|
195
|
-
return {
|
|
196
|
-
name: className,
|
|
197
|
-
schema: {
|
|
198
|
-
$ref: `#/components/schemas/${className}`,
|
|
199
|
-
description: `Reference to ${className} (circular reference detected)`,
|
|
200
|
-
},
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
// Mark this class as being processed
|
|
204
|
-
this.processingClasses.add(cacheKey);
|
|
205
|
-
try {
|
|
206
|
-
const result = this.transformClass(classNode, contextSourceFile);
|
|
207
|
-
this.classCache.set(cacheKey, result);
|
|
208
|
-
this.cleanupCache();
|
|
209
|
-
return result;
|
|
210
|
-
}
|
|
211
|
-
finally {
|
|
212
|
-
// Always remove from processing set when done
|
|
213
|
-
this.processingClasses.delete(cacheKey);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
// Fallback to searching all files, but prioritize files that are more likely to be relevant
|
|
219
|
-
const prioritizedFiles = this.prioritizeSourceFiles(sourceFiles, contextFilePath);
|
|
220
|
-
for (const sourceFile of prioritizedFiles) {
|
|
221
|
-
const classNode = this.findClassByName(sourceFile, className);
|
|
222
|
-
if (classNode && sourceFile?.fileName) {
|
|
223
|
-
const cacheKey = this.getCacheKey(sourceFile.fileName, className);
|
|
224
|
-
// Check cache first using fileName:className as key
|
|
225
|
-
if (this.classCache.has(cacheKey)) {
|
|
226
|
-
return this.classCache.get(cacheKey);
|
|
227
|
-
}
|
|
228
|
-
// Check for circular reference before processing
|
|
229
|
-
if (this.processingClasses.has(cacheKey)) {
|
|
230
|
-
// Return a $ref reference to break circular dependency (OpenAPI 3.1 style)
|
|
231
|
-
return {
|
|
232
|
-
name: className,
|
|
233
|
-
schema: {
|
|
234
|
-
$ref: `#/components/schemas/${className}`,
|
|
235
|
-
description: `Reference to ${className} (circular reference detected)`,
|
|
236
|
-
},
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
// Mark this class as being processed
|
|
240
|
-
this.processingClasses.add(cacheKey);
|
|
241
|
-
try {
|
|
242
|
-
const result = this.transformClass(classNode, sourceFile);
|
|
243
|
-
// Cache using fileName:className as key for uniqueness
|
|
244
|
-
this.classCache.set(cacheKey, result);
|
|
245
|
-
// Clean up cache if it gets too large
|
|
246
|
-
this.cleanupCache();
|
|
247
|
-
return result;
|
|
248
|
-
}
|
|
249
|
-
finally {
|
|
250
|
-
// Always remove from processing set when done
|
|
251
|
-
this.processingClasses.delete(cacheKey);
|
|
71
|
+
getPropertiesByClassDeclaration(classNode, visitedDeclarations = new Set()) {
|
|
72
|
+
if (visitedDeclarations.has(classNode)) {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
visitedDeclarations.add(classNode);
|
|
76
|
+
// if no heritage clauses, get properties directly from class
|
|
77
|
+
if (!classNode.heritageClauses) {
|
|
78
|
+
return this.getPropertiesByClassMembers(classNode.members, classNode);
|
|
79
|
+
} // use heritage clauses to get properties from base classes
|
|
80
|
+
else {
|
|
81
|
+
const heritageClause = classNode.heritageClauses[0];
|
|
82
|
+
if (heritageClause &&
|
|
83
|
+
heritageClause.token === ts.SyntaxKind.ExtendsKeyword) {
|
|
84
|
+
const type = heritageClause.types[0];
|
|
85
|
+
let properties = [];
|
|
86
|
+
let baseProperties = [];
|
|
87
|
+
if (!type)
|
|
88
|
+
return [];
|
|
89
|
+
const symbol = this.checker.getSymbolAtLocation(type.expression);
|
|
90
|
+
if (!symbol)
|
|
91
|
+
return [];
|
|
92
|
+
const declaration = symbol.declarations?.[0];
|
|
93
|
+
if (declaration && ts.isClassDeclaration(declaration)) {
|
|
94
|
+
baseProperties = this.getPropertiesByClassDeclaration(declaration, visitedDeclarations);
|
|
252
95
|
}
|
|
96
|
+
properties = this.getPropertiesByClassMembers(classNode.members, classNode);
|
|
97
|
+
return baseProperties.concat(properties);
|
|
253
98
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
257
|
-
/**
|
|
258
|
-
* Prioritizes source files based on context to resolve class name conflicts.
|
|
259
|
-
* Gives priority to files in the same directory or with similar names.
|
|
260
|
-
*
|
|
261
|
-
* @param sourceFiles - Array of source files to prioritize
|
|
262
|
-
* @param contextFilePath - Optional path to context file for prioritization
|
|
263
|
-
* @returns Prioritized array of source files
|
|
264
|
-
* @private
|
|
265
|
-
*/
|
|
266
|
-
prioritizeSourceFiles(sourceFiles, contextFilePath) {
|
|
267
|
-
if (!contextFilePath) {
|
|
268
|
-
return sourceFiles;
|
|
269
|
-
}
|
|
270
|
-
const contextDir = contextFilePath.substring(0, contextFilePath.lastIndexOf('/'));
|
|
271
|
-
return sourceFiles.sort((a, b) => {
|
|
272
|
-
const aDir = a.fileName.substring(0, a.fileName.lastIndexOf('/'));
|
|
273
|
-
const bDir = b.fileName.substring(0, b.fileName.lastIndexOf('/'));
|
|
274
|
-
// Prioritize files in the same directory as context
|
|
275
|
-
const aInSameDir = aDir === contextDir ? 1 : 0;
|
|
276
|
-
const bInSameDir = bDir === contextDir ? 1 : 0;
|
|
277
|
-
if (aInSameDir !== bInSameDir) {
|
|
278
|
-
return bInSameDir - aInSameDir; // Higher priority first
|
|
279
|
-
}
|
|
280
|
-
// Prioritize non-test files over test files
|
|
281
|
-
const aIsTest = a.fileName.includes('test') || a.fileName.includes('spec') ? 0 : 1;
|
|
282
|
-
const bIsTest = b.fileName.includes('test') || b.fileName.includes('spec') ? 0 : 1;
|
|
283
|
-
if (aIsTest !== bIsTest) {
|
|
284
|
-
return bIsTest - aIsTest; // Non-test files first
|
|
285
|
-
}
|
|
286
|
-
return 0;
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
/**
|
|
290
|
-
* Gets the singleton instance of SchemaTransformer.
|
|
291
|
-
*
|
|
292
|
-
* @param tsConfigPath - Optional path to a specific TypeScript config file (only used on first call)
|
|
293
|
-
* @param options - Configuration options for memory management (only used on first call)
|
|
294
|
-
* @returns The singleton instance
|
|
295
|
-
*
|
|
296
|
-
* @example
|
|
297
|
-
* ```typescript
|
|
298
|
-
* const transformer = SchemaTransformer.getInstance();
|
|
299
|
-
* ```
|
|
300
|
-
*
|
|
301
|
-
* @example
|
|
302
|
-
* ```typescript
|
|
303
|
-
* // With memory optimization options
|
|
304
|
-
* const transformer = SchemaTransformer.getInstance('./tsconfig.json', {
|
|
305
|
-
* maxCacheSize: 50,
|
|
306
|
-
* autoCleanup: true
|
|
307
|
-
* });
|
|
308
|
-
* ```
|
|
309
|
-
*
|
|
310
|
-
* @public
|
|
311
|
-
*/
|
|
312
|
-
/**
|
|
313
|
-
* Clears the current singleton instance. Useful for testing or when you need
|
|
314
|
-
* to create a new instance with different configuration.
|
|
315
|
-
* @private
|
|
316
|
-
*/
|
|
317
|
-
static clearInstance() {
|
|
318
|
-
SchemaTransformer.instance = undefined;
|
|
319
|
-
}
|
|
320
|
-
/**
|
|
321
|
-
* Flag to prevent recursive disposal calls
|
|
322
|
-
* @private
|
|
323
|
-
*/
|
|
324
|
-
static disposingInProgress = false;
|
|
325
|
-
/**
|
|
326
|
-
* Completely disposes of the current singleton instance and releases all resources.
|
|
327
|
-
* This is a static method that can be called without having an instance reference.
|
|
328
|
-
* Ensures complete memory cleanup regardless of the current state.
|
|
329
|
-
*
|
|
330
|
-
* @example
|
|
331
|
-
* ```typescript
|
|
332
|
-
* SchemaTransformer.disposeInstance();
|
|
333
|
-
* // All resources released, next getInstance() will create fresh instance
|
|
334
|
-
* ```
|
|
335
|
-
*
|
|
336
|
-
* @public
|
|
337
|
-
*/
|
|
338
|
-
static disposeInstance() {
|
|
339
|
-
// Prevent recursive disposal calls
|
|
340
|
-
if (SchemaTransformer.disposingInProgress) {
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
|
-
SchemaTransformer.disposingInProgress = true;
|
|
344
|
-
try {
|
|
345
|
-
if (SchemaTransformer.instance) {
|
|
346
|
-
SchemaTransformer.instance.dispose();
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
catch (error) {
|
|
350
|
-
// Log any disposal errors but continue with cleanup
|
|
351
|
-
console.warn('Warning during static disposal:', error);
|
|
352
|
-
}
|
|
353
|
-
finally {
|
|
354
|
-
// Always ensure the static instance is cleared
|
|
355
|
-
SchemaTransformer.instance = undefined;
|
|
356
|
-
SchemaTransformer.disposingInProgress = false;
|
|
357
|
-
// Force garbage collection for cleanup
|
|
358
|
-
if (global.gc) {
|
|
359
|
-
global.gc();
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
/**
|
|
364
|
-
* @deprecated Use disposeInstance() instead for better clarity
|
|
365
|
-
* @private
|
|
366
|
-
*/
|
|
367
|
-
static dispose() {
|
|
368
|
-
SchemaTransformer.disposeInstance();
|
|
369
|
-
}
|
|
370
|
-
static getInstance(tsConfigPath, options) {
|
|
371
|
-
if (!SchemaTransformer.instance || SchemaTransformer.isInstanceDisposed()) {
|
|
372
|
-
SchemaTransformer.instance = new SchemaTransformer(tsConfigPath, options);
|
|
373
|
-
}
|
|
374
|
-
return SchemaTransformer.instance;
|
|
375
|
-
}
|
|
376
|
-
/**
|
|
377
|
-
* Internal method to check if current instance is disposed
|
|
378
|
-
* @private
|
|
379
|
-
*/
|
|
380
|
-
static isInstanceDisposed() {
|
|
381
|
-
return SchemaTransformer.instance
|
|
382
|
-
? SchemaTransformer.instance.isDisposed()
|
|
383
|
-
: true;
|
|
384
|
-
}
|
|
385
|
-
/**
|
|
386
|
-
* Transforms a class using the singleton instance
|
|
387
|
-
* @param cls - The class constructor function to transform
|
|
388
|
-
* @param options - Optional configuration for memory management (only used if no instance exists)
|
|
389
|
-
* @returns Object containing the class name and its corresponding JSON schema
|
|
390
|
-
* @public
|
|
391
|
-
*/
|
|
392
|
-
static transformClass(cls, options) {
|
|
393
|
-
// Use the singleton instance instead of creating a temporary one
|
|
394
|
-
const transformer = SchemaTransformer.getInstance(undefined, options);
|
|
395
|
-
return transformer.transform(cls);
|
|
396
|
-
}
|
|
397
|
-
/**
|
|
398
|
-
* Transforms a class constructor function into an OpenAPI schema object.
|
|
399
|
-
*
|
|
400
|
-
* @param cls - The class constructor function to transform
|
|
401
|
-
* @returns Object containing the class name and its corresponding JSON schema
|
|
402
|
-
*
|
|
403
|
-
* @example
|
|
404
|
-
* ```typescript
|
|
405
|
-
* import { User } from './entities/user.js';
|
|
406
|
-
* const transformer = SchemaTransformer.getInstance();
|
|
407
|
-
* const schema = transformer.transform(User);
|
|
408
|
-
* ```
|
|
409
|
-
*
|
|
410
|
-
* @public
|
|
411
|
-
*/
|
|
412
|
-
transform(cls) {
|
|
413
|
-
return this.transformByName(cls.name);
|
|
414
|
-
}
|
|
415
|
-
/**
|
|
416
|
-
* Clears all cached schemas and loaded file references to free memory.
|
|
417
|
-
* Useful for long-running applications or when processing many different classes.
|
|
418
|
-
*
|
|
419
|
-
* @example
|
|
420
|
-
* ```typescript
|
|
421
|
-
* const transformer = SchemaTransformer.getInstance();
|
|
422
|
-
* // After processing many classes...
|
|
423
|
-
* transformer.clearCache();
|
|
424
|
-
* ```
|
|
425
|
-
*
|
|
426
|
-
* @public
|
|
427
|
-
*/
|
|
428
|
-
clearCache() {
|
|
429
|
-
this.classCache.clear();
|
|
430
|
-
this.loadedFiles.clear();
|
|
431
|
-
this.processingClasses.clear();
|
|
432
|
-
// Force garbage collection hint if available
|
|
433
|
-
if (global.gc) {
|
|
434
|
-
global.gc();
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
/**
|
|
438
|
-
* Completely disposes of the transformer instance and releases all resources.
|
|
439
|
-
* This includes clearing all caches, releasing TypeScript program resources,
|
|
440
|
-
* and resetting the singleton instance.
|
|
441
|
-
*
|
|
442
|
-
* After calling this method, you need to call getInstance() again to get a new instance.
|
|
443
|
-
*
|
|
444
|
-
* @example
|
|
445
|
-
* ```typescript
|
|
446
|
-
* const transformer = SchemaTransformer.getInstance();
|
|
447
|
-
* // ... use transformer
|
|
448
|
-
* transformer.dispose();
|
|
449
|
-
* // transformer is now unusable, need to get new instance
|
|
450
|
-
* const newTransformer = SchemaTransformer.getInstance();
|
|
451
|
-
* ```
|
|
452
|
-
*
|
|
453
|
-
* @private
|
|
454
|
-
*/
|
|
455
|
-
dispose() {
|
|
456
|
-
try {
|
|
457
|
-
// Clear all caches and sets completely
|
|
458
|
-
this.classCache.clear();
|
|
459
|
-
this.loadedFiles.clear();
|
|
460
|
-
this.processingClasses.clear();
|
|
461
|
-
// Release TypeScript program resources
|
|
462
|
-
// While TypeScript doesn't provide explicit disposal methods,
|
|
463
|
-
// we can help garbage collection by clearing all references
|
|
464
|
-
// Clear all references to TypeScript objects
|
|
465
|
-
// @ts-ignore - We're intentionally setting these to null for cleanup
|
|
466
|
-
this.program = null;
|
|
467
|
-
// @ts-ignore - We're intentionally setting these to null for cleanup
|
|
468
|
-
this.checker = null;
|
|
469
|
-
}
|
|
470
|
-
catch (error) {
|
|
471
|
-
// If there's any error during disposal, log it but continue
|
|
472
|
-
console.warn('Warning during transformer disposal:', error);
|
|
473
|
-
}
|
|
474
|
-
finally {
|
|
475
|
-
// Force garbage collection for cleanup
|
|
476
|
-
if (global.gc) {
|
|
477
|
-
global.gc();
|
|
99
|
+
else {
|
|
100
|
+
return this.getPropertiesByClassMembers(classNode.members, classNode);
|
|
478
101
|
}
|
|
479
102
|
}
|
|
480
103
|
}
|
|
481
|
-
|
|
482
|
-
* Completely resets the transformer by disposing current instance and creating a new one.
|
|
483
|
-
* This is useful when you need a fresh start with different TypeScript configuration
|
|
484
|
-
* or want to ensure all resources are properly released and recreated.
|
|
485
|
-
*
|
|
486
|
-
* @param tsConfigPath - Optional path to a specific TypeScript config file for the new instance
|
|
487
|
-
* @param options - Configuration options for memory management for the new instance
|
|
488
|
-
* @returns A fresh SchemaTransformer instance
|
|
489
|
-
*
|
|
490
|
-
* @example
|
|
491
|
-
* ```typescript
|
|
492
|
-
* const transformer = SchemaTransformer.getInstance();
|
|
493
|
-
* // ... use transformer
|
|
494
|
-
* const freshTransformer = transformer.reset('./new-tsconfig.json');
|
|
495
|
-
* ```
|
|
496
|
-
*
|
|
497
|
-
* @private
|
|
498
|
-
*/
|
|
499
|
-
reset(tsConfigPath, options) {
|
|
500
|
-
// Dispose current instance using static method to properly clear instance reference
|
|
501
|
-
SchemaTransformer.disposeInstance();
|
|
502
|
-
// Create and return new instance
|
|
503
|
-
return SchemaTransformer.getInstance(tsConfigPath, options);
|
|
504
|
-
}
|
|
505
|
-
/**
|
|
506
|
-
* Gets memory usage statistics for monitoring and debugging.
|
|
507
|
-
*
|
|
508
|
-
* @returns Object containing cache size, loaded files count, and processing status
|
|
509
|
-
*
|
|
510
|
-
* @example
|
|
511
|
-
* ```typescript
|
|
512
|
-
* const transformer = SchemaTransformer.getInstance();
|
|
513
|
-
* const stats = transformer.getMemoryStats();
|
|
514
|
-
* console.log(`Cache entries: ${stats.cacheSize}, Files loaded: ${stats.loadedFiles}`);
|
|
515
|
-
* console.log(`Currently processing: ${stats.currentlyProcessing} classes`);
|
|
516
|
-
* ```
|
|
517
|
-
*
|
|
518
|
-
* @private
|
|
519
|
-
*/
|
|
520
|
-
getMemoryStats() {
|
|
521
|
-
return {
|
|
522
|
-
cacheSize: this.classCache?.size || 0,
|
|
523
|
-
loadedFiles: this.loadedFiles?.size || 0,
|
|
524
|
-
currentlyProcessing: this.processingClasses?.size || 0,
|
|
525
|
-
maxCacheSize: this.maxCacheSize || 0,
|
|
526
|
-
autoCleanup: this.autoCleanup || false,
|
|
527
|
-
isDisposed: !this.program || !this.checker,
|
|
528
|
-
};
|
|
529
|
-
}
|
|
530
|
-
/**
|
|
531
|
-
* Checks if the transformer instance has been disposed and is no longer usable.
|
|
532
|
-
*
|
|
533
|
-
* @returns True if the instance has been disposed
|
|
534
|
-
*
|
|
535
|
-
* @example
|
|
536
|
-
* ```typescript
|
|
537
|
-
* const transformer = SchemaTransformer.getInstance();
|
|
538
|
-
* transformer.dispose();
|
|
539
|
-
* console.log(transformer.isDisposed()); // true
|
|
540
|
-
* ```
|
|
541
|
-
*
|
|
542
|
-
* @private
|
|
543
|
-
*/
|
|
544
|
-
isDisposed() {
|
|
545
|
-
return (!this.program ||
|
|
546
|
-
!this.checker ||
|
|
547
|
-
!this.classCache ||
|
|
548
|
-
!this.loadedFiles ||
|
|
549
|
-
!this.processingClasses);
|
|
550
|
-
}
|
|
551
|
-
/**
|
|
552
|
-
* Static method to check if there's an active singleton instance.
|
|
553
|
-
*
|
|
554
|
-
* @returns True if there's an active instance, false if disposed or never created
|
|
555
|
-
*
|
|
556
|
-
* @example
|
|
557
|
-
* ```typescript
|
|
558
|
-
* console.log(SchemaTransformer.hasActiveInstance()); // false
|
|
559
|
-
* const transformer = SchemaTransformer.getInstance();
|
|
560
|
-
* console.log(SchemaTransformer.hasActiveInstance()); // true
|
|
561
|
-
* SchemaTransformer.dispose();
|
|
562
|
-
* console.log(SchemaTransformer.hasActiveInstance()); // false
|
|
563
|
-
* ```
|
|
564
|
-
*
|
|
565
|
-
* @private
|
|
566
|
-
*/
|
|
567
|
-
static hasActiveInstance() {
|
|
568
|
-
return (SchemaTransformer.instance !== null &&
|
|
569
|
-
SchemaTransformer.instance !== undefined &&
|
|
570
|
-
!SchemaTransformer.isInstanceDisposed());
|
|
571
|
-
}
|
|
572
|
-
/**
|
|
573
|
-
* Finds a class declaration by name within a source file.
|
|
574
|
-
*
|
|
575
|
-
* @param sourceFile - The TypeScript source file to search in
|
|
576
|
-
* @param className - The name of the class to find
|
|
577
|
-
* @returns The class declaration node if found, undefined otherwise
|
|
578
|
-
* @private
|
|
579
|
-
*/
|
|
580
|
-
findClassByName(sourceFile, className) {
|
|
581
|
-
let result;
|
|
582
|
-
const visit = (node) => {
|
|
583
|
-
if (ts.isClassDeclaration(node) && node.name?.text === className) {
|
|
584
|
-
result = node;
|
|
585
|
-
return;
|
|
586
|
-
}
|
|
587
|
-
ts.forEachChild(node, visit);
|
|
588
|
-
};
|
|
589
|
-
visit(sourceFile);
|
|
590
|
-
return result;
|
|
591
|
-
}
|
|
592
|
-
/**
|
|
593
|
-
* Transforms a TypeScript class declaration into a schema object.
|
|
594
|
-
*
|
|
595
|
-
* @param classNode - The TypeScript class declaration node
|
|
596
|
-
* @param sourceFile - The source file containing the class (for context)
|
|
597
|
-
* @returns Object containing class name and generated schema
|
|
598
|
-
* @private
|
|
599
|
-
*/
|
|
600
|
-
transformClass(classNode, sourceFile) {
|
|
601
|
-
const className = classNode.name?.text || 'Unknown';
|
|
602
|
-
const properties = this.extractProperties(classNode);
|
|
603
|
-
const schema = this.generateSchema(properties, sourceFile?.fileName);
|
|
604
|
-
return { name: className, schema };
|
|
605
|
-
}
|
|
606
|
-
/**
|
|
607
|
-
* Extracts property information from a class declaration.
|
|
608
|
-
*
|
|
609
|
-
* @param classNode - The TypeScript class declaration node
|
|
610
|
-
* @returns Array of property information including names, types, decorators, and optional status
|
|
611
|
-
* @private
|
|
612
|
-
*/
|
|
613
|
-
extractProperties(classNode) {
|
|
104
|
+
getPropertiesByClassMembers(members, parentClassNode) {
|
|
614
105
|
const properties = [];
|
|
615
|
-
for (const member of
|
|
106
|
+
for (const member of members) {
|
|
616
107
|
if (ts.isPropertyDeclaration(member) &&
|
|
617
108
|
member.name &&
|
|
618
109
|
ts.isIdentifier(member.name)) {
|
|
@@ -620,272 +111,53 @@ class SchemaTransformer {
|
|
|
620
111
|
const type = this.getPropertyType(member);
|
|
621
112
|
const decorators = this.extractDecorators(member);
|
|
622
113
|
const isOptional = !!member.questionToken;
|
|
623
|
-
|
|
114
|
+
const isGeneric = this.isPropertyTypeGeneric(member);
|
|
115
|
+
const isPrimitive = this.isPrimitiveType(type);
|
|
116
|
+
const isClassType = this.isClassType(member);
|
|
117
|
+
const isArray = this.isArrayProperty(member);
|
|
118
|
+
const property = {
|
|
624
119
|
name: propertyName,
|
|
625
120
|
type,
|
|
626
121
|
decorators,
|
|
627
122
|
isOptional,
|
|
628
|
-
|
|
123
|
+
isGeneric,
|
|
124
|
+
originalProperty: member,
|
|
125
|
+
isPrimitive,
|
|
126
|
+
isClassType,
|
|
127
|
+
isArray,
|
|
128
|
+
isRef: false,
|
|
129
|
+
};
|
|
130
|
+
// Check for self-referencing properties to mark as $ref
|
|
131
|
+
if (property.isClassType) {
|
|
132
|
+
const declaration = this.getDeclarationProperty(property);
|
|
133
|
+
if (parentClassNode) {
|
|
134
|
+
if (declaration &&
|
|
135
|
+
declaration.name &&
|
|
136
|
+
this.checker.getSymbolAtLocation(declaration.name) ===
|
|
137
|
+
this.checker.getSymbolAtLocation(parentClassNode.name)) {
|
|
138
|
+
property.isRef = true;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
debugger;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
properties.push(property);
|
|
629
146
|
}
|
|
630
147
|
}
|
|
631
148
|
return properties;
|
|
632
149
|
}
|
|
633
|
-
/**
|
|
634
|
-
* Gets the TypeScript type of a property as a string.
|
|
635
|
-
*
|
|
636
|
-
* @param property - The property declaration to analyze
|
|
637
|
-
* @returns String representation of the property's type
|
|
638
|
-
* @private
|
|
639
|
-
*/
|
|
640
150
|
getPropertyType(property) {
|
|
641
151
|
if (property.type) {
|
|
642
152
|
return this.getTypeNodeToString(property.type);
|
|
643
153
|
}
|
|
644
154
|
const type = this.checker.getTypeAtLocation(property);
|
|
645
|
-
return this.
|
|
646
|
-
}
|
|
647
|
-
/**
|
|
648
|
-
* Resolves generic types by analyzing the type alias and its arguments.
|
|
649
|
-
* For example, User<Role> where User is a type alias will be resolved to its structure.
|
|
650
|
-
*
|
|
651
|
-
* @param typeNode - The TypeScript type reference node with generic arguments
|
|
652
|
-
* @returns String representation of the resolved type or schema
|
|
653
|
-
* @private
|
|
654
|
-
*/
|
|
655
|
-
resolveGenericType(typeNode) {
|
|
656
|
-
const typeName = typeNode.typeName.text;
|
|
657
|
-
const typeArguments = typeNode.typeArguments;
|
|
658
|
-
if (!typeArguments || typeArguments.length === 0) {
|
|
659
|
-
return typeName;
|
|
660
|
-
}
|
|
661
|
-
// Try to resolve the type using the TypeScript type checker
|
|
662
|
-
const type = this.checker.getTypeAtLocation(typeNode);
|
|
663
|
-
const resolvedType = this.checker.typeToString(type);
|
|
664
|
-
// If we can resolve it to a meaningful structure, use that
|
|
665
|
-
if (resolvedType &&
|
|
666
|
-
resolvedType !== typeName &&
|
|
667
|
-
!resolvedType.includes('any')) {
|
|
668
|
-
// For type aliases like User<Role>, we want to create a synthetic type name
|
|
669
|
-
// that represents the resolved structure
|
|
670
|
-
const typeArgNames = typeArguments.map(arg => {
|
|
671
|
-
if (ts.isTypeReferenceNode(arg) && ts.isIdentifier(arg.typeName)) {
|
|
672
|
-
return arg.typeName.text;
|
|
673
|
-
}
|
|
674
|
-
return this.getTypeNodeToString(arg);
|
|
675
|
-
});
|
|
676
|
-
return `${typeName}_${typeArgNames.join('_')}`;
|
|
677
|
-
}
|
|
678
|
-
return typeName;
|
|
679
|
-
}
|
|
680
|
-
/**
|
|
681
|
-
* Checks if a type string represents a resolved generic type.
|
|
682
|
-
*
|
|
683
|
-
* @param type - The type string to check
|
|
684
|
-
* @returns True if it's a resolved generic type
|
|
685
|
-
* @private
|
|
686
|
-
*/
|
|
687
|
-
isResolvedGenericType(type) {
|
|
688
|
-
// Simple heuristic: resolved generic types contain underscores and
|
|
689
|
-
// the parts after underscore should be known types
|
|
690
|
-
const parts = type.split('_');
|
|
691
|
-
return (parts.length > 1 &&
|
|
692
|
-
parts
|
|
693
|
-
.slice(1)
|
|
694
|
-
.every(part => this.isKnownType(part) || this.isPrimitiveType(part)));
|
|
695
|
-
}
|
|
696
|
-
/**
|
|
697
|
-
* Checks if a type is a known class or interface.
|
|
698
|
-
*
|
|
699
|
-
* @param typeName - The type name to check
|
|
700
|
-
* @returns True if it's a known type
|
|
701
|
-
* @private
|
|
702
|
-
*/
|
|
703
|
-
isKnownType(typeName) {
|
|
704
|
-
// First check if it's a primitive type to avoid unnecessary lookups
|
|
705
|
-
if (this.isPrimitiveType(typeName)) {
|
|
706
|
-
return true;
|
|
707
|
-
}
|
|
708
|
-
try {
|
|
709
|
-
// Use a more conservative approach - check if we can find the class
|
|
710
|
-
// without actually transforming it to avoid side effects
|
|
711
|
-
const found = this.findClassInProject(typeName);
|
|
712
|
-
return found !== null;
|
|
713
|
-
}
|
|
714
|
-
catch {
|
|
715
|
-
return false;
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
/**
|
|
719
|
-
* Finds a class by name in the project without transforming it.
|
|
720
|
-
*
|
|
721
|
-
* @param className - The class name to find
|
|
722
|
-
* @returns True if found, false otherwise
|
|
723
|
-
* @private
|
|
724
|
-
*/
|
|
725
|
-
findClassInProject(className) {
|
|
726
|
-
const sourceFiles = this.program.getSourceFiles().filter(sf => {
|
|
727
|
-
if (sf.isDeclarationFile)
|
|
728
|
-
return false;
|
|
729
|
-
if (sf.fileName.includes('.d.ts'))
|
|
730
|
-
return false;
|
|
731
|
-
if (sf.fileName.includes('node_modules'))
|
|
732
|
-
return false;
|
|
733
|
-
return true;
|
|
734
|
-
});
|
|
735
|
-
for (const sourceFile of sourceFiles) {
|
|
736
|
-
const found = this.findClassByName(sourceFile, className);
|
|
737
|
-
if (found)
|
|
738
|
-
return true;
|
|
739
|
-
}
|
|
740
|
-
return false;
|
|
155
|
+
return this.getStringFromType(type);
|
|
741
156
|
}
|
|
742
|
-
/**
|
|
743
|
-
* Checks if a type is a primitive type.
|
|
744
|
-
*
|
|
745
|
-
* @param typeName - The type name to check
|
|
746
|
-
* @returns True if it's a primitive type
|
|
747
|
-
* @private
|
|
748
|
-
*/
|
|
749
|
-
isPrimitiveType(typeName) {
|
|
750
|
-
const lowerTypeName = typeName.toLowerCase();
|
|
751
|
-
// Check against all primitive types from constants
|
|
752
|
-
const primitiveTypes = [
|
|
753
|
-
constants.jsPrimitives.String.type.toLowerCase(),
|
|
754
|
-
constants.jsPrimitives.Number.type.toLowerCase(),
|
|
755
|
-
constants.jsPrimitives.Boolean.type.toLowerCase(),
|
|
756
|
-
constants.jsPrimitives.Date.type.toLowerCase(),
|
|
757
|
-
constants.jsPrimitives.Buffer.type.toLowerCase(),
|
|
758
|
-
constants.jsPrimitives.Uint8Array.type.toLowerCase(),
|
|
759
|
-
constants.jsPrimitives.File.type.toLowerCase(),
|
|
760
|
-
constants.jsPrimitives.UploadFile.type.toLowerCase(),
|
|
761
|
-
constants.jsPrimitives.BigInt.type.toLowerCase(),
|
|
762
|
-
];
|
|
763
|
-
return primitiveTypes.includes(lowerTypeName);
|
|
764
|
-
}
|
|
765
|
-
/**
|
|
766
|
-
* Resolves a generic type schema by analyzing the type alias structure.
|
|
767
|
-
*
|
|
768
|
-
* @param resolvedTypeName - The resolved generic type name (e.g., User_Role)
|
|
769
|
-
* @returns OpenAPI schema for the resolved generic type
|
|
770
|
-
* @private
|
|
771
|
-
*/
|
|
772
|
-
resolveGenericTypeSchema(resolvedTypeName) {
|
|
773
|
-
const parts = resolvedTypeName.split('_');
|
|
774
|
-
const baseTypeName = parts[0];
|
|
775
|
-
const typeArgNames = parts.slice(1);
|
|
776
|
-
if (!baseTypeName) {
|
|
777
|
-
return null;
|
|
778
|
-
}
|
|
779
|
-
// Find the original type alias declaration
|
|
780
|
-
const typeAliasSymbol = this.findTypeAliasDeclaration(baseTypeName);
|
|
781
|
-
if (!typeAliasSymbol) {
|
|
782
|
-
return null;
|
|
783
|
-
}
|
|
784
|
-
// Create a schema based on the type alias structure, substituting type parameters
|
|
785
|
-
return this.createSchemaFromTypeAlias(typeAliasSymbol, typeArgNames);
|
|
786
|
-
}
|
|
787
|
-
/**
|
|
788
|
-
* Finds a type alias declaration by name.
|
|
789
|
-
*
|
|
790
|
-
* @param typeName - The type alias name to find
|
|
791
|
-
* @returns The type alias declaration node or null
|
|
792
|
-
* @private
|
|
793
|
-
*/
|
|
794
|
-
findTypeAliasDeclaration(typeName) {
|
|
795
|
-
for (const sourceFile of this.program.getSourceFiles()) {
|
|
796
|
-
if (sourceFile.isDeclarationFile)
|
|
797
|
-
continue;
|
|
798
|
-
const findTypeAlias = (node) => {
|
|
799
|
-
if (ts.isTypeAliasDeclaration(node) && node.name.text === typeName) {
|
|
800
|
-
return node;
|
|
801
|
-
}
|
|
802
|
-
return ts.forEachChild(node, findTypeAlias) || null;
|
|
803
|
-
};
|
|
804
|
-
const result = findTypeAlias(sourceFile);
|
|
805
|
-
if (result)
|
|
806
|
-
return result;
|
|
807
|
-
}
|
|
808
|
-
return null;
|
|
809
|
-
}
|
|
810
|
-
/**
|
|
811
|
-
* Creates a schema from a type alias declaration, substituting type parameters.
|
|
812
|
-
*
|
|
813
|
-
* @param typeAlias - The type alias declaration
|
|
814
|
-
* @param typeArgNames - The concrete type arguments
|
|
815
|
-
* @returns OpenAPI schema for the type alias
|
|
816
|
-
* @private
|
|
817
|
-
*/
|
|
818
|
-
createSchemaFromTypeAlias(typeAlias, typeArgNames) {
|
|
819
|
-
const typeNode = typeAlias.type;
|
|
820
|
-
if (ts.isTypeLiteralNode(typeNode)) {
|
|
821
|
-
const schema = {
|
|
822
|
-
type: 'object',
|
|
823
|
-
properties: {},
|
|
824
|
-
required: [],
|
|
825
|
-
};
|
|
826
|
-
for (const member of typeNode.members) {
|
|
827
|
-
if (ts.isPropertySignature(member) &&
|
|
828
|
-
member.name &&
|
|
829
|
-
ts.isIdentifier(member.name)) {
|
|
830
|
-
const propertyName = member.name.text;
|
|
831
|
-
const isOptional = !!member.questionToken;
|
|
832
|
-
if (member.type) {
|
|
833
|
-
const propertyType = this.resolveTypeParameterInTypeAlias(member.type, typeAlias.typeParameters, typeArgNames);
|
|
834
|
-
const { type, format, nestedSchema } = this.mapTypeToSchema(propertyType);
|
|
835
|
-
if (nestedSchema) {
|
|
836
|
-
schema.properties[propertyName] = nestedSchema;
|
|
837
|
-
}
|
|
838
|
-
else {
|
|
839
|
-
schema.properties[propertyName] = { type };
|
|
840
|
-
if (format)
|
|
841
|
-
schema.properties[propertyName].format = format;
|
|
842
|
-
}
|
|
843
|
-
if (!isOptional) {
|
|
844
|
-
schema.required.push(propertyName);
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
return schema;
|
|
850
|
-
}
|
|
851
|
-
return null;
|
|
852
|
-
}
|
|
853
|
-
/**
|
|
854
|
-
* Resolves type parameters in a type alias to concrete types.
|
|
855
|
-
*
|
|
856
|
-
* @param typeNode - The type node to resolve
|
|
857
|
-
* @param typeParameters - The type parameters of the type alias
|
|
858
|
-
* @param typeArgNames - The concrete type arguments
|
|
859
|
-
* @returns The resolved type string
|
|
860
|
-
* @private
|
|
861
|
-
*/
|
|
862
|
-
resolveTypeParameterInTypeAlias(typeNode, typeParameters, typeArgNames) {
|
|
863
|
-
if (ts.isTypeReferenceNode(typeNode) &&
|
|
864
|
-
ts.isIdentifier(typeNode.typeName)) {
|
|
865
|
-
const typeName = typeNode.typeName.text;
|
|
866
|
-
// Check if this is a type parameter
|
|
867
|
-
if (typeParameters) {
|
|
868
|
-
const paramIndex = typeParameters.findIndex(param => param.name.text === typeName);
|
|
869
|
-
if (paramIndex !== -1 && paramIndex < typeArgNames.length) {
|
|
870
|
-
const resolvedType = typeArgNames[paramIndex];
|
|
871
|
-
return resolvedType || typeName;
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
return typeName;
|
|
875
|
-
}
|
|
876
|
-
return this.getTypeNodeToString(typeNode);
|
|
877
|
-
}
|
|
878
|
-
/**
|
|
879
|
-
* Converts a TypeScript type node to its string representation.
|
|
880
|
-
*
|
|
881
|
-
* @param typeNode - The TypeScript type node to convert
|
|
882
|
-
* @returns String representation of the type
|
|
883
|
-
* @private
|
|
884
|
-
*/
|
|
885
157
|
getTypeNodeToString(typeNode) {
|
|
886
158
|
if (ts.isTypeReferenceNode(typeNode) &&
|
|
887
159
|
ts.isIdentifier(typeNode.typeName)) {
|
|
888
|
-
if (typeNode.typeName.text.toLowerCase()
|
|
160
|
+
if (typeNode.typeName.text.toLowerCase() === 'uploadfile') {
|
|
889
161
|
return 'UploadFile';
|
|
890
162
|
}
|
|
891
163
|
if (typeNode.typeArguments && typeNode.typeArguments.length > 0) {
|
|
@@ -893,12 +165,9 @@ class SchemaTransformer {
|
|
|
893
165
|
if (firstTypeArg &&
|
|
894
166
|
ts.isTypeReferenceNode(firstTypeArg) &&
|
|
895
167
|
ts.isIdentifier(firstTypeArg.typeName)) {
|
|
896
|
-
if (firstTypeArg.typeName.text.toLowerCase()
|
|
168
|
+
if (firstTypeArg.typeName.text.toLowerCase() === 'uploadfile') {
|
|
897
169
|
return 'UploadFile';
|
|
898
170
|
}
|
|
899
|
-
if (typeNode.typeName.text === 'BaseDto') {
|
|
900
|
-
return firstTypeArg.typeName.text;
|
|
901
|
-
}
|
|
902
171
|
}
|
|
903
172
|
return this.resolveGenericType(typeNode);
|
|
904
173
|
}
|
|
@@ -937,13 +206,24 @@ class SchemaTransformer {
|
|
|
937
206
|
return typeText;
|
|
938
207
|
}
|
|
939
208
|
}
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
209
|
+
resolveGenericType(typeNode) {
|
|
210
|
+
const typeName = typeNode.typeName.text;
|
|
211
|
+
const typeArguments = typeNode.typeArguments;
|
|
212
|
+
if (!typeArguments || typeArguments.length === 0) {
|
|
213
|
+
return typeName;
|
|
214
|
+
}
|
|
215
|
+
const type = this.checker.getTypeAtLocation(typeNode);
|
|
216
|
+
const resolvedType = this.getStringFromType(type);
|
|
217
|
+
if (resolvedType &&
|
|
218
|
+
resolvedType !== typeName &&
|
|
219
|
+
!resolvedType.includes('any')) {
|
|
220
|
+
return resolvedType;
|
|
221
|
+
}
|
|
222
|
+
return typeName;
|
|
223
|
+
}
|
|
224
|
+
getStringFromType(type) {
|
|
225
|
+
return this.checker.typeToString(type);
|
|
226
|
+
}
|
|
947
227
|
extractDecorators(member) {
|
|
948
228
|
const decorators = [];
|
|
949
229
|
if (member.modifiers) {
|
|
@@ -962,26 +242,12 @@ class SchemaTransformer {
|
|
|
962
242
|
}
|
|
963
243
|
return decorators;
|
|
964
244
|
}
|
|
965
|
-
/**
|
|
966
|
-
* Gets the name of a decorator from a call expression.
|
|
967
|
-
*
|
|
968
|
-
* @param callExpression - The decorator call expression
|
|
969
|
-
* @returns The decorator name or "unknown" if not identifiable
|
|
970
|
-
* @private
|
|
971
|
-
*/
|
|
972
245
|
getDecoratorName(callExpression) {
|
|
973
246
|
if (ts.isIdentifier(callExpression.expression)) {
|
|
974
247
|
return callExpression.expression.text;
|
|
975
248
|
}
|
|
976
249
|
return 'unknown';
|
|
977
250
|
}
|
|
978
|
-
/**
|
|
979
|
-
* Extracts arguments from a decorator call expression.
|
|
980
|
-
*
|
|
981
|
-
* @param callExpression - The decorator call expression
|
|
982
|
-
* @returns Array of parsed decorator arguments
|
|
983
|
-
* @private
|
|
984
|
-
*/
|
|
985
251
|
getDecoratorArguments(callExpression) {
|
|
986
252
|
return callExpression.arguments.map(arg => {
|
|
987
253
|
if (ts.isNumericLiteral(arg))
|
|
@@ -995,566 +261,526 @@ class SchemaTransformer {
|
|
|
995
261
|
return arg.getText();
|
|
996
262
|
});
|
|
997
263
|
}
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
264
|
+
isPropertyTypeGeneric(property) {
|
|
265
|
+
if (property.type && this.isGenericTypeFromNode(property.type)) {
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
const type = this.checker.getTypeAtLocation(property);
|
|
270
|
+
return this.isGenericTypeFromSymbol(type);
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
console.warn('Error analyzing property type for generics:', error);
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
isGenericTypeFromNode(typeNode) {
|
|
278
|
+
if (ts.isTypeReferenceNode(typeNode) && typeNode.typeArguments) {
|
|
279
|
+
return typeNode.typeArguments.length > 0;
|
|
280
|
+
}
|
|
281
|
+
// Check for mapped types (e.g., { [K in keyof T]: T[K] })
|
|
282
|
+
if (ts.isMappedTypeNode(typeNode)) {
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
// Check for conditional types (e.g., T extends U ? X : Y)
|
|
286
|
+
if (ts.isConditionalTypeNode(typeNode)) {
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
// Check for indexed access types (e.g., T[K])
|
|
290
|
+
if (ts.isIndexedAccessTypeNode(typeNode)) {
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
// Check for type operators like keyof, typeof
|
|
294
|
+
if (ts.isTypeOperatorNode(typeNode)) {
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
isGenericTypeFromSymbol(type) {
|
|
300
|
+
// First check if it's a simple array type - these should NOT be considered generic
|
|
301
|
+
if (this.isSimpleArrayType(type)) {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
// Check if the type has type parameters
|
|
305
|
+
if (type.aliasTypeArguments && type.aliasTypeArguments.length > 0) {
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
// Check if it's a type reference with type arguments
|
|
309
|
+
// But exclude simple arrays which internally use Array<T> representation
|
|
310
|
+
if (type.typeArguments && type.typeArguments.length > 0) {
|
|
311
|
+
const symbol = type.getSymbol();
|
|
312
|
+
if (symbol && symbol.getName() === 'Array') {
|
|
313
|
+
// This is Array<T> - only consider it generic if T itself is a utility type
|
|
314
|
+
const elementType = type.typeArguments[0];
|
|
315
|
+
if (elementType) {
|
|
316
|
+
return this.isUtilityTypeFromType(elementType);
|
|
1019
317
|
}
|
|
318
|
+
return false;
|
|
1020
319
|
}
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
320
|
+
const elementType = type.typeArguments[0];
|
|
321
|
+
return this.isUtilityTypeFromType(elementType);
|
|
322
|
+
}
|
|
323
|
+
// Check type flags for generic indicators
|
|
324
|
+
if (type.flags & ts.TypeFlags.TypeParameter) {
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
if (type.flags & ts.TypeFlags.Conditional) {
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
if (type.flags & ts.TypeFlags.Index) {
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
if (type.flags & ts.TypeFlags.IndexedAccess) {
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
// Check if the type symbol indicates a generic type
|
|
337
|
+
const symbol = type.getSymbol();
|
|
338
|
+
if (symbol && symbol.declarations) {
|
|
339
|
+
for (const declaration of symbol.declarations) {
|
|
340
|
+
// Check for type alias declarations with type parameters
|
|
341
|
+
if (ts.isTypeAliasDeclaration(declaration) &&
|
|
342
|
+
declaration.typeParameters) {
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
// Check for interface declarations with type parameters
|
|
346
|
+
if (ts.isInterfaceDeclaration(declaration) &&
|
|
347
|
+
declaration.typeParameters) {
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
// Check for class declarations with type parameters
|
|
351
|
+
if (ts.isClassDeclaration(declaration) && declaration.typeParameters) {
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
1025
354
|
}
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
355
|
+
}
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
isUtilityTypeFromType(type) {
|
|
359
|
+
if (!type.aliasSymbol)
|
|
360
|
+
return false;
|
|
361
|
+
const aliasName = type.aliasSymbol.getName();
|
|
362
|
+
const utilityTypes = [
|
|
363
|
+
'Partial',
|
|
364
|
+
'Required',
|
|
365
|
+
'Readonly',
|
|
366
|
+
'Pick',
|
|
367
|
+
'Omit',
|
|
368
|
+
'Record',
|
|
369
|
+
'Exclude',
|
|
370
|
+
'Extract',
|
|
371
|
+
'NonNullable',
|
|
372
|
+
];
|
|
373
|
+
return utilityTypes.includes(aliasName);
|
|
374
|
+
}
|
|
375
|
+
isSimpleArrayType(type) {
|
|
376
|
+
const symbol = type.getSymbol();
|
|
377
|
+
if (!symbol || symbol.getName() !== 'Array') {
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
// Check if this is Array<T> where T is a simple, non-generic type
|
|
381
|
+
if (type.typeArguments &&
|
|
382
|
+
type.typeArguments.length === 1) {
|
|
383
|
+
const elementType = type.typeArguments[0];
|
|
384
|
+
if (!elementType)
|
|
385
|
+
return false;
|
|
386
|
+
// If the element type is a utility type, then this array should be considered generic
|
|
387
|
+
if (this.isUtilityTypeFromType(elementType)) {
|
|
388
|
+
return false;
|
|
1031
389
|
}
|
|
1032
|
-
//
|
|
1033
|
-
|
|
390
|
+
// If the element type itself has generic parameters, this array is generic
|
|
391
|
+
if (elementType.typeArguments &&
|
|
392
|
+
elementType.typeArguments.length > 0) {
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
return true;
|
|
1034
396
|
}
|
|
1035
|
-
return
|
|
397
|
+
return false;
|
|
1036
398
|
}
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
};
|
|
399
|
+
isPrimitiveType(typeName) {
|
|
400
|
+
const lowerTypeName = typeName.toLowerCase();
|
|
401
|
+
// Check against all primitive types from constants
|
|
402
|
+
const primitiveTypes = [
|
|
403
|
+
constants.jsPrimitives.String.type.toLowerCase(),
|
|
404
|
+
constants.jsPrimitives.Number.type.toLowerCase(),
|
|
405
|
+
constants.jsPrimitives.Boolean.type.toLowerCase(),
|
|
406
|
+
constants.jsPrimitives.Date.type.toLowerCase(),
|
|
407
|
+
constants.jsPrimitives.Buffer.type.toLowerCase(),
|
|
408
|
+
constants.jsPrimitives.Uint8Array.type.toLowerCase(),
|
|
409
|
+
constants.jsPrimitives.File.type.toLowerCase(),
|
|
410
|
+
constants.jsPrimitives.UploadFile.type.toLowerCase(),
|
|
411
|
+
constants.jsPrimitives.BigInt.type.toLowerCase(),
|
|
412
|
+
constants.jsPrimitives.Symbol.type.toLowerCase(),
|
|
413
|
+
constants.jsPrimitives.null.type.toLowerCase(),
|
|
414
|
+
constants.jsPrimitives.Object.type.toLowerCase(),
|
|
415
|
+
constants.jsPrimitives.Array.type.toLowerCase(),
|
|
416
|
+
constants.jsPrimitives.Any.type.toLowerCase(),
|
|
417
|
+
constants.jsPrimitives.Unknown.type.toLowerCase(),
|
|
418
|
+
];
|
|
419
|
+
const primitivesArray = primitiveTypes.map(t => t.concat('[]'));
|
|
420
|
+
return (primitiveTypes.includes(lowerTypeName) ||
|
|
421
|
+
primitivesArray.includes(lowerTypeName));
|
|
422
|
+
}
|
|
423
|
+
static getInstance(tsConfigPath, options) {
|
|
424
|
+
if (!SchemaTransformer.instance) {
|
|
425
|
+
SchemaTransformer.instance = new SchemaTransformer(tsConfigPath, options);
|
|
1065
426
|
}
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
return
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
427
|
+
return SchemaTransformer.instance;
|
|
428
|
+
}
|
|
429
|
+
getSourceFileByClassName(className, sourceOptions) {
|
|
430
|
+
let sourceFiles = [];
|
|
431
|
+
if (sourceOptions?.isExternal) {
|
|
432
|
+
sourceFiles = this.program.getSourceFiles().filter(sf => {
|
|
433
|
+
return (sf.fileName.includes(sourceOptions.packageName) &&
|
|
434
|
+
(!sourceOptions.filePath || sf.fileName === sourceOptions.filePath));
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
sourceFiles = this.program.getSourceFiles().filter(sf => {
|
|
439
|
+
if (sf.isDeclarationFile)
|
|
440
|
+
return false;
|
|
441
|
+
if (sf.fileName.includes('.d.ts'))
|
|
442
|
+
return false;
|
|
443
|
+
if (sf.fileName.includes('node_modules'))
|
|
444
|
+
return false;
|
|
445
|
+
return true;
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
for (const sourceFile of sourceFiles) {
|
|
449
|
+
let node;
|
|
450
|
+
const found = sourceFile.statements.some(stmt => {
|
|
451
|
+
node = stmt;
|
|
452
|
+
return (ts.isClassDeclaration(stmt) &&
|
|
453
|
+
stmt.name &&
|
|
454
|
+
stmt.name.text === className);
|
|
455
|
+
});
|
|
456
|
+
if (found) {
|
|
457
|
+
return { sourceFile, node: node };
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
isClassType(propertyDeclaration) {
|
|
462
|
+
// If there's no explicit type annotation, we can't determine reliably
|
|
463
|
+
if (!propertyDeclaration.type) {
|
|
464
|
+
return false;
|
|
465
|
+
}
|
|
466
|
+
// Check if the original property type is an array type
|
|
467
|
+
if (this.isArrayProperty(propertyDeclaration) &&
|
|
468
|
+
ts.isTypeReferenceNode(propertyDeclaration.type
|
|
469
|
+
.elementType)) {
|
|
470
|
+
const type = this.checker.getTypeAtLocation(propertyDeclaration.type.elementType);
|
|
471
|
+
const symbol = type.getSymbol();
|
|
472
|
+
if (symbol && symbol.declarations) {
|
|
473
|
+
return symbol.declarations.some(decl => ts.isClassDeclaration(decl));
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
else if (ts.isTypeReferenceNode(propertyDeclaration.type)) {
|
|
477
|
+
const type = this.checker.getTypeAtLocation(propertyDeclaration.type);
|
|
478
|
+
const symbol = type.getSymbol();
|
|
479
|
+
if (symbol && symbol.declarations) {
|
|
480
|
+
return symbol.declarations.some(decl => ts.isClassDeclaration(decl));
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
getDeclarationProperty(property) {
|
|
486
|
+
if (!property.originalProperty.type) {
|
|
487
|
+
return undefined;
|
|
488
|
+
}
|
|
489
|
+
if (ts.isArrayTypeNode(property.originalProperty.type) &&
|
|
490
|
+
ts.isTypeReferenceNode(property.originalProperty.type.elementType)) {
|
|
491
|
+
const type = this.checker.getTypeAtLocation(property.originalProperty.type.elementType);
|
|
492
|
+
const symbol = type.getSymbol();
|
|
493
|
+
if (symbol && symbol.declarations) {
|
|
494
|
+
return symbol.declarations[0];
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
else if (ts.isTypeReferenceNode(property.originalProperty.type)) {
|
|
498
|
+
const type = this.checker.getTypeAtLocation(property.originalProperty.type);
|
|
499
|
+
const symbol = type.getSymbol();
|
|
500
|
+
if (symbol && symbol.declarations) {
|
|
501
|
+
return symbol.declarations[0];
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return undefined;
|
|
505
|
+
}
|
|
506
|
+
isArrayProperty(propertyDeclaration) {
|
|
507
|
+
if (!propertyDeclaration.type) {
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
510
|
+
return ts.isArrayTypeNode(propertyDeclaration.type);
|
|
511
|
+
}
|
|
512
|
+
getSchemaFromProperties({ properties, visitedClass, transformedSchema, classDeclaration, }) {
|
|
513
|
+
let schema = {};
|
|
514
|
+
const required = [];
|
|
515
|
+
for (const property of properties) {
|
|
516
|
+
schema[property.name] = this.getSchemaFromProperty({
|
|
517
|
+
property,
|
|
518
|
+
visitedClass,
|
|
519
|
+
transformedSchema,
|
|
520
|
+
classDeclaration,
|
|
521
|
+
});
|
|
522
|
+
// this.applyDecorators(property, schema as SchemaType)
|
|
523
|
+
if (!property.isOptional) {
|
|
524
|
+
required.push(property.name);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return {
|
|
528
|
+
type: 'object',
|
|
529
|
+
properties: schema,
|
|
530
|
+
required: required.length ? required : undefined,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
getSchemaFromProperty({ property, visitedClass, transformedSchema, classDeclaration, }) {
|
|
534
|
+
let schema = {};
|
|
535
|
+
if (property.isPrimitive) {
|
|
536
|
+
schema = this.getSchemaFromPrimitive(property);
|
|
537
|
+
}
|
|
538
|
+
else if (property.isClassType) {
|
|
539
|
+
const declaration = this.getDeclarationProperty(property);
|
|
540
|
+
if (property.isRef && classDeclaration.name) {
|
|
541
|
+
// Self-referencing property, handle as a reference to avoid infinite recursion
|
|
542
|
+
if (property.isArray) {
|
|
543
|
+
schema.type = 'array';
|
|
544
|
+
schema.items = {
|
|
545
|
+
$ref: `#/components/schemas/${classDeclaration.name.text}`,
|
|
1122
546
|
};
|
|
1123
547
|
}
|
|
1124
|
-
|
|
1125
|
-
|
|
548
|
+
else {
|
|
549
|
+
schema = {
|
|
550
|
+
$ref: `#/components/schemas/${classDeclaration.name.text}`,
|
|
551
|
+
};
|
|
1126
552
|
}
|
|
553
|
+
}
|
|
554
|
+
else {
|
|
555
|
+
schema = this.getSchemaFromClass({
|
|
556
|
+
isArray: property.isArray,
|
|
557
|
+
visitedClass,
|
|
558
|
+
transformedSchema,
|
|
559
|
+
declaration,
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
else {
|
|
564
|
+
schema = { type: 'object', properties: {}, additionalProperties: true };
|
|
1127
565
|
}
|
|
566
|
+
this.applyDecorators(property, schema);
|
|
567
|
+
return schema;
|
|
1128
568
|
}
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
569
|
+
getSchemaFromClass({ transformedSchema = new Map(), visitedClass = new Set(), declaration, isArray, }) {
|
|
570
|
+
let schema = { type: 'object' };
|
|
571
|
+
if (!declaration ||
|
|
572
|
+
!ts.isClassDeclaration(declaration) ||
|
|
573
|
+
!declaration.name) {
|
|
574
|
+
return { type: 'object' };
|
|
575
|
+
}
|
|
576
|
+
if (visitedClass.has(declaration)) {
|
|
577
|
+
if (isArray) {
|
|
578
|
+
schema.type = 'array';
|
|
579
|
+
schema.items = {
|
|
580
|
+
$ref: `#/components/schemas/${declaration.name.text}`,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
schema = {
|
|
585
|
+
$ref: `#/components/schemas/${declaration.name.text}`,
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
return schema;
|
|
589
|
+
}
|
|
590
|
+
visitedClass.add(declaration);
|
|
591
|
+
const properties = this.getPropertiesByClassDeclaration(declaration);
|
|
592
|
+
let transformerProps = this.getSchemaFromProperties({
|
|
593
|
+
properties,
|
|
594
|
+
visitedClass,
|
|
595
|
+
transformedSchema: transformedSchema,
|
|
596
|
+
classDeclaration: declaration,
|
|
597
|
+
});
|
|
598
|
+
if (isArray) {
|
|
599
|
+
schema.type = 'array';
|
|
600
|
+
schema.items = {
|
|
601
|
+
type: transformerProps.type,
|
|
602
|
+
properties: transformerProps.properties,
|
|
603
|
+
required: transformerProps.required,
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
else {
|
|
607
|
+
schema.type = transformerProps.type;
|
|
608
|
+
schema.properties = transformerProps.properties;
|
|
609
|
+
schema.required = transformerProps.required;
|
|
610
|
+
}
|
|
611
|
+
transformedSchema.set(declaration.name.text, schema);
|
|
612
|
+
return schema;
|
|
1138
613
|
}
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
614
|
+
getSchemaFromPrimitive(property) {
|
|
615
|
+
const propertySchema = { type: 'object' };
|
|
616
|
+
const propertyType = property.type.toLowerCase().replace('[]', '').trim();
|
|
617
|
+
switch (propertyType) {
|
|
618
|
+
case constants.jsPrimitives.String.value:
|
|
619
|
+
propertySchema.type = constants.jsPrimitives.String.value;
|
|
620
|
+
break;
|
|
621
|
+
case constants.jsPrimitives.Number.value:
|
|
622
|
+
propertySchema.type = constants.jsPrimitives.Number.value;
|
|
623
|
+
propertySchema.format = constants.jsPrimitives.Number.format;
|
|
624
|
+
break;
|
|
625
|
+
case constants.jsPrimitives.BigInt.type.toLocaleLowerCase():
|
|
626
|
+
propertySchema.type = constants.jsPrimitives.BigInt.value;
|
|
627
|
+
propertySchema.format = constants.jsPrimitives.BigInt.format;
|
|
628
|
+
break;
|
|
629
|
+
case constants.jsPrimitives.Date.type.toLocaleLowerCase():
|
|
630
|
+
propertySchema.type = constants.jsPrimitives.Date.value;
|
|
631
|
+
propertySchema.format = constants.jsPrimitives.Date.format;
|
|
632
|
+
break;
|
|
633
|
+
case constants.jsPrimitives.Buffer.value:
|
|
634
|
+
case constants.jsPrimitives.Uint8Array.value:
|
|
635
|
+
case constants.jsPrimitives.File.value:
|
|
636
|
+
case constants.jsPrimitives.UploadFile.value:
|
|
637
|
+
propertySchema.type = constants.jsPrimitives.UploadFile.value;
|
|
638
|
+
propertySchema.format = constants.jsPrimitives.UploadFile.format;
|
|
639
|
+
break;
|
|
640
|
+
case constants.jsPrimitives.Array.value:
|
|
641
|
+
propertySchema.type = constants.jsPrimitives.Array.value;
|
|
642
|
+
break;
|
|
643
|
+
case constants.jsPrimitives.Boolean.value:
|
|
644
|
+
propertySchema.type = constants.jsPrimitives.Boolean.value;
|
|
645
|
+
break;
|
|
646
|
+
case constants.jsPrimitives.Symbol.type.toLocaleLowerCase():
|
|
647
|
+
propertySchema.type = constants.jsPrimitives.Symbol.value;
|
|
648
|
+
break;
|
|
649
|
+
case constants.jsPrimitives.Object.value:
|
|
650
|
+
propertySchema.type = constants.jsPrimitives.Object.value;
|
|
651
|
+
break;
|
|
652
|
+
default:
|
|
653
|
+
propertySchema.type = constants.jsPrimitives.String.value;
|
|
654
|
+
}
|
|
655
|
+
if (property.isArray) {
|
|
656
|
+
propertySchema.type = `array`;
|
|
657
|
+
propertySchema.items = { type: propertyType };
|
|
1152
658
|
}
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
659
|
+
return propertySchema;
|
|
660
|
+
}
|
|
661
|
+
//Todo: implement properly
|
|
662
|
+
applyEnumDecorator(decorator, schema) { }
|
|
663
|
+
applyDecorators(property, schema) {
|
|
664
|
+
for (const decorator of property.decorators) {
|
|
1156
665
|
const decoratorName = decorator.name;
|
|
1157
666
|
switch (decoratorName) {
|
|
1158
667
|
case constants.validatorDecorators.IsString.name:
|
|
1159
|
-
if (!
|
|
1160
|
-
schema.
|
|
1161
|
-
constants.validatorDecorators.IsString.type;
|
|
668
|
+
if (!property.isArray) {
|
|
669
|
+
schema.type = constants.validatorDecorators.IsString.type;
|
|
1162
670
|
}
|
|
1163
|
-
else if (schema.
|
|
1164
|
-
schema.
|
|
1165
|
-
constants.validatorDecorators.IsString.type;
|
|
671
|
+
else if (schema.items) {
|
|
672
|
+
schema.items.type = constants.validatorDecorators.IsString.type;
|
|
1166
673
|
}
|
|
1167
674
|
break;
|
|
1168
675
|
case constants.validatorDecorators.IsInt.name:
|
|
1169
|
-
if (!
|
|
1170
|
-
schema.
|
|
1171
|
-
|
|
1172
|
-
schema.properties[propertyName].format =
|
|
1173
|
-
constants.validatorDecorators.IsInt.format;
|
|
676
|
+
if (!property.isArray) {
|
|
677
|
+
schema.type = constants.validatorDecorators.IsInt.type;
|
|
678
|
+
schema.format = constants.validatorDecorators.IsInt.format;
|
|
1174
679
|
}
|
|
1175
|
-
else if (schema.
|
|
1176
|
-
schema.
|
|
1177
|
-
|
|
1178
|
-
schema.properties[propertyName].items.format =
|
|
1179
|
-
constants.validatorDecorators.IsInt.format;
|
|
680
|
+
else if (schema.items) {
|
|
681
|
+
schema.items.type = constants.validatorDecorators.IsInt.type;
|
|
682
|
+
schema.items.format = constants.validatorDecorators.IsInt.format;
|
|
1180
683
|
}
|
|
1181
684
|
break;
|
|
1182
685
|
case constants.validatorDecorators.IsNumber.name:
|
|
1183
|
-
if (!
|
|
1184
|
-
schema.
|
|
1185
|
-
constants.validatorDecorators.IsNumber.type;
|
|
686
|
+
if (!property.isArray) {
|
|
687
|
+
schema.type = constants.validatorDecorators.IsNumber.type;
|
|
1186
688
|
}
|
|
1187
|
-
else if (schema.
|
|
1188
|
-
schema.
|
|
1189
|
-
constants.validatorDecorators.IsNumber.type;
|
|
689
|
+
else if (schema.items) {
|
|
690
|
+
schema.items.type = constants.validatorDecorators.IsNumber.type;
|
|
1190
691
|
}
|
|
1191
692
|
break;
|
|
1192
693
|
case constants.validatorDecorators.IsBoolean.name:
|
|
1193
|
-
if (!
|
|
1194
|
-
schema.
|
|
1195
|
-
constants.validatorDecorators.IsBoolean.type;
|
|
694
|
+
if (!property.isArray) {
|
|
695
|
+
schema.type = constants.validatorDecorators.IsBoolean.type;
|
|
1196
696
|
}
|
|
1197
|
-
else if (schema.
|
|
1198
|
-
schema.
|
|
1199
|
-
constants.validatorDecorators.IsBoolean.type;
|
|
697
|
+
else if (schema.items) {
|
|
698
|
+
schema.items.type = constants.validatorDecorators.IsBoolean.type;
|
|
1200
699
|
}
|
|
1201
700
|
break;
|
|
1202
701
|
case constants.validatorDecorators.IsEmail.name:
|
|
1203
|
-
if (!
|
|
1204
|
-
schema.
|
|
1205
|
-
constants.validatorDecorators.IsEmail.format;
|
|
702
|
+
if (!property.isArray) {
|
|
703
|
+
schema.format = constants.validatorDecorators.IsEmail.format;
|
|
1206
704
|
}
|
|
1207
|
-
else if (schema.
|
|
1208
|
-
schema.
|
|
1209
|
-
constants.validatorDecorators.IsEmail.format;
|
|
705
|
+
else if (schema.items) {
|
|
706
|
+
schema.items.format = constants.validatorDecorators.IsEmail.format;
|
|
1210
707
|
}
|
|
1211
708
|
break;
|
|
1212
709
|
case constants.validatorDecorators.IsDate.name:
|
|
1213
|
-
if (!
|
|
1214
|
-
schema.
|
|
1215
|
-
|
|
1216
|
-
schema.properties[propertyName].format =
|
|
1217
|
-
constants.validatorDecorators.IsDate.format;
|
|
710
|
+
if (!property.isArray) {
|
|
711
|
+
schema.type = constants.validatorDecorators.IsDate.type;
|
|
712
|
+
schema.format = constants.validatorDecorators.IsDate.format;
|
|
1218
713
|
}
|
|
1219
|
-
else if (schema.
|
|
1220
|
-
schema.
|
|
1221
|
-
|
|
1222
|
-
schema.properties[propertyName].items.format =
|
|
1223
|
-
constants.validatorDecorators.IsDate.format;
|
|
714
|
+
else if (schema.items) {
|
|
715
|
+
schema.items.type = constants.validatorDecorators.IsDate.type;
|
|
716
|
+
schema.items.format = constants.validatorDecorators.IsDate.format;
|
|
1224
717
|
}
|
|
1225
718
|
break;
|
|
1226
719
|
case constants.validatorDecorators.IsNotEmpty.name:
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
720
|
+
property.isOptional = false;
|
|
721
|
+
break;
|
|
722
|
+
case constants.validatorDecorators.IsOptional.name:
|
|
723
|
+
property.isOptional = true;
|
|
1230
724
|
break;
|
|
1231
725
|
case constants.validatorDecorators.MinLength.name:
|
|
1232
|
-
schema.
|
|
726
|
+
schema.minLength = decorator.arguments[0];
|
|
1233
727
|
break;
|
|
1234
728
|
case constants.validatorDecorators.MaxLength.name:
|
|
1235
|
-
schema.
|
|
729
|
+
schema.maxLength = decorator.arguments[0];
|
|
1236
730
|
break;
|
|
1237
731
|
case constants.validatorDecorators.Length.name:
|
|
1238
|
-
schema.
|
|
732
|
+
schema.minLength = decorator.arguments[0];
|
|
1239
733
|
if (decorator.arguments[1]) {
|
|
1240
|
-
schema.
|
|
734
|
+
schema.maxLength = decorator.arguments[1];
|
|
1241
735
|
}
|
|
1242
736
|
break;
|
|
1243
737
|
case constants.validatorDecorators.Min.name:
|
|
1244
|
-
schema.
|
|
738
|
+
schema.minimum = decorator.arguments[0];
|
|
1245
739
|
break;
|
|
1246
740
|
case constants.validatorDecorators.Max.name:
|
|
1247
|
-
schema.
|
|
741
|
+
schema.maximum = decorator.arguments[0];
|
|
1248
742
|
break;
|
|
1249
743
|
case constants.validatorDecorators.IsPositive.name:
|
|
1250
|
-
schema.
|
|
744
|
+
schema.minimum = 0;
|
|
1251
745
|
break;
|
|
1252
746
|
case constants.validatorDecorators.IsArray.name:
|
|
1253
|
-
schema.
|
|
1254
|
-
constants.jsPrimitives.Array.value;
|
|
747
|
+
schema.type = constants.jsPrimitives.Array.value;
|
|
1255
748
|
break;
|
|
1256
749
|
case constants.validatorDecorators.ArrayNotEmpty.name:
|
|
1257
|
-
schema.
|
|
1258
|
-
|
|
1259
|
-
schema.required.push(propertyName);
|
|
1260
|
-
}
|
|
750
|
+
schema.minItems = 1;
|
|
751
|
+
property.isOptional = false;
|
|
1261
752
|
break;
|
|
1262
753
|
case constants.validatorDecorators.ArrayMinSize.name:
|
|
1263
|
-
schema.
|
|
754
|
+
schema.minItems = decorator.arguments[0];
|
|
1264
755
|
break;
|
|
1265
756
|
case constants.validatorDecorators.ArrayMaxSize.name:
|
|
1266
|
-
schema.
|
|
757
|
+
schema.maxItems = decorator.arguments[0];
|
|
1267
758
|
break;
|
|
1268
759
|
case constants.validatorDecorators.IsEnum.name:
|
|
1269
|
-
this.applyEnumDecorator(decorator, schema
|
|
760
|
+
this.applyEnumDecorator(decorator, schema);
|
|
1270
761
|
break;
|
|
1271
762
|
}
|
|
1272
763
|
}
|
|
1273
764
|
}
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
* @param propertyName - The name of the property
|
|
1281
|
-
* @param isArrayType - Whether the property is an array type
|
|
1282
|
-
* @private
|
|
1283
|
-
*/
|
|
1284
|
-
applyEnumDecorator(decorator, schema, propertyName, isArrayType) {
|
|
1285
|
-
if (!decorator.arguments || decorator.arguments.length === 0) {
|
|
1286
|
-
return;
|
|
1287
|
-
}
|
|
1288
|
-
const enumArg = decorator.arguments[0];
|
|
1289
|
-
let enumValues = [];
|
|
1290
|
-
// Handle different enum argument types
|
|
1291
|
-
if (typeof enumArg === 'string') {
|
|
1292
|
-
// This is likely a reference to an enum type name
|
|
1293
|
-
// We need to try to resolve this to actual enum values
|
|
1294
|
-
enumValues = this.resolveEnumValues(enumArg);
|
|
1295
|
-
}
|
|
1296
|
-
else if (typeof enumArg === 'object' && enumArg !== null) {
|
|
1297
|
-
// Object enum - extract values
|
|
1298
|
-
if (Array.isArray(enumArg)) {
|
|
1299
|
-
// Already an array of values
|
|
1300
|
-
enumValues = enumArg;
|
|
1301
|
-
}
|
|
1302
|
-
else {
|
|
1303
|
-
// Enum object - get all values
|
|
1304
|
-
enumValues = Object.values(enumArg);
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
// If we couldn't resolve enum values, fall back to string type without enum constraint
|
|
1308
|
-
if (enumValues.length === 0) {
|
|
1309
|
-
if (!isArrayType) {
|
|
1310
|
-
schema.properties[propertyName].type = 'string';
|
|
1311
|
-
}
|
|
1312
|
-
else if (schema.properties[propertyName].items) {
|
|
1313
|
-
schema.properties[propertyName].items.type = 'string';
|
|
1314
|
-
}
|
|
1315
|
-
return;
|
|
1316
|
-
}
|
|
1317
|
-
// Determine the type based on enum values
|
|
1318
|
-
let enumType = 'string';
|
|
1319
|
-
if (enumValues.length > 0) {
|
|
1320
|
-
const firstValue = enumValues[0];
|
|
1321
|
-
if (typeof firstValue === 'number') {
|
|
1322
|
-
enumType = 'number';
|
|
1323
|
-
}
|
|
1324
|
-
else if (typeof firstValue === 'boolean') {
|
|
1325
|
-
enumType = 'boolean';
|
|
1326
|
-
}
|
|
1327
|
-
}
|
|
1328
|
-
// Apply enum to schema
|
|
1329
|
-
if (!isArrayType) {
|
|
1330
|
-
schema.properties[propertyName].type = enumType;
|
|
1331
|
-
schema.properties[propertyName].enum = enumValues;
|
|
1332
|
-
}
|
|
1333
|
-
else if (schema.properties[propertyName].items) {
|
|
1334
|
-
schema.properties[propertyName].items.type = enumType;
|
|
1335
|
-
schema.properties[propertyName].items.enum = enumValues;
|
|
1336
|
-
}
|
|
1337
|
-
}
|
|
1338
|
-
/**
|
|
1339
|
-
* Attempts to resolve enum values from an enum type name.
|
|
1340
|
-
* This searches through the TypeScript AST to find the enum declaration
|
|
1341
|
-
* and extract its values.
|
|
1342
|
-
*
|
|
1343
|
-
* @param enumTypeName - The name of the enum type
|
|
1344
|
-
* @returns Array of enum values if found, empty array otherwise
|
|
1345
|
-
* @private
|
|
1346
|
-
*/
|
|
1347
|
-
resolveEnumValues(enumTypeName) {
|
|
1348
|
-
// Search for enum declarations in source files
|
|
1349
|
-
for (const sourceFile of this.program.getSourceFiles()) {
|
|
1350
|
-
if (sourceFile.isDeclarationFile)
|
|
1351
|
-
continue;
|
|
1352
|
-
if (sourceFile.fileName.includes('node_modules'))
|
|
1353
|
-
continue;
|
|
1354
|
-
const enumValues = this.findEnumValues(sourceFile, enumTypeName);
|
|
1355
|
-
if (enumValues.length > 0) {
|
|
1356
|
-
return enumValues;
|
|
1357
|
-
}
|
|
1358
|
-
}
|
|
1359
|
-
return [];
|
|
1360
|
-
}
|
|
1361
|
-
/**
|
|
1362
|
-
* Finds enum values in a specific source file.
|
|
1363
|
-
*
|
|
1364
|
-
* @param sourceFile - The source file to search
|
|
1365
|
-
* @param enumTypeName - The name of the enum to find
|
|
1366
|
-
* @returns Array of enum values if found, empty array otherwise
|
|
1367
|
-
* @private
|
|
1368
|
-
*/
|
|
1369
|
-
findEnumValues(sourceFile, enumTypeName) {
|
|
1370
|
-
let enumValues = [];
|
|
1371
|
-
const visit = (node) => {
|
|
1372
|
-
// Handle TypeScript enum declarations
|
|
1373
|
-
if (ts.isEnumDeclaration(node) && node.name?.text === enumTypeName) {
|
|
1374
|
-
enumValues = this.extractEnumValues(node);
|
|
1375
|
-
return;
|
|
1376
|
-
}
|
|
1377
|
-
// Handle const object declarations (like const Status = { ... } as const)
|
|
1378
|
-
if (ts.isVariableStatement(node)) {
|
|
1379
|
-
for (const declaration of node.declarationList.declarations) {
|
|
1380
|
-
if (ts.isVariableDeclaration(declaration) &&
|
|
1381
|
-
ts.isIdentifier(declaration.name) &&
|
|
1382
|
-
declaration.name.text === enumTypeName &&
|
|
1383
|
-
declaration.initializer) {
|
|
1384
|
-
let initializer = declaration.initializer;
|
|
1385
|
-
// Handle "as const" assertions
|
|
1386
|
-
if (ts.isAsExpression(initializer) && initializer.expression) {
|
|
1387
|
-
initializer = initializer.expression;
|
|
1388
|
-
}
|
|
1389
|
-
enumValues = this.extractObjectEnumValues(initializer);
|
|
1390
|
-
return;
|
|
1391
|
-
}
|
|
1392
|
-
}
|
|
1393
|
-
}
|
|
1394
|
-
ts.forEachChild(node, visit);
|
|
1395
|
-
};
|
|
1396
|
-
visit(sourceFile);
|
|
1397
|
-
return enumValues;
|
|
1398
|
-
}
|
|
1399
|
-
/**
|
|
1400
|
-
* Extracts values from a TypeScript enum declaration.
|
|
1401
|
-
*
|
|
1402
|
-
* @param enumNode - The enum declaration node
|
|
1403
|
-
* @returns Array of enum values
|
|
1404
|
-
* @private
|
|
1405
|
-
*/
|
|
1406
|
-
extractEnumValues(enumNode) {
|
|
1407
|
-
const values = [];
|
|
1408
|
-
for (const member of enumNode.members) {
|
|
1409
|
-
if (member.initializer) {
|
|
1410
|
-
// Handle initialized enum members
|
|
1411
|
-
if (ts.isStringLiteral(member.initializer)) {
|
|
1412
|
-
values.push(member.initializer.text);
|
|
1413
|
-
}
|
|
1414
|
-
else if (ts.isNumericLiteral(member.initializer)) {
|
|
1415
|
-
values.push(Number(member.initializer.text));
|
|
1416
|
-
}
|
|
1417
|
-
}
|
|
1418
|
-
else {
|
|
1419
|
-
// Handle auto-incremented numeric enums
|
|
1420
|
-
if (values.length === 0) {
|
|
1421
|
-
values.push(0);
|
|
1422
|
-
}
|
|
1423
|
-
else {
|
|
1424
|
-
const lastValue = values[values.length - 1];
|
|
1425
|
-
if (typeof lastValue === 'number') {
|
|
1426
|
-
values.push(lastValue + 1);
|
|
1427
|
-
}
|
|
1428
|
-
}
|
|
1429
|
-
}
|
|
1430
|
-
}
|
|
1431
|
-
return values;
|
|
1432
|
-
}
|
|
1433
|
-
/**
|
|
1434
|
-
* Extracts values from object literal enum (const object as const).
|
|
1435
|
-
*
|
|
1436
|
-
* @param initializer - The object literal initializer
|
|
1437
|
-
* @returns Array of enum values
|
|
1438
|
-
* @private
|
|
1439
|
-
*/
|
|
1440
|
-
extractObjectEnumValues(initializer) {
|
|
1441
|
-
const values = [];
|
|
1442
|
-
if (ts.isObjectLiteralExpression(initializer)) {
|
|
1443
|
-
for (const property of initializer.properties) {
|
|
1444
|
-
if (ts.isPropertyAssignment(property) && property.initializer) {
|
|
1445
|
-
if (ts.isStringLiteral(property.initializer)) {
|
|
1446
|
-
values.push(property.initializer.text);
|
|
1447
|
-
}
|
|
1448
|
-
else if (ts.isNumericLiteral(property.initializer)) {
|
|
1449
|
-
values.push(Number(property.initializer.text));
|
|
1450
|
-
}
|
|
1451
|
-
}
|
|
1452
|
-
}
|
|
1453
|
-
}
|
|
1454
|
-
return values;
|
|
1455
|
-
}
|
|
1456
|
-
/**
|
|
1457
|
-
* Applies sensible default behaviors for properties without class-validator decorators.
|
|
1458
|
-
* This allows the schema generator to work with plain TypeScript classes.
|
|
1459
|
-
*
|
|
1460
|
-
* @param property - The property information
|
|
1461
|
-
* @param schema - The schema object to modify
|
|
1462
|
-
* @private
|
|
1463
|
-
*/
|
|
1464
|
-
/**
|
|
1465
|
-
* Applies OpenAPI format specifications based on TypeScript types.
|
|
1466
|
-
* This method is called when no decorators are present to set appropriate
|
|
1467
|
-
* format values for primitive types according to OpenAPI specification.
|
|
1468
|
-
*
|
|
1469
|
-
* @param property - The property information containing type details
|
|
1470
|
-
* @param schema - The schema object to modify
|
|
1471
|
-
* @private
|
|
1472
|
-
*/
|
|
1473
|
-
applyTypeBasedFormats(property, schema) {
|
|
1474
|
-
// Skip applying type-based formats to $ref schemas
|
|
1475
|
-
if (this.isRefSchema(schema)) {
|
|
1476
|
-
return;
|
|
1477
|
-
}
|
|
1478
|
-
const propertyName = property.name;
|
|
1479
|
-
const propertyType = property.type.toLowerCase();
|
|
1480
|
-
const propertySchema = schema.properties[propertyName];
|
|
1481
|
-
switch (propertyType) {
|
|
1482
|
-
case constants.jsPrimitives.Number.value:
|
|
1483
|
-
propertySchema.format = constants.jsPrimitives.Number.format;
|
|
1484
|
-
break;
|
|
1485
|
-
case constants.jsPrimitives.BigInt.value:
|
|
1486
|
-
propertySchema.format = constants.jsPrimitives.BigInt.format;
|
|
1487
|
-
break;
|
|
1488
|
-
case constants.jsPrimitives.Date.value:
|
|
1489
|
-
propertySchema.format = constants.jsPrimitives.Date.format;
|
|
1490
|
-
break;
|
|
1491
|
-
case constants.jsPrimitives.Buffer.value:
|
|
1492
|
-
case constants.jsPrimitives.Uint8Array.value:
|
|
1493
|
-
case constants.jsPrimitives.File.value:
|
|
1494
|
-
case constants.jsPrimitives.UploadFile.value:
|
|
1495
|
-
propertySchema.format = constants.jsPrimitives.UploadFile.format;
|
|
1496
|
-
break;
|
|
1497
|
-
}
|
|
1498
|
-
}
|
|
1499
|
-
/**
|
|
1500
|
-
* Determines if a property should be required based on decorators and optional status.
|
|
1501
|
-
*
|
|
1502
|
-
* Logic:
|
|
1503
|
-
* - If property has IsNotEmpty or ArrayNotEmpty decorator, it's required (handled in applyDecorators)
|
|
1504
|
-
* - Otherwise, the property is not required (preserving original behavior)
|
|
1505
|
-
* - The isOptional information is stored for future use and documentation
|
|
1506
|
-
*
|
|
1507
|
-
* @param property - The property information
|
|
1508
|
-
* @param schema - The schema object to modify
|
|
1509
|
-
* @private
|
|
1510
|
-
*/
|
|
1511
|
-
determineRequiredStatus(property, schema) {
|
|
1512
|
-
// Skip determining required status for $ref schemas
|
|
1513
|
-
if (this.isRefSchema(schema)) {
|
|
1514
|
-
return;
|
|
765
|
+
transform(cls, sourceOptions) {
|
|
766
|
+
let schema = { type: 'object', properties: {} };
|
|
767
|
+
const result = this.getSourceFileByClassName(cls.name, sourceOptions);
|
|
768
|
+
if (!result?.sourceFile) {
|
|
769
|
+
console.warn(`Class ${cls.name} not found in any source file.`);
|
|
770
|
+
return { name: cls.name, schema: {} };
|
|
1515
771
|
}
|
|
1516
|
-
const
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
}
|
|
1523
|
-
// If property is optional (has ?), it should not be required unless explicitly marked
|
|
1524
|
-
if (property.isOptional) {
|
|
1525
|
-
return;
|
|
1526
|
-
}
|
|
1527
|
-
// If property is not optional and not already required, make it required
|
|
1528
|
-
schema.required.push(propertyName);
|
|
772
|
+
const properties = this.getPropertiesByClassDeclaration(result.node);
|
|
773
|
+
schema = this.getSchemaFromProperties({
|
|
774
|
+
properties,
|
|
775
|
+
classDeclaration: result.node,
|
|
776
|
+
});
|
|
777
|
+
return { name: cls.name, schema };
|
|
1529
778
|
}
|
|
1530
779
|
}
|
|
1531
|
-
/**
|
|
1532
|
-
* Convenience function to transform a class using the singleton instance.
|
|
1533
|
-
*
|
|
1534
|
-
* @param cls - The class constructor function to transform
|
|
1535
|
-
* @param options - Optional configuration for memory management
|
|
1536
|
-
* @returns Object containing the class name and its corresponding JSON schema
|
|
1537
|
-
*
|
|
1538
|
-
* @example
|
|
1539
|
-
* ```typescript
|
|
1540
|
-
* import { transform } from 'class-validator-to-open-api'
|
|
1541
|
-
* import { User } from './entities/user.js'
|
|
1542
|
-
*
|
|
1543
|
-
* const schema = transform(User)
|
|
1544
|
-
* console.log(schema)
|
|
1545
|
-
* ```
|
|
1546
|
-
*
|
|
1547
|
-
* @example
|
|
1548
|
-
* ```typescript
|
|
1549
|
-
* // With memory optimization
|
|
1550
|
-
* const schema = transform(User, { maxCacheSize: 50, autoCleanup: true })
|
|
1551
|
-
* ```
|
|
1552
|
-
*
|
|
1553
|
-
* @public
|
|
1554
|
-
*/
|
|
1555
780
|
function transform(cls, options) {
|
|
1556
|
-
|
|
781
|
+
// Use the singleton instance instead of creating a temporary one
|
|
782
|
+
const transformer = SchemaTransformer.getInstance(undefined, options);
|
|
783
|
+
return transformer.transform(cls, options?.sourceOptions);
|
|
1557
784
|
}
|
|
1558
785
|
|
|
1559
|
-
exports.SchemaTransformer = SchemaTransformer;
|
|
1560
786
|
exports.transform = transform;
|