ts-class-to-openapi 1.0.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/LICENSE +21 -0
- package/README.md +959 -0
- package/dist/__test__/entities/address.entity.d.ts +5 -0
- package/dist/__test__/entities/array.entity.d.ts +7 -0
- package/dist/__test__/entities/broken.entity.d.ts +7 -0
- package/dist/__test__/entities/complete.entity.d.ts +16 -0
- package/dist/__test__/entities/generic.entity.d.ts +11 -0
- package/dist/__test__/entities/optional-properties.entity.d.ts +11 -0
- package/dist/__test__/entities/plain.entity.d.ts +19 -0
- package/dist/__test__/entities/simple.entity.d.ts +5 -0
- package/dist/__test__/entities/upload.entity.d.ts +8 -0
- package/dist/__test__/index.d.ts +4 -0
- package/dist/__test__/integration.test.d.ts +1 -0
- package/dist/__test__/main.test.d.ts +1 -0
- package/dist/__test__/optional-properties.test.d.ts +1 -0
- package/dist/__test__/plain.test.d.ts +1 -0
- package/dist/__test__/test-entities/duplicate-name.entity.d.ts +5 -0
- package/dist/__test__/test-entities/generic.entity.d.ts +11 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.esm.js +838 -0
- package/dist/index.js +840 -0
- package/dist/run.d.ts +1 -0
- package/dist/run.js +880 -0
- package/dist/transformer.d.ts +329 -0
- package/dist/transformer.fixtures.d.ts +143 -0
- package/dist/types.d.ts +41 -0
- package/package.json +88 -0
|
@@ -0,0 +1,838 @@
|
|
|
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
|
+
Number: { type: 'Number', value: 'number' },
|
|
8
|
+
Boolean: { type: 'Boolean', value: 'boolean' },
|
|
9
|
+
Symbol: { type: 'Symbol', value: 'symbol' },
|
|
10
|
+
BigInt: { type: 'BigInt', value: 'integer' },
|
|
11
|
+
null: { type: 'null', value: 'null' },
|
|
12
|
+
Object: { type: 'Object', value: 'object' },
|
|
13
|
+
Array: { type: 'Array', value: 'array' },
|
|
14
|
+
Date: { type: 'Date', value: 'string', format: 'date-time' },
|
|
15
|
+
Function: { type: 'Function', value: 'function' },
|
|
16
|
+
Buffer: { type: 'Buffer', value: 'string', format: 'binary' },
|
|
17
|
+
Uint8Array: { type: 'Uint8Array', value: 'string', format: 'binary' },
|
|
18
|
+
UploadFile: { type: 'UploadFile', value: 'string', format: 'binary' },
|
|
19
|
+
File: { type: 'File', value: 'string', format: 'binary' },
|
|
20
|
+
};
|
|
21
|
+
const validatorDecorators = {
|
|
22
|
+
Length: { name: 'Length', type: 'string' },
|
|
23
|
+
MinLength: { name: 'MinLength', type: 'string' },
|
|
24
|
+
MaxLength: { name: 'MaxLength', type: 'string' },
|
|
25
|
+
IsInt: { name: 'IsInt', type: 'integer', format: 'int32' },
|
|
26
|
+
IsNumber: { name: 'IsNumber', type: 'number', format: 'double' },
|
|
27
|
+
IsString: { name: 'IsString', type: 'string', format: 'string' },
|
|
28
|
+
IsPositive: { name: 'IsPositive', type: 'number' },
|
|
29
|
+
IsDate: { name: 'IsDate', type: 'string', format: 'date-time' },
|
|
30
|
+
IsEmail: { name: 'IsEmail', type: 'string', format: 'email' },
|
|
31
|
+
IsNotEmpty: { name: 'IsNotEmpty' },
|
|
32
|
+
IsBoolean: { name: 'IsBoolean', type: 'boolean' },
|
|
33
|
+
IsArray: { name: 'IsArray', type: 'array' },
|
|
34
|
+
Min: { name: 'Min' },
|
|
35
|
+
Max: { name: 'Max' },
|
|
36
|
+
ArrayNotEmpty: { name: 'ArrayNotEmpty' },
|
|
37
|
+
ArrayMaxSize: { name: 'ArrayMaxSize' },
|
|
38
|
+
ArrayMinSize: { name: 'ArrayMinSize' },
|
|
39
|
+
};
|
|
40
|
+
const constants = {
|
|
41
|
+
TS_CONFIG_DEFAULT_PATH,
|
|
42
|
+
jsPrimitives,
|
|
43
|
+
validatorDecorators,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Transforms class-validator decorated classes into OpenAPI schema objects.
|
|
48
|
+
* Analyzes TypeScript source files directly using the TypeScript compiler API.
|
|
49
|
+
* Implemented as a singleton for performance optimization.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```typescript
|
|
53
|
+
* const transformer = SchemaTransformer.getInstance();
|
|
54
|
+
* const schema = transformer.transform(User);
|
|
55
|
+
* console.log(schema);
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* @public
|
|
59
|
+
*/
|
|
60
|
+
class SchemaTransformer {
|
|
61
|
+
/**
|
|
62
|
+
* Singleton instance
|
|
63
|
+
* @private
|
|
64
|
+
*/
|
|
65
|
+
static instance = null;
|
|
66
|
+
/**
|
|
67
|
+
* TypeScript program instance for analyzing source files.
|
|
68
|
+
* @private
|
|
69
|
+
*/
|
|
70
|
+
program;
|
|
71
|
+
/**
|
|
72
|
+
* TypeScript type checker for resolving types.
|
|
73
|
+
* @private
|
|
74
|
+
*/
|
|
75
|
+
checker;
|
|
76
|
+
/**
|
|
77
|
+
* Cache for storing transformed class schemas to avoid reprocessing.
|
|
78
|
+
* Key format: "fileName:className" for uniqueness across different files.
|
|
79
|
+
* @private
|
|
80
|
+
*/
|
|
81
|
+
classCache = new Map();
|
|
82
|
+
/**
|
|
83
|
+
* Maximum number of entries to keep in cache before cleanup
|
|
84
|
+
* @private
|
|
85
|
+
*/
|
|
86
|
+
maxCacheSize;
|
|
87
|
+
/**
|
|
88
|
+
* Whether to automatically clean up cache
|
|
89
|
+
* @private
|
|
90
|
+
*/
|
|
91
|
+
autoCleanup;
|
|
92
|
+
/**
|
|
93
|
+
* Set of file paths that have been loaded to avoid redundant processing
|
|
94
|
+
* @private
|
|
95
|
+
*/
|
|
96
|
+
loadedFiles = new Set();
|
|
97
|
+
/**
|
|
98
|
+
* Private constructor for singleton pattern.
|
|
99
|
+
*
|
|
100
|
+
* @param tsConfigPath - Optional path to a specific TypeScript config file
|
|
101
|
+
* @param options - Configuration options for memory management
|
|
102
|
+
* @throws {Error} When TypeScript configuration cannot be loaded
|
|
103
|
+
* @private
|
|
104
|
+
*/
|
|
105
|
+
constructor(tsConfigPath = constants.TS_CONFIG_DEFAULT_PATH, options = {}) {
|
|
106
|
+
// Initialize configuration with defaults
|
|
107
|
+
this.maxCacheSize = options.maxCacheSize ?? 100;
|
|
108
|
+
this.autoCleanup = options.autoCleanup ?? true;
|
|
109
|
+
const { config, error } = ts.readConfigFile(tsConfigPath || 'tsconfig.json', ts.sys.readFile);
|
|
110
|
+
if (error) {
|
|
111
|
+
console.log(new Error(`Error reading tsconfig file: ${error.messageText}`).message);
|
|
112
|
+
throw new Error(`Error reading tsconfig file: ${error.messageText}`);
|
|
113
|
+
}
|
|
114
|
+
const { options: tsOptions, fileNames } = ts.parseJsonConfigFileContent(config, ts.sys, './');
|
|
115
|
+
this.program = ts.createProgram(fileNames, tsOptions);
|
|
116
|
+
this.checker = this.program.getTypeChecker();
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Generates a unique cache key using file name and class name.
|
|
120
|
+
*
|
|
121
|
+
* @param fileName - The source file name
|
|
122
|
+
* @param className - The class name
|
|
123
|
+
* @returns Unique cache key in format "fileName:className"
|
|
124
|
+
* @private
|
|
125
|
+
*/
|
|
126
|
+
getCacheKey(fileName, className) {
|
|
127
|
+
return `${fileName}:${className}`;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Cleans up cache when it exceeds maximum size to prevent memory leaks.
|
|
131
|
+
* Removes oldest entries using LRU strategy.
|
|
132
|
+
* @private
|
|
133
|
+
*/
|
|
134
|
+
cleanupCache() {
|
|
135
|
+
if (!this.autoCleanup || this.classCache.size <= this.maxCacheSize) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const entries = Array.from(this.classCache.entries());
|
|
139
|
+
const toDelete = entries.slice(0, Math.floor(this.maxCacheSize / 2));
|
|
140
|
+
for (const [key] of toDelete) {
|
|
141
|
+
this.classCache.delete(key);
|
|
142
|
+
}
|
|
143
|
+
// Force garbage collection hint
|
|
144
|
+
if (global.gc) {
|
|
145
|
+
global.gc();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Gets relevant source files for a class, filtering out unnecessary files to save memory.
|
|
150
|
+
*
|
|
151
|
+
* @param className - The name of the class to find files for
|
|
152
|
+
* @param filePath - Optional specific file path
|
|
153
|
+
* @returns Array of relevant source files
|
|
154
|
+
* @private
|
|
155
|
+
*/
|
|
156
|
+
getRelevantSourceFiles(className, filePath) {
|
|
157
|
+
if (filePath) {
|
|
158
|
+
const sourceFile = this.program.getSourceFile(filePath);
|
|
159
|
+
return sourceFile ? [sourceFile] : [];
|
|
160
|
+
}
|
|
161
|
+
// Only get source files that are not declaration files and not in node_modules
|
|
162
|
+
return this.program.getSourceFiles().filter(sf => {
|
|
163
|
+
if (sf.isDeclarationFile)
|
|
164
|
+
return false;
|
|
165
|
+
if (sf.fileName.includes('.d.ts'))
|
|
166
|
+
return false;
|
|
167
|
+
if (sf.fileName.includes('node_modules'))
|
|
168
|
+
return false;
|
|
169
|
+
// Mark file as loaded for memory tracking
|
|
170
|
+
this.loadedFiles.add(sf.fileName);
|
|
171
|
+
return true;
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Transforms a class by its name into an OpenAPI schema object.
|
|
176
|
+
*
|
|
177
|
+
* @param className - The name of the class to transform
|
|
178
|
+
* @param filePath - Optional path to the file containing the class
|
|
179
|
+
* @returns Object containing the class name and its corresponding JSON schema
|
|
180
|
+
* @throws {Error} When the specified class cannot be found
|
|
181
|
+
* @private
|
|
182
|
+
*/
|
|
183
|
+
transformByName(className, filePath) {
|
|
184
|
+
const sourceFiles = this.getRelevantSourceFiles(className, filePath);
|
|
185
|
+
for (const sourceFile of sourceFiles) {
|
|
186
|
+
const classNode = this.findClassByName(sourceFile, className);
|
|
187
|
+
if (classNode && sourceFile?.fileName) {
|
|
188
|
+
const cacheKey = this.getCacheKey(sourceFile.fileName, className);
|
|
189
|
+
// Check cache first using fileName:className as key
|
|
190
|
+
if (this.classCache.has(cacheKey)) {
|
|
191
|
+
return this.classCache.get(cacheKey);
|
|
192
|
+
}
|
|
193
|
+
const result = this.transformClass(classNode);
|
|
194
|
+
// Cache using fileName:className as key for uniqueness
|
|
195
|
+
this.classCache.set(cacheKey, result);
|
|
196
|
+
// Clean up cache if it gets too large
|
|
197
|
+
this.cleanupCache();
|
|
198
|
+
return result;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
throw new Error(`Class ${className} not found`);
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Gets the singleton instance of SchemaTransformer.
|
|
205
|
+
*
|
|
206
|
+
* @param tsConfigPath - Optional path to a specific TypeScript config file (only used on first call)
|
|
207
|
+
* @param options - Configuration options for memory management (only used on first call)
|
|
208
|
+
* @returns The singleton instance
|
|
209
|
+
*
|
|
210
|
+
* @example
|
|
211
|
+
* ```typescript
|
|
212
|
+
* const transformer = SchemaTransformer.getInstance();
|
|
213
|
+
* ```
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* ```typescript
|
|
217
|
+
* // With memory optimization options
|
|
218
|
+
* const transformer = SchemaTransformer.getInstance('./tsconfig.json', {
|
|
219
|
+
* maxCacheSize: 50,
|
|
220
|
+
* autoCleanup: true
|
|
221
|
+
* });
|
|
222
|
+
* ```
|
|
223
|
+
*
|
|
224
|
+
* @public
|
|
225
|
+
*/
|
|
226
|
+
/**
|
|
227
|
+
* Clears the current singleton instance. Useful for testing or when you need
|
|
228
|
+
* to create a new instance with different configuration.
|
|
229
|
+
*/
|
|
230
|
+
static clearInstance() {
|
|
231
|
+
SchemaTransformer.instance = null;
|
|
232
|
+
}
|
|
233
|
+
static getInstance(tsConfigPath, options) {
|
|
234
|
+
if (!SchemaTransformer.instance) {
|
|
235
|
+
SchemaTransformer.instance = new SchemaTransformer(tsConfigPath, options);
|
|
236
|
+
}
|
|
237
|
+
return SchemaTransformer.instance;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Transforms a class constructor function into an OpenAPI schema object.
|
|
241
|
+
*
|
|
242
|
+
* @param cls - The class constructor function to transform
|
|
243
|
+
* @returns Object containing the class name and its corresponding JSON schema
|
|
244
|
+
*
|
|
245
|
+
* @example
|
|
246
|
+
* ```typescript
|
|
247
|
+
* import { User } from './entities/user.js';
|
|
248
|
+
* const transformer = SchemaTransformer.getInstance();
|
|
249
|
+
* const schema = transformer.transform(User);
|
|
250
|
+
* ```
|
|
251
|
+
*
|
|
252
|
+
* @public
|
|
253
|
+
*/
|
|
254
|
+
transform(cls) {
|
|
255
|
+
return this.transformByName(cls.name);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Clears all cached schemas and loaded file references to free memory.
|
|
259
|
+
* Useful for long-running applications or when processing many different classes.
|
|
260
|
+
*
|
|
261
|
+
* @example
|
|
262
|
+
* ```typescript
|
|
263
|
+
* const transformer = SchemaTransformer.getInstance();
|
|
264
|
+
* // After processing many classes...
|
|
265
|
+
* transformer.clearCache();
|
|
266
|
+
* ```
|
|
267
|
+
*
|
|
268
|
+
* @public
|
|
269
|
+
*/
|
|
270
|
+
clearCache() {
|
|
271
|
+
this.classCache.clear();
|
|
272
|
+
this.loadedFiles.clear();
|
|
273
|
+
// Force garbage collection hint if available
|
|
274
|
+
if (global.gc) {
|
|
275
|
+
global.gc();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Gets memory usage statistics for monitoring and debugging.
|
|
280
|
+
*
|
|
281
|
+
* @returns Object containing cache size and loaded files count
|
|
282
|
+
*
|
|
283
|
+
* @example
|
|
284
|
+
* ```typescript
|
|
285
|
+
* const transformer = SchemaTransformer.getInstance();
|
|
286
|
+
* const stats = transformer.getMemoryStats();
|
|
287
|
+
* console.log(`Cache entries: ${stats.cacheSize}, Files loaded: ${stats.loadedFiles}`);
|
|
288
|
+
* ```
|
|
289
|
+
*
|
|
290
|
+
* @public
|
|
291
|
+
*/
|
|
292
|
+
getMemoryStats() {
|
|
293
|
+
return {
|
|
294
|
+
cacheSize: this.classCache.size,
|
|
295
|
+
loadedFiles: this.loadedFiles.size,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Finds a class declaration by name within a source file.
|
|
300
|
+
*
|
|
301
|
+
* @param sourceFile - The TypeScript source file to search in
|
|
302
|
+
* @param className - The name of the class to find
|
|
303
|
+
* @returns The class declaration node if found, undefined otherwise
|
|
304
|
+
* @private
|
|
305
|
+
*/
|
|
306
|
+
findClassByName(sourceFile, className) {
|
|
307
|
+
let result;
|
|
308
|
+
const visit = (node) => {
|
|
309
|
+
if (ts.isClassDeclaration(node) && node.name?.text === className) {
|
|
310
|
+
result = node;
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
ts.forEachChild(node, visit);
|
|
314
|
+
};
|
|
315
|
+
visit(sourceFile);
|
|
316
|
+
return result;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Transforms a TypeScript class declaration into a schema object.
|
|
320
|
+
*
|
|
321
|
+
* @param classNode - The TypeScript class declaration node
|
|
322
|
+
* @returns Object containing class name and generated schema
|
|
323
|
+
* @private
|
|
324
|
+
*/
|
|
325
|
+
transformClass(classNode) {
|
|
326
|
+
const className = classNode.name?.text || 'Unknown';
|
|
327
|
+
const properties = this.extractProperties(classNode);
|
|
328
|
+
const schema = this.generateSchema(properties);
|
|
329
|
+
return { name: className, schema };
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Extracts property information from a class declaration.
|
|
333
|
+
*
|
|
334
|
+
* @param classNode - The TypeScript class declaration node
|
|
335
|
+
* @returns Array of property information including names, types, decorators, and optional status
|
|
336
|
+
* @private
|
|
337
|
+
*/
|
|
338
|
+
extractProperties(classNode) {
|
|
339
|
+
const properties = [];
|
|
340
|
+
for (const member of classNode.members) {
|
|
341
|
+
if (ts.isPropertyDeclaration(member) &&
|
|
342
|
+
member.name &&
|
|
343
|
+
ts.isIdentifier(member.name)) {
|
|
344
|
+
const propertyName = member.name.text;
|
|
345
|
+
const type = this.getPropertyType(member);
|
|
346
|
+
const decorators = this.extractDecorators(member);
|
|
347
|
+
const isOptional = !!member.questionToken;
|
|
348
|
+
properties.push({
|
|
349
|
+
name: propertyName,
|
|
350
|
+
type,
|
|
351
|
+
decorators,
|
|
352
|
+
isOptional,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return properties;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Gets the TypeScript type of a property as a string.
|
|
360
|
+
*
|
|
361
|
+
* @param property - The property declaration to analyze
|
|
362
|
+
* @returns String representation of the property's type
|
|
363
|
+
* @private
|
|
364
|
+
*/
|
|
365
|
+
getPropertyType(property) {
|
|
366
|
+
if (property.type) {
|
|
367
|
+
return this.getTypeNodeToString(property.type);
|
|
368
|
+
}
|
|
369
|
+
const type = this.checker.getTypeAtLocation(property);
|
|
370
|
+
return this.checker.typeToString(type);
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Converts a TypeScript type node to its string representation.
|
|
374
|
+
*
|
|
375
|
+
* @param typeNode - The TypeScript type node to convert
|
|
376
|
+
* @returns String representation of the type
|
|
377
|
+
* @private
|
|
378
|
+
*/
|
|
379
|
+
getTypeNodeToString(typeNode) {
|
|
380
|
+
if (ts.isTypeReferenceNode(typeNode) &&
|
|
381
|
+
ts.isIdentifier(typeNode.typeName)) {
|
|
382
|
+
if (typeNode.typeName.text.toLowerCase().includes('uploadfile')) {
|
|
383
|
+
return 'UploadFile';
|
|
384
|
+
}
|
|
385
|
+
if (typeNode.typeArguments && typeNode.typeArguments.length > 0) {
|
|
386
|
+
const firstTypeArg = typeNode.typeArguments[0];
|
|
387
|
+
if (firstTypeArg &&
|
|
388
|
+
ts.isTypeReferenceNode(firstTypeArg) &&
|
|
389
|
+
ts.isIdentifier(firstTypeArg.typeName)) {
|
|
390
|
+
if (firstTypeArg.typeName.text.toLowerCase().includes('uploadfile')) {
|
|
391
|
+
return 'UploadFile';
|
|
392
|
+
}
|
|
393
|
+
if (typeNode.typeName.text === 'BaseDto') {
|
|
394
|
+
return firstTypeArg.typeName.text;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return typeNode.typeName.text;
|
|
399
|
+
}
|
|
400
|
+
switch (typeNode.kind) {
|
|
401
|
+
case ts.SyntaxKind.StringKeyword:
|
|
402
|
+
return constants.jsPrimitives.String.type;
|
|
403
|
+
case ts.SyntaxKind.NumberKeyword:
|
|
404
|
+
return constants.jsPrimitives.Number.type;
|
|
405
|
+
case ts.SyntaxKind.BooleanKeyword:
|
|
406
|
+
return constants.jsPrimitives.Boolean.type;
|
|
407
|
+
case ts.SyntaxKind.ArrayType:
|
|
408
|
+
const arrayType = typeNode;
|
|
409
|
+
return `${this.getTypeNodeToString(arrayType.elementType)}[]`;
|
|
410
|
+
case ts.SyntaxKind.UnionType:
|
|
411
|
+
// Handle union types like string | null
|
|
412
|
+
const unionType = typeNode;
|
|
413
|
+
const types = unionType.types.map(t => this.getTypeNodeToString(t));
|
|
414
|
+
// Filter out null and undefined, return the first meaningful type
|
|
415
|
+
const meaningfulTypes = types.filter(t => t !== 'null' && t !== 'undefined');
|
|
416
|
+
if (meaningfulTypes.length > 0 && meaningfulTypes[0]) {
|
|
417
|
+
return meaningfulTypes[0];
|
|
418
|
+
}
|
|
419
|
+
if (types.length > 0 && types[0]) {
|
|
420
|
+
return types[0];
|
|
421
|
+
}
|
|
422
|
+
return 'object';
|
|
423
|
+
default:
|
|
424
|
+
const typeText = typeNode.getText();
|
|
425
|
+
// Handle some common TypeScript utility types
|
|
426
|
+
if (typeText.startsWith('Date'))
|
|
427
|
+
return constants.jsPrimitives.Date.type;
|
|
428
|
+
if (typeText.includes('Buffer') || typeText.includes('Uint8Array'))
|
|
429
|
+
return constants.jsPrimitives.Buffer.type;
|
|
430
|
+
return typeText;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Extracts decorator information from a property declaration.
|
|
435
|
+
*
|
|
436
|
+
* @param member - The property declaration to analyze
|
|
437
|
+
* @returns Array of decorator information including names and arguments
|
|
438
|
+
* @private
|
|
439
|
+
*/
|
|
440
|
+
extractDecorators(member) {
|
|
441
|
+
const decorators = [];
|
|
442
|
+
if (member.modifiers) {
|
|
443
|
+
for (const modifier of member.modifiers) {
|
|
444
|
+
if (ts.isDecorator(modifier) &&
|
|
445
|
+
ts.isCallExpression(modifier.expression)) {
|
|
446
|
+
const decoratorName = this.getDecoratorName(modifier.expression);
|
|
447
|
+
const args = this.getDecoratorArguments(modifier.expression);
|
|
448
|
+
decorators.push({ name: decoratorName, arguments: args });
|
|
449
|
+
}
|
|
450
|
+
else if (ts.isDecorator(modifier) &&
|
|
451
|
+
ts.isIdentifier(modifier.expression)) {
|
|
452
|
+
decorators.push({ name: modifier.expression.text, arguments: [] });
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return decorators;
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Gets the name of a decorator from a call expression.
|
|
460
|
+
*
|
|
461
|
+
* @param callExpression - The decorator call expression
|
|
462
|
+
* @returns The decorator name or "unknown" if not identifiable
|
|
463
|
+
* @private
|
|
464
|
+
*/
|
|
465
|
+
getDecoratorName(callExpression) {
|
|
466
|
+
if (ts.isIdentifier(callExpression.expression)) {
|
|
467
|
+
return callExpression.expression.text;
|
|
468
|
+
}
|
|
469
|
+
return 'unknown';
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Extracts arguments from a decorator call expression.
|
|
473
|
+
*
|
|
474
|
+
* @param callExpression - The decorator call expression
|
|
475
|
+
* @returns Array of parsed decorator arguments
|
|
476
|
+
* @private
|
|
477
|
+
*/
|
|
478
|
+
getDecoratorArguments(callExpression) {
|
|
479
|
+
return callExpression.arguments.map(arg => {
|
|
480
|
+
if (ts.isNumericLiteral(arg))
|
|
481
|
+
return Number(arg.text);
|
|
482
|
+
if (ts.isStringLiteral(arg))
|
|
483
|
+
return arg.text;
|
|
484
|
+
if (arg.kind === ts.SyntaxKind.TrueKeyword)
|
|
485
|
+
return true;
|
|
486
|
+
if (arg.kind === ts.SyntaxKind.FalseKeyword)
|
|
487
|
+
return false;
|
|
488
|
+
return arg.getText();
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Generates an OpenAPI schema from extracted property information.
|
|
493
|
+
*
|
|
494
|
+
* @param properties - Array of property information to process
|
|
495
|
+
* @returns Complete OpenAPI schema object with properties and validation rules
|
|
496
|
+
* @private
|
|
497
|
+
*/
|
|
498
|
+
generateSchema(properties) {
|
|
499
|
+
const schema = {
|
|
500
|
+
type: 'object',
|
|
501
|
+
properties: {},
|
|
502
|
+
required: [],
|
|
503
|
+
};
|
|
504
|
+
for (const property of properties) {
|
|
505
|
+
const { type, format, nestedSchema } = this.mapTypeToSchema(property.type);
|
|
506
|
+
if (nestedSchema) {
|
|
507
|
+
schema.properties[property.name] = nestedSchema;
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
schema.properties[property.name] = { type };
|
|
511
|
+
if (format)
|
|
512
|
+
schema.properties[property.name].format = format;
|
|
513
|
+
}
|
|
514
|
+
// Apply decorators if present
|
|
515
|
+
this.applyDecorators(property.decorators, schema, property.name);
|
|
516
|
+
// If no decorators are present, apply sensible defaults based on TypeScript types
|
|
517
|
+
if (property.decorators.length === 0) {
|
|
518
|
+
this.applySensibleDefaults(property, schema);
|
|
519
|
+
}
|
|
520
|
+
// Determine if property should be required based on decorators and optional status
|
|
521
|
+
this.determineRequiredStatus(property, schema);
|
|
522
|
+
}
|
|
523
|
+
return schema;
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Maps TypeScript types to OpenAPI schema types and formats.
|
|
527
|
+
* Handles primitive types, arrays, and nested objects recursively.
|
|
528
|
+
*
|
|
529
|
+
* @param type - The TypeScript type string to map
|
|
530
|
+
* @returns Object containing OpenAPI type, optional format, and nested schema
|
|
531
|
+
* @private
|
|
532
|
+
*/
|
|
533
|
+
mapTypeToSchema(type) {
|
|
534
|
+
// Handle arrays
|
|
535
|
+
if (type.endsWith('[]')) {
|
|
536
|
+
const elementType = type.slice(0, -2);
|
|
537
|
+
const elementSchema = this.mapTypeToSchema(elementType);
|
|
538
|
+
const items = elementSchema.nestedSchema || {
|
|
539
|
+
type: elementSchema.type,
|
|
540
|
+
};
|
|
541
|
+
if (elementSchema.format)
|
|
542
|
+
items.format = elementSchema.format;
|
|
543
|
+
return {
|
|
544
|
+
type: 'array',
|
|
545
|
+
nestedSchema: {
|
|
546
|
+
type: 'array',
|
|
547
|
+
items,
|
|
548
|
+
properties: {},
|
|
549
|
+
required: [],
|
|
550
|
+
},
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
if (type.toLocaleLowerCase().includes('uploadfile'))
|
|
554
|
+
type = 'UploadFile';
|
|
555
|
+
// Handle primitives
|
|
556
|
+
switch (type.toLowerCase()) {
|
|
557
|
+
case constants.jsPrimitives.String.type.toLowerCase():
|
|
558
|
+
return { type: constants.jsPrimitives.String.value };
|
|
559
|
+
case constants.jsPrimitives.Number.type.toLowerCase():
|
|
560
|
+
return { type: constants.jsPrimitives.Number.value };
|
|
561
|
+
case constants.jsPrimitives.Boolean.type.toLowerCase():
|
|
562
|
+
return { type: constants.jsPrimitives.Boolean.value };
|
|
563
|
+
case constants.jsPrimitives.Date.type.toLowerCase():
|
|
564
|
+
return {
|
|
565
|
+
type: constants.jsPrimitives.Date.value,
|
|
566
|
+
format: constants.jsPrimitives.Date.format,
|
|
567
|
+
};
|
|
568
|
+
case constants.jsPrimitives.Buffer.type.toLowerCase():
|
|
569
|
+
case constants.jsPrimitives.Uint8Array.type.toLowerCase():
|
|
570
|
+
case constants.jsPrimitives.File.type.toLowerCase():
|
|
571
|
+
return {
|
|
572
|
+
type: constants.jsPrimitives.Buffer.value,
|
|
573
|
+
format: constants.jsPrimitives.Buffer.format,
|
|
574
|
+
};
|
|
575
|
+
case constants.jsPrimitives.UploadFile.type.toLowerCase():
|
|
576
|
+
return {
|
|
577
|
+
type: constants.jsPrimitives.UploadFile.value,
|
|
578
|
+
format: constants.jsPrimitives.UploadFile.format,
|
|
579
|
+
};
|
|
580
|
+
default:
|
|
581
|
+
// Handle nested objects
|
|
582
|
+
try {
|
|
583
|
+
const nestedResult = this.transformByName(type);
|
|
584
|
+
return {
|
|
585
|
+
type: constants.jsPrimitives.Object.value,
|
|
586
|
+
nestedSchema: nestedResult.schema,
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
catch {
|
|
590
|
+
return { type: constants.jsPrimitives.Object.value };
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Applies class-validator decorators to schema properties.
|
|
596
|
+
* Maps validation decorators to their corresponding OpenAPI schema constraints.
|
|
597
|
+
*
|
|
598
|
+
* @param decorators - Array of decorator information to apply
|
|
599
|
+
* @param schema - The schema object to modify
|
|
600
|
+
* @param propertyName - Name of the property being processed
|
|
601
|
+
* @private
|
|
602
|
+
*/
|
|
603
|
+
applyDecorators(decorators, schema, propertyName) {
|
|
604
|
+
const isArrayType = schema.properties[propertyName].type ===
|
|
605
|
+
constants.jsPrimitives.Array.value;
|
|
606
|
+
for (const decorator of decorators) {
|
|
607
|
+
const decoratorName = decorator.name;
|
|
608
|
+
switch (decoratorName) {
|
|
609
|
+
case constants.validatorDecorators.IsString.name:
|
|
610
|
+
if (!isArrayType) {
|
|
611
|
+
schema.properties[propertyName].type =
|
|
612
|
+
constants.validatorDecorators.IsString.type;
|
|
613
|
+
}
|
|
614
|
+
else if (schema.properties[propertyName].items) {
|
|
615
|
+
schema.properties[propertyName].items.type =
|
|
616
|
+
constants.validatorDecorators.IsString.type;
|
|
617
|
+
}
|
|
618
|
+
break;
|
|
619
|
+
case constants.validatorDecorators.IsInt.name:
|
|
620
|
+
if (!isArrayType) {
|
|
621
|
+
schema.properties[propertyName].type =
|
|
622
|
+
constants.validatorDecorators.IsInt.type;
|
|
623
|
+
schema.properties[propertyName].format =
|
|
624
|
+
constants.validatorDecorators.IsInt.format;
|
|
625
|
+
}
|
|
626
|
+
else if (schema.properties[propertyName].items) {
|
|
627
|
+
schema.properties[propertyName].items.type =
|
|
628
|
+
constants.validatorDecorators.IsInt.type;
|
|
629
|
+
schema.properties[propertyName].items.format =
|
|
630
|
+
constants.validatorDecorators.IsInt.format;
|
|
631
|
+
}
|
|
632
|
+
break;
|
|
633
|
+
case constants.validatorDecorators.IsNumber.name:
|
|
634
|
+
if (!isArrayType) {
|
|
635
|
+
schema.properties[propertyName].type =
|
|
636
|
+
constants.validatorDecorators.IsNumber.type;
|
|
637
|
+
}
|
|
638
|
+
else if (schema.properties[propertyName].items) {
|
|
639
|
+
schema.properties[propertyName].items.type =
|
|
640
|
+
constants.validatorDecorators.IsNumber.type;
|
|
641
|
+
}
|
|
642
|
+
break;
|
|
643
|
+
case constants.validatorDecorators.IsBoolean.name:
|
|
644
|
+
if (!isArrayType) {
|
|
645
|
+
schema.properties[propertyName].type =
|
|
646
|
+
constants.validatorDecorators.IsBoolean.type;
|
|
647
|
+
}
|
|
648
|
+
else if (schema.properties[propertyName].items) {
|
|
649
|
+
schema.properties[propertyName].items.type =
|
|
650
|
+
constants.validatorDecorators.IsBoolean.type;
|
|
651
|
+
}
|
|
652
|
+
break;
|
|
653
|
+
case constants.validatorDecorators.IsEmail.name:
|
|
654
|
+
if (!isArrayType) {
|
|
655
|
+
schema.properties[propertyName].format =
|
|
656
|
+
constants.validatorDecorators.IsEmail.format;
|
|
657
|
+
}
|
|
658
|
+
else if (schema.properties[propertyName].items) {
|
|
659
|
+
schema.properties[propertyName].items.format =
|
|
660
|
+
constants.validatorDecorators.IsEmail.format;
|
|
661
|
+
}
|
|
662
|
+
break;
|
|
663
|
+
case constants.validatorDecorators.IsDate.name:
|
|
664
|
+
if (!isArrayType) {
|
|
665
|
+
schema.properties[propertyName].type =
|
|
666
|
+
constants.validatorDecorators.IsDate.type;
|
|
667
|
+
schema.properties[propertyName].format =
|
|
668
|
+
constants.validatorDecorators.IsDate.format;
|
|
669
|
+
}
|
|
670
|
+
else if (schema.properties[propertyName].items) {
|
|
671
|
+
schema.properties[propertyName].items.type =
|
|
672
|
+
constants.validatorDecorators.IsDate.type;
|
|
673
|
+
schema.properties[propertyName].items.format =
|
|
674
|
+
constants.validatorDecorators.IsDate.format;
|
|
675
|
+
}
|
|
676
|
+
break;
|
|
677
|
+
case constants.validatorDecorators.IsNotEmpty.name:
|
|
678
|
+
if (!schema.required.includes(propertyName)) {
|
|
679
|
+
schema.required.push(propertyName);
|
|
680
|
+
}
|
|
681
|
+
break;
|
|
682
|
+
case constants.validatorDecorators.MinLength.name:
|
|
683
|
+
schema.properties[propertyName].minLength = decorator.arguments[0];
|
|
684
|
+
break;
|
|
685
|
+
case constants.validatorDecorators.MaxLength.name:
|
|
686
|
+
schema.properties[propertyName].maxLength = decorator.arguments[0];
|
|
687
|
+
break;
|
|
688
|
+
case constants.validatorDecorators.Length.name:
|
|
689
|
+
schema.properties[propertyName].minLength = decorator.arguments[0];
|
|
690
|
+
if (decorator.arguments[1]) {
|
|
691
|
+
schema.properties[propertyName].maxLength = decorator.arguments[1];
|
|
692
|
+
}
|
|
693
|
+
break;
|
|
694
|
+
case constants.validatorDecorators.Min.name:
|
|
695
|
+
schema.properties[propertyName].minimum = decorator.arguments[0];
|
|
696
|
+
break;
|
|
697
|
+
case constants.validatorDecorators.Max.name:
|
|
698
|
+
schema.properties[propertyName].maximum = decorator.arguments[0];
|
|
699
|
+
break;
|
|
700
|
+
case constants.validatorDecorators.IsPositive.name:
|
|
701
|
+
schema.properties[propertyName].minimum = 0;
|
|
702
|
+
break;
|
|
703
|
+
case constants.validatorDecorators.IsArray.name:
|
|
704
|
+
schema.properties[propertyName].type =
|
|
705
|
+
constants.jsPrimitives.Array.value;
|
|
706
|
+
break;
|
|
707
|
+
case constants.validatorDecorators.ArrayNotEmpty.name:
|
|
708
|
+
schema.properties[propertyName].minItems = 1;
|
|
709
|
+
if (!schema.required.includes(propertyName)) {
|
|
710
|
+
schema.required.push(propertyName);
|
|
711
|
+
}
|
|
712
|
+
break;
|
|
713
|
+
case constants.validatorDecorators.ArrayMinSize.name:
|
|
714
|
+
schema.properties[propertyName].minItems = decorator.arguments[0];
|
|
715
|
+
break;
|
|
716
|
+
case constants.validatorDecorators.ArrayMaxSize.name:
|
|
717
|
+
schema.properties[propertyName].maxItems = decorator.arguments[0];
|
|
718
|
+
break;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Applies sensible default behaviors for properties without class-validator decorators.
|
|
724
|
+
* This allows the schema generator to work with plain TypeScript classes.
|
|
725
|
+
*
|
|
726
|
+
* @param property - The property information
|
|
727
|
+
* @param schema - The schema object to modify
|
|
728
|
+
* @private
|
|
729
|
+
*/
|
|
730
|
+
applySensibleDefaults(property, schema) {
|
|
731
|
+
const propertyName = property.name;
|
|
732
|
+
property.type.toLowerCase();
|
|
733
|
+
// Add examples based on property names and types
|
|
734
|
+
const propertySchema = schema.properties[propertyName];
|
|
735
|
+
// Add common format hints based on property names
|
|
736
|
+
if (propertyName.includes('email') && propertySchema.type === 'string') {
|
|
737
|
+
propertySchema.format = 'email';
|
|
738
|
+
}
|
|
739
|
+
else if (propertyName.includes('password') &&
|
|
740
|
+
propertySchema.type === 'string') {
|
|
741
|
+
propertySchema.format = 'password';
|
|
742
|
+
propertySchema.minLength = 8;
|
|
743
|
+
}
|
|
744
|
+
else if (propertyName.includes('url') &&
|
|
745
|
+
propertySchema.type === 'string') {
|
|
746
|
+
propertySchema.format = 'uri';
|
|
747
|
+
}
|
|
748
|
+
else if (propertyName.includes('phone') &&
|
|
749
|
+
propertySchema.type === 'string') {
|
|
750
|
+
propertySchema.pattern = '^[+]?[1-9]\\d{1,14}$';
|
|
751
|
+
}
|
|
752
|
+
// Add reasonable constraints based on common property names
|
|
753
|
+
if (propertySchema.type === 'string') {
|
|
754
|
+
if (propertyName === 'name' ||
|
|
755
|
+
propertyName === 'firstName' ||
|
|
756
|
+
propertyName === 'lastName') {
|
|
757
|
+
propertySchema.minLength = 1;
|
|
758
|
+
propertySchema.maxLength = 100;
|
|
759
|
+
}
|
|
760
|
+
else if (propertyName === 'description' || propertyName === 'bio') {
|
|
761
|
+
propertySchema.maxLength = 500;
|
|
762
|
+
}
|
|
763
|
+
else if (propertyName === 'title') {
|
|
764
|
+
propertySchema.minLength = 1;
|
|
765
|
+
propertySchema.maxLength = 200;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
if (propertySchema.type === 'integer' || propertySchema.type === 'number') {
|
|
769
|
+
if (propertyName === 'age') {
|
|
770
|
+
propertySchema.minimum = 0;
|
|
771
|
+
propertySchema.maximum = 150;
|
|
772
|
+
}
|
|
773
|
+
else if (propertyName === 'id') {
|
|
774
|
+
propertySchema.minimum = 1;
|
|
775
|
+
}
|
|
776
|
+
else if (propertyName.includes('count') ||
|
|
777
|
+
propertyName.includes('quantity')) {
|
|
778
|
+
propertySchema.minimum = 0;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Determines if a property should be required based on decorators and optional status.
|
|
784
|
+
*
|
|
785
|
+
* Logic:
|
|
786
|
+
* - If property has IsNotEmpty or ArrayNotEmpty decorator, it's required (handled in applyDecorators)
|
|
787
|
+
* - Otherwise, the property is not required (preserving original behavior)
|
|
788
|
+
* - The isOptional information is stored for future use and documentation
|
|
789
|
+
*
|
|
790
|
+
* @param property - The property information
|
|
791
|
+
* @param schema - The schema object to modify
|
|
792
|
+
* @private
|
|
793
|
+
*/
|
|
794
|
+
determineRequiredStatus(property, schema) {
|
|
795
|
+
const propertyName = property.name;
|
|
796
|
+
// Check if already marked as required by IsNotEmpty or ArrayNotEmpty decorator
|
|
797
|
+
const isAlreadyRequired = schema.required.includes(propertyName);
|
|
798
|
+
// If already required by decorators, don't change it
|
|
799
|
+
if (isAlreadyRequired) {
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
// If property is optional (has ?), it should not be required unless explicitly marked
|
|
803
|
+
if (property.isOptional) {
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
// If property is not optional and not already required, make it required
|
|
807
|
+
schema.required.push(propertyName);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Convenience function to transform a class using the singleton instance.
|
|
812
|
+
*
|
|
813
|
+
* @param cls - The class constructor function to transform
|
|
814
|
+
* @param options - Optional configuration for memory management
|
|
815
|
+
* @returns Object containing the class name and its corresponding JSON schema
|
|
816
|
+
*
|
|
817
|
+
* @example
|
|
818
|
+
* ```typescript
|
|
819
|
+
* import { transform } from 'class-validator-to-open-api'
|
|
820
|
+
* import { User } from './entities/user.js'
|
|
821
|
+
*
|
|
822
|
+
* const schema = transform(User)
|
|
823
|
+
* console.log(schema)
|
|
824
|
+
* ```
|
|
825
|
+
*
|
|
826
|
+
* @example
|
|
827
|
+
* ```typescript
|
|
828
|
+
* // With memory optimization
|
|
829
|
+
* const schema = transform(User, { maxCacheSize: 50, autoCleanup: true })
|
|
830
|
+
* ```
|
|
831
|
+
*
|
|
832
|
+
* @public
|
|
833
|
+
*/
|
|
834
|
+
function transform(cls, options) {
|
|
835
|
+
return SchemaTransformer.getInstance(undefined, options).transform(cls);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
export { transform };
|