Skip to content

Commit

Permalink
feat: new ValidatePromise decorator - resolve promise before valida…
Browse files Browse the repository at this point in the history
…te (#369)
  • Loading branch information
queses authored and vlapo committed Jul 24, 2019
1 parent 36684ec commit 35ec04d
Show file tree
Hide file tree
Showing 5 changed files with 276 additions and 25 deletions.
33 changes: 32 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Class-validator works on both browser and node.js platforms.
+ [Validating sets](#validating-sets)
+ [Validating maps](#validating-maps)
+ [Validating nested objects](#validating-nested-objects)
+ [Validating promises](#validating-promises)
+ [Inheriting Validation decorators](#inheriting-validation-decorators)
+ [Conditional validation](#conditional-validation)
+ [Whitelisting](#whitelisting)
Expand Down Expand Up @@ -317,6 +318,36 @@ export class Post {
}
```

## Validating promises

If your object contains property with `Promise`-returned value that should be validated, then you need to use the `@ValidatePromise()` decorator:

```typescript
import {ValidatePromise, Min} from "class-validator";

export class Post {

@Min(0)
@ValidatePromise()
userId: Promise<number>;

}
```

It also works great with `@ValidateNested` decorator:

```typescript
import {ValidateNested, ValidatePromise} from "class-validator";

export class Post {

@ValidateNested()
@ValidatePromise()
user: Promise<User>;

}
```

## Inheriting Validation decorators

When you define a subclass which extends from another one, the subclass will automatically inherit the parent's decorators. If a property is redefined in the descendant class decorators will be applied on it both from that and the base class.
Expand Down Expand Up @@ -1013,4 +1044,4 @@ See information about breaking changes and release notes [here][3].

[1]: https://github.com/chriso/validator.js
[2]: https://github.com/pleerock/typedi
[3]: CHANGELOG.md
[3]: CHANGELOG.md
15 changes: 15 additions & 0 deletions src/decorator/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,21 @@ export function ValidateNested(validationOptions?: ValidationOptions) {
};
}

/**
* Objects / object arrays marked with this decorator will also be validated.
*/
export function ValidatePromise(validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) {
const args: ValidationMetadataArgs = {
type: ValidationTypes.PROMISE_VALIDATION,
target: object.constructor,
propertyName: propertyName,
validationOptions: validationOptions
};
getFromContainer(MetadataStorage).addValidationMetadata(new ValidationMetadata(args));
};
}

/**
* If object has both allowed and not allowed properties a validation error will be thrown.
*/
Expand Down
82 changes: 58 additions & 24 deletions src/validation/ValidationExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,30 +82,14 @@ export class ValidationExecutor {
const definedMetadatas = groupedMetadatas[propertyName].filter(metadata => metadata.type === ValidationTypes.IS_DEFINED);
const metadatas = groupedMetadatas[propertyName].filter(
metadata => metadata.type !== ValidationTypes.IS_DEFINED && metadata.type !== ValidationTypes.WHITELIST);
const customValidationMetadatas = metadatas.filter(metadata => metadata.type === ValidationTypes.CUSTOM_VALIDATION);
const nestedValidationMetadatas = metadatas.filter(metadata => metadata.type === ValidationTypes.NESTED_VALIDATION);
const conditionalValidationMetadatas = metadatas.filter(metadata => metadata.type === ValidationTypes.CONDITIONAL_VALIDATION);

const validationError = this.generateValidationError(object, value, propertyName);
validationErrors.push(validationError);

const canValidate = this.conditionalValidations(object, value, conditionalValidationMetadatas);
if (!canValidate) {
return;
}

// handle IS_DEFINED validation type the special way - it should work no matter skipMissingProperties is set or not
this.defaultValidations(object, value, definedMetadatas, validationError.constraints);

if ((value === null || value === undefined) && this.validatorOptions && this.validatorOptions.skipMissingProperties === true) {
return;

if (value instanceof Promise && metadatas.find(metadata => metadata.type === ValidationTypes.PROMISE_VALIDATION)) {
this.awaitingPromises.push(value.then((resolvedValue) => {
this.performValidations(object, resolvedValue, propertyName, definedMetadatas, metadatas, validationErrors);
}));
} else {
this.performValidations(object, value, propertyName, definedMetadatas, metadatas, validationErrors);
}

this.defaultValidations(object, value, metadatas, validationError.constraints);
this.customValidations(object, value, customValidationMetadatas, validationError.constraints);
this.nestedValidations(value, nestedValidationMetadatas, validationError.children);

this.mapContexts(object, value, metadatas, validationError);
});
}

Expand Down Expand Up @@ -163,6 +147,38 @@ export class ValidationExecutor {
// Private Methods
// -------------------------------------------------------------------------

private performValidations (object: any,
value: any, propertyName: string,
definedMetadatas: ValidationMetadata[],
metadatas: ValidationMetadata[],
validationErrors: ValidationError[]) {

const customValidationMetadatas = metadatas.filter(metadata => metadata.type === ValidationTypes.CUSTOM_VALIDATION);
const nestedValidationMetadatas = metadatas.filter(metadata => metadata.type === ValidationTypes.NESTED_VALIDATION);
const conditionalValidationMetadatas = metadatas.filter(metadata => metadata.type === ValidationTypes.CONDITIONAL_VALIDATION);

const validationError = this.generateValidationError(object, value, propertyName);
validationErrors.push(validationError);

const canValidate = this.conditionalValidations(object, value, conditionalValidationMetadatas);
if (!canValidate) {
return;
}

// handle IS_DEFINED validation type the special way - it should work no matter skipMissingProperties is set or not
this.defaultValidations(object, value, definedMetadatas, validationError.constraints);

if ((value === null || value === undefined) && this.validatorOptions && this.validatorOptions.skipMissingProperties === true) {
return;
}

this.defaultValidations(object, value, metadatas, validationError.constraints);
this.customValidations(object, value, customValidationMetadatas, validationError.constraints);
this.nestedValidations(value, nestedValidationMetadatas, validationError.children);

this.mapContexts(object, value, metadatas, validationError);
}

private generateValidationError(object: Object, value: any, propertyName: string) {
const validationError = new ValidationError();

Expand Down Expand Up @@ -252,19 +268,37 @@ export class ValidationExecutor {
});
}

private nestedPromiseValidations(value: any, metadatas: ValidationMetadata[], errors: ValidationError[]) {

if (!(value instanceof Promise)) {
return;
}

this.awaitingPromises.push(
value.then(resolvedValue => this.nestedValidations(resolvedValue, metadatas, errors))
);
}

private nestedValidations(value: any, metadatas: ValidationMetadata[], errors: ValidationError[]) {

if (value === void 0) {
return;
}

metadatas.forEach(metadata => {
if (metadata.type !== ValidationTypes.NESTED_VALIDATION) return;
if (
metadata.type !== ValidationTypes.NESTED_VALIDATION &&
metadata.type !== ValidationTypes.PROMISE_VALIDATION
) {
return;
}

const targetSchema = typeof metadata.target === "string" ? metadata.target as string : undefined;

if (value instanceof Array) {
value.forEach((subValue: any, index: number) => {
const validationError = this.generateValidationError(value, subValue, index.toString());
console.log("VE", validationError);
errors.push(validationError);

this.execute(subValue, targetSchema, validationError.children);
Expand Down
1 change: 1 addition & 0 deletions src/validation/ValidationTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export class ValidationTypes {
/* system */
static CUSTOM_VALIDATION = "customValidation";
static NESTED_VALIDATION = "nestedValidation";
static PROMISE_VALIDATION = "promiseValidation";
static CONDITIONAL_VALIDATION = "conditionalValidation";
static WHITELIST = "whitelistValidation";

Expand Down
170 changes: 170 additions & 0 deletions test/functional/promise-validation.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import "es6-shim";
import {Contains, IsDefined, MinLength, ValidateNested, ValidatePromise, MaxLength} from "../../src/decorator/decorators";
import {Validator} from "../../src/validation/Validator";
import {expect} from "chai";
import {ValidationTypes} from "../../src/validation/ValidationTypes";

import {should, use } from "chai";

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

should();
use(chaiAsPromised);

// -------------------------------------------------------------------------
// Setup
// -------------------------------------------------------------------------

const validator = new Validator();

// -------------------------------------------------------------------------
// Specifications: common decorators
// -------------------------------------------------------------------------

describe("promise validation", function () {

it("should not validate missing nested objects", function () {

class MySubClass {
@MinLength(5)
name: string;
}

class MyClass {
@Contains("hello")
title: string;

@ValidatePromise() @ValidateNested() @IsDefined()
mySubClass: Promise<MySubClass>;
}

const model: any = new MyClass();

model.title = "helo";
return validator.validate(model).then(errors => {
errors[1].target.should.be.equal(model);
expect(errors[1].value).to.be.undefined;
errors[1].property.should.be.equal("mySubClass");
errors[1].constraints.should.be.eql({isDefined: "mySubClass should not be null or undefined"});
});
});


it("should validate nested objects", function () {

class MySubClass {
@MinLength(5)
name: string;
}

class MyClass {
@Contains("hello")
title: string;

@ValidatePromise() @ValidateNested()
mySubClass: Promise<MySubClass>;

@ValidatePromise() @ValidateNested()
mySubClasses: Promise<MySubClass[]>;
}

const model = new MyClass();
model.title = "helo world";
const mySubClass = new MySubClass();
mySubClass.name = "my";
model.mySubClass = Promise.resolve(mySubClass);
const mySubClasses = [new MySubClass(), new MySubClass()];
mySubClasses[0].name = "my";
mySubClasses[1].name = "not-short";
model.mySubClasses = Promise.resolve(mySubClasses);
return validator.validate(model).then(errors => {
return Promise.all([
model.mySubClass,
model.mySubClasses
]).then(([modelMySubClass, modelMySubClasses]) => {
errors.length.should.be.equal(3);

errors[0].target.should.be.equal(model);
errors[0].property.should.be.equal("title");
errors[0].constraints.should.be.eql({contains: "title must contain a hello string"});
errors[0].value.should.be.equal("helo world");

errors[1].target.should.be.equal(model);
errors[1].property.should.be.equal("mySubClass");
errors[1].value.should.be.equal(modelMySubClass);
expect(errors[1].constraints).to.be.undefined;
const subError1 = errors[1].children[0];
subError1.target.should.be.equal(modelMySubClass);
subError1.property.should.be.equal("name");
subError1.constraints.should.be.eql({minLength: "name must be longer than or equal to 5 characters"});
subError1.value.should.be.equal("my");

errors[2].target.should.be.equal(model);
errors[2].property.should.be.equal("mySubClasses");
errors[2].value.should.be.equal(modelMySubClasses);
expect(errors[2].constraints).to.be.undefined;
const subError2 = errors[2].children[0];
subError2.target.should.be.equal(modelMySubClasses);
subError2.value.should.be.equal(modelMySubClasses[0]);
subError2.property.should.be.equal("0");
const subSubError = subError2.children[0];
subSubError.target.should.be.equal(modelMySubClasses[0]);
subSubError.property.should.be.equal("name");
subSubError.constraints.should.be.eql({minLength: "name must be longer than or equal to 5 characters"});
subSubError.value.should.be.equal("my");
});
});
});

it("should validate when nested is not object", () => {

class MySubClass {
@MinLength(5)
name: string;
}

class MyClass {
@ValidatePromise() @ValidateNested()
mySubClass: MySubClass;

}

const model = new MyClass();
model.mySubClass = <any> "invalidnested object";

return validator.validate(model).then(errors => {

expect(errors[0].target).to.equal(model);
expect(errors[0].property).to.equal("mySubClass");
expect(errors[0].children.length).to.equal(1);

const subError = errors[0].children[0];
subError.constraints.should.be.eql({[ValidationTypes.NESTED_VALIDATION]: "nested property mySubClass must be either object or array"});
});

});

it("should validate array promise", function () {

class MyClass {
@ValidatePromise() @MinLength(2)
arrProperty: Promise<string[]>;
}

const model = new MyClass();
model.arrProperty = Promise.resolve(["one"]);

return validator.validate(model).then(errors => {
return Promise.all([
model.arrProperty,
]).then(([modelArrProperty]) => {
errors.length.should.be.equal(1);

errors[0].target.should.be.equal(model);
errors[0].property.should.be.equal("arrProperty");
errors[0].constraints.should.be.eql({minLength: "arrProperty must be longer than or equal to 2 characters"});
errors[0].value.should.be.equal(modelArrProperty);
});
});
});
});

0 comments on commit 35ec04d

Please sign in to comment.