Skip to content

Commit 5bb704e

Browse files
zd333vlapo
authored andcommittedOct 1, 2019
fix: apply custom constraint class validation to each item in the array (#295)
Close #260
1 parent 6e98223 commit 5bb704e

File tree

2 files changed

+151
-13
lines changed

2 files changed

+151
-13
lines changed
 

‎src/validation/ValidationExecutor.ts

+44-10
Original file line numberDiff line numberDiff line change
@@ -258,20 +258,54 @@ export class ValidationExecutor {
258258
value: value,
259259
constraints: metadata.constraints
260260
};
261-
const validatedValue = customConstraintMetadata.instance.validate(value, validationArguments);
262-
if (isPromise(validatedValue)) {
263-
const promise = validatedValue.then(isValid => {
264-
if (!isValid) {
261+
262+
if (!metadata.each || !(value instanceof Array)) {
263+
const validatedValue = customConstraintMetadata.instance.validate(value, validationArguments);
264+
if (isPromise(validatedValue)) {
265+
const promise = validatedValue.then(isValid => {
266+
if (!isValid) {
267+
const [type, message] = this.createValidationError(object, value, metadata, customConstraintMetadata);
268+
errorMap[type] = message;
269+
}
270+
});
271+
this.awaitingPromises.push(promise);
272+
} else {
273+
if (!validatedValue) {
265274
const [type, message] = this.createValidationError(object, value, metadata, customConstraintMetadata);
266275
errorMap[type] = message;
267276
}
268-
});
269-
this.awaitingPromises.push(promise);
270-
} else {
271-
if (!validatedValue) {
272-
const [type, message] = this.createValidationError(object, value, metadata, customConstraintMetadata);
273-
errorMap[type] = message;
274277
}
278+
279+
return;
280+
}
281+
282+
// Validation needs to be applied to each array item
283+
const validatedSubValues = value.map((subValue: any) => customConstraintMetadata.instance.validate(subValue, validationArguments));
284+
const validationIsAsync = validatedSubValues
285+
.some((validatedSubValue: boolean | Promise<boolean>) => isPromise(validatedSubValue));
286+
287+
if (validationIsAsync) {
288+
// Wrap plain values (if any) in promises, so that all are async
289+
const asyncValidatedSubValues = validatedSubValues
290+
.map((validatedSubValue: boolean | Promise<boolean>) => isPromise(validatedSubValue) ? validatedSubValue : Promise.resolve(validatedSubValue));
291+
const asyncValidationIsFinishedPromise = Promise.all(asyncValidatedSubValues)
292+
.then((flatValidatedValues: boolean[]) => {
293+
const validationResult = flatValidatedValues.every((isValid: boolean) => isValid);
294+
if (!validationResult) {
295+
const [type, message] = this.createValidationError(object, value, metadata, customConstraintMetadata);
296+
errorMap[type] = message;
297+
}
298+
});
299+
300+
this.awaitingPromises.push(asyncValidationIsFinishedPromise);
301+
302+
return;
303+
}
304+
305+
const validationResult = validatedSubValues.every((isValid: boolean) => isValid);
306+
if (!validationResult) {
307+
const [type, message] = this.createValidationError(object, value, metadata, customConstraintMetadata);
308+
errorMap[type] = message;
275309
}
276310
});
277311
});

‎test/functional/validation-options.spec.ts

+107-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import "es6-shim";
2-
import {Contains, Matches, MinLength, ValidateNested} from "../../src/decorator/decorators";
2+
import {Contains, Matches, MinLength, ValidateNested, ValidatorConstraint, Validate } from "../../src/decorator/decorators";
33
import {Validator} from "../../src/validation/Validator";
4-
import {ValidationError} from "../../src";
4+
import {ValidationError, ValidatorConstraintInterface} from "../../src";
55

6-
import {should, use } from "chai";
6+
import {should, use} from "chai";
77

88
import * as chaiAsPromised from "chai-as-promised";
99

@@ -163,6 +163,110 @@ describe("validation options", function() {
163163
});
164164
});
165165

166+
it("should apply validation via custom constraint class to array items (but not array itself)", function() {
167+
@ValidatorConstraint({ name: "customIsNotArrayConstraint", async: false })
168+
class CustomIsNotArrayConstraint implements ValidatorConstraintInterface {
169+
validate(value: any) {
170+
return !(value instanceof Array);
171+
}
172+
}
173+
174+
class MyClass {
175+
@Validate(CustomIsNotArrayConstraint, {
176+
each: true
177+
})
178+
someArrayOfNonArrayItems: string[];
179+
}
180+
181+
const model = new MyClass();
182+
model.someArrayOfNonArrayItems = ["not array", "also not array", "not array at all"];
183+
return validator.validate(model).then(errors => {
184+
errors.length.should.be.equal(0);
185+
});
186+
});
187+
188+
it("should apply validation via custom constraint class with synchronous logic to each item in the array", function() {
189+
@ValidatorConstraint({ name: "customContainsHelloConstraint", async: false })
190+
class CustomContainsHelloConstraint implements ValidatorConstraintInterface {
191+
validate(value: any) {
192+
return !(value instanceof Array) && String(value).includes("hello");
193+
}
194+
}
195+
196+
class MyClass {
197+
@Validate(CustomContainsHelloConstraint, {
198+
each: true
199+
})
200+
someProperty: string[];
201+
}
202+
203+
const model = new MyClass();
204+
model.someProperty = ["hell no world", "hello", "helo world", "hello world", "hello dear friend"];
205+
return validator.validate(model).then(errors => {
206+
errors.length.should.be.equal(1);
207+
errors[0].constraints.should.be.eql({ customContainsHelloConstraint: "" });
208+
errors[0].value.should.be.equal(model.someProperty);
209+
errors[0].target.should.be.equal(model);
210+
errors[0].property.should.be.equal("someProperty");
211+
});
212+
});
213+
214+
it("should apply validation via custom constraint class with async logic to each item in the array", function() {
215+
@ValidatorConstraint({ name: "customAsyncContainsHelloConstraint", async: true })
216+
class CustomAsyncContainsHelloConstraint implements ValidatorConstraintInterface {
217+
validate(value: any) {
218+
const isValid = !(value instanceof Array) && String(value).includes("hello");
219+
220+
return Promise.resolve(isValid);
221+
}
222+
}
223+
224+
class MyClass {
225+
@Validate(CustomAsyncContainsHelloConstraint, {
226+
each: true
227+
})
228+
someProperty: string[];
229+
}
230+
231+
const model = new MyClass();
232+
model.someProperty = ["hell no world", "hello", "helo world", "hello world", "hello dear friend"];
233+
return validator.validate(model).then(errors => {
234+
errors.length.should.be.equal(1);
235+
errors[0].constraints.should.be.eql({ customAsyncContainsHelloConstraint: "" });
236+
errors[0].value.should.be.equal(model.someProperty);
237+
errors[0].target.should.be.equal(model);
238+
errors[0].property.should.be.equal("someProperty");
239+
});
240+
});
241+
242+
it("should apply validation via custom constraint class with mixed (synchronous + async) logic to each item in the array", function() {
243+
@ValidatorConstraint({ name: "customMixedContainsHelloConstraint", async: true })
244+
class CustomMixedContainsHelloConstraint implements ValidatorConstraintInterface {
245+
validate(value: any) {
246+
const isValid = !(value instanceof Array) && String(value).includes("hello");
247+
248+
return isValid ? isValid : Promise.resolve(isValid);
249+
}
250+
}
251+
252+
class MyClass {
253+
@Validate(CustomMixedContainsHelloConstraint, {
254+
each: true
255+
})
256+
someProperty: string[];
257+
}
258+
259+
const model = new MyClass();
260+
model.someProperty = ["hell no world", "hello", "helo world", "hello world", "hello dear friend"];
261+
return validator.validate(model).then(errors => {
262+
errors.length.should.be.equal(1);
263+
errors[0].constraints.should.be.eql({ customMixedContainsHelloConstraint: "" });
264+
errors[0].value.should.be.equal(model.someProperty);
265+
errors[0].target.should.be.equal(model);
266+
errors[0].property.should.be.equal("someProperty");
267+
});
268+
});
269+
166270
});
167271

168272
describe("groups", function() {

0 commit comments

Comments
 (0)
Please sign in to comment.