Skip to content

Commit

Permalink
fix: apply custom constraint class validation to each item in the arr…
Browse files Browse the repository at this point in the history
…ay (#295)

Close #260
  • Loading branch information
zd333 authored and vlapo committed Oct 1, 2019
1 parent 6e98223 commit 5bb704e
Show file tree
Hide file tree
Showing 2 changed files with 151 additions and 13 deletions.
54 changes: 44 additions & 10 deletions src/validation/ValidationExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean>) => isPromise(validatedSubValue));

if (validationIsAsync) {
// Wrap plain values (if any) in promises, so that all are async
const asyncValidatedSubValues = validatedSubValues
.map((validatedSubValue: boolean | Promise<boolean>) => 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;
}
});
});
Expand Down
110 changes: 107 additions & 3 deletions test/functional/validation-options.spec.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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() {
Expand Down

0 comments on commit 5bb704e

Please sign in to comment.