ts-class-to-openapi 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -1
- package/dist/index.esm.js +817 -0
- package/dist/index.js +819 -0
- package/dist/run.js +859 -0
- package/dist/transformer.d.ts +10 -1
- package/dist/transformer.fixtures.d.ts +2 -0
- package/package.json +1 -1
- package/dist/test.js +0 -1657
- package/dist/test.js.map +0 -1
package/dist/test.js
DELETED
|
@@ -1,1657 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
var node_test = require('node:test');
|
|
4
|
-
var assert = require('node:assert');
|
|
5
|
-
var ts = require('typescript');
|
|
6
|
-
var path = require('path');
|
|
7
|
-
var classValidator = require('class-validator');
|
|
8
|
-
|
|
9
|
-
/******************************************************************************
|
|
10
|
-
Copyright (c) Microsoft Corporation.
|
|
11
|
-
|
|
12
|
-
Permission to use, copy, modify, and/or distribute this software for any
|
|
13
|
-
purpose with or without fee is hereby granted.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
16
|
-
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
17
|
-
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
18
|
-
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
19
|
-
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
20
|
-
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
21
|
-
PERFORMANCE OF THIS SOFTWARE.
|
|
22
|
-
***************************************************************************** */
|
|
23
|
-
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
function __decorate(decorators, target, key, desc) {
|
|
27
|
-
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
28
|
-
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
29
|
-
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
30
|
-
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
34
|
-
var e = new Error(message);
|
|
35
|
-
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const TS_CONFIG_DEFAULT_PATH = path.resolve(process.cwd(), 'tsconfig.json');
|
|
39
|
-
const jsPrimitives = {
|
|
40
|
-
String: { type: 'String', value: 'string' },
|
|
41
|
-
Number: { type: 'Number', value: 'number' },
|
|
42
|
-
Boolean: { type: 'Boolean', value: 'boolean' },
|
|
43
|
-
Symbol: { type: 'Symbol', value: 'symbol' },
|
|
44
|
-
BigInt: { type: 'BigInt', value: 'integer' },
|
|
45
|
-
null: { type: 'null', value: 'null' },
|
|
46
|
-
Object: { type: 'Object', value: 'object' },
|
|
47
|
-
Array: { type: 'Array', value: 'array' },
|
|
48
|
-
Date: { type: 'Date', value: 'string', format: 'date-time' },
|
|
49
|
-
Function: { type: 'Function', value: 'function' },
|
|
50
|
-
Buffer: { type: 'Buffer', value: 'string', format: 'binary' },
|
|
51
|
-
Uint8Array: { type: 'Uint8Array', value: 'string', format: 'binary' },
|
|
52
|
-
UploadFile: { type: 'UploadFile', value: 'string', format: 'binary' },
|
|
53
|
-
File: { type: 'File', value: 'string', format: 'binary' },
|
|
54
|
-
};
|
|
55
|
-
const validatorDecorators = {
|
|
56
|
-
Length: { name: 'Length', type: 'string' },
|
|
57
|
-
MinLength: { name: 'MinLength', type: 'string' },
|
|
58
|
-
MaxLength: { name: 'MaxLength', type: 'string' },
|
|
59
|
-
IsInt: { name: 'IsInt', type: 'integer', format: 'int32' },
|
|
60
|
-
IsNumber: { name: 'IsNumber', type: 'number', format: 'double' },
|
|
61
|
-
IsString: { name: 'IsString', type: 'string', format: 'string' },
|
|
62
|
-
IsPositive: { name: 'IsPositive', type: 'number' },
|
|
63
|
-
IsDate: { name: 'IsDate', type: 'string', format: 'date-time' },
|
|
64
|
-
IsEmail: { name: 'IsEmail', type: 'string', format: 'email' },
|
|
65
|
-
IsNotEmpty: { name: 'IsNotEmpty' },
|
|
66
|
-
IsBoolean: { name: 'IsBoolean', type: 'boolean' },
|
|
67
|
-
IsArray: { name: 'IsArray', type: 'array' },
|
|
68
|
-
Min: { name: 'Min' },
|
|
69
|
-
Max: { name: 'Max' },
|
|
70
|
-
ArrayNotEmpty: { name: 'ArrayNotEmpty' },
|
|
71
|
-
ArrayMaxSize: { name: 'ArrayMaxSize' },
|
|
72
|
-
ArrayMinSize: { name: 'ArrayMinSize' },
|
|
73
|
-
};
|
|
74
|
-
const constants = {
|
|
75
|
-
TS_CONFIG_DEFAULT_PATH,
|
|
76
|
-
jsPrimitives,
|
|
77
|
-
validatorDecorators,
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Transforms class-validator decorated classes into OpenAPI schema objects.
|
|
82
|
-
* Analyzes TypeScript source files directly using the TypeScript compiler API.
|
|
83
|
-
* Implemented as a singleton for performance optimization.
|
|
84
|
-
*
|
|
85
|
-
* @example
|
|
86
|
-
* ```typescript
|
|
87
|
-
* const transformer = SchemaTransformer.getInstance();
|
|
88
|
-
* const schema = transformer.transform(User);
|
|
89
|
-
* console.log(schema);
|
|
90
|
-
* ```
|
|
91
|
-
*
|
|
92
|
-
* @public
|
|
93
|
-
*/
|
|
94
|
-
class SchemaTransformer {
|
|
95
|
-
/**
|
|
96
|
-
* Singleton instance
|
|
97
|
-
* @private
|
|
98
|
-
*/
|
|
99
|
-
static instance = null;
|
|
100
|
-
/**
|
|
101
|
-
* TypeScript program instance for analyzing source files.
|
|
102
|
-
* @private
|
|
103
|
-
*/
|
|
104
|
-
program;
|
|
105
|
-
/**
|
|
106
|
-
* TypeScript type checker for resolving types.
|
|
107
|
-
* @private
|
|
108
|
-
*/
|
|
109
|
-
checker;
|
|
110
|
-
/**
|
|
111
|
-
* Cache for storing transformed class schemas to avoid reprocessing.
|
|
112
|
-
* Key format: "fileName:className" for uniqueness across different files.
|
|
113
|
-
* @private
|
|
114
|
-
*/
|
|
115
|
-
classCache = new Map();
|
|
116
|
-
/**
|
|
117
|
-
* Maximum number of entries to keep in cache before cleanup
|
|
118
|
-
* @private
|
|
119
|
-
*/
|
|
120
|
-
maxCacheSize;
|
|
121
|
-
/**
|
|
122
|
-
* Whether to automatically clean up cache
|
|
123
|
-
* @private
|
|
124
|
-
*/
|
|
125
|
-
autoCleanup;
|
|
126
|
-
/**
|
|
127
|
-
* Set of file paths that have been loaded to avoid redundant processing
|
|
128
|
-
* @private
|
|
129
|
-
*/
|
|
130
|
-
loadedFiles = new Set();
|
|
131
|
-
/**
|
|
132
|
-
* Private constructor for singleton pattern.
|
|
133
|
-
*
|
|
134
|
-
* @param tsConfigPath - Optional path to a specific TypeScript config file
|
|
135
|
-
* @param options - Configuration options for memory management
|
|
136
|
-
* @throws {Error} When TypeScript configuration cannot be loaded
|
|
137
|
-
* @private
|
|
138
|
-
*/
|
|
139
|
-
constructor(tsConfigPath = constants.TS_CONFIG_DEFAULT_PATH, options = {}) {
|
|
140
|
-
// Initialize configuration with defaults
|
|
141
|
-
this.maxCacheSize = options.maxCacheSize ?? 100;
|
|
142
|
-
this.autoCleanup = options.autoCleanup ?? true;
|
|
143
|
-
const { config, error } = ts.readConfigFile(tsConfigPath || 'tsconfig.json', ts.sys.readFile);
|
|
144
|
-
if (error) {
|
|
145
|
-
console.log(new Error(`Error reading tsconfig file: ${error.messageText}`).message);
|
|
146
|
-
throw new Error(`Error reading tsconfig file: ${error.messageText}`);
|
|
147
|
-
}
|
|
148
|
-
const { options: tsOptions, fileNames } = ts.parseJsonConfigFileContent(config, ts.sys, './');
|
|
149
|
-
this.program = ts.createProgram(fileNames, tsOptions);
|
|
150
|
-
this.checker = this.program.getTypeChecker();
|
|
151
|
-
}
|
|
152
|
-
/**
|
|
153
|
-
* Generates a unique cache key using file name and class name.
|
|
154
|
-
*
|
|
155
|
-
* @param fileName - The source file name
|
|
156
|
-
* @param className - The class name
|
|
157
|
-
* @returns Unique cache key in format "fileName:className"
|
|
158
|
-
* @private
|
|
159
|
-
*/
|
|
160
|
-
getCacheKey(fileName, className) {
|
|
161
|
-
return `${fileName}:${className}`;
|
|
162
|
-
}
|
|
163
|
-
/**
|
|
164
|
-
* Cleans up cache when it exceeds maximum size to prevent memory leaks.
|
|
165
|
-
* Removes oldest entries using LRU strategy.
|
|
166
|
-
* @private
|
|
167
|
-
*/
|
|
168
|
-
cleanupCache() {
|
|
169
|
-
if (!this.autoCleanup || this.classCache.size <= this.maxCacheSize) {
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
const entries = Array.from(this.classCache.entries());
|
|
173
|
-
const toDelete = entries.slice(0, Math.floor(this.maxCacheSize / 2));
|
|
174
|
-
for (const [key] of toDelete) {
|
|
175
|
-
this.classCache.delete(key);
|
|
176
|
-
}
|
|
177
|
-
// Force garbage collection hint
|
|
178
|
-
if (global.gc) {
|
|
179
|
-
global.gc();
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
/**
|
|
183
|
-
* Gets relevant source files for a class, filtering out unnecessary files to save memory.
|
|
184
|
-
*
|
|
185
|
-
* @param className - The name of the class to find files for
|
|
186
|
-
* @param filePath - Optional specific file path
|
|
187
|
-
* @returns Array of relevant source files
|
|
188
|
-
* @private
|
|
189
|
-
*/
|
|
190
|
-
getRelevantSourceFiles(className, filePath) {
|
|
191
|
-
if (filePath) {
|
|
192
|
-
const sourceFile = this.program.getSourceFile(filePath);
|
|
193
|
-
return sourceFile ? [sourceFile] : [];
|
|
194
|
-
}
|
|
195
|
-
// Only get source files that are not declaration files and not in node_modules
|
|
196
|
-
return this.program.getSourceFiles().filter(sf => {
|
|
197
|
-
if (sf.isDeclarationFile)
|
|
198
|
-
return false;
|
|
199
|
-
if (sf.fileName.includes('.d.ts'))
|
|
200
|
-
return false;
|
|
201
|
-
if (sf.fileName.includes('node_modules'))
|
|
202
|
-
return false;
|
|
203
|
-
// Mark file as loaded for memory tracking
|
|
204
|
-
this.loadedFiles.add(sf.fileName);
|
|
205
|
-
return true;
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
/**
|
|
209
|
-
* Transforms a class by its name into an OpenAPI schema object.
|
|
210
|
-
*
|
|
211
|
-
* @param className - The name of the class to transform
|
|
212
|
-
* @param filePath - Optional path to the file containing the class
|
|
213
|
-
* @returns Object containing the class name and its corresponding JSON schema
|
|
214
|
-
* @throws {Error} When the specified class cannot be found
|
|
215
|
-
* @private
|
|
216
|
-
*/
|
|
217
|
-
transformByName(className, filePath) {
|
|
218
|
-
const sourceFiles = this.getRelevantSourceFiles(className, filePath);
|
|
219
|
-
for (const sourceFile of sourceFiles) {
|
|
220
|
-
const classNode = this.findClassByName(sourceFile, className);
|
|
221
|
-
if (classNode && sourceFile?.fileName) {
|
|
222
|
-
const cacheKey = this.getCacheKey(sourceFile.fileName, className);
|
|
223
|
-
// Check cache first using fileName:className as key
|
|
224
|
-
if (this.classCache.has(cacheKey)) {
|
|
225
|
-
return this.classCache.get(cacheKey);
|
|
226
|
-
}
|
|
227
|
-
const result = this.transformClass(classNode);
|
|
228
|
-
// Cache using fileName:className as key for uniqueness
|
|
229
|
-
this.classCache.set(cacheKey, result);
|
|
230
|
-
// Clean up cache if it gets too large
|
|
231
|
-
this.cleanupCache();
|
|
232
|
-
return result;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
throw new Error(`Class ${className} not found`);
|
|
236
|
-
}
|
|
237
|
-
/**
|
|
238
|
-
* Gets the singleton instance of SchemaTransformer.
|
|
239
|
-
*
|
|
240
|
-
* @param tsConfigPath - Optional path to a specific TypeScript config file (only used on first call)
|
|
241
|
-
* @param options - Configuration options for memory management (only used on first call)
|
|
242
|
-
* @returns The singleton instance
|
|
243
|
-
*
|
|
244
|
-
* @example
|
|
245
|
-
* ```typescript
|
|
246
|
-
* const transformer = SchemaTransformer.getInstance();
|
|
247
|
-
* ```
|
|
248
|
-
*
|
|
249
|
-
* @example
|
|
250
|
-
* ```typescript
|
|
251
|
-
* // With memory optimization options
|
|
252
|
-
* const transformer = SchemaTransformer.getInstance('./tsconfig.json', {
|
|
253
|
-
* maxCacheSize: 50,
|
|
254
|
-
* autoCleanup: true
|
|
255
|
-
* });
|
|
256
|
-
* ```
|
|
257
|
-
*
|
|
258
|
-
* @public
|
|
259
|
-
*/
|
|
260
|
-
/**
|
|
261
|
-
* Clears the current singleton instance. Useful for testing or when you need
|
|
262
|
-
* to create a new instance with different configuration.
|
|
263
|
-
*/
|
|
264
|
-
static clearInstance() {
|
|
265
|
-
SchemaTransformer.instance = null;
|
|
266
|
-
}
|
|
267
|
-
static getInstance(tsConfigPath, options) {
|
|
268
|
-
if (!SchemaTransformer.instance) {
|
|
269
|
-
SchemaTransformer.instance = new SchemaTransformer(tsConfigPath, options);
|
|
270
|
-
}
|
|
271
|
-
return SchemaTransformer.instance;
|
|
272
|
-
}
|
|
273
|
-
/**
|
|
274
|
-
* Transforms a class constructor function into an OpenAPI schema object.
|
|
275
|
-
*
|
|
276
|
-
* @param cls - The class constructor function to transform
|
|
277
|
-
* @returns Object containing the class name and its corresponding JSON schema
|
|
278
|
-
*
|
|
279
|
-
* @example
|
|
280
|
-
* ```typescript
|
|
281
|
-
* import { User } from './entities/user.js';
|
|
282
|
-
* const transformer = SchemaTransformer.getInstance();
|
|
283
|
-
* const schema = transformer.transform(User);
|
|
284
|
-
* ```
|
|
285
|
-
*
|
|
286
|
-
* @public
|
|
287
|
-
*/
|
|
288
|
-
transform(cls) {
|
|
289
|
-
return this.transformByName(cls.name);
|
|
290
|
-
}
|
|
291
|
-
/**
|
|
292
|
-
* Clears all cached schemas and loaded file references to free memory.
|
|
293
|
-
* Useful for long-running applications or when processing many different classes.
|
|
294
|
-
*
|
|
295
|
-
* @example
|
|
296
|
-
* ```typescript
|
|
297
|
-
* const transformer = SchemaTransformer.getInstance();
|
|
298
|
-
* // After processing many classes...
|
|
299
|
-
* transformer.clearCache();
|
|
300
|
-
* ```
|
|
301
|
-
*
|
|
302
|
-
* @public
|
|
303
|
-
*/
|
|
304
|
-
clearCache() {
|
|
305
|
-
this.classCache.clear();
|
|
306
|
-
this.loadedFiles.clear();
|
|
307
|
-
// Force garbage collection hint if available
|
|
308
|
-
if (global.gc) {
|
|
309
|
-
global.gc();
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
/**
|
|
313
|
-
* Gets memory usage statistics for monitoring and debugging.
|
|
314
|
-
*
|
|
315
|
-
* @returns Object containing cache size and loaded files count
|
|
316
|
-
*
|
|
317
|
-
* @example
|
|
318
|
-
* ```typescript
|
|
319
|
-
* const transformer = SchemaTransformer.getInstance();
|
|
320
|
-
* const stats = transformer.getMemoryStats();
|
|
321
|
-
* console.log(`Cache entries: ${stats.cacheSize}, Files loaded: ${stats.loadedFiles}`);
|
|
322
|
-
* ```
|
|
323
|
-
*
|
|
324
|
-
* @public
|
|
325
|
-
*/
|
|
326
|
-
getMemoryStats() {
|
|
327
|
-
return {
|
|
328
|
-
cacheSize: this.classCache.size,
|
|
329
|
-
loadedFiles: this.loadedFiles.size,
|
|
330
|
-
};
|
|
331
|
-
}
|
|
332
|
-
/**
|
|
333
|
-
* Finds a class declaration by name within a source file.
|
|
334
|
-
*
|
|
335
|
-
* @param sourceFile - The TypeScript source file to search in
|
|
336
|
-
* @param className - The name of the class to find
|
|
337
|
-
* @returns The class declaration node if found, undefined otherwise
|
|
338
|
-
* @private
|
|
339
|
-
*/
|
|
340
|
-
findClassByName(sourceFile, className) {
|
|
341
|
-
let result;
|
|
342
|
-
const visit = (node) => {
|
|
343
|
-
if (ts.isClassDeclaration(node) && node.name?.text === className) {
|
|
344
|
-
result = node;
|
|
345
|
-
return;
|
|
346
|
-
}
|
|
347
|
-
ts.forEachChild(node, visit);
|
|
348
|
-
};
|
|
349
|
-
visit(sourceFile);
|
|
350
|
-
return result;
|
|
351
|
-
}
|
|
352
|
-
/**
|
|
353
|
-
* Transforms a TypeScript class declaration into a schema object.
|
|
354
|
-
*
|
|
355
|
-
* @param classNode - The TypeScript class declaration node
|
|
356
|
-
* @returns Object containing class name and generated schema
|
|
357
|
-
* @private
|
|
358
|
-
*/
|
|
359
|
-
transformClass(classNode) {
|
|
360
|
-
const className = classNode.name?.text || 'Unknown';
|
|
361
|
-
const properties = this.extractProperties(classNode);
|
|
362
|
-
const schema = this.generateSchema(properties);
|
|
363
|
-
return { name: className, schema };
|
|
364
|
-
}
|
|
365
|
-
/**
|
|
366
|
-
* Extracts property information from a class declaration.
|
|
367
|
-
*
|
|
368
|
-
* @param classNode - The TypeScript class declaration node
|
|
369
|
-
* @returns Array of property information including names, types, decorators, and optional status
|
|
370
|
-
* @private
|
|
371
|
-
*/
|
|
372
|
-
extractProperties(classNode) {
|
|
373
|
-
const properties = [];
|
|
374
|
-
for (const member of classNode.members) {
|
|
375
|
-
if (ts.isPropertyDeclaration(member) &&
|
|
376
|
-
member.name &&
|
|
377
|
-
ts.isIdentifier(member.name)) {
|
|
378
|
-
const propertyName = member.name.text;
|
|
379
|
-
const type = this.getPropertyType(member);
|
|
380
|
-
const decorators = this.extractDecorators(member);
|
|
381
|
-
const isOptional = !!member.questionToken;
|
|
382
|
-
properties.push({
|
|
383
|
-
name: propertyName,
|
|
384
|
-
type,
|
|
385
|
-
decorators,
|
|
386
|
-
isOptional,
|
|
387
|
-
});
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
return properties;
|
|
391
|
-
}
|
|
392
|
-
/**
|
|
393
|
-
* Gets the TypeScript type of a property as a string.
|
|
394
|
-
*
|
|
395
|
-
* @param property - The property declaration to analyze
|
|
396
|
-
* @returns String representation of the property's type
|
|
397
|
-
* @private
|
|
398
|
-
*/
|
|
399
|
-
getPropertyType(property) {
|
|
400
|
-
if (property.type) {
|
|
401
|
-
return this.getTypeNodeToString(property.type);
|
|
402
|
-
}
|
|
403
|
-
const type = this.checker.getTypeAtLocation(property);
|
|
404
|
-
return this.checker.typeToString(type);
|
|
405
|
-
}
|
|
406
|
-
/**
|
|
407
|
-
* Converts a TypeScript type node to its string representation.
|
|
408
|
-
*
|
|
409
|
-
* @param typeNode - The TypeScript type node to convert
|
|
410
|
-
* @returns String representation of the type
|
|
411
|
-
* @private
|
|
412
|
-
*/
|
|
413
|
-
getTypeNodeToString(typeNode) {
|
|
414
|
-
if (ts.isTypeReferenceNode(typeNode) &&
|
|
415
|
-
ts.isIdentifier(typeNode.typeName)) {
|
|
416
|
-
if (typeNode.typeName.text.toLowerCase().includes('uploadfile')) {
|
|
417
|
-
return 'UploadFile';
|
|
418
|
-
}
|
|
419
|
-
if (typeNode.typeArguments && typeNode.typeArguments.length > 0) {
|
|
420
|
-
const firstTypeArg = typeNode.typeArguments[0];
|
|
421
|
-
if (firstTypeArg &&
|
|
422
|
-
ts.isTypeReferenceNode(firstTypeArg) &&
|
|
423
|
-
ts.isIdentifier(firstTypeArg.typeName)) {
|
|
424
|
-
if (firstTypeArg.typeName.text.toLowerCase().includes('uploadfile')) {
|
|
425
|
-
return 'UploadFile';
|
|
426
|
-
}
|
|
427
|
-
if (typeNode.typeName.text === 'BaseDto') {
|
|
428
|
-
return firstTypeArg.typeName.text;
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
return typeNode.typeName.text;
|
|
433
|
-
}
|
|
434
|
-
switch (typeNode.kind) {
|
|
435
|
-
case ts.SyntaxKind.StringKeyword:
|
|
436
|
-
return constants.jsPrimitives.String.type;
|
|
437
|
-
case ts.SyntaxKind.NumberKeyword:
|
|
438
|
-
return constants.jsPrimitives.Number.type;
|
|
439
|
-
case ts.SyntaxKind.BooleanKeyword:
|
|
440
|
-
return constants.jsPrimitives.Boolean.type;
|
|
441
|
-
case ts.SyntaxKind.ArrayType:
|
|
442
|
-
const arrayType = typeNode;
|
|
443
|
-
return `${this.getTypeNodeToString(arrayType.elementType)}[]`;
|
|
444
|
-
case ts.SyntaxKind.UnionType:
|
|
445
|
-
// Handle union types like string | null
|
|
446
|
-
const unionType = typeNode;
|
|
447
|
-
const types = unionType.types.map(t => this.getTypeNodeToString(t));
|
|
448
|
-
// Filter out null and undefined, return the first meaningful type
|
|
449
|
-
const meaningfulTypes = types.filter(t => t !== 'null' && t !== 'undefined');
|
|
450
|
-
if (meaningfulTypes.length > 0 && meaningfulTypes[0]) {
|
|
451
|
-
return meaningfulTypes[0];
|
|
452
|
-
}
|
|
453
|
-
if (types.length > 0 && types[0]) {
|
|
454
|
-
return types[0];
|
|
455
|
-
}
|
|
456
|
-
return 'object';
|
|
457
|
-
default:
|
|
458
|
-
const typeText = typeNode.getText();
|
|
459
|
-
// Handle some common TypeScript utility types
|
|
460
|
-
if (typeText.startsWith('Date'))
|
|
461
|
-
return constants.jsPrimitives.Date.type;
|
|
462
|
-
if (typeText.includes('Buffer') || typeText.includes('Uint8Array'))
|
|
463
|
-
return constants.jsPrimitives.Buffer.type;
|
|
464
|
-
return typeText;
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
/**
|
|
468
|
-
* Extracts decorator information from a property declaration.
|
|
469
|
-
*
|
|
470
|
-
* @param member - The property declaration to analyze
|
|
471
|
-
* @returns Array of decorator information including names and arguments
|
|
472
|
-
* @private
|
|
473
|
-
*/
|
|
474
|
-
extractDecorators(member) {
|
|
475
|
-
const decorators = [];
|
|
476
|
-
if (member.modifiers) {
|
|
477
|
-
for (const modifier of member.modifiers) {
|
|
478
|
-
if (ts.isDecorator(modifier) &&
|
|
479
|
-
ts.isCallExpression(modifier.expression)) {
|
|
480
|
-
const decoratorName = this.getDecoratorName(modifier.expression);
|
|
481
|
-
const args = this.getDecoratorArguments(modifier.expression);
|
|
482
|
-
decorators.push({ name: decoratorName, arguments: args });
|
|
483
|
-
}
|
|
484
|
-
else if (ts.isDecorator(modifier) &&
|
|
485
|
-
ts.isIdentifier(modifier.expression)) {
|
|
486
|
-
decorators.push({ name: modifier.expression.text, arguments: [] });
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
return decorators;
|
|
491
|
-
}
|
|
492
|
-
/**
|
|
493
|
-
* Gets the name of a decorator from a call expression.
|
|
494
|
-
*
|
|
495
|
-
* @param callExpression - The decorator call expression
|
|
496
|
-
* @returns The decorator name or "unknown" if not identifiable
|
|
497
|
-
* @private
|
|
498
|
-
*/
|
|
499
|
-
getDecoratorName(callExpression) {
|
|
500
|
-
if (ts.isIdentifier(callExpression.expression)) {
|
|
501
|
-
return callExpression.expression.text;
|
|
502
|
-
}
|
|
503
|
-
return 'unknown';
|
|
504
|
-
}
|
|
505
|
-
/**
|
|
506
|
-
* Extracts arguments from a decorator call expression.
|
|
507
|
-
*
|
|
508
|
-
* @param callExpression - The decorator call expression
|
|
509
|
-
* @returns Array of parsed decorator arguments
|
|
510
|
-
* @private
|
|
511
|
-
*/
|
|
512
|
-
getDecoratorArguments(callExpression) {
|
|
513
|
-
return callExpression.arguments.map(arg => {
|
|
514
|
-
if (ts.isNumericLiteral(arg))
|
|
515
|
-
return Number(arg.text);
|
|
516
|
-
if (ts.isStringLiteral(arg))
|
|
517
|
-
return arg.text;
|
|
518
|
-
if (arg.kind === ts.SyntaxKind.TrueKeyword)
|
|
519
|
-
return true;
|
|
520
|
-
if (arg.kind === ts.SyntaxKind.FalseKeyword)
|
|
521
|
-
return false;
|
|
522
|
-
return arg.getText();
|
|
523
|
-
});
|
|
524
|
-
}
|
|
525
|
-
/**
|
|
526
|
-
* Generates an OpenAPI schema from extracted property information.
|
|
527
|
-
*
|
|
528
|
-
* @param properties - Array of property information to process
|
|
529
|
-
* @returns Complete OpenAPI schema object with properties and validation rules
|
|
530
|
-
* @private
|
|
531
|
-
*/
|
|
532
|
-
generateSchema(properties) {
|
|
533
|
-
const schema = {
|
|
534
|
-
type: 'object',
|
|
535
|
-
properties: {},
|
|
536
|
-
required: [],
|
|
537
|
-
};
|
|
538
|
-
for (const property of properties) {
|
|
539
|
-
const { type, format, nestedSchema } = this.mapTypeToSchema(property.type);
|
|
540
|
-
if (nestedSchema) {
|
|
541
|
-
schema.properties[property.name] = nestedSchema;
|
|
542
|
-
}
|
|
543
|
-
else {
|
|
544
|
-
schema.properties[property.name] = { type };
|
|
545
|
-
if (format)
|
|
546
|
-
schema.properties[property.name].format = format;
|
|
547
|
-
}
|
|
548
|
-
// Apply decorators if present
|
|
549
|
-
this.applyDecorators(property.decorators, schema, property.name);
|
|
550
|
-
// If no decorators are present, apply sensible defaults based on TypeScript types
|
|
551
|
-
if (property.decorators.length === 0) {
|
|
552
|
-
this.applySensibleDefaults(property, schema);
|
|
553
|
-
}
|
|
554
|
-
// Determine if property should be required based on decorators and optional status
|
|
555
|
-
this.determineRequiredStatus(property, schema);
|
|
556
|
-
}
|
|
557
|
-
return schema;
|
|
558
|
-
}
|
|
559
|
-
/**
|
|
560
|
-
* Maps TypeScript types to OpenAPI schema types and formats.
|
|
561
|
-
* Handles primitive types, arrays, and nested objects recursively.
|
|
562
|
-
*
|
|
563
|
-
* @param type - The TypeScript type string to map
|
|
564
|
-
* @returns Object containing OpenAPI type, optional format, and nested schema
|
|
565
|
-
* @private
|
|
566
|
-
*/
|
|
567
|
-
mapTypeToSchema(type) {
|
|
568
|
-
// Handle arrays
|
|
569
|
-
if (type.endsWith('[]')) {
|
|
570
|
-
const elementType = type.slice(0, -2);
|
|
571
|
-
const elementSchema = this.mapTypeToSchema(elementType);
|
|
572
|
-
const items = elementSchema.nestedSchema || {
|
|
573
|
-
type: elementSchema.type,
|
|
574
|
-
};
|
|
575
|
-
if (elementSchema.format)
|
|
576
|
-
items.format = elementSchema.format;
|
|
577
|
-
return {
|
|
578
|
-
type: 'array',
|
|
579
|
-
nestedSchema: {
|
|
580
|
-
type: 'array',
|
|
581
|
-
items,
|
|
582
|
-
properties: {},
|
|
583
|
-
required: [],
|
|
584
|
-
},
|
|
585
|
-
};
|
|
586
|
-
}
|
|
587
|
-
if (type.toLocaleLowerCase().includes('uploadfile'))
|
|
588
|
-
type = 'UploadFile';
|
|
589
|
-
// Handle primitives
|
|
590
|
-
switch (type.toLowerCase()) {
|
|
591
|
-
case constants.jsPrimitives.String.type.toLowerCase():
|
|
592
|
-
return { type: constants.jsPrimitives.String.value };
|
|
593
|
-
case constants.jsPrimitives.Number.type.toLowerCase():
|
|
594
|
-
return { type: constants.jsPrimitives.Number.value };
|
|
595
|
-
case constants.jsPrimitives.Boolean.type.toLowerCase():
|
|
596
|
-
return { type: constants.jsPrimitives.Boolean.value };
|
|
597
|
-
case constants.jsPrimitives.Date.type.toLowerCase():
|
|
598
|
-
return {
|
|
599
|
-
type: constants.jsPrimitives.Date.value,
|
|
600
|
-
format: constants.jsPrimitives.Date.format,
|
|
601
|
-
};
|
|
602
|
-
case constants.jsPrimitives.Buffer.type.toLowerCase():
|
|
603
|
-
case constants.jsPrimitives.Uint8Array.type.toLowerCase():
|
|
604
|
-
case constants.jsPrimitives.File.type.toLowerCase():
|
|
605
|
-
return {
|
|
606
|
-
type: constants.jsPrimitives.Buffer.value,
|
|
607
|
-
format: constants.jsPrimitives.Buffer.format,
|
|
608
|
-
};
|
|
609
|
-
case constants.jsPrimitives.UploadFile.type.toLowerCase():
|
|
610
|
-
return {
|
|
611
|
-
type: constants.jsPrimitives.UploadFile.value,
|
|
612
|
-
format: constants.jsPrimitives.UploadFile.format,
|
|
613
|
-
};
|
|
614
|
-
default:
|
|
615
|
-
// Handle nested objects
|
|
616
|
-
try {
|
|
617
|
-
const nestedResult = this.transformByName(type);
|
|
618
|
-
return {
|
|
619
|
-
type: constants.jsPrimitives.Object.value,
|
|
620
|
-
nestedSchema: nestedResult.schema,
|
|
621
|
-
};
|
|
622
|
-
}
|
|
623
|
-
catch {
|
|
624
|
-
return { type: constants.jsPrimitives.Object.value };
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
/**
|
|
629
|
-
* Applies class-validator decorators to schema properties.
|
|
630
|
-
* Maps validation decorators to their corresponding OpenAPI schema constraints.
|
|
631
|
-
*
|
|
632
|
-
* @param decorators - Array of decorator information to apply
|
|
633
|
-
* @param schema - The schema object to modify
|
|
634
|
-
* @param propertyName - Name of the property being processed
|
|
635
|
-
* @private
|
|
636
|
-
*/
|
|
637
|
-
applyDecorators(decorators, schema, propertyName) {
|
|
638
|
-
const isArrayType = schema.properties[propertyName].type ===
|
|
639
|
-
constants.jsPrimitives.Array.value;
|
|
640
|
-
for (const decorator of decorators) {
|
|
641
|
-
const decoratorName = decorator.name;
|
|
642
|
-
switch (decoratorName) {
|
|
643
|
-
case constants.validatorDecorators.IsString.name:
|
|
644
|
-
if (!isArrayType) {
|
|
645
|
-
schema.properties[propertyName].type =
|
|
646
|
-
constants.validatorDecorators.IsString.type;
|
|
647
|
-
}
|
|
648
|
-
else if (schema.properties[propertyName].items) {
|
|
649
|
-
schema.properties[propertyName].items.type =
|
|
650
|
-
constants.validatorDecorators.IsString.type;
|
|
651
|
-
}
|
|
652
|
-
break;
|
|
653
|
-
case constants.validatorDecorators.IsInt.name:
|
|
654
|
-
if (!isArrayType) {
|
|
655
|
-
schema.properties[propertyName].type =
|
|
656
|
-
constants.validatorDecorators.IsInt.type;
|
|
657
|
-
schema.properties[propertyName].format =
|
|
658
|
-
constants.validatorDecorators.IsInt.format;
|
|
659
|
-
}
|
|
660
|
-
else if (schema.properties[propertyName].items) {
|
|
661
|
-
schema.properties[propertyName].items.type =
|
|
662
|
-
constants.validatorDecorators.IsInt.type;
|
|
663
|
-
schema.properties[propertyName].items.format =
|
|
664
|
-
constants.validatorDecorators.IsInt.format;
|
|
665
|
-
}
|
|
666
|
-
break;
|
|
667
|
-
case constants.validatorDecorators.IsNumber.name:
|
|
668
|
-
if (!isArrayType) {
|
|
669
|
-
schema.properties[propertyName].type =
|
|
670
|
-
constants.validatorDecorators.IsNumber.type;
|
|
671
|
-
}
|
|
672
|
-
else if (schema.properties[propertyName].items) {
|
|
673
|
-
schema.properties[propertyName].items.type =
|
|
674
|
-
constants.validatorDecorators.IsNumber.type;
|
|
675
|
-
}
|
|
676
|
-
break;
|
|
677
|
-
case constants.validatorDecorators.IsBoolean.name:
|
|
678
|
-
if (!isArrayType) {
|
|
679
|
-
schema.properties[propertyName].type =
|
|
680
|
-
constants.validatorDecorators.IsBoolean.type;
|
|
681
|
-
}
|
|
682
|
-
else if (schema.properties[propertyName].items) {
|
|
683
|
-
schema.properties[propertyName].items.type =
|
|
684
|
-
constants.validatorDecorators.IsBoolean.type;
|
|
685
|
-
}
|
|
686
|
-
break;
|
|
687
|
-
case constants.validatorDecorators.IsEmail.name:
|
|
688
|
-
if (!isArrayType) {
|
|
689
|
-
schema.properties[propertyName].format =
|
|
690
|
-
constants.validatorDecorators.IsEmail.format;
|
|
691
|
-
}
|
|
692
|
-
else if (schema.properties[propertyName].items) {
|
|
693
|
-
schema.properties[propertyName].items.format =
|
|
694
|
-
constants.validatorDecorators.IsEmail.format;
|
|
695
|
-
}
|
|
696
|
-
break;
|
|
697
|
-
case constants.validatorDecorators.IsDate.name:
|
|
698
|
-
if (!isArrayType) {
|
|
699
|
-
schema.properties[propertyName].type =
|
|
700
|
-
constants.validatorDecorators.IsDate.type;
|
|
701
|
-
schema.properties[propertyName].format =
|
|
702
|
-
constants.validatorDecorators.IsDate.format;
|
|
703
|
-
}
|
|
704
|
-
else if (schema.properties[propertyName].items) {
|
|
705
|
-
schema.properties[propertyName].items.type =
|
|
706
|
-
constants.validatorDecorators.IsDate.type;
|
|
707
|
-
schema.properties[propertyName].items.format =
|
|
708
|
-
constants.validatorDecorators.IsDate.format;
|
|
709
|
-
}
|
|
710
|
-
break;
|
|
711
|
-
case constants.validatorDecorators.IsNotEmpty.name:
|
|
712
|
-
if (!schema.required.includes(propertyName)) {
|
|
713
|
-
schema.required.push(propertyName);
|
|
714
|
-
}
|
|
715
|
-
break;
|
|
716
|
-
case constants.validatorDecorators.MinLength.name:
|
|
717
|
-
schema.properties[propertyName].minLength = decorator.arguments[0];
|
|
718
|
-
break;
|
|
719
|
-
case constants.validatorDecorators.MaxLength.name:
|
|
720
|
-
schema.properties[propertyName].maxLength = decorator.arguments[0];
|
|
721
|
-
break;
|
|
722
|
-
case constants.validatorDecorators.Length.name:
|
|
723
|
-
schema.properties[propertyName].minLength = decorator.arguments[0];
|
|
724
|
-
if (decorator.arguments[1]) {
|
|
725
|
-
schema.properties[propertyName].maxLength = decorator.arguments[1];
|
|
726
|
-
}
|
|
727
|
-
break;
|
|
728
|
-
case constants.validatorDecorators.Min.name:
|
|
729
|
-
schema.properties[propertyName].minimum = decorator.arguments[0];
|
|
730
|
-
break;
|
|
731
|
-
case constants.validatorDecorators.Max.name:
|
|
732
|
-
schema.properties[propertyName].maximum = decorator.arguments[0];
|
|
733
|
-
break;
|
|
734
|
-
case constants.validatorDecorators.IsPositive.name:
|
|
735
|
-
schema.properties[propertyName].minimum = 0;
|
|
736
|
-
break;
|
|
737
|
-
case constants.validatorDecorators.IsArray.name:
|
|
738
|
-
schema.properties[propertyName].type =
|
|
739
|
-
constants.jsPrimitives.Array.value;
|
|
740
|
-
break;
|
|
741
|
-
case constants.validatorDecorators.ArrayNotEmpty.name:
|
|
742
|
-
schema.properties[propertyName].minItems = 1;
|
|
743
|
-
if (!schema.required.includes(propertyName)) {
|
|
744
|
-
schema.required.push(propertyName);
|
|
745
|
-
}
|
|
746
|
-
break;
|
|
747
|
-
case constants.validatorDecorators.ArrayMinSize.name:
|
|
748
|
-
schema.properties[propertyName].minItems = decorator.arguments[0];
|
|
749
|
-
break;
|
|
750
|
-
case constants.validatorDecorators.ArrayMaxSize.name:
|
|
751
|
-
schema.properties[propertyName].maxItems = decorator.arguments[0];
|
|
752
|
-
break;
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
/**
|
|
757
|
-
* Applies sensible default behaviors for properties without class-validator decorators.
|
|
758
|
-
* This allows the schema generator to work with plain TypeScript classes.
|
|
759
|
-
*
|
|
760
|
-
* @param property - The property information
|
|
761
|
-
* @param schema - The schema object to modify
|
|
762
|
-
* @private
|
|
763
|
-
*/
|
|
764
|
-
applySensibleDefaults(property, schema) {
|
|
765
|
-
const propertyName = property.name;
|
|
766
|
-
property.type.toLowerCase();
|
|
767
|
-
// Add examples based on property names and types
|
|
768
|
-
const propertySchema = schema.properties[propertyName];
|
|
769
|
-
// Add common format hints based on property names
|
|
770
|
-
if (propertyName.includes('email') && propertySchema.type === 'string') {
|
|
771
|
-
propertySchema.format = 'email';
|
|
772
|
-
}
|
|
773
|
-
else if (propertyName.includes('password') &&
|
|
774
|
-
propertySchema.type === 'string') {
|
|
775
|
-
propertySchema.format = 'password';
|
|
776
|
-
propertySchema.minLength = 8;
|
|
777
|
-
}
|
|
778
|
-
else if (propertyName.includes('url') &&
|
|
779
|
-
propertySchema.type === 'string') {
|
|
780
|
-
propertySchema.format = 'uri';
|
|
781
|
-
}
|
|
782
|
-
else if (propertyName.includes('phone') &&
|
|
783
|
-
propertySchema.type === 'string') {
|
|
784
|
-
propertySchema.pattern = '^[+]?[1-9]\\d{1,14}$';
|
|
785
|
-
}
|
|
786
|
-
// Add reasonable constraints based on common property names
|
|
787
|
-
if (propertySchema.type === 'string') {
|
|
788
|
-
if (propertyName === 'name' ||
|
|
789
|
-
propertyName === 'firstName' ||
|
|
790
|
-
propertyName === 'lastName') {
|
|
791
|
-
propertySchema.minLength = 1;
|
|
792
|
-
propertySchema.maxLength = 100;
|
|
793
|
-
}
|
|
794
|
-
else if (propertyName === 'description' || propertyName === 'bio') {
|
|
795
|
-
propertySchema.maxLength = 500;
|
|
796
|
-
}
|
|
797
|
-
else if (propertyName === 'title') {
|
|
798
|
-
propertySchema.minLength = 1;
|
|
799
|
-
propertySchema.maxLength = 200;
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
if (propertySchema.type === 'integer' || propertySchema.type === 'number') {
|
|
803
|
-
if (propertyName === 'age') {
|
|
804
|
-
propertySchema.minimum = 0;
|
|
805
|
-
propertySchema.maximum = 150;
|
|
806
|
-
}
|
|
807
|
-
else if (propertyName === 'id') {
|
|
808
|
-
propertySchema.minimum = 1;
|
|
809
|
-
}
|
|
810
|
-
else if (propertyName.includes('count') ||
|
|
811
|
-
propertyName.includes('quantity')) {
|
|
812
|
-
propertySchema.minimum = 0;
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
/**
|
|
817
|
-
* Determines if a property should be required based on decorators and optional status.
|
|
818
|
-
*
|
|
819
|
-
* Logic:
|
|
820
|
-
* - If property has IsNotEmpty or ArrayNotEmpty decorator, it's required (handled in applyDecorators)
|
|
821
|
-
* - Otherwise, the property is not required (preserving original behavior)
|
|
822
|
-
* - The isOptional information is stored for future use and documentation
|
|
823
|
-
*
|
|
824
|
-
* @param property - The property information
|
|
825
|
-
* @param schema - The schema object to modify
|
|
826
|
-
* @private
|
|
827
|
-
*/
|
|
828
|
-
determineRequiredStatus(property, schema) {
|
|
829
|
-
const propertyName = property.name;
|
|
830
|
-
// Check if already marked as required by IsNotEmpty or ArrayNotEmpty decorator
|
|
831
|
-
const isAlreadyRequired = schema.required.includes(propertyName);
|
|
832
|
-
// If already required by decorators, don't change it
|
|
833
|
-
if (isAlreadyRequired) {
|
|
834
|
-
return;
|
|
835
|
-
}
|
|
836
|
-
// If property is optional (has ?), it should not be required unless explicitly marked
|
|
837
|
-
if (property.isOptional) {
|
|
838
|
-
return;
|
|
839
|
-
}
|
|
840
|
-
// If property is not optional and not already required, make it required
|
|
841
|
-
schema.required.push(propertyName);
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
/**
|
|
845
|
-
* Convenience function to transform a class using the singleton instance.
|
|
846
|
-
*
|
|
847
|
-
* @param cls - The class constructor function to transform
|
|
848
|
-
* @param options - Optional configuration for memory management
|
|
849
|
-
* @returns Object containing the class name and its corresponding JSON schema
|
|
850
|
-
*
|
|
851
|
-
* @example
|
|
852
|
-
* ```typescript
|
|
853
|
-
* import { transform } from 'class-validator-to-open-api'
|
|
854
|
-
* import { User } from './entities/user.js'
|
|
855
|
-
*
|
|
856
|
-
* const schema = transform(User)
|
|
857
|
-
* console.log(schema)
|
|
858
|
-
* ```
|
|
859
|
-
*
|
|
860
|
-
* @example
|
|
861
|
-
* ```typescript
|
|
862
|
-
* // With memory optimization
|
|
863
|
-
* const schema = transform(User, { maxCacheSize: 50, autoCleanup: true })
|
|
864
|
-
* ```
|
|
865
|
-
*
|
|
866
|
-
* @public
|
|
867
|
-
*/
|
|
868
|
-
function transform(cls, options) {
|
|
869
|
-
return SchemaTransformer.getInstance(undefined, options).transform(cls);
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
node_test.describe('Transform Function', () => {
|
|
873
|
-
node_test.test('should transform basic class with string property', () => {
|
|
874
|
-
class TestUser {
|
|
875
|
-
name;
|
|
876
|
-
}
|
|
877
|
-
__decorate([
|
|
878
|
-
classValidator.IsString(),
|
|
879
|
-
classValidator.IsNotEmpty()
|
|
880
|
-
], TestUser.prototype, "name", void 0);
|
|
881
|
-
const result = transform(TestUser);
|
|
882
|
-
assert.strictEqual(result.name, 'TestUser');
|
|
883
|
-
assert.strictEqual(result.schema.type, 'object');
|
|
884
|
-
assert.strictEqual(result.schema.properties.name.type, 'string');
|
|
885
|
-
assert.ok(result.schema.required.includes('name'));
|
|
886
|
-
});
|
|
887
|
-
node_test.test('should handle different primitive types', () => {
|
|
888
|
-
class PrimitiveTest {
|
|
889
|
-
name;
|
|
890
|
-
price;
|
|
891
|
-
active;
|
|
892
|
-
createdAt;
|
|
893
|
-
}
|
|
894
|
-
__decorate([
|
|
895
|
-
classValidator.IsString()
|
|
896
|
-
], PrimitiveTest.prototype, "name", void 0);
|
|
897
|
-
__decorate([
|
|
898
|
-
classValidator.IsNumber()
|
|
899
|
-
], PrimitiveTest.prototype, "price", void 0);
|
|
900
|
-
__decorate([
|
|
901
|
-
classValidator.IsBoolean()
|
|
902
|
-
], PrimitiveTest.prototype, "active", void 0);
|
|
903
|
-
__decorate([
|
|
904
|
-
classValidator.IsDate()
|
|
905
|
-
], PrimitiveTest.prototype, "createdAt", void 0);
|
|
906
|
-
const result = transform(PrimitiveTest);
|
|
907
|
-
assert.strictEqual(result.schema.properties.name.type, 'string');
|
|
908
|
-
assert.strictEqual(result.schema.properties.price.type, 'number');
|
|
909
|
-
assert.strictEqual(result.schema.properties.active.type, 'boolean');
|
|
910
|
-
assert.strictEqual(result.schema.properties.createdAt.type, 'string');
|
|
911
|
-
assert.strictEqual(result.schema.properties.createdAt.format, 'date-time');
|
|
912
|
-
});
|
|
913
|
-
node_test.test('should handle array types', () => {
|
|
914
|
-
class ArrayTest {
|
|
915
|
-
tags;
|
|
916
|
-
}
|
|
917
|
-
__decorate([
|
|
918
|
-
classValidator.IsArray(),
|
|
919
|
-
classValidator.IsString({ each: true })
|
|
920
|
-
], ArrayTest.prototype, "tags", void 0);
|
|
921
|
-
const result = transform(ArrayTest);
|
|
922
|
-
assert.strictEqual(result.schema.properties.tags.type, 'array');
|
|
923
|
-
assert.ok(result.schema.properties.tags.items);
|
|
924
|
-
assert.strictEqual(result.schema.properties.tags.items.type, 'string');
|
|
925
|
-
});
|
|
926
|
-
node_test.test('should handle file upload types', () => {
|
|
927
|
-
class FileTest {
|
|
928
|
-
avatar;
|
|
929
|
-
files;
|
|
930
|
-
}
|
|
931
|
-
const result = transform(FileTest);
|
|
932
|
-
assert.strictEqual(result.schema.properties.avatar.type, 'string');
|
|
933
|
-
assert.strictEqual(result.schema.properties.avatar.format, 'binary');
|
|
934
|
-
assert.strictEqual(result.schema.properties.files.type, 'array');
|
|
935
|
-
assert.strictEqual(result.schema.properties.files.items.type, 'string');
|
|
936
|
-
assert.strictEqual(result.schema.properties.files.items.format, 'binary');
|
|
937
|
-
});
|
|
938
|
-
node_test.test('should handle validation decorators', () => {
|
|
939
|
-
class ValidationTest {
|
|
940
|
-
name;
|
|
941
|
-
age;
|
|
942
|
-
email;
|
|
943
|
-
price;
|
|
944
|
-
}
|
|
945
|
-
__decorate([
|
|
946
|
-
classValidator.IsString(),
|
|
947
|
-
classValidator.MinLength(5),
|
|
948
|
-
classValidator.MaxLength(100)
|
|
949
|
-
], ValidationTest.prototype, "name", void 0);
|
|
950
|
-
__decorate([
|
|
951
|
-
classValidator.IsInt(),
|
|
952
|
-
classValidator.Min(18),
|
|
953
|
-
classValidator.Max(100)
|
|
954
|
-
], ValidationTest.prototype, "age", void 0);
|
|
955
|
-
__decorate([
|
|
956
|
-
classValidator.IsEmail()
|
|
957
|
-
], ValidationTest.prototype, "email", void 0);
|
|
958
|
-
__decorate([
|
|
959
|
-
classValidator.IsNumber(),
|
|
960
|
-
classValidator.IsPositive()
|
|
961
|
-
], ValidationTest.prototype, "price", void 0);
|
|
962
|
-
const result = transform(ValidationTest);
|
|
963
|
-
// String with length constraints
|
|
964
|
-
assert.strictEqual(result.schema.properties.name.type, 'string');
|
|
965
|
-
assert.strictEqual(result.schema.properties.name.minLength, 5);
|
|
966
|
-
assert.strictEqual(result.schema.properties.name.maxLength, 100);
|
|
967
|
-
// Integer with min/max
|
|
968
|
-
assert.strictEqual(result.schema.properties.age.type, 'integer');
|
|
969
|
-
assert.strictEqual(result.schema.properties.age.format, 'int32');
|
|
970
|
-
assert.strictEqual(result.schema.properties.age.minimum, 18);
|
|
971
|
-
assert.strictEqual(result.schema.properties.age.maximum, 100);
|
|
972
|
-
// Email format
|
|
973
|
-
assert.strictEqual(result.schema.properties.email.format, 'email');
|
|
974
|
-
// Positive number
|
|
975
|
-
assert.strictEqual(result.schema.properties.price.minimum, 0);
|
|
976
|
-
});
|
|
977
|
-
node_test.test('should handle array validation decorators', () => {
|
|
978
|
-
class ArrayValidationTest {
|
|
979
|
-
requiredTags;
|
|
980
|
-
boundedArray;
|
|
981
|
-
}
|
|
982
|
-
__decorate([
|
|
983
|
-
classValidator.IsArray(),
|
|
984
|
-
classValidator.ArrayNotEmpty(),
|
|
985
|
-
classValidator.IsString({ each: true })
|
|
986
|
-
], ArrayValidationTest.prototype, "requiredTags", void 0);
|
|
987
|
-
__decorate([
|
|
988
|
-
classValidator.IsArray(),
|
|
989
|
-
classValidator.ArrayMinSize(2),
|
|
990
|
-
classValidator.ArrayMaxSize(10)
|
|
991
|
-
], ArrayValidationTest.prototype, "boundedArray", void 0);
|
|
992
|
-
const result = transform(ArrayValidationTest);
|
|
993
|
-
// Required array with minimum items
|
|
994
|
-
assert.strictEqual(result.schema.properties.requiredTags.type, 'array');
|
|
995
|
-
assert.strictEqual(result.schema.properties.requiredTags.minItems, 1);
|
|
996
|
-
assert.ok(result.schema.required.includes('requiredTags'));
|
|
997
|
-
// Array with size bounds
|
|
998
|
-
assert.strictEqual(result.schema.properties.boundedArray.type, 'array');
|
|
999
|
-
assert.strictEqual(result.schema.properties.boundedArray.minItems, 2);
|
|
1000
|
-
assert.strictEqual(result.schema.properties.boundedArray.maxItems, 10);
|
|
1001
|
-
});
|
|
1002
|
-
node_test.test('should handle Length decorator variations', () => {
|
|
1003
|
-
class LengthTest {
|
|
1004
|
-
code;
|
|
1005
|
-
shortCode;
|
|
1006
|
-
}
|
|
1007
|
-
__decorate([
|
|
1008
|
-
classValidator.IsString(),
|
|
1009
|
-
classValidator.Length(3, 10)
|
|
1010
|
-
], LengthTest.prototype, "code", void 0);
|
|
1011
|
-
__decorate([
|
|
1012
|
-
classValidator.IsString(),
|
|
1013
|
-
classValidator.Length(5)
|
|
1014
|
-
], LengthTest.prototype, "shortCode", void 0);
|
|
1015
|
-
const result = transform(LengthTest);
|
|
1016
|
-
// Length with min and max
|
|
1017
|
-
assert.strictEqual(result.schema.properties.code.minLength, 3);
|
|
1018
|
-
assert.strictEqual(result.schema.properties.code.maxLength, 10);
|
|
1019
|
-
// Length with only min
|
|
1020
|
-
assert.strictEqual(result.schema.properties.shortCode.minLength, 5);
|
|
1021
|
-
assert.strictEqual(result.schema.properties.shortCode.maxLength, undefined);
|
|
1022
|
-
});
|
|
1023
|
-
node_test.test('should handle required fields with IsNotEmpty', () => {
|
|
1024
|
-
class RequiredTest {
|
|
1025
|
-
name;
|
|
1026
|
-
optionalField; // Added ? to make it optional
|
|
1027
|
-
}
|
|
1028
|
-
__decorate([
|
|
1029
|
-
classValidator.IsString(),
|
|
1030
|
-
classValidator.IsNotEmpty()
|
|
1031
|
-
], RequiredTest.prototype, "name", void 0);
|
|
1032
|
-
__decorate([
|
|
1033
|
-
classValidator.IsString()
|
|
1034
|
-
], RequiredTest.prototype, "optionalField", void 0);
|
|
1035
|
-
const result = transform(RequiredTest);
|
|
1036
|
-
assert.ok(result.schema.required.includes('name'));
|
|
1037
|
-
assert.ok(!result.schema.required.includes('optionalField'));
|
|
1038
|
-
});
|
|
1039
|
-
node_test.test('should handle nested objects', () => {
|
|
1040
|
-
class Address {
|
|
1041
|
-
street;
|
|
1042
|
-
city;
|
|
1043
|
-
}
|
|
1044
|
-
__decorate([
|
|
1045
|
-
classValidator.IsString(),
|
|
1046
|
-
classValidator.IsNotEmpty()
|
|
1047
|
-
], Address.prototype, "street", void 0);
|
|
1048
|
-
__decorate([
|
|
1049
|
-
classValidator.IsString()
|
|
1050
|
-
], Address.prototype, "city", void 0);
|
|
1051
|
-
class User {
|
|
1052
|
-
name;
|
|
1053
|
-
address;
|
|
1054
|
-
}
|
|
1055
|
-
__decorate([
|
|
1056
|
-
classValidator.IsString()
|
|
1057
|
-
], User.prototype, "name", void 0);
|
|
1058
|
-
__decorate([
|
|
1059
|
-
classValidator.IsNotEmpty()
|
|
1060
|
-
], User.prototype, "address", void 0);
|
|
1061
|
-
const result = transform(User);
|
|
1062
|
-
assert.strictEqual(result.schema.properties.address.type, 'object');
|
|
1063
|
-
assert.ok(result.schema.properties.address.properties);
|
|
1064
|
-
assert.ok(result.schema.properties.address.properties.street);
|
|
1065
|
-
assert.ok(result.schema.properties.address.properties.city);
|
|
1066
|
-
assert.ok(result.schema.required.includes('address'));
|
|
1067
|
-
});
|
|
1068
|
-
node_test.test('should handle generic types like BaseDto<T>', () => {
|
|
1069
|
-
class GenericTest {
|
|
1070
|
-
user;
|
|
1071
|
-
description;
|
|
1072
|
-
}
|
|
1073
|
-
const result = transform(GenericTest);
|
|
1074
|
-
assert.strictEqual(result.schema.properties.user.type, 'object');
|
|
1075
|
-
assert.ok(result.schema.properties.user.properties);
|
|
1076
|
-
assert.ok(result.schema.properties.user.properties.name);
|
|
1077
|
-
assert.ok(result.schema.properties.user.properties.id);
|
|
1078
|
-
assert.strictEqual(result.schema.properties.user.properties.name.type, 'string');
|
|
1079
|
-
assert.strictEqual(result.schema.properties.user.properties.id.type, 'number');
|
|
1080
|
-
assert.strictEqual(result.schema.properties.description.type, 'string');
|
|
1081
|
-
});
|
|
1082
|
-
node_test.test('should use proper cache keys to avoid conflicts between classes with same names', () => {
|
|
1083
|
-
class TestClass {
|
|
1084
|
-
name;
|
|
1085
|
-
value;
|
|
1086
|
-
}
|
|
1087
|
-
__decorate([
|
|
1088
|
-
classValidator.IsString()
|
|
1089
|
-
], TestClass.prototype, "name", void 0);
|
|
1090
|
-
__decorate([
|
|
1091
|
-
classValidator.IsNumber()
|
|
1092
|
-
], TestClass.prototype, "value", void 0);
|
|
1093
|
-
// Transform an existing class multiple times
|
|
1094
|
-
const result1 = transform(TestClass);
|
|
1095
|
-
const result2 = transform(TestClass);
|
|
1096
|
-
// Both results should be identical (this indicates that the cache works)
|
|
1097
|
-
assert.deepStrictEqual(result1, result2);
|
|
1098
|
-
// Verify that the properties are correct
|
|
1099
|
-
assert.ok(result1.schema.properties.name);
|
|
1100
|
-
assert.ok(result1.schema.properties.value);
|
|
1101
|
-
assert.strictEqual(result1.schema.properties.name.type, 'string');
|
|
1102
|
-
assert.strictEqual(result1.schema.properties.value.type, 'number');
|
|
1103
|
-
});
|
|
1104
|
-
node_test.test('should manage memory efficiently with cache cleanup', () => {
|
|
1105
|
-
// Clear any existing singleton instance
|
|
1106
|
-
SchemaTransformer.clearInstance();
|
|
1107
|
-
// Create a transformer with small cache size for testing
|
|
1108
|
-
const transformer = SchemaTransformer.getInstance(undefined, {
|
|
1109
|
-
maxCacheSize: 2,
|
|
1110
|
-
autoCleanup: true,
|
|
1111
|
-
});
|
|
1112
|
-
// Clear any existing cache
|
|
1113
|
-
transformer.clearCache();
|
|
1114
|
-
class TestClass1 {
|
|
1115
|
-
name;
|
|
1116
|
-
}
|
|
1117
|
-
__decorate([
|
|
1118
|
-
classValidator.IsString()
|
|
1119
|
-
], TestClass1.prototype, "name", void 0);
|
|
1120
|
-
class TestClass2 {
|
|
1121
|
-
value;
|
|
1122
|
-
}
|
|
1123
|
-
__decorate([
|
|
1124
|
-
classValidator.IsNumber()
|
|
1125
|
-
], TestClass2.prototype, "value", void 0);
|
|
1126
|
-
class TestClass3 {
|
|
1127
|
-
active;
|
|
1128
|
-
}
|
|
1129
|
-
__decorate([
|
|
1130
|
-
classValidator.IsBoolean()
|
|
1131
|
-
], TestClass3.prototype, "active", void 0);
|
|
1132
|
-
// Transform multiple classes to trigger cache cleanup
|
|
1133
|
-
const result1 = transformer.transform(TestClass1);
|
|
1134
|
-
const result2 = transformer.transform(TestClass2);
|
|
1135
|
-
let stats = transformer.getMemoryStats();
|
|
1136
|
-
assert.strictEqual(stats.cacheSize, 2);
|
|
1137
|
-
// This should trigger cache cleanup due to maxCacheSize: 2
|
|
1138
|
-
const result3 = transformer.transform(TestClass3);
|
|
1139
|
-
stats = transformer.getMemoryStats();
|
|
1140
|
-
assert.ok(stats.cacheSize <= 2, 'Cache should be cleaned up automatically');
|
|
1141
|
-
// Verify results are still correct
|
|
1142
|
-
assert.strictEqual(result1.schema.properties.name.type, 'string');
|
|
1143
|
-
assert.strictEqual(result2.schema.properties.value.type, 'number');
|
|
1144
|
-
assert.strictEqual(result3.schema.properties.active.type, 'boolean');
|
|
1145
|
-
// Test manual cache clearing
|
|
1146
|
-
transformer.clearCache();
|
|
1147
|
-
stats = transformer.getMemoryStats();
|
|
1148
|
-
assert.strictEqual(stats.cacheSize, 0);
|
|
1149
|
-
assert.strictEqual(stats.loadedFiles, 0);
|
|
1150
|
-
});
|
|
1151
|
-
});
|
|
1152
|
-
|
|
1153
|
-
class SimpleUser {
|
|
1154
|
-
name;
|
|
1155
|
-
email;
|
|
1156
|
-
age;
|
|
1157
|
-
}
|
|
1158
|
-
__decorate([
|
|
1159
|
-
classValidator.IsString(),
|
|
1160
|
-
classValidator.IsNotEmpty()
|
|
1161
|
-
], SimpleUser.prototype, "name", void 0);
|
|
1162
|
-
__decorate([
|
|
1163
|
-
classValidator.IsEmail()
|
|
1164
|
-
], SimpleUser.prototype, "email", void 0);
|
|
1165
|
-
__decorate([
|
|
1166
|
-
classValidator.IsInt(),
|
|
1167
|
-
classValidator.Min(18),
|
|
1168
|
-
classValidator.Max(100)
|
|
1169
|
-
], SimpleUser.prototype, "age", void 0);
|
|
1170
|
-
|
|
1171
|
-
class ArrayEntity {
|
|
1172
|
-
basicArray;
|
|
1173
|
-
requiredArray;
|
|
1174
|
-
minSizeArray;
|
|
1175
|
-
maxSizeArray;
|
|
1176
|
-
boundedArray;
|
|
1177
|
-
}
|
|
1178
|
-
__decorate([
|
|
1179
|
-
classValidator.IsArray(),
|
|
1180
|
-
classValidator.IsString({ each: true })
|
|
1181
|
-
], ArrayEntity.prototype, "basicArray", void 0);
|
|
1182
|
-
__decorate([
|
|
1183
|
-
classValidator.IsArray(),
|
|
1184
|
-
classValidator.ArrayNotEmpty(),
|
|
1185
|
-
classValidator.IsString({ each: true })
|
|
1186
|
-
], ArrayEntity.prototype, "requiredArray", void 0);
|
|
1187
|
-
__decorate([
|
|
1188
|
-
classValidator.IsArray(),
|
|
1189
|
-
classValidator.ArrayMinSize(2),
|
|
1190
|
-
classValidator.IsString({ each: true })
|
|
1191
|
-
], ArrayEntity.prototype, "minSizeArray", void 0);
|
|
1192
|
-
__decorate([
|
|
1193
|
-
classValidator.IsArray(),
|
|
1194
|
-
classValidator.ArrayMaxSize(5),
|
|
1195
|
-
classValidator.IsString({ each: true })
|
|
1196
|
-
], ArrayEntity.prototype, "maxSizeArray", void 0);
|
|
1197
|
-
__decorate([
|
|
1198
|
-
classValidator.IsArray(),
|
|
1199
|
-
classValidator.ArrayMinSize(1),
|
|
1200
|
-
classValidator.ArrayMaxSize(3),
|
|
1201
|
-
classValidator.IsString({ each: true })
|
|
1202
|
-
], ArrayEntity.prototype, "boundedArray", void 0);
|
|
1203
|
-
|
|
1204
|
-
class CompleteEntity {
|
|
1205
|
-
id;
|
|
1206
|
-
name;
|
|
1207
|
-
email;
|
|
1208
|
-
active;
|
|
1209
|
-
createdAt;
|
|
1210
|
-
price;
|
|
1211
|
-
code;
|
|
1212
|
-
shortCode;
|
|
1213
|
-
tags;
|
|
1214
|
-
emails;
|
|
1215
|
-
numbers;
|
|
1216
|
-
address;
|
|
1217
|
-
profile;
|
|
1218
|
-
}
|
|
1219
|
-
__decorate([
|
|
1220
|
-
classValidator.IsInt(),
|
|
1221
|
-
classValidator.Min(1)
|
|
1222
|
-
], CompleteEntity.prototype, "id", void 0);
|
|
1223
|
-
__decorate([
|
|
1224
|
-
classValidator.IsString(),
|
|
1225
|
-
classValidator.IsNotEmpty(),
|
|
1226
|
-
classValidator.MinLength(2),
|
|
1227
|
-
classValidator.MaxLength(50)
|
|
1228
|
-
], CompleteEntity.prototype, "name", void 0);
|
|
1229
|
-
__decorate([
|
|
1230
|
-
classValidator.IsEmail()
|
|
1231
|
-
], CompleteEntity.prototype, "email", void 0);
|
|
1232
|
-
__decorate([
|
|
1233
|
-
classValidator.IsBoolean()
|
|
1234
|
-
], CompleteEntity.prototype, "active", void 0);
|
|
1235
|
-
__decorate([
|
|
1236
|
-
classValidator.IsDate()
|
|
1237
|
-
], CompleteEntity.prototype, "createdAt", void 0);
|
|
1238
|
-
__decorate([
|
|
1239
|
-
classValidator.IsNumber(),
|
|
1240
|
-
classValidator.IsPositive()
|
|
1241
|
-
], CompleteEntity.prototype, "price", void 0);
|
|
1242
|
-
__decorate([
|
|
1243
|
-
classValidator.IsString(),
|
|
1244
|
-
classValidator.Length(3, 10)
|
|
1245
|
-
], CompleteEntity.prototype, "code", void 0);
|
|
1246
|
-
__decorate([
|
|
1247
|
-
classValidator.IsString(),
|
|
1248
|
-
classValidator.Length(5)
|
|
1249
|
-
], CompleteEntity.prototype, "shortCode", void 0);
|
|
1250
|
-
__decorate([
|
|
1251
|
-
classValidator.IsArray(),
|
|
1252
|
-
classValidator.IsString({ each: true }),
|
|
1253
|
-
classValidator.ArrayNotEmpty()
|
|
1254
|
-
], CompleteEntity.prototype, "tags", void 0);
|
|
1255
|
-
__decorate([
|
|
1256
|
-
classValidator.IsArray()
|
|
1257
|
-
], CompleteEntity.prototype, "emails", void 0);
|
|
1258
|
-
__decorate([
|
|
1259
|
-
classValidator.IsArray(),
|
|
1260
|
-
classValidator.ArrayMinSize(1),
|
|
1261
|
-
classValidator.ArrayMaxSize(5)
|
|
1262
|
-
], CompleteEntity.prototype, "numbers", void 0);
|
|
1263
|
-
__decorate([
|
|
1264
|
-
classValidator.IsNotEmpty()
|
|
1265
|
-
], CompleteEntity.prototype, "address", void 0);
|
|
1266
|
-
|
|
1267
|
-
class BrokenEntity {
|
|
1268
|
-
name;
|
|
1269
|
-
// This should cause issues - circular reference
|
|
1270
|
-
parent;
|
|
1271
|
-
// Array without proper type decoration
|
|
1272
|
-
items;
|
|
1273
|
-
// Property without any decorators
|
|
1274
|
-
undecoratedProperty;
|
|
1275
|
-
// Complex type that doesn't exist
|
|
1276
|
-
complexType;
|
|
1277
|
-
}
|
|
1278
|
-
__decorate([
|
|
1279
|
-
classValidator.IsString(),
|
|
1280
|
-
classValidator.IsNotEmpty()
|
|
1281
|
-
], BrokenEntity.prototype, "name", void 0);
|
|
1282
|
-
__decorate([
|
|
1283
|
-
classValidator.IsArray()
|
|
1284
|
-
], BrokenEntity.prototype, "items", void 0);
|
|
1285
|
-
|
|
1286
|
-
node_test.describe('Transform Function Integration Tests', () => {
|
|
1287
|
-
node_test.test('should transform SimpleUser class correctly', () => {
|
|
1288
|
-
const result = transform(SimpleUser);
|
|
1289
|
-
assert.strictEqual(result.name, 'SimpleUser');
|
|
1290
|
-
assert.strictEqual(result.schema.type, 'object');
|
|
1291
|
-
// Check properties
|
|
1292
|
-
assert.ok(result.schema.properties.name);
|
|
1293
|
-
assert.strictEqual(result.schema.properties.name.type, 'string');
|
|
1294
|
-
assert.ok(result.schema.properties.email);
|
|
1295
|
-
assert.strictEqual(result.schema.properties.email.type, 'string');
|
|
1296
|
-
assert.strictEqual(result.schema.properties.email.format, 'email');
|
|
1297
|
-
assert.ok(result.schema.properties.age);
|
|
1298
|
-
assert.strictEqual(result.schema.properties.age.type, 'integer');
|
|
1299
|
-
assert.strictEqual(result.schema.properties.age.format, 'int32');
|
|
1300
|
-
assert.strictEqual(result.schema.properties.age.minimum, 18);
|
|
1301
|
-
assert.strictEqual(result.schema.properties.age.maximum, 100);
|
|
1302
|
-
// Check required fields
|
|
1303
|
-
assert.ok(result.schema.required.includes('name'));
|
|
1304
|
-
});
|
|
1305
|
-
node_test.test('should transform ArrayEntity with array decorators correctly', () => {
|
|
1306
|
-
const result = transform(ArrayEntity);
|
|
1307
|
-
assert.strictEqual(result.name, 'ArrayEntity');
|
|
1308
|
-
// Basic array
|
|
1309
|
-
assert.strictEqual(result.schema.properties.basicArray.type, 'array');
|
|
1310
|
-
// Required array with ArrayNotEmpty
|
|
1311
|
-
assert.strictEqual(result.schema.properties.requiredArray.type, 'array');
|
|
1312
|
-
assert.strictEqual(result.schema.properties.requiredArray.minItems, 1);
|
|
1313
|
-
assert.ok(result.schema.required.includes('requiredArray'));
|
|
1314
|
-
// Array with minimum size
|
|
1315
|
-
assert.strictEqual(result.schema.properties.minSizeArray.type, 'array');
|
|
1316
|
-
assert.strictEqual(result.schema.properties.minSizeArray.minItems, 2);
|
|
1317
|
-
// Array with maximum size
|
|
1318
|
-
assert.strictEqual(result.schema.properties.maxSizeArray.type, 'array');
|
|
1319
|
-
assert.strictEqual(result.schema.properties.maxSizeArray.maxItems, 5);
|
|
1320
|
-
// Array with both min and max size
|
|
1321
|
-
assert.strictEqual(result.schema.properties.boundedArray.type, 'array');
|
|
1322
|
-
assert.strictEqual(result.schema.properties.boundedArray.minItems, 1);
|
|
1323
|
-
assert.strictEqual(result.schema.properties.boundedArray.maxItems, 3);
|
|
1324
|
-
});
|
|
1325
|
-
node_test.test('should transform CompleteEntity with all decorators correctly', () => {
|
|
1326
|
-
const result = transform(CompleteEntity);
|
|
1327
|
-
assert.strictEqual(result.name, 'CompleteEntity');
|
|
1328
|
-
// Validate all properties exist
|
|
1329
|
-
const expectedProperties = [
|
|
1330
|
-
'id',
|
|
1331
|
-
'name',
|
|
1332
|
-
'email',
|
|
1333
|
-
'active',
|
|
1334
|
-
'createdAt',
|
|
1335
|
-
'price',
|
|
1336
|
-
'code',
|
|
1337
|
-
'shortCode',
|
|
1338
|
-
'tags',
|
|
1339
|
-
'emails',
|
|
1340
|
-
'numbers',
|
|
1341
|
-
'address',
|
|
1342
|
-
'profile',
|
|
1343
|
-
];
|
|
1344
|
-
expectedProperties.forEach(prop => {
|
|
1345
|
-
assert.ok(result.schema.properties[prop], `Property ${prop} should exist`);
|
|
1346
|
-
});
|
|
1347
|
-
// Validate required fields
|
|
1348
|
-
const expectedRequired = ['name', 'tags', 'address'];
|
|
1349
|
-
expectedRequired.forEach(field => {
|
|
1350
|
-
assert.ok(result.schema.required.includes(field), `Field ${field} should be required`);
|
|
1351
|
-
});
|
|
1352
|
-
// IsInt with Min
|
|
1353
|
-
assert.strictEqual(result.schema.properties.id.type, 'integer');
|
|
1354
|
-
assert.strictEqual(result.schema.properties.id.format, 'int32');
|
|
1355
|
-
assert.strictEqual(result.schema.properties.id.minimum, 1);
|
|
1356
|
-
// IsString with constraints
|
|
1357
|
-
assert.strictEqual(result.schema.properties.name.type, 'string');
|
|
1358
|
-
assert.strictEqual(result.schema.properties.name.minLength, 2);
|
|
1359
|
-
assert.strictEqual(result.schema.properties.name.maxLength, 50);
|
|
1360
|
-
assert.ok(result.schema.required.includes('name'));
|
|
1361
|
-
// IsEmail
|
|
1362
|
-
assert.strictEqual(result.schema.properties.email.format, 'email');
|
|
1363
|
-
// IsBoolean
|
|
1364
|
-
assert.strictEqual(result.schema.properties.active.type, 'boolean');
|
|
1365
|
-
// IsDate
|
|
1366
|
-
assert.strictEqual(result.schema.properties.createdAt.type, 'string');
|
|
1367
|
-
assert.strictEqual(result.schema.properties.createdAt.format, 'date-time');
|
|
1368
|
-
// IsNumber with IsPositive
|
|
1369
|
-
assert.strictEqual(result.schema.properties.price.type, 'number');
|
|
1370
|
-
assert.strictEqual(result.schema.properties.price.minimum, 0);
|
|
1371
|
-
// Length with both min and max
|
|
1372
|
-
assert.strictEqual(result.schema.properties.code.minLength, 3);
|
|
1373
|
-
assert.strictEqual(result.schema.properties.code.maxLength, 10);
|
|
1374
|
-
// Length with only min
|
|
1375
|
-
assert.strictEqual(result.schema.properties.shortCode.minLength, 5);
|
|
1376
|
-
// Array with string items and ArrayNotEmpty
|
|
1377
|
-
assert.strictEqual(result.schema.properties.tags.type, 'array');
|
|
1378
|
-
assert.strictEqual(result.schema.properties.tags.minItems, 1);
|
|
1379
|
-
assert.ok(result.schema.required.includes('tags'));
|
|
1380
|
-
// Array with email validation on items
|
|
1381
|
-
assert.strictEqual(result.schema.properties.emails.type, 'array');
|
|
1382
|
-
// Array with size constraints and int items
|
|
1383
|
-
assert.strictEqual(result.schema.properties.numbers.type, 'array');
|
|
1384
|
-
assert.strictEqual(result.schema.properties.numbers.minItems, 1);
|
|
1385
|
-
assert.strictEqual(result.schema.properties.numbers.maxItems, 5);
|
|
1386
|
-
// Nested object reference - complete Address schema
|
|
1387
|
-
assert.strictEqual(result.schema.properties.address.type, 'object');
|
|
1388
|
-
assert.ok(result.schema.required.includes('address'));
|
|
1389
|
-
assert.ok(result.schema.properties.address.properties);
|
|
1390
|
-
// Validate Address schema structure
|
|
1391
|
-
const addressSchema = result.schema.properties.address;
|
|
1392
|
-
assert.ok(addressSchema.properties, 'Address should have properties');
|
|
1393
|
-
assert.ok(addressSchema.properties.street, 'Address should have street property');
|
|
1394
|
-
assert.strictEqual(addressSchema.properties.street.type, 'string');
|
|
1395
|
-
assert.ok(addressSchema.properties.city, 'Address should have city property');
|
|
1396
|
-
assert.strictEqual(addressSchema.properties.city.type, 'string');
|
|
1397
|
-
// Note: Some properties might not be detected due to TypeScript compilation
|
|
1398
|
-
// This is a known limitation of the current implementation
|
|
1399
|
-
if (addressSchema.properties.country) {
|
|
1400
|
-
assert.strictEqual(addressSchema.properties.country.type, 'string');
|
|
1401
|
-
if (addressSchema.properties.country.minLength) {
|
|
1402
|
-
assert.strictEqual(addressSchema.properties.country.minLength, 2);
|
|
1403
|
-
}
|
|
1404
|
-
}
|
|
1405
|
-
assert.ok(addressSchema.required, 'Address should have required array');
|
|
1406
|
-
assert.ok(addressSchema.required.includes('street'), 'Street should be required');
|
|
1407
|
-
// City might not be required in all cases
|
|
1408
|
-
if (addressSchema.required.includes('city')) {
|
|
1409
|
-
assert.ok(true, 'City is required');
|
|
1410
|
-
}
|
|
1411
|
-
// Partial reference - this should actually fail for complex types
|
|
1412
|
-
assert.strictEqual(result.schema.properties.profile.type, 'object');
|
|
1413
|
-
// This might fail - Partial types shouldn't expand nested schemas
|
|
1414
|
-
assert.strictEqual(result.schema.properties.profile.properties, undefined, 'Partial should not expand nested properties');
|
|
1415
|
-
});
|
|
1416
|
-
node_test.test('should handle problematic BrokenEntity and expose issues', () => {
|
|
1417
|
-
const result = transform(BrokenEntity);
|
|
1418
|
-
assert.strictEqual(result.name, 'BrokenEntity');
|
|
1419
|
-
// Circular reference should be handled
|
|
1420
|
-
assert.ok(result.schema.properties.parent, 'Should have parent property');
|
|
1421
|
-
assert.strictEqual(result.schema.properties.parent.type, 'object');
|
|
1422
|
-
// Array without specific item type
|
|
1423
|
-
assert.strictEqual(result.schema.properties.items.type, 'array');
|
|
1424
|
-
assert.ok(result.schema.properties.items.items, 'Array should have items definition');
|
|
1425
|
-
// Undecorated property should still appear
|
|
1426
|
-
assert.ok(result.schema.properties.undecoratedProperty, 'Undecorated property should exist');
|
|
1427
|
-
// Complex type should fallback to object
|
|
1428
|
-
assert.strictEqual(result.schema.properties.complexType.type, 'object');
|
|
1429
|
-
// Only name should be required
|
|
1430
|
-
assert.deepStrictEqual(result.schema.required, ['name']);
|
|
1431
|
-
});
|
|
1432
|
-
});
|
|
1433
|
-
|
|
1434
|
-
node_test.describe('Plain Classes Without Decorators', () => {
|
|
1435
|
-
node_test.test('should handle plain class without decorators', () => {
|
|
1436
|
-
class PlainUser {
|
|
1437
|
-
id;
|
|
1438
|
-
name;
|
|
1439
|
-
email;
|
|
1440
|
-
age;
|
|
1441
|
-
isActive;
|
|
1442
|
-
tags;
|
|
1443
|
-
createdAt;
|
|
1444
|
-
}
|
|
1445
|
-
try {
|
|
1446
|
-
const result = transform(PlainUser);
|
|
1447
|
-
// Verify basic structure
|
|
1448
|
-
assert.strictEqual(result.name, 'PlainUser');
|
|
1449
|
-
assert.strictEqual(result.schema.type, 'object');
|
|
1450
|
-
// Check if properties are detected
|
|
1451
|
-
const properties = result.schema.properties;
|
|
1452
|
-
assert.ok(properties.id, 'Should have id property');
|
|
1453
|
-
assert.ok(properties.name, 'Should have name property');
|
|
1454
|
-
assert.ok(properties.email, 'Should have email property');
|
|
1455
|
-
assert.ok(properties.age, 'Should have age property');
|
|
1456
|
-
assert.ok(properties.isActive, 'Should have isActive property');
|
|
1457
|
-
assert.ok(properties.tags, 'Should have tags property');
|
|
1458
|
-
assert.ok(properties.createdAt, 'Should have createdAt property');
|
|
1459
|
-
}
|
|
1460
|
-
catch (error) {
|
|
1461
|
-
console.error('Error transforming plain class:', error instanceof Error ? error.message : String(error));
|
|
1462
|
-
throw error;
|
|
1463
|
-
}
|
|
1464
|
-
});
|
|
1465
|
-
});
|
|
1466
|
-
|
|
1467
|
-
class OptionalPropertiesUser {
|
|
1468
|
-
// Required property without decorator (no ?)
|
|
1469
|
-
name;
|
|
1470
|
-
// Optional property without decorator (has ?)
|
|
1471
|
-
nickname;
|
|
1472
|
-
// Required property with decorator (no ?)
|
|
1473
|
-
email;
|
|
1474
|
-
// Optional property with @IsOptional decorator (has ?)
|
|
1475
|
-
middleName;
|
|
1476
|
-
// Required property with decorator but no IsNotEmpty (no ?)
|
|
1477
|
-
age;
|
|
1478
|
-
// Optional property with decorator but no IsOptional (has ?)
|
|
1479
|
-
score;
|
|
1480
|
-
// Property with IsNotEmpty but marked as optional (has ?)
|
|
1481
|
-
// This should still be required because IsNotEmpty overrides the optional status
|
|
1482
|
-
requiredButOptionalSyntax;
|
|
1483
|
-
// Plain optional property without any decorators (has ?)
|
|
1484
|
-
bio;
|
|
1485
|
-
// Plain required property without any decorators (no ?)
|
|
1486
|
-
username;
|
|
1487
|
-
}
|
|
1488
|
-
__decorate([
|
|
1489
|
-
classValidator.IsString(),
|
|
1490
|
-
classValidator.IsNotEmpty()
|
|
1491
|
-
], OptionalPropertiesUser.prototype, "email", void 0);
|
|
1492
|
-
__decorate([
|
|
1493
|
-
classValidator.IsOptional(),
|
|
1494
|
-
classValidator.IsString()
|
|
1495
|
-
], OptionalPropertiesUser.prototype, "middleName", void 0);
|
|
1496
|
-
__decorate([
|
|
1497
|
-
classValidator.IsInt(),
|
|
1498
|
-
classValidator.Min(18),
|
|
1499
|
-
classValidator.Max(100)
|
|
1500
|
-
], OptionalPropertiesUser.prototype, "age", void 0);
|
|
1501
|
-
__decorate([
|
|
1502
|
-
classValidator.IsInt(),
|
|
1503
|
-
classValidator.Min(0)
|
|
1504
|
-
], OptionalPropertiesUser.prototype, "score", void 0);
|
|
1505
|
-
__decorate([
|
|
1506
|
-
classValidator.IsString(),
|
|
1507
|
-
classValidator.IsNotEmpty()
|
|
1508
|
-
], OptionalPropertiesUser.prototype, "requiredButOptionalSyntax", void 0);
|
|
1509
|
-
|
|
1510
|
-
node_test.describe('Optional Properties Tests', () => {
|
|
1511
|
-
node_test.test('should correctly identify required and optional properties based on TypeScript optional operator', () => {
|
|
1512
|
-
// Clear any existing instance to ensure clean state
|
|
1513
|
-
SchemaTransformer.clearInstance();
|
|
1514
|
-
const transformer = SchemaTransformer.getInstance();
|
|
1515
|
-
const result = transformer.transform(OptionalPropertiesUser);
|
|
1516
|
-
assert.strictEqual(result.name, 'OptionalPropertiesUser');
|
|
1517
|
-
assert.strictEqual(result.schema.type, 'object');
|
|
1518
|
-
assert(result.schema.properties);
|
|
1519
|
-
assert(result.schema.required);
|
|
1520
|
-
// Properties that should be required:
|
|
1521
|
-
// - name (no ? operator, no decorators)
|
|
1522
|
-
// - email (no ? operator, has IsNotEmpty)
|
|
1523
|
-
// - age (no ? operator, has decorators but no IsNotEmpty)
|
|
1524
|
-
// - requiredButOptionalSyntax (has ? but also has IsNotEmpty which overrides)
|
|
1525
|
-
// - username (no ? operator, no decorators)
|
|
1526
|
-
const expectedRequired = [
|
|
1527
|
-
'email',
|
|
1528
|
-
'age',
|
|
1529
|
-
'requiredButOptionalSyntax',
|
|
1530
|
-
'name',
|
|
1531
|
-
'username', // No ? operator, no decorators
|
|
1532
|
-
];
|
|
1533
|
-
// Properties that should NOT be required:
|
|
1534
|
-
// - nickname (has ? operator, no decorators)
|
|
1535
|
-
// - middleName (has ? operator, has IsOptional)
|
|
1536
|
-
// - score (has ? operator, has decorators but no IsNotEmpty)
|
|
1537
|
-
// - bio (has ? operator, no decorators)
|
|
1538
|
-
const expectedOptional = ['nickname', 'middleName', 'score', 'bio'];
|
|
1539
|
-
// Check that all expected required properties are in the required array
|
|
1540
|
-
for (const prop of expectedRequired) {
|
|
1541
|
-
assert(result.schema.required.includes(prop), `Property ${prop} should be required`);
|
|
1542
|
-
}
|
|
1543
|
-
// Check that optional properties are NOT in the required array
|
|
1544
|
-
for (const prop of expectedOptional) {
|
|
1545
|
-
assert(!result.schema.required.includes(prop), `Property ${prop} should be optional`);
|
|
1546
|
-
}
|
|
1547
|
-
// Verify specific property types and formats
|
|
1548
|
-
assert.strictEqual(result.schema.properties.name.type, 'string');
|
|
1549
|
-
assert.strictEqual(result.schema.properties.nickname.type, 'string');
|
|
1550
|
-
assert.strictEqual(result.schema.properties.email.type, 'string');
|
|
1551
|
-
assert.strictEqual(result.schema.properties.middleName.type, 'string');
|
|
1552
|
-
assert.strictEqual(result.schema.properties.age.type, 'integer');
|
|
1553
|
-
assert.strictEqual(result.schema.properties.score.type, 'integer');
|
|
1554
|
-
assert.strictEqual(result.schema.properties.bio.type, 'string');
|
|
1555
|
-
assert.strictEqual(result.schema.properties.username.type, 'string');
|
|
1556
|
-
// Verify decorator constraints are still applied
|
|
1557
|
-
assert.strictEqual(result.schema.properties.age.minimum, 18);
|
|
1558
|
-
assert.strictEqual(result.schema.properties.age.maximum, 100);
|
|
1559
|
-
assert.strictEqual(result.schema.properties.score.minimum, 0);
|
|
1560
|
-
});
|
|
1561
|
-
node_test.test('should handle properties with only TypeScript optional operator (no class-validator decorators)', () => {
|
|
1562
|
-
// Clear any existing instance to ensure clean state
|
|
1563
|
-
SchemaTransformer.clearInstance();
|
|
1564
|
-
const transformer = SchemaTransformer.getInstance();
|
|
1565
|
-
class PlainTypeScriptClass {
|
|
1566
|
-
requiredProp;
|
|
1567
|
-
optionalProp;
|
|
1568
|
-
requiredNumber;
|
|
1569
|
-
optionalNumber;
|
|
1570
|
-
}
|
|
1571
|
-
const result = transformer.transform(PlainTypeScriptClass);
|
|
1572
|
-
// Required properties (no ? operator)
|
|
1573
|
-
assert(result.schema.required.includes('requiredProp'), 'requiredProp should be required');
|
|
1574
|
-
assert(result.schema.required.includes('requiredNumber'), 'requiredNumber should be required');
|
|
1575
|
-
// Optional properties (has ? operator)
|
|
1576
|
-
assert(!result.schema.required.includes('optionalProp'), 'optionalProp should be optional');
|
|
1577
|
-
assert(!result.schema.required.includes('optionalNumber'), 'optionalNumber should be optional');
|
|
1578
|
-
// All properties should exist in the schema
|
|
1579
|
-
assert(result.schema.properties.requiredProp);
|
|
1580
|
-
assert(result.schema.properties.optionalProp);
|
|
1581
|
-
assert(result.schema.properties.requiredNumber);
|
|
1582
|
-
assert(result.schema.properties.optionalNumber);
|
|
1583
|
-
});
|
|
1584
|
-
node_test.test('should prioritize IsNotEmpty decorator over TypeScript optional operator', () => {
|
|
1585
|
-
// Clear any existing instance to ensure clean state
|
|
1586
|
-
SchemaTransformer.clearInstance();
|
|
1587
|
-
const transformer = SchemaTransformer.getInstance();
|
|
1588
|
-
class MixedOptionalClass {
|
|
1589
|
-
requiredEvenIfOptional; // Should be required due to IsNotEmpty
|
|
1590
|
-
optionalWithDecorator; // Should be optional despite having decorators
|
|
1591
|
-
}
|
|
1592
|
-
__decorate([
|
|
1593
|
-
classValidator.IsString(),
|
|
1594
|
-
classValidator.IsNotEmpty()
|
|
1595
|
-
], MixedOptionalClass.prototype, "requiredEvenIfOptional", void 0);
|
|
1596
|
-
__decorate([
|
|
1597
|
-
classValidator.IsString()
|
|
1598
|
-
], MixedOptionalClass.prototype, "optionalWithDecorator", void 0);
|
|
1599
|
-
const result = transformer.transform(MixedOptionalClass);
|
|
1600
|
-
// IsNotEmpty should override the ? operator
|
|
1601
|
-
assert(result.schema.required.includes('requiredEvenIfOptional'), 'requiredEvenIfOptional should be required due to IsNotEmpty');
|
|
1602
|
-
// Should remain optional since no IsNotEmpty
|
|
1603
|
-
assert(!result.schema.required.includes('optionalWithDecorator'), 'optionalWithDecorator should remain optional');
|
|
1604
|
-
});
|
|
1605
|
-
node_test.test('should work correctly with nested objects and optional properties', () => {
|
|
1606
|
-
// Clear any existing instance to ensure clean state
|
|
1607
|
-
SchemaTransformer.clearInstance();
|
|
1608
|
-
const transformer = SchemaTransformer.getInstance();
|
|
1609
|
-
class UserWithAddress {
|
|
1610
|
-
name;
|
|
1611
|
-
address;
|
|
1612
|
-
requiredAddress;
|
|
1613
|
-
}
|
|
1614
|
-
__decorate([
|
|
1615
|
-
classValidator.IsNotEmpty()
|
|
1616
|
-
], UserWithAddress.prototype, "requiredAddress", void 0);
|
|
1617
|
-
const result = transformer.transform(UserWithAddress);
|
|
1618
|
-
// name and requiredAddress should be required
|
|
1619
|
-
assert(result.schema.required.includes('name'), 'name should be required');
|
|
1620
|
-
assert(result.schema.required.includes('requiredAddress'), 'requiredAddress should be required');
|
|
1621
|
-
// address should be optional
|
|
1622
|
-
assert(!result.schema.required.includes('address'), 'address should be optional');
|
|
1623
|
-
// All properties should exist
|
|
1624
|
-
assert(result.schema.properties.name);
|
|
1625
|
-
assert(result.schema.properties.address);
|
|
1626
|
-
assert(result.schema.properties.requiredAddress);
|
|
1627
|
-
});
|
|
1628
|
-
node_test.test('should demonstrate backward compatibility with existing class-validator decorators', () => {
|
|
1629
|
-
// Clear any existing instance to ensure clean state
|
|
1630
|
-
SchemaTransformer.clearInstance();
|
|
1631
|
-
const transformer = SchemaTransformer.getInstance();
|
|
1632
|
-
class LegacyClass {
|
|
1633
|
-
// Old behavior: only marked as required with IsNotEmpty
|
|
1634
|
-
description; // This will now be required due to no ? operator
|
|
1635
|
-
name; // This remains required due to IsNotEmpty
|
|
1636
|
-
// New behavior: respects TypeScript optional syntax
|
|
1637
|
-
nickname; // This is optional due to ? operator
|
|
1638
|
-
}
|
|
1639
|
-
__decorate([
|
|
1640
|
-
classValidator.IsString()
|
|
1641
|
-
], LegacyClass.prototype, "description", void 0);
|
|
1642
|
-
__decorate([
|
|
1643
|
-
classValidator.IsString(),
|
|
1644
|
-
classValidator.IsNotEmpty()
|
|
1645
|
-
], LegacyClass.prototype, "name", void 0);
|
|
1646
|
-
__decorate([
|
|
1647
|
-
classValidator.IsString()
|
|
1648
|
-
], LegacyClass.prototype, "nickname", void 0);
|
|
1649
|
-
const result = transformer.transform(LegacyClass);
|
|
1650
|
-
// Both description and name should be required now
|
|
1651
|
-
assert(result.schema.required.includes('description'), 'description should be required (no ? operator)');
|
|
1652
|
-
assert(result.schema.required.includes('name'), 'name should be required (IsNotEmpty)');
|
|
1653
|
-
// nickname should be optional
|
|
1654
|
-
assert(!result.schema.required.includes('nickname'), 'nickname should be optional (? operator)');
|
|
1655
|
-
});
|
|
1656
|
-
});
|
|
1657
|
-
//# sourceMappingURL=test.js.map
|