diff --git a/src/validation/ValidationExecutor.ts b/src/validation/ValidationExecutor.ts index 8965db4dc3..d23ccdda61 100644 --- a/src/validation/ValidationExecutor.ts +++ b/src/validation/ValidationExecutor.ts @@ -258,20 +258,54 @@ export class ValidationExecutor { value: value, constraints: metadata.constraints }; - const validatedValue = customConstraintMetadata.instance.validate(value, validationArguments); - if (isPromise(validatedValue)) { - const promise = validatedValue.then(isValid => { - if (!isValid) { + + if (!metadata.each || !(value instanceof Array)) { + const validatedValue = customConstraintMetadata.instance.validate(value, validationArguments); + if (isPromise(validatedValue)) { + const promise = validatedValue.then(isValid => { + if (!isValid) { + const [type, message] = this.createValidationError(object, value, metadata, customConstraintMetadata); + errorMap[type] = message; + } + }); + this.awaitingPromises.push(promise); + } else { + if (!validatedValue) { const [type, message] = this.createValidationError(object, value, metadata, customConstraintMetadata); errorMap[type] = message; } - }); - this.awaitingPromises.push(promise); - } else { - if (!validatedValue) { - const [type, message] = this.createValidationError(object, value, metadata, customConstraintMetadata); - errorMap[type] = message; } + + return; + } + + // Validation needs to be applied to each array item + const validatedSubValues = value.map((subValue: any) => customConstraintMetadata.instance.validate(subValue, validationArguments)); + const validationIsAsync = validatedSubValues + .some((validatedSubValue: boolean | Promise) => isPromise(validatedSubValue)); + + if (validationIsAsync) { + // Wrap plain values (if any) in promises, so that all are async + const asyncValidatedSubValues = validatedSubValues + .map((validatedSubValue: boolean | Promise) => isPromise(validatedSubValue) ? validatedSubValue : Promise.resolve(validatedSubValue)); + const asyncValidationIsFinishedPromise = Promise.all(asyncValidatedSubValues) + .then((flatValidatedValues: boolean[]) => { + const validationResult = flatValidatedValues.every((isValid: boolean) => isValid); + if (!validationResult) { + const [type, message] = this.createValidationError(object, value, metadata, customConstraintMetadata); + errorMap[type] = message; + } + }); + + this.awaitingPromises.push(asyncValidationIsFinishedPromise); + + return; + } + + const validationResult = validatedSubValues.every((isValid: boolean) => isValid); + if (!validationResult) { + const [type, message] = this.createValidationError(object, value, metadata, customConstraintMetadata); + errorMap[type] = message; } }); }); diff --git a/test/functional/validation-options.spec.ts b/test/functional/validation-options.spec.ts index 77eb07faee..f812c4a236 100644 --- a/test/functional/validation-options.spec.ts +++ b/test/functional/validation-options.spec.ts @@ -1,9 +1,9 @@ import "es6-shim"; -import {Contains, Matches, MinLength, ValidateNested} from "../../src/decorator/decorators"; +import {Contains, Matches, MinLength, ValidateNested, ValidatorConstraint, Validate } from "../../src/decorator/decorators"; import {Validator} from "../../src/validation/Validator"; -import {ValidationError} from "../../src"; +import {ValidationError, ValidatorConstraintInterface} from "../../src"; -import {should, use } from "chai"; +import {should, use} from "chai"; import * as chaiAsPromised from "chai-as-promised"; @@ -163,6 +163,110 @@ describe("validation options", function() { }); }); + it("should apply validation via custom constraint class to array items (but not array itself)", function() { + @ValidatorConstraint({ name: "customIsNotArrayConstraint", async: false }) + class CustomIsNotArrayConstraint implements ValidatorConstraintInterface { + validate(value: any) { + return !(value instanceof Array); + } + } + + class MyClass { + @Validate(CustomIsNotArrayConstraint, { + each: true + }) + someArrayOfNonArrayItems: string[]; + } + + const model = new MyClass(); + model.someArrayOfNonArrayItems = ["not array", "also not array", "not array at all"]; + return validator.validate(model).then(errors => { + errors.length.should.be.equal(0); + }); + }); + + it("should apply validation via custom constraint class with synchronous logic to each item in the array", function() { + @ValidatorConstraint({ name: "customContainsHelloConstraint", async: false }) + class CustomContainsHelloConstraint implements ValidatorConstraintInterface { + validate(value: any) { + return !(value instanceof Array) && String(value).includes("hello"); + } + } + + class MyClass { + @Validate(CustomContainsHelloConstraint, { + each: true + }) + someProperty: string[]; + } + + const model = new MyClass(); + model.someProperty = ["hell no world", "hello", "helo world", "hello world", "hello dear friend"]; + return validator.validate(model).then(errors => { + errors.length.should.be.equal(1); + errors[0].constraints.should.be.eql({ customContainsHelloConstraint: "" }); + errors[0].value.should.be.equal(model.someProperty); + errors[0].target.should.be.equal(model); + errors[0].property.should.be.equal("someProperty"); + }); + }); + + it("should apply validation via custom constraint class with async logic to each item in the array", function() { + @ValidatorConstraint({ name: "customAsyncContainsHelloConstraint", async: true }) + class CustomAsyncContainsHelloConstraint implements ValidatorConstraintInterface { + validate(value: any) { + const isValid = !(value instanceof Array) && String(value).includes("hello"); + + return Promise.resolve(isValid); + } + } + + class MyClass { + @Validate(CustomAsyncContainsHelloConstraint, { + each: true + }) + someProperty: string[]; + } + + const model = new MyClass(); + model.someProperty = ["hell no world", "hello", "helo world", "hello world", "hello dear friend"]; + return validator.validate(model).then(errors => { + errors.length.should.be.equal(1); + errors[0].constraints.should.be.eql({ customAsyncContainsHelloConstraint: "" }); + errors[0].value.should.be.equal(model.someProperty); + errors[0].target.should.be.equal(model); + errors[0].property.should.be.equal("someProperty"); + }); + }); + + it("should apply validation via custom constraint class with mixed (synchronous + async) logic to each item in the array", function() { + @ValidatorConstraint({ name: "customMixedContainsHelloConstraint", async: true }) + class CustomMixedContainsHelloConstraint implements ValidatorConstraintInterface { + validate(value: any) { + const isValid = !(value instanceof Array) && String(value).includes("hello"); + + return isValid ? isValid : Promise.resolve(isValid); + } + } + + class MyClass { + @Validate(CustomMixedContainsHelloConstraint, { + each: true + }) + someProperty: string[]; + } + + const model = new MyClass(); + model.someProperty = ["hell no world", "hello", "helo world", "hello world", "hello dear friend"]; + return validator.validate(model).then(errors => { + errors.length.should.be.equal(1); + errors[0].constraints.should.be.eql({ customMixedContainsHelloConstraint: "" }); + errors[0].value.should.be.equal(model.someProperty); + errors[0].target.should.be.equal(model); + errors[0].property.should.be.equal("someProperty"); + }); + }); + }); describe("groups", function() {