Skip to content

Commit c913e3c

Browse files
authored
fix: assign @NestedValidation error to parent when property is not a class instance (#673)
1 parent 33ed7ad commit c913e3c

File tree

4 files changed

+71
-23
lines changed

4 files changed

+71
-23
lines changed

src/validation/ValidationExecutor.ts

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ export class ValidationExecutor {
205205
}
206206

207207
this.customValidations(object, value, customValidationMetadatas, validationError);
208-
this.nestedValidations(value, nestedValidationMetadatas, validationError.children);
208+
this.nestedValidations(value, nestedValidationMetadatas, validationError);
209209

210210
this.mapContexts(object, value, metadatas, validationError);
211211
this.mapContexts(object, value, customValidationMetadatas, validationError);
@@ -333,35 +333,34 @@ export class ValidationExecutor {
333333
});
334334
}
335335

336-
private nestedValidations(value: any, metadatas: ValidationMetadata[], errors: ValidationError[]): void {
336+
private nestedValidations(value: any, metadatas: ValidationMetadata[], error: ValidationError): void {
337337
if (value === void 0) {
338338
return;
339339
}
340340

341341
metadatas.forEach(metadata => {
342342
if (metadata.type !== ValidationTypes.NESTED_VALIDATION && metadata.type !== ValidationTypes.PROMISE_VALIDATION) {
343343
return;
344+
} else if (
345+
this.validatorOptions &&
346+
this.validatorOptions.stopAtFirstError &&
347+
Object.keys(error.constraints || {}).length > 0
348+
) {
349+
return;
344350
}
345351

346352
if (Array.isArray(value) || value instanceof Set || value instanceof Map) {
347353
// Treats Set as an array - as index of Set value is value itself and it is common case to have Object as value
348354
const arrayLikeValue = value instanceof Set ? Array.from(value) : value;
349355
arrayLikeValue.forEach((subValue: any, index: any) => {
350-
this.performValidations(value, subValue, index.toString(), [], metadatas, errors);
356+
this.performValidations(value, subValue, index.toString(), [], metadatas, error.children);
351357
});
352358
} else if (value instanceof Object) {
353359
const targetSchema = typeof metadata.target === 'string' ? metadata.target : metadata.target.name;
354-
this.execute(value, targetSchema, errors);
360+
this.execute(value, targetSchema, error.children);
355361
} else {
356-
const error = new ValidationError();
357-
error.value = value;
358-
error.property = metadata.propertyName;
359-
error.target = metadata.target as object;
360362
const [type, message] = this.createValidationError(metadata.target as object, value, metadata);
361-
error.constraints = {
362-
[type]: message,
363-
};
364-
errors.push(error);
363+
error.constraints[type] = message;
365364
}
366365
});
367366
}

test/functional/nested-validation.spec.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -165,10 +165,8 @@ describe('nested validation', () => {
165165
return validator.validate(model).then(errors => {
166166
expect(errors[0].target).toEqual(model);
167167
expect(errors[0].property).toEqual('mySubClass');
168-
expect(errors[0].children.length).toEqual(1);
169-
170-
const subError = errors[0].children[0];
171-
expect(subError.constraints).toEqual({
168+
expect(errors[0].children.length).toEqual(0);
169+
expect(errors[0].constraints).toEqual({
172170
[ValidationTypes.NESTED_VALIDATION]: 'nested property mySubClass must be either object or array',
173171
});
174172
});
@@ -307,4 +305,31 @@ describe('nested validation', () => {
307305
expect(subSubError.value).toEqual('my');
308306
});
309307
});
308+
309+
it('nestedValidation should be defined as an error for the property specifying the decorator when validation fails.', () => {
310+
class MySubClass {
311+
@MinLength(5)
312+
name: string;
313+
}
314+
315+
class MyClass {
316+
@ValidateNested()
317+
nestedWithClassValue: MySubClass;
318+
319+
@ValidateNested()
320+
nestedWithPrimitiveValue: MySubClass;
321+
}
322+
323+
const model = new MyClass();
324+
model.nestedWithClassValue = new MySubClass();
325+
model.nestedWithPrimitiveValue = 'invalid' as any;
326+
327+
return validator.validate(model, { stopAtFirstError: true }).then(errors => {
328+
expect(errors[0].property).toEqual('nestedWithClassValue');
329+
expect(errors[0].children.length).toEqual(1);
330+
expect(errors[0].children[0].constraints).toHaveProperty('minLength');
331+
expect(errors[1].property).toEqual('nestedWithPrimitiveValue');
332+
expect(errors[1].constraints).toHaveProperty('nestedValidation');
333+
});
334+
});
310335
});

test/functional/promise-validation.spec.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,10 +120,8 @@ describe('promise validation', () => {
120120
return validator.validate(model).then(errors => {
121121
expect(errors[0].target).toEqual(model);
122122
expect(errors[0].property).toEqual('mySubClass');
123-
expect(errors[0].children.length).toEqual(1);
124-
125-
const subError = errors[0].children[0];
126-
expect(subError.constraints).toEqual({
123+
expect(errors[0].children.length).toEqual(0);
124+
expect(errors[0].constraints).toEqual({
127125
[ValidationTypes.NESTED_VALIDATION]: 'nested property mySubClass must be either object or array',
128126
});
129127
});

test/functional/validation-options.spec.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
IsDefined,
44
Matches,
55
MinLength,
6+
IsArray,
67
Validate,
78
ValidateNested,
89
ValidatorConstraint,
@@ -1196,6 +1197,13 @@ describe('context', () => {
11961197
});
11971198

11981199
it('should stop at first error.', () => {
1200+
class MySubClass {
1201+
@IsDefined({
1202+
message: 'isDefined',
1203+
})
1204+
name: string;
1205+
}
1206+
11991207
class MyClass {
12001208
@IsDefined({
12011209
message: 'isDefined',
@@ -1204,14 +1212,32 @@ describe('context', () => {
12041212
message: 'String is not valid. You string must contain a hello word',
12051213
})
12061214
sameProperty: string;
1215+
1216+
@ValidateNested()
1217+
@IsArray()
1218+
nestedWithPrimitiveValue: MySubClass[];
12071219
}
12081220

12091221
const model = new MyClass();
1210-
return validator.validate(model, { stopAtFirstError: true }).then(errors => {
1211-
console.log();
1212-
expect(errors.length).toEqual(1);
1222+
model.nestedWithPrimitiveValue = 'invalid' as any;
1223+
1224+
const hasStopAtFirstError = validator.validate(model, { stopAtFirstError: true }).then(errors => {
1225+
expect(errors.length).toEqual(2);
12131226
expect(Object.keys(errors[0].constraints).length).toBe(1);
12141227
expect(errors[0].constraints['isDefined']).toBe('isDefined');
1228+
expect(Object.keys(errors[1].constraints).length).toBe(1);
1229+
expect(errors[1].constraints).toHaveProperty('isArray');
12151230
});
1231+
const hasNotStopAtFirstError = validator.validate(model, { stopAtFirstError: false }).then(errors => {
1232+
expect(errors.length).toEqual(2);
1233+
expect(Object.keys(errors[0].constraints).length).toBe(2);
1234+
expect(errors[0].constraints).toHaveProperty('contains');
1235+
expect(errors[0].constraints).toHaveProperty('isDefined');
1236+
expect(Object.keys(errors[1].constraints).length).toBe(2);
1237+
expect(errors[1].constraints).toHaveProperty('isArray');
1238+
expect(errors[1].constraints).toHaveProperty('nestedValidation');
1239+
});
1240+
1241+
return Promise.all([hasStopAtFirstError, hasNotStopAtFirstError]);
12161242
});
12171243
});

0 commit comments

Comments
 (0)