Skip to content

Commit 35ec04d

Browse files
quesesvlapo
authored andcommitted
feat: new ValidatePromise decorator - resolve promise before validate (#369)
1 parent 36684ec commit 35ec04d

File tree

5 files changed

+276
-25
lines changed

5 files changed

+276
-25
lines changed

Diff for: README.md

+32-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Class-validator works on both browser and node.js platforms.
1919
+ [Validating sets](#validating-sets)
2020
+ [Validating maps](#validating-maps)
2121
+ [Validating nested objects](#validating-nested-objects)
22+
+ [Validating promises](#validating-promises)
2223
+ [Inheriting Validation decorators](#inheriting-validation-decorators)
2324
+ [Conditional validation](#conditional-validation)
2425
+ [Whitelisting](#whitelisting)
@@ -317,6 +318,36 @@ export class Post {
317318
}
318319
```
319320

321+
## Validating promises
322+
323+
If your object contains property with `Promise`-returned value that should be validated, then you need to use the `@ValidatePromise()` decorator:
324+
325+
```typescript
326+
import {ValidatePromise, Min} from "class-validator";
327+
328+
export class Post {
329+
330+
@Min(0)
331+
@ValidatePromise()
332+
userId: Promise<number>;
333+
334+
}
335+
```
336+
337+
It also works great with `@ValidateNested` decorator:
338+
339+
```typescript
340+
import {ValidateNested, ValidatePromise} from "class-validator";
341+
342+
export class Post {
343+
344+
@ValidateNested()
345+
@ValidatePromise()
346+
user: Promise<User>;
347+
348+
}
349+
```
350+
320351
## Inheriting Validation decorators
321352

322353
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.
@@ -1013,4 +1044,4 @@ See information about breaking changes and release notes [here][3].
10131044

10141045
[1]: https://github.com/chriso/validator.js
10151046
[2]: https://github.com/pleerock/typedi
1016-
[3]: CHANGELOG.md
1047+
[3]: CHANGELOG.md

Diff for: src/decorator/decorators.ts

+15
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,21 @@ export function ValidateNested(validationOptions?: ValidationOptions) {
6363
};
6464
}
6565

66+
/**
67+
* Objects / object arrays marked with this decorator will also be validated.
68+
*/
69+
export function ValidatePromise(validationOptions?: ValidationOptions) {
70+
return function (object: Object, propertyName: string) {
71+
const args: ValidationMetadataArgs = {
72+
type: ValidationTypes.PROMISE_VALIDATION,
73+
target: object.constructor,
74+
propertyName: propertyName,
75+
validationOptions: validationOptions
76+
};
77+
getFromContainer(MetadataStorage).addValidationMetadata(new ValidationMetadata(args));
78+
};
79+
}
80+
6681
/**
6782
* If object has both allowed and not allowed properties a validation error will be thrown.
6883
*/

Diff for: src/validation/ValidationExecutor.ts

+58-24
Original file line numberDiff line numberDiff line change
@@ -82,30 +82,14 @@ export class ValidationExecutor {
8282
const definedMetadatas = groupedMetadatas[propertyName].filter(metadata => metadata.type === ValidationTypes.IS_DEFINED);
8383
const metadatas = groupedMetadatas[propertyName].filter(
8484
metadata => metadata.type !== ValidationTypes.IS_DEFINED && metadata.type !== ValidationTypes.WHITELIST);
85-
const customValidationMetadatas = metadatas.filter(metadata => metadata.type === ValidationTypes.CUSTOM_VALIDATION);
86-
const nestedValidationMetadatas = metadatas.filter(metadata => metadata.type === ValidationTypes.NESTED_VALIDATION);
87-
const conditionalValidationMetadatas = metadatas.filter(metadata => metadata.type === ValidationTypes.CONDITIONAL_VALIDATION);
88-
89-
const validationError = this.generateValidationError(object, value, propertyName);
90-
validationErrors.push(validationError);
91-
92-
const canValidate = this.conditionalValidations(object, value, conditionalValidationMetadatas);
93-
if (!canValidate) {
94-
return;
95-
}
96-
97-
// handle IS_DEFINED validation type the special way - it should work no matter skipMissingProperties is set or not
98-
this.defaultValidations(object, value, definedMetadatas, validationError.constraints);
99-
100-
if ((value === null || value === undefined) && this.validatorOptions && this.validatorOptions.skipMissingProperties === true) {
101-
return;
85+
86+
if (value instanceof Promise && metadatas.find(metadata => metadata.type === ValidationTypes.PROMISE_VALIDATION)) {
87+
this.awaitingPromises.push(value.then((resolvedValue) => {
88+
this.performValidations(object, resolvedValue, propertyName, definedMetadatas, metadatas, validationErrors);
89+
}));
90+
} else {
91+
this.performValidations(object, value, propertyName, definedMetadatas, metadatas, validationErrors);
10292
}
103-
104-
this.defaultValidations(object, value, metadatas, validationError.constraints);
105-
this.customValidations(object, value, customValidationMetadatas, validationError.constraints);
106-
this.nestedValidations(value, nestedValidationMetadatas, validationError.children);
107-
108-
this.mapContexts(object, value, metadatas, validationError);
10993
});
11094
}
11195

@@ -163,6 +147,38 @@ export class ValidationExecutor {
163147
// Private Methods
164148
// -------------------------------------------------------------------------
165149

150+
private performValidations (object: any,
151+
value: any, propertyName: string,
152+
definedMetadatas: ValidationMetadata[],
153+
metadatas: ValidationMetadata[],
154+
validationErrors: ValidationError[]) {
155+
156+
const customValidationMetadatas = metadatas.filter(metadata => metadata.type === ValidationTypes.CUSTOM_VALIDATION);
157+
const nestedValidationMetadatas = metadatas.filter(metadata => metadata.type === ValidationTypes.NESTED_VALIDATION);
158+
const conditionalValidationMetadatas = metadatas.filter(metadata => metadata.type === ValidationTypes.CONDITIONAL_VALIDATION);
159+
160+
const validationError = this.generateValidationError(object, value, propertyName);
161+
validationErrors.push(validationError);
162+
163+
const canValidate = this.conditionalValidations(object, value, conditionalValidationMetadatas);
164+
if (!canValidate) {
165+
return;
166+
}
167+
168+
// handle IS_DEFINED validation type the special way - it should work no matter skipMissingProperties is set or not
169+
this.defaultValidations(object, value, definedMetadatas, validationError.constraints);
170+
171+
if ((value === null || value === undefined) && this.validatorOptions && this.validatorOptions.skipMissingProperties === true) {
172+
return;
173+
}
174+
175+
this.defaultValidations(object, value, metadatas, validationError.constraints);
176+
this.customValidations(object, value, customValidationMetadatas, validationError.constraints);
177+
this.nestedValidations(value, nestedValidationMetadatas, validationError.children);
178+
179+
this.mapContexts(object, value, metadatas, validationError);
180+
}
181+
166182
private generateValidationError(object: Object, value: any, propertyName: string) {
167183
const validationError = new ValidationError();
168184

@@ -252,19 +268,37 @@ export class ValidationExecutor {
252268
});
253269
}
254270

271+
private nestedPromiseValidations(value: any, metadatas: ValidationMetadata[], errors: ValidationError[]) {
272+
273+
if (!(value instanceof Promise)) {
274+
return;
275+
}
276+
277+
this.awaitingPromises.push(
278+
value.then(resolvedValue => this.nestedValidations(resolvedValue, metadatas, errors))
279+
);
280+
}
281+
255282
private nestedValidations(value: any, metadatas: ValidationMetadata[], errors: ValidationError[]) {
256283

257284
if (value === void 0) {
258285
return;
259286
}
260287

261288
metadatas.forEach(metadata => {
262-
if (metadata.type !== ValidationTypes.NESTED_VALIDATION) return;
289+
if (
290+
metadata.type !== ValidationTypes.NESTED_VALIDATION &&
291+
metadata.type !== ValidationTypes.PROMISE_VALIDATION
292+
) {
293+
return;
294+
}
295+
263296
const targetSchema = typeof metadata.target === "string" ? metadata.target as string : undefined;
264297

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

270304
this.execute(subValue, targetSchema, validationError.children);

Diff for: src/validation/ValidationTypes.ts

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export class ValidationTypes {
88
/* system */
99
static CUSTOM_VALIDATION = "customValidation";
1010
static NESTED_VALIDATION = "nestedValidation";
11+
static PROMISE_VALIDATION = "promiseValidation";
1112
static CONDITIONAL_VALIDATION = "conditionalValidation";
1213
static WHITELIST = "whitelistValidation";
1314

Diff for: test/functional/promise-validation.spec.ts

+170
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import "es6-shim";
2+
import {Contains, IsDefined, MinLength, ValidateNested, ValidatePromise, MaxLength} from "../../src/decorator/decorators";
3+
import {Validator} from "../../src/validation/Validator";
4+
import {expect} from "chai";
5+
import {ValidationTypes} from "../../src/validation/ValidationTypes";
6+
7+
import {should, use } from "chai";
8+
9+
import * as chaiAsPromised from "chai-as-promised";
10+
11+
should();
12+
use(chaiAsPromised);
13+
14+
// -------------------------------------------------------------------------
15+
// Setup
16+
// -------------------------------------------------------------------------
17+
18+
const validator = new Validator();
19+
20+
// -------------------------------------------------------------------------
21+
// Specifications: common decorators
22+
// -------------------------------------------------------------------------
23+
24+
describe("promise validation", function () {
25+
26+
it("should not validate missing nested objects", function () {
27+
28+
class MySubClass {
29+
@MinLength(5)
30+
name: string;
31+
}
32+
33+
class MyClass {
34+
@Contains("hello")
35+
title: string;
36+
37+
@ValidatePromise() @ValidateNested() @IsDefined()
38+
mySubClass: Promise<MySubClass>;
39+
}
40+
41+
const model: any = new MyClass();
42+
43+
model.title = "helo";
44+
return validator.validate(model).then(errors => {
45+
errors[1].target.should.be.equal(model);
46+
expect(errors[1].value).to.be.undefined;
47+
errors[1].property.should.be.equal("mySubClass");
48+
errors[1].constraints.should.be.eql({isDefined: "mySubClass should not be null or undefined"});
49+
});
50+
});
51+
52+
53+
it("should validate nested objects", function () {
54+
55+
class MySubClass {
56+
@MinLength(5)
57+
name: string;
58+
}
59+
60+
class MyClass {
61+
@Contains("hello")
62+
title: string;
63+
64+
@ValidatePromise() @ValidateNested()
65+
mySubClass: Promise<MySubClass>;
66+
67+
@ValidatePromise() @ValidateNested()
68+
mySubClasses: Promise<MySubClass[]>;
69+
}
70+
71+
const model = new MyClass();
72+
model.title = "helo world";
73+
const mySubClass = new MySubClass();
74+
mySubClass.name = "my";
75+
model.mySubClass = Promise.resolve(mySubClass);
76+
const mySubClasses = [new MySubClass(), new MySubClass()];
77+
mySubClasses[0].name = "my";
78+
mySubClasses[1].name = "not-short";
79+
model.mySubClasses = Promise.resolve(mySubClasses);
80+
return validator.validate(model).then(errors => {
81+
return Promise.all([
82+
model.mySubClass,
83+
model.mySubClasses
84+
]).then(([modelMySubClass, modelMySubClasses]) => {
85+
errors.length.should.be.equal(3);
86+
87+
errors[0].target.should.be.equal(model);
88+
errors[0].property.should.be.equal("title");
89+
errors[0].constraints.should.be.eql({contains: "title must contain a hello string"});
90+
errors[0].value.should.be.equal("helo world");
91+
92+
errors[1].target.should.be.equal(model);
93+
errors[1].property.should.be.equal("mySubClass");
94+
errors[1].value.should.be.equal(modelMySubClass);
95+
expect(errors[1].constraints).to.be.undefined;
96+
const subError1 = errors[1].children[0];
97+
subError1.target.should.be.equal(modelMySubClass);
98+
subError1.property.should.be.equal("name");
99+
subError1.constraints.should.be.eql({minLength: "name must be longer than or equal to 5 characters"});
100+
subError1.value.should.be.equal("my");
101+
102+
errors[2].target.should.be.equal(model);
103+
errors[2].property.should.be.equal("mySubClasses");
104+
errors[2].value.should.be.equal(modelMySubClasses);
105+
expect(errors[2].constraints).to.be.undefined;
106+
const subError2 = errors[2].children[0];
107+
subError2.target.should.be.equal(modelMySubClasses);
108+
subError2.value.should.be.equal(modelMySubClasses[0]);
109+
subError2.property.should.be.equal("0");
110+
const subSubError = subError2.children[0];
111+
subSubError.target.should.be.equal(modelMySubClasses[0]);
112+
subSubError.property.should.be.equal("name");
113+
subSubError.constraints.should.be.eql({minLength: "name must be longer than or equal to 5 characters"});
114+
subSubError.value.should.be.equal("my");
115+
});
116+
});
117+
});
118+
119+
it("should validate when nested is not object", () => {
120+
121+
class MySubClass {
122+
@MinLength(5)
123+
name: string;
124+
}
125+
126+
class MyClass {
127+
@ValidatePromise() @ValidateNested()
128+
mySubClass: MySubClass;
129+
130+
}
131+
132+
const model = new MyClass();
133+
model.mySubClass = <any> "invalidnested object";
134+
135+
return validator.validate(model).then(errors => {
136+
137+
expect(errors[0].target).to.equal(model);
138+
expect(errors[0].property).to.equal("mySubClass");
139+
expect(errors[0].children.length).to.equal(1);
140+
141+
const subError = errors[0].children[0];
142+
subError.constraints.should.be.eql({[ValidationTypes.NESTED_VALIDATION]: "nested property mySubClass must be either object or array"});
143+
});
144+
145+
});
146+
147+
it("should validate array promise", function () {
148+
149+
class MyClass {
150+
@ValidatePromise() @MinLength(2)
151+
arrProperty: Promise<string[]>;
152+
}
153+
154+
const model = new MyClass();
155+
model.arrProperty = Promise.resolve(["one"]);
156+
157+
return validator.validate(model).then(errors => {
158+
return Promise.all([
159+
model.arrProperty,
160+
]).then(([modelArrProperty]) => {
161+
errors.length.should.be.equal(1);
162+
163+
errors[0].target.should.be.equal(model);
164+
errors[0].property.should.be.equal("arrProperty");
165+
errors[0].constraints.should.be.eql({minLength: "arrProperty must be longer than or equal to 2 characters"});
166+
errors[0].value.should.be.equal(modelArrProperty);
167+
});
168+
});
169+
});
170+
});

0 commit comments

Comments
 (0)