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/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