@lenne.tech/nest-server 11.4.3 → 11.4.5

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.
@@ -1,13 +1,620 @@
1
1
  import { ArgumentMetadata, BadRequestException, Injectable, PipeTransform } from '@nestjs/common';
2
2
  import { plainToInstance } from 'class-transformer';
3
- import { validate, ValidationError } from 'class-validator';
3
+ import {
4
+ arrayMaxSize,
5
+ arrayMinSize,
6
+ getMetadataStorage,
7
+ isArray,
8
+ isBoolean,
9
+ isDate,
10
+ isDateString,
11
+ isDefined,
12
+ isEmail,
13
+ isEmpty,
14
+ isEnum,
15
+ isInt,
16
+ isNumber,
17
+ isString,
18
+ isURL,
19
+ max,
20
+ maxLength,
21
+ min,
22
+ minLength,
23
+ ValidationError,
24
+ } from 'class-validator';
25
+ import { ValidationMetadata } from 'class-validator/types/metadata/ValidationMetadata';
4
26
  import { inspect } from 'util';
5
27
 
28
+ import { nestedTypeRegistry } from '../decorators/unified-field.decorator';
6
29
  import { isBasicType } from '../helpers/input.helper';
7
30
 
8
31
  // Debug mode can be enabled via environment variable: DEBUG_VALIDATION=true
9
32
  const DEBUG_VALIDATION = process.env.DEBUG_VALIDATION === 'true';
10
33
 
34
+ // Type for constructor functions
35
+ type Constructor = new (...args: any[]) => any;
36
+
37
+ /**
38
+ * Collects all parent classes in the prototype chain
39
+ */
40
+ function getPrototypeChain(target: any): Constructor[] {
41
+ const chain: Constructor[] = [];
42
+ let current = target;
43
+
44
+ while (current && current !== Object.prototype) {
45
+ if (typeof current === 'function') {
46
+ chain.push(current);
47
+ current = Object.getPrototypeOf(current);
48
+ } else {
49
+ current = Object.getPrototypeOf(current.constructor);
50
+ }
51
+ }
52
+
53
+ return chain;
54
+ }
55
+
56
+ /**
57
+ * Validates an object against all classes in its prototype chain
58
+ * This ensures inherited validation decorators are also checked
59
+ */
60
+ async function validateWithInheritance(object: any, originalPlainValue: any): Promise<ValidationError[]> {
61
+ const errors: ValidationError[] = [];
62
+ const metadataStorage = getMetadataStorage();
63
+ const chain = getPrototypeChain(object.constructor);
64
+
65
+ if (DEBUG_VALIDATION) {
66
+ console.debug(
67
+ 'Prototype chain for validation:',
68
+ chain.map((c) => c.name),
69
+ );
70
+ console.debug('Original plain object had keys:', Object.keys(originalPlainValue || {}));
71
+ }
72
+
73
+ // Track which properties have been validated to avoid duplicates from override
74
+ const validatedProperties = new Set<string>();
75
+
76
+ // Validate against each class in the chain
77
+ for (const targetClass of chain) {
78
+ // Get all metadata for this target
79
+ const allMetadata = metadataStorage.getTargetValidationMetadatas(targetClass, null as any, false, false);
80
+
81
+ // Filter to only include metadata that was registered directly on this class
82
+ // This prevents parent class @IsOptional decorators from interfering with child class @IsDefined decorators
83
+ const targetMetadata = allMetadata.filter((m: any) => m.target === targetClass);
84
+
85
+ if (targetMetadata && targetMetadata.length > 0) {
86
+ if (DEBUG_VALIDATION) {
87
+ console.debug(`Validating against ${targetClass.name} (${targetMetadata.length} constraints)`);
88
+ }
89
+
90
+ // Create a temporary instance of this specific class for validation
91
+ // IMPORTANT: Only copy properties that were present in the original plain object
92
+ // This prevents properties initialized with = undefined from being considered "defined"
93
+ const tempInstance = Object.create(targetClass.prototype);
94
+
95
+ // Only copy properties that existed in the original plain input
96
+ // BUT use the transformed values from 'object' (not originalPlainValue) to preserve
97
+ // transformations like Date conversion from @Type() decorators
98
+ if (originalPlainValue) {
99
+ for (const key in originalPlainValue) {
100
+ if (Object.prototype.hasOwnProperty.call(originalPlainValue, key)) {
101
+ // Use transformed value if available, otherwise use original
102
+ let value = object.hasOwnProperty(key) ? object[key] : originalPlainValue[key];
103
+
104
+ // Manual Date transformation for ISO strings
105
+ // Check if the design:type is Date and value is a string
106
+ const designType = Reflect.getMetadata('design:type', targetClass.prototype, key);
107
+ if (designType === Date && typeof value === 'string') {
108
+ try {
109
+ const dateValue = new Date(value);
110
+ // Only use the Date if it's valid (not 'Invalid Date')
111
+ if (!isNaN(dateValue.getTime())) {
112
+ value = dateValue;
113
+ }
114
+ } catch (error) {
115
+ // If Date parsing fails, keep the original value
116
+ // Validation will catch it later
117
+ }
118
+ }
119
+
120
+ tempInstance[key] = value;
121
+ }
122
+ }
123
+ }
124
+
125
+ if (DEBUG_VALIDATION) {
126
+ console.debug(` temp instance keys:`, Object.keys(tempInstance));
127
+ }
128
+
129
+ // IMPORTANT: We need to manually validate using only the filtered metadata
130
+ // because class-validator's validate() function will see ALL metadata including parent classes
131
+ // This would cause @IsOptional() from parent classes to interfere with @IsDefined() from child classes
132
+
133
+ // Group metadata by property
134
+ const propertiesByName: Map<string, ValidationMetadata[]> = new Map();
135
+ targetMetadata.forEach((m: ValidationMetadata) => {
136
+ if (!propertiesByName.has(m.propertyName)) {
137
+ propertiesByName.set(m.propertyName, []);
138
+ }
139
+ propertiesByName.get(m.propertyName)!.push(m);
140
+ });
141
+
142
+ // Validate each property using class-validator functions
143
+ const classErrors: ValidationError[] = [];
144
+ for (const [propertyName, metadataList] of Array.from(propertiesByName.entries())) {
145
+ // Skip if this property was already validated in a child class (override)
146
+ if (validatedProperties.has(propertyName)) {
147
+ if (DEBUG_VALIDATION) {
148
+ console.debug(` Skipping ${propertyName} - already validated in child class`);
149
+ }
150
+ continue;
151
+ }
152
+
153
+ const propertyValue = tempInstance[propertyName];
154
+
155
+ // Check if property is optional (has @IsOptional())
156
+ const isOptional = metadataList.some((m) => m.type === 'conditionalValidation' && m.name === 'isOptional');
157
+
158
+ // Check if field is explicitly defined as required with @IsDefined()
159
+ // If isDefined exists, the field is required even if a parent class has @IsOptional()
160
+ const hasIsDefined = metadataList.some((m) => m.type === 'isDefined');
161
+
162
+ // Check for @ValidateIf() conditional validation
163
+ // Note: @ValidateIf() has type='conditionalValidation' with name=undefined
164
+ // while @IsOptional() has type='conditionalValidation' with name='isOptional'
165
+ const validateIfMetadata = metadataList.find((m) => m.type === 'conditionalValidation' && !m.name);
166
+
167
+ // If @ValidateIf() exists, evaluate its condition
168
+ if (validateIfMetadata && validateIfMetadata.constraints?.[0]) {
169
+ const conditionFn = validateIfMetadata.constraints[0];
170
+ let shouldValidate = false;
171
+
172
+ try {
173
+ // Call the condition function with the object and value
174
+ shouldValidate = conditionFn(tempInstance, propertyValue);
175
+ } catch (error) {
176
+ if (DEBUG_VALIDATION) {
177
+ console.debug(` Error evaluating ValidateIf condition for ${propertyName}:`, error);
178
+ }
179
+ // If condition evaluation fails, skip validation
180
+ shouldValidate = false;
181
+ }
182
+
183
+ // If condition returns false, skip all validation for this property
184
+ if (!shouldValidate) {
185
+ if (DEBUG_VALIDATION) {
186
+ console.debug(` Property ${propertyName} skipped by ValidateIf condition`);
187
+ }
188
+ continue;
189
+ }
190
+ }
191
+
192
+ // If property is optional (and not overridden with isDefined) and value is undefined/null, skip all validators
193
+ if (!hasIsDefined && isOptional && (propertyValue === undefined || propertyValue === null)) {
194
+ if (DEBUG_VALIDATION) {
195
+ console.debug(` Property ${propertyName} is optional and undefined/null - skipping validation`);
196
+ }
197
+ continue;
198
+ }
199
+
200
+ const propertyError = new ValidationError();
201
+ propertyError.property = propertyName;
202
+ propertyError.value = propertyValue;
203
+ propertyError.target = tempInstance;
204
+ propertyError.constraints = {};
205
+
206
+ // Apply each constraint for this property
207
+ for (const metadata of metadataList) {
208
+ const constraintType = metadata.type;
209
+ let isValid = true;
210
+ let errorMessage = '';
211
+
212
+ // Check if 'each' validation should be applied (for array elements)
213
+ const shouldValidateEach =
214
+ Array.isArray(propertyValue) && (metadata.validationTypeOptions?.each || metadata.each);
215
+
216
+ // Use class-validator's exported functions directly - this is the official API
217
+ // and will automatically stay updated with class-validator
218
+ switch (constraintType) {
219
+ case 'arrayMaxSize':
220
+ isValid = arrayMaxSize(propertyValue, metadata.constraints?.[0]);
221
+ errorMessage = `${propertyName} must contain no more than ${metadata.constraints?.[0]} elements`;
222
+ break;
223
+ case 'arrayMinSize':
224
+ isValid = arrayMinSize(propertyValue, metadata.constraints?.[0]);
225
+ errorMessage = `${propertyName} must contain at least ${metadata.constraints?.[0]} elements`;
226
+ break;
227
+ case 'customValidation':
228
+ // Execute custom validators using the constraint class
229
+ if (metadata.constraintCls) {
230
+ try {
231
+ const constraintInstance = new (metadata.constraintCls as any)();
232
+ if (typeof constraintInstance.validate === 'function') {
233
+ // Create validation args for error messages
234
+ const validationArgs = {
235
+ constraints: metadata.constraints || [],
236
+ object: tempInstance,
237
+ property: propertyName,
238
+ targetName: targetClass.name,
239
+ value: propertyValue,
240
+ };
241
+
242
+ // Special handling for validators with arrays when 'each' option is set
243
+ // The 'each' property indicates validation should be applied to each array element
244
+ const isArrayValue = Array.isArray(propertyValue);
245
+ const shouldValidateEach =
246
+ metadata.each === true || (metadata as any).validationOptions?.each === true;
247
+
248
+ if (isArrayValue && shouldValidateEach) {
249
+ // Validate each array element individually
250
+ const results: boolean[] = [];
251
+ for (const item of propertyValue) {
252
+ const itemArgs = {
253
+ ...validationArgs,
254
+ value: item,
255
+ };
256
+ const itemResult = constraintInstance.validate(item, itemArgs);
257
+ const itemValid = itemResult instanceof Promise ? await itemResult : itemResult;
258
+ results.push(itemValid);
259
+ }
260
+ isValid = results.every((r) => r === true);
261
+ } else {
262
+ // Call the validate function with the property value and validation arguments
263
+ const validationResult = constraintInstance.validate(propertyValue, validationArgs);
264
+
265
+ // Handle async validators - if it returns a Promise, await it
266
+ isValid = validationResult instanceof Promise ? await validationResult : validationResult;
267
+ }
268
+
269
+ // Get default message and constraint name if validation failed
270
+ if (!isValid) {
271
+ // Use metadata.name for the constraint key (e.g., "isEmail", "isString")
272
+ const constraintName = metadata.name || 'customValidation';
273
+
274
+ if (typeof constraintInstance.defaultMessage === 'function') {
275
+ errorMessage = constraintInstance.defaultMessage(validationArgs);
276
+ // Replace $property placeholder with actual property name
277
+ errorMessage = errorMessage.replace(/\$property/g, propertyName);
278
+ } else {
279
+ errorMessage = `${propertyName} failed custom validation`;
280
+ }
281
+
282
+ // Add to constraints with the proper name
283
+ propertyError.constraints[constraintName] = errorMessage;
284
+ // Don't let the default handler add it again
285
+ continue;
286
+ }
287
+ } else {
288
+ if (DEBUG_VALIDATION) {
289
+ console.debug(` Skipping customValidation for ${propertyName} - no validate method`);
290
+ }
291
+ continue;
292
+ }
293
+ } catch (error) {
294
+ if (DEBUG_VALIDATION) {
295
+ console.debug(` Error executing customValidation for ${propertyName}:`, error);
296
+ }
297
+ // If there's an error executing the validator, skip it
298
+ continue;
299
+ }
300
+ } else {
301
+ if (DEBUG_VALIDATION) {
302
+ console.debug(` Skipping customValidation for ${propertyName} - no constraint class`);
303
+ }
304
+ continue;
305
+ }
306
+ // If validation passed, continue to next constraint
307
+ continue;
308
+ case 'nestedValidation':
309
+ // Validate nested objects or arrays of nested objects
310
+ if (DEBUG_VALIDATION) {
311
+ console.debug(` Nested validation for ${propertyName}`);
312
+ }
313
+
314
+ if (propertyValue !== undefined && propertyValue !== null) {
315
+ const nestedErrors: ValidationError[] = [];
316
+
317
+ // Get the target type from the nested type registry
318
+ const registryKey = `${targetClass.name}.${propertyName}`;
319
+ const nestedType = nestedTypeRegistry.get(registryKey);
320
+
321
+ if (DEBUG_VALIDATION) {
322
+ console.debug(`[NESTED] Looking up ${registryKey}, found:`, nestedType?.name);
323
+ }
324
+
325
+ if (Array.isArray(propertyValue)) {
326
+ // Array of nested objects - validate each element
327
+ for (let i = 0; i < propertyValue.length; i++) {
328
+ const item = propertyValue[i];
329
+
330
+ if (item && typeof item === 'object') {
331
+ // Skip validation if we don't have type information
332
+ if (!nestedType) {
333
+ if (DEBUG_VALIDATION) {
334
+ console.debug(`[NESTED] Skipping validation for array item ${i} - no type info`);
335
+ }
336
+ continue;
337
+ }
338
+
339
+ // Transform plain object to class instance if needed
340
+ let transformedItem = item;
341
+ if (!item.constructor || item.constructor === Object) {
342
+ transformedItem = plainToInstance(nestedType, item, {
343
+ enableImplicitConversion: false,
344
+ excludeExtraneousValues: false,
345
+ });
346
+ }
347
+
348
+ if (DEBUG_VALIDATION) {
349
+ console.debug(`[NESTED] Validating array item ${i}:`);
350
+ console.debug(`[NESTED] Original type: ${item.constructor?.name}`);
351
+ console.debug(`[NESTED] Transformed type: ${transformedItem.constructor?.name}`);
352
+ console.debug(`[NESTED] Target type: ${nestedType?.name}`);
353
+ }
354
+
355
+ // Skip if transformation resulted in invalid value
356
+ if (!transformedItem || typeof transformedItem !== 'object') {
357
+ if (DEBUG_VALIDATION) {
358
+ console.debug(`[NESTED] Skipping validation - invalid transformed item`);
359
+ }
360
+ continue;
361
+ }
362
+
363
+ // Use validateWithInheritance() for nested objects to handle override semantics
364
+ const itemErrors = await validateWithInheritance(transformedItem, item);
365
+
366
+ if (itemErrors.length > 0 && DEBUG_VALIDATION) {
367
+ console.debug(`[NESTED] Errors (${itemErrors.length}):`);
368
+ itemErrors.forEach((err, idx) => {
369
+ console.debug(
370
+ `[NESTED] Error ${idx}: property="${err.property}", constraints=${JSON.stringify(err.constraints)}`,
371
+ );
372
+ });
373
+ }
374
+
375
+ if (itemErrors.length > 0) {
376
+ // Prefix property names with array index for better error messages
377
+ itemErrors.forEach((err) => {
378
+ err.property = `${i}.${err.property}`;
379
+ });
380
+ nestedErrors.push(...itemErrors);
381
+
382
+ if (DEBUG_VALIDATION) {
383
+ console.debug(` Found ${itemErrors.length} errors in item ${i}`);
384
+ }
385
+ }
386
+ }
387
+ }
388
+ } else if (typeof propertyValue === 'object') {
389
+ // Single nested object
390
+ // Skip validation if we don't have type information
391
+ if (!nestedType) {
392
+ if (DEBUG_VALIDATION) {
393
+ console.debug(`[NESTED] Skipping validation for single object - no type info`);
394
+ }
395
+ } else {
396
+ // Transform plain object to class instance if needed
397
+ let transformedItem = propertyValue;
398
+ if (!propertyValue.constructor || propertyValue.constructor === Object) {
399
+ transformedItem = plainToInstance(nestedType, propertyValue, {
400
+ enableImplicitConversion: false,
401
+ excludeExtraneousValues: false,
402
+ });
403
+ }
404
+
405
+ if (DEBUG_VALIDATION) {
406
+ console.debug(`[NESTED] Validating single object:`);
407
+ console.debug(`[NESTED] Original type: ${propertyValue.constructor?.name}`);
408
+ console.debug(`[NESTED] Transformed type: ${transformedItem.constructor?.name}`);
409
+ console.debug(`[NESTED] Target type: ${nestedType?.name}`);
410
+ }
411
+
412
+ // Skip if transformation resulted in invalid value
413
+ if (!transformedItem || typeof transformedItem !== 'object') {
414
+ if (DEBUG_VALIDATION) {
415
+ console.debug(`[NESTED] Skipping validation - invalid transformed item`);
416
+ }
417
+ } else {
418
+ // Use validateWithInheritance() for nested objects to handle override semantics
419
+ const itemErrors = await validateWithInheritance(transformedItem, propertyValue);
420
+
421
+ if (itemErrors.length > 0 && DEBUG_VALIDATION) {
422
+ console.debug(`[NESTED] Errors (${itemErrors.length}):`, itemErrors);
423
+ }
424
+
425
+ if (itemErrors.length > 0) {
426
+ nestedErrors.push(...itemErrors);
427
+
428
+ if (DEBUG_VALIDATION) {
429
+ console.debug(` Found ${itemErrors.length} errors in nested object`);
430
+ }
431
+ }
432
+ }
433
+ }
434
+ }
435
+
436
+ if (nestedErrors.length > 0) {
437
+ propertyError.children = nestedErrors;
438
+
439
+ if (DEBUG_VALIDATION) {
440
+ console.debug(` Total nested errors for ${propertyName}: ${nestedErrors.length}`);
441
+ }
442
+ }
443
+ }
444
+
445
+ // Nested validation doesn't add constraints, only children
446
+ // Continue to next validator
447
+ continue;
448
+ case 'isArray':
449
+ isValid = isArray(propertyValue);
450
+ errorMessage = `${propertyName} must be an array`;
451
+ break;
452
+ case 'isBoolean':
453
+ if (shouldValidateEach) {
454
+ isValid = propertyValue.every((item) => isBoolean(item));
455
+ } else {
456
+ isValid = isBoolean(propertyValue);
457
+ }
458
+ errorMessage = `${propertyName} must be a boolean value`;
459
+ break;
460
+ case 'isDate':
461
+ if (shouldValidateEach) {
462
+ isValid = propertyValue.every((item) => isDate(item));
463
+ } else {
464
+ isValid = isDate(propertyValue);
465
+ }
466
+ errorMessage = `${propertyName} must be a Date instance`;
467
+ break;
468
+ case 'isDateString':
469
+ if (shouldValidateEach) {
470
+ isValid = propertyValue.every((item) => isDateString(item, metadata.constraints?.[0]));
471
+ } else {
472
+ isValid = isDateString(propertyValue, metadata.constraints?.[0]);
473
+ }
474
+ errorMessage = `${propertyName} must be a valid ISO 8601 date string`;
475
+ break;
476
+ case 'isDefined':
477
+ isValid = isDefined(propertyValue);
478
+ errorMessage = `${propertyName} should not be null or undefined`;
479
+ break;
480
+ case 'isEmail':
481
+ if (shouldValidateEach) {
482
+ isValid = propertyValue.every((item) => isEmail(item, metadata.constraints?.[0]));
483
+ } else {
484
+ isValid = isEmail(propertyValue, metadata.constraints?.[0]);
485
+ }
486
+ errorMessage = `${propertyName} must be an email`;
487
+ break;
488
+ case 'isEnum':
489
+ if (shouldValidateEach) {
490
+ isValid = propertyValue.every((item) => isEnum(item, metadata.constraints?.[0]));
491
+ } else {
492
+ isValid = isEnum(propertyValue, metadata.constraints?.[0]);
493
+ }
494
+ errorMessage = `${propertyName} must be a valid enum value`;
495
+ break;
496
+ case 'isInt':
497
+ if (shouldValidateEach) {
498
+ isValid = propertyValue.every((item) => isInt(item));
499
+ } else {
500
+ isValid = isInt(propertyValue);
501
+ }
502
+ errorMessage = `${propertyName} must be an integer number`;
503
+ break;
504
+ case 'isNotEmpty':
505
+ if (shouldValidateEach) {
506
+ isValid = propertyValue.every((item) => !isEmpty(item));
507
+ } else {
508
+ isValid = !isEmpty(propertyValue);
509
+ }
510
+ errorMessage = `${propertyName} should not be empty`;
511
+ break;
512
+ case 'isNumber':
513
+ if (shouldValidateEach) {
514
+ isValid = propertyValue.every((item) => isNumber(item, metadata.constraints?.[0]));
515
+ } else {
516
+ isValid = isNumber(propertyValue, metadata.constraints?.[0]);
517
+ }
518
+ errorMessage = `${propertyName} must be a number conforming to the specified constraints`;
519
+ break;
520
+ case 'isString':
521
+ if (shouldValidateEach) {
522
+ isValid = propertyValue.every((item) => isString(item));
523
+ } else {
524
+ isValid = isString(propertyValue);
525
+ }
526
+ errorMessage = `${propertyName} must be a string`;
527
+ break;
528
+ case 'isUrl':
529
+ if (shouldValidateEach) {
530
+ isValid = propertyValue.every((item) => isURL(item, metadata.constraints?.[0]));
531
+ } else {
532
+ isValid = isURL(propertyValue, metadata.constraints?.[0]);
533
+ }
534
+ errorMessage = `${propertyName} must be an URL address`;
535
+ break;
536
+ case 'max':
537
+ if (shouldValidateEach) {
538
+ isValid = propertyValue.every((item) => max(item, metadata.constraints?.[0]));
539
+ } else {
540
+ isValid = max(propertyValue, metadata.constraints?.[0]);
541
+ }
542
+ errorMessage = `${propertyName} must not be greater than ${metadata.constraints?.[0]}`;
543
+ break;
544
+ case 'maxLength':
545
+ if (shouldValidateEach) {
546
+ isValid = propertyValue.every((item) => maxLength(item, metadata.constraints?.[0]));
547
+ } else {
548
+ isValid = maxLength(propertyValue, metadata.constraints?.[0]);
549
+ }
550
+ errorMessage = `${propertyName} must be shorter than or equal to ${metadata.constraints?.[0]} characters`;
551
+ break;
552
+ case 'min':
553
+ if (shouldValidateEach) {
554
+ isValid = propertyValue.every((item) => min(item, metadata.constraints?.[0]));
555
+ } else {
556
+ isValid = min(propertyValue, metadata.constraints?.[0]);
557
+ }
558
+ errorMessage = `${propertyName} must not be less than ${metadata.constraints?.[0]}`;
559
+ break;
560
+ case 'minLength':
561
+ if (shouldValidateEach) {
562
+ isValid = propertyValue.every((item) => minLength(item, metadata.constraints?.[0]));
563
+ } else {
564
+ isValid = minLength(propertyValue, metadata.constraints?.[0]);
565
+ }
566
+ errorMessage = `${propertyName} must be longer than or equal to ${metadata.constraints?.[0]} characters`;
567
+ break;
568
+ default:
569
+ // For any constraint type we haven't explicitly handled,
570
+ // skip it rather than fail - this maintains forward compatibility
571
+ if (DEBUG_VALIDATION) {
572
+ console.debug(` Skipping unknown constraint type: ${constraintType} for ${propertyName}`);
573
+ }
574
+ continue;
575
+ }
576
+
577
+ // Add constraint violation if validation failed
578
+ if (!isValid) {
579
+ propertyError.constraints[constraintType] = errorMessage;
580
+ }
581
+ }
582
+
583
+ // Add error if there are constraints violated OR nested validation errors
584
+ if (
585
+ Object.keys(propertyError.constraints).length > 0 ||
586
+ (propertyError.children && propertyError.children.length > 0)
587
+ ) {
588
+ classErrors.push(propertyError);
589
+ }
590
+
591
+ // Mark this property as validated
592
+ validatedProperties.add(propertyName);
593
+ }
594
+
595
+ if (DEBUG_VALIDATION) {
596
+ console.debug(` Manual validation found ${classErrors.length} errors`);
597
+ if (classErrors.length > 0) {
598
+ classErrors.forEach((err, idx) => {
599
+ console.debug(
600
+ ` Error ${idx + 1}: property="${err.property}", constraints=${JSON.stringify(err.constraints)}`,
601
+ );
602
+ });
603
+ }
604
+ }
605
+
606
+ if (classErrors.length > 0) {
607
+ if (DEBUG_VALIDATION) {
608
+ console.debug(`Found ${classErrors.length} errors in ${targetClass.name}`);
609
+ }
610
+ errors.push(...classErrors);
611
+ }
612
+ }
613
+ }
614
+
615
+ return errors;
616
+ }
617
+
11
618
  @Injectable()
12
619
  export class MapAndValidatePipe implements PipeTransform {
13
620
  async transform(value: any, metadata: ArgumentMetadata) {
@@ -33,29 +640,39 @@ export class MapAndValidatePipe implements PipeTransform {
33
640
  }
34
641
 
35
642
  // Convert to metatype
643
+ let originalPlainValue: any = null;
644
+ let originalPlainKeys: string[] = [];
645
+ const hasCustomMap = !!(metatype as any)?.map;
646
+
36
647
  if (!(value instanceof metatype)) {
37
- if ((metatype as any)?.map) {
38
- if (DEBUG_VALIDATION) {
39
- console.debug('Using custom map function');
40
- }
41
- value = (metatype as any)?.map(value);
42
- } else {
43
- if (DEBUG_VALIDATION) {
44
- console.debug('Using plainToInstance to transform to:', metatype.name);
45
- }
46
- value = plainToInstance(metatype, value);
47
- if (DEBUG_VALIDATION) {
48
- console.debug('Transformed value:', inspect(value, { colors: true, depth: 3 }));
49
- console.debug('Transformed value instance of:', value?.constructor?.name);
50
- }
648
+ // Store original plain value before transformation
649
+ originalPlainValue = value;
650
+ originalPlainKeys = Object.keys(value);
651
+
652
+ if (DEBUG_VALIDATION) {
653
+ console.debug('Using plainToInstance to transform to:', metatype.name);
654
+ }
655
+ // Use plainToInstance (not Clean) to preserve all properties for validation
656
+ // Disable implicit conversion to avoid unwanted type coercion (e.g., "123" -> 123)
657
+ // Date transformation is handled separately in validateWithInheritance
658
+ value = plainToInstance(metatype, value, {
659
+ enableImplicitConversion: false,
660
+ excludeExtraneousValues: false,
661
+ exposeDefaultValues: false,
662
+ exposeUnsetFields: false,
663
+ });
664
+ if (DEBUG_VALIDATION) {
665
+ console.debug('Transformed value:', inspect(value, { colors: true, depth: 3 }));
666
+ console.debug('Transformed value instance of:', value?.constructor?.name);
51
667
  }
52
668
  }
53
669
 
54
- // Validate
670
+ // Validate with inheritance (checks all parent classes in the prototype chain)
55
671
  if (DEBUG_VALIDATION) {
56
- console.debug('Starting validation...');
672
+ console.debug('Starting validation with inheritance');
57
673
  }
58
- const errors = await validate(value, { forbidUnknownValues: false });
674
+
675
+ const errors = await validateWithInheritance(value, originalPlainValue);
59
676
 
60
677
  if (errors.length > 0) {
61
678
  if (DEBUG_VALIDATION) {
@@ -132,6 +749,28 @@ export class MapAndValidatePipe implements PipeTransform {
132
749
 
133
750
  if (DEBUG_VALIDATION) {
134
751
  console.debug('Validation successful - no errors');
752
+ }
753
+
754
+ // After successful validation: Apply CoreInput.map() cleanup logic
755
+ // Remove properties that did not exist in source and have undefined value
756
+ // This prevents overwriting existing data on update operations
757
+ if (hasCustomMap && originalPlainKeys.length > 0) {
758
+ for (const key in value) {
759
+ if (
760
+ Object.prototype.hasOwnProperty.call(value, key) &&
761
+ !originalPlainKeys.includes(key) &&
762
+ value[key] === undefined
763
+ ) {
764
+ delete value[key];
765
+ }
766
+ }
767
+
768
+ if (DEBUG_VALIDATION) {
769
+ console.debug('After CoreInput cleanup:', inspect(value, { colors: true, depth: 3 }));
770
+ }
771
+ }
772
+
773
+ if (DEBUG_VALIDATION) {
135
774
  console.debug('=== End Debug ===\n');
136
775
  }
137
776