@lenne.tech/nest-server 11.4.4 → 11.4.6
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/core/common/decorators/unified-field.decorator.d.ts +2 -0
- package/dist/core/common/decorators/unified-field.decorator.js +49 -3
- package/dist/core/common/decorators/unified-field.decorator.js.map +1 -1
- package/dist/core/common/helpers/register-enum.helper.d.ts +11 -0
- package/dist/core/common/helpers/register-enum.helper.js +22 -0
- package/dist/core/common/helpers/register-enum.helper.js.map +1 -0
- package/dist/core/common/pipes/map-and-validate.pipe.js +489 -16
- package/dist/core/common/pipes/map-and-validate.pipe.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/core/common/decorators/unified-field.decorator.ts +88 -4
- package/src/core/common/helpers/register-enum.helper.ts +93 -0
- package/src/core/common/pipes/map-and-validate.pipe.ts +659 -19
- package/src/index.ts +1 -0
|
@@ -1,12 +1,620 @@
|
|
|
1
1
|
import { ArgumentMetadata, BadRequestException, Injectable, PipeTransform } from '@nestjs/common';
|
|
2
|
-
import {
|
|
2
|
+
import { plainToInstance } from 'class-transformer';
|
|
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';
|
|
3
26
|
import { inspect } from 'util';
|
|
4
27
|
|
|
5
|
-
import {
|
|
28
|
+
import { nestedTypeRegistry } from '../decorators/unified-field.decorator';
|
|
29
|
+
import { isBasicType } from '../helpers/input.helper';
|
|
6
30
|
|
|
7
31
|
// Debug mode can be enabled via environment variable: DEBUG_VALIDATION=true
|
|
8
32
|
const DEBUG_VALIDATION = process.env.DEBUG_VALIDATION === 'true';
|
|
9
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
|
+
|
|
10
618
|
@Injectable()
|
|
11
619
|
export class MapAndValidatePipe implements PipeTransform {
|
|
12
620
|
async transform(value: any, metadata: ArgumentMetadata) {
|
|
@@ -32,29 +640,39 @@ export class MapAndValidatePipe implements PipeTransform {
|
|
|
32
640
|
}
|
|
33
641
|
|
|
34
642
|
// Convert to metatype
|
|
643
|
+
let originalPlainValue: any = null;
|
|
644
|
+
let originalPlainKeys: string[] = [];
|
|
645
|
+
const hasCustomMap = !!(metatype as any)?.map;
|
|
646
|
+
|
|
35
647
|
if (!(value instanceof metatype)) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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);
|
|
50
667
|
}
|
|
51
668
|
}
|
|
52
669
|
|
|
53
|
-
// Validate
|
|
670
|
+
// Validate with inheritance (checks all parent classes in the prototype chain)
|
|
54
671
|
if (DEBUG_VALIDATION) {
|
|
55
|
-
console.debug('Starting validation
|
|
672
|
+
console.debug('Starting validation with inheritance');
|
|
56
673
|
}
|
|
57
|
-
|
|
674
|
+
|
|
675
|
+
const errors = await validateWithInheritance(value, originalPlainValue);
|
|
58
676
|
|
|
59
677
|
if (errors.length > 0) {
|
|
60
678
|
if (DEBUG_VALIDATION) {
|
|
@@ -131,6 +749,28 @@ export class MapAndValidatePipe implements PipeTransform {
|
|
|
131
749
|
|
|
132
750
|
if (DEBUG_VALIDATION) {
|
|
133
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) {
|
|
134
774
|
console.debug('=== End Debug ===\n');
|
|
135
775
|
}
|
|
136
776
|
|