From 5ed8f444f54f0a67cc13bd73763eb07aba267ef8 Mon Sep 17 00:00:00 2001 From: aljazerzen Date: Thu, 7 Sep 2017 12:27:09 +0200 Subject: [PATCH 1/3] Added @Allowed validation --- README.md | 31 ++++++ src/decorator/decorators.ts | 16 ++++ src/validation/ValidationExecutor.ts | 23 ++++- src/validation/ValidationTypes.ts | 1 + test/functional/allowed-validation.spec.ts | 104 +++++++++++++++++++++ 5 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 test/functional/allowed-validation.spec.ts diff --git a/README.md b/README.md index b0d623a0c5..5dbf6436a3 100644 --- a/README.md +++ b/README.md @@ -296,6 +296,37 @@ In the example above, the validation rules applied to `example` won't be run unl Note that when the condition is false all validation decorators are ignored, including `isDefined`. +## Additional properties + +Even if your object is an instance of a validation class it can contain additional properties that are not defined. +If you want to have an error thrown when these unwanted properties are present add `@Allowed()` decorator to +all defined properties: + +```typescript +import {validate} from "class-validator"; + +export class Post { + + @Allowed() + title: string; + + @Allowed() + views: number; + +} + +let post = new Post(); +post.title = 'Hello world!'; +post.views = 420; + +(post as any).unallowedProperty = 69; + +validate(post).then(errors => { + ... +}); // will return error for unallowedProperty + +``` + ## Skipping missing properties Sometimes you may want to skip validation of the properties that does not exist in the validating object. This is diff --git a/src/decorator/decorators.ts b/src/decorator/decorators.ts index 4cbdd1f846..94f2c2969b 100644 --- a/src/decorator/decorators.ts +++ b/src/decorator/decorators.ts @@ -63,6 +63,22 @@ export function ValidateNested(validationOptions?: ValidationOptions) { }; } +/** + * If object has both allowed and not allowed properties a validation error will be thrown. + */ +export function Allowed(validationOptions?: ValidationOptions) { + return function (object: Object, propertyName: string) { + const args: ValidationMetadataArgs = { + type: ValidationTypes.ALLOWED, + target: object.constructor, + propertyName: propertyName, + validationOptions: validationOptions + }; + getFromContainer(MetadataStorage).addValidationMetadata(new ValidationMetadata(args)); + }; +} + + /** * Objects / object arrays marked with this decorator will also be validated. */ diff --git a/src/validation/ValidationExecutor.ts b/src/validation/ValidationExecutor.ts index e5b5ce3bbc..3edd5a72d4 100644 --- a/src/validation/ValidationExecutor.ts +++ b/src/validation/ValidationExecutor.ts @@ -44,10 +44,31 @@ export class ValidationExecutor { const targetMetadatas = this.metadataStorage.getTargetValidationMetadatas(object.constructor, targetSchema, groups); const groupedMetadatas = this.metadataStorage.groupByPropertyName(targetMetadatas); + let performAllowedValidation = false; + let notAllowedProperties: string[] = []; + + Object.keys(object).forEach(propertyName => { + const allowedMetadatas = groupedMetadatas[propertyName] ? groupedMetadatas[propertyName].filter(metadata => metadata.type === ValidationTypes.ALLOWED) : []; + + performAllowedValidation = performAllowedValidation || allowedMetadatas.length > 0; + if (allowedMetadatas.length === 0) + notAllowedProperties.push(propertyName); + }); + + if (performAllowedValidation && notAllowedProperties.length > 0) { + notAllowedProperties.forEach(property => { + validationErrors.push({ + target: object, property, value: (object as any)[property], children: undefined, + constraints: { [ValidationTypes.ALLOWED]: `property ${property} is not allowed` } + }); + }); + } + Object.keys(groupedMetadatas).forEach(propertyName => { const value = (object as any)[propertyName]; const definedMetadatas = groupedMetadatas[propertyName].filter(metadata => metadata.type === ValidationTypes.IS_DEFINED); - const metadatas = groupedMetadatas[propertyName].filter(metadata => metadata.type !== ValidationTypes.IS_DEFINED); + const metadatas = groupedMetadatas[propertyName].filter( + metadata => metadata.type !== ValidationTypes.IS_DEFINED && metadata.type !== ValidationTypes.ALLOWED); 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); diff --git a/src/validation/ValidationTypes.ts b/src/validation/ValidationTypes.ts index b719098818..e0ca39a3ab 100644 --- a/src/validation/ValidationTypes.ts +++ b/src/validation/ValidationTypes.ts @@ -9,6 +9,7 @@ export class ValidationTypes { static CUSTOM_VALIDATION = "customValidation"; static NESTED_VALIDATION = "nestedValidation"; static CONDITIONAL_VALIDATION = "conditionalValidation"; + static ALLOWED = "allowedValidation"; /* common checkers */ static IS_DEFINED = "isDefined"; diff --git a/test/functional/allowed-validation.spec.ts b/test/functional/allowed-validation.spec.ts new file mode 100644 index 0000000000..c4dda172bd --- /dev/null +++ b/test/functional/allowed-validation.spec.ts @@ -0,0 +1,104 @@ +import "es6-shim"; +import {Allowed, ValidateNested} from "../../src/decorator/decorators"; +import {Validator} from "../../src/validation/Validator"; +import {expect} from "chai"; +import {ValidationTypes} from "../../src/validation/ValidationTypes"; + +// ------------------------------------------------------------------------- +// Setup +// ------------------------------------------------------------------------- + +const validator = new Validator(); + +// ------------------------------------------------------------------------- +// Specifications: allowed validation +// ------------------------------------------------------------------------- + +describe("allowed validation", function () { + + it("should fail if an object has both allowed and not allowed properties", function () { + + class MyClass { + @Allowed() + title: string; + + @Allowed() + views: number; + } + + const model: any = new MyClass(); + + model.title = "hello"; + model.unallowedProperty = 42; + return validator.validate(model).then(errors => { + expect(errors.length).to.be.equal(1); + errors[0].target.should.be.equal(model); + errors[0].property.should.be.equal("unallowedProperty"); + errors[0].constraints.should.haveOwnProperty(ValidationTypes.ALLOWED); + }); + }); + + it("should succeed if an object only not allowed properties", function () { + + class MyClass { + title: string; + views: number; + } + + const model: any = new MyClass(); + + model.title = "hello"; + model.unallowedProperty = 42; + return validator.validate(model).then(errors => { + expect(errors.length).to.be.equal(0); + }); + }); + + it("should succeed if an object only allowed properties", function () { + + class MyClass { + @Allowed() + title: string; + @Allowed() + views: number; + } + + const model: any = new MyClass(); + + model.title = "hello"; + return validator.validate(model).then(errors => { + expect(errors.length).to.be.equal(0); + }); + }); + + it("should validate only nested object if parent has no allowed decorators", function () { + + class SubClass { + @Allowed() + title: string; + } + + class MyClass { + title: string; + + @ValidateNested() + sub: SubClass; + } + + const model: any = new MyClass(); + + model.title = "hello"; + model.sub = new SubClass(); + model.sub.title = "world!\n"; + model.sub.unallowedProperty = 42; + return validator.validate(model).then(errors => { + errors.should.have.lengthOf(1); + errors[0].children.should.have.lengthOf(1); + errors[0].children[0].target.should.be.equal(model.sub); + errors[0].children[0].property.should.be.equal("unallowedProperty"); + errors[0].children[0].constraints.should.haveOwnProperty(ValidationTypes.ALLOWED); + }); + }); + + +}); From 0f35eca37493679ef88bdeb8750a2acdeaf75d55 Mon Sep 17 00:00:00 2001 From: aljazerzen Date: Fri, 1 Dec 2017 18:40:47 +0100 Subject: [PATCH 2/3] added stripping of not allowed properties and added forbidNotAllowedProperties flag --- README.md | 26 ++++++--- src/decorator/decorators.ts | 4 +- src/validation/ValidationExecutor.ts | 66 +++++++++++++++------- src/validation/ValidationTypes.ts | 2 +- src/validation/ValidatorOptions.ts | 8 +++ test/functional/allowed-validation.spec.ts | 20 +++---- 6 files changed, 85 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 5dbf6436a3..9abde6992c 100644 --- a/README.md +++ b/README.md @@ -296,21 +296,20 @@ In the example above, the validation rules applied to `example` won't be run unl Note that when the condition is false all validation decorators are ignored, including `isDefined`. -## Additional properties +## Non-defined properties Even if your object is an instance of a validation class it can contain additional properties that are not defined. -If you want to have an error thrown when these unwanted properties are present add `@Allowed()` decorator to -all defined properties: +If you do not want to have such properties on your object, add `@Allow()` decorator to all defined properties: ```typescript -import {validate} from "class-validator"; +import {validate, Allow} from "class-validator"; export class Post { - @Allowed() + @Allow() title: string; - @Allowed() + @Allow() views: number; } @@ -322,10 +321,21 @@ post.views = 420; (post as any).unallowedProperty = 69; validate(post).then(errors => { - ... -}); // will return error for unallowedProperty + ... +}); // (post as any).unallowedProperty is not undefined +``` + +> If there none of the properties have `@Allow` decorator non-allowed properties will not be stripped. + +If you would rather to have an error thrown when any un-allowed properties are present, pass special flag to `validate` +method: +```typescript +import {validate} from "class-validator"; +// ... +validate(post, { forbidNotAllowedProperties: true }); ``` + ## Skipping missing properties diff --git a/src/decorator/decorators.ts b/src/decorator/decorators.ts index 94f2c2969b..bafa18bf94 100644 --- a/src/decorator/decorators.ts +++ b/src/decorator/decorators.ts @@ -66,10 +66,10 @@ export function ValidateNested(validationOptions?: ValidationOptions) { /** * If object has both allowed and not allowed properties a validation error will be thrown. */ -export function Allowed(validationOptions?: ValidationOptions) { +export function Allow(validationOptions?: ValidationOptions) { return function (object: Object, propertyName: string) { const args: ValidationMetadataArgs = { - type: ValidationTypes.ALLOWED, + type: ValidationTypes.ALLOW, target: object.constructor, propertyName: propertyName, validationOptions: validationOptions diff --git a/src/validation/ValidationExecutor.ts b/src/validation/ValidationExecutor.ts index 3edd5a72d4..99f4d403c7 100644 --- a/src/validation/ValidationExecutor.ts +++ b/src/validation/ValidationExecutor.ts @@ -44,31 +44,14 @@ export class ValidationExecutor { const targetMetadatas = this.metadataStorage.getTargetValidationMetadatas(object.constructor, targetSchema, groups); const groupedMetadatas = this.metadataStorage.groupByPropertyName(targetMetadatas); - let performAllowedValidation = false; - let notAllowedProperties: string[] = []; - - Object.keys(object).forEach(propertyName => { - const allowedMetadatas = groupedMetadatas[propertyName] ? groupedMetadatas[propertyName].filter(metadata => metadata.type === ValidationTypes.ALLOWED) : []; - - performAllowedValidation = performAllowedValidation || allowedMetadatas.length > 0; - if (allowedMetadatas.length === 0) - notAllowedProperties.push(propertyName); - }); - - if (performAllowedValidation && notAllowedProperties.length > 0) { - notAllowedProperties.forEach(property => { - validationErrors.push({ - target: object, property, value: (object as any)[property], children: undefined, - constraints: { [ValidationTypes.ALLOWED]: `property ${property} is not allowed` } - }); - }); - } + this.executeAllowedValidation(object, groupedMetadatas, validationErrors); + // General validation Object.keys(groupedMetadatas).forEach(propertyName => { const value = (object as any)[propertyName]; 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.ALLOWED); + metadata => metadata.type !== ValidationTypes.IS_DEFINED && metadata.type !== ValidationTypes.ALLOW); 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); @@ -94,6 +77,49 @@ export class ValidationExecutor { }); } + executeAllowedValidation(object: any, + groupedMetadatas: { [propertyName: string]: ValidationMetadata[] }, + validationErrors: ValidationError[]) { + let performAllowedValidation = false; + let notAllowedProperties: string[] = []; + + Object.keys(object).forEach(propertyName => { + let allowedMetadatas = 0; + + // count allowed decorators on this property + if (groupedMetadatas[propertyName]) + allowedMetadatas = groupedMetadatas[propertyName] + .filter(metadata => metadata.type === ValidationTypes.ALLOW).length ; + + // perform validation if any of the properties has more than zero allowed decorators + performAllowedValidation = performAllowedValidation || allowedMetadatas > 0; + + // is this property not allowed? + if (allowedMetadatas === 0) + notAllowedProperties.push(propertyName); + }); + + if (performAllowedValidation && notAllowedProperties.length > 0) { + + if (this.validatorOptions.forbidNotAllowedProperties) { + + // throw errors + notAllowedProperties.forEach(property => { + validationErrors.push({ + target: object, property, value: (object as any)[property], children: undefined, + constraints: { [ValidationTypes.ALLOW]: `property ${property} is not allowed` } + }); + }); + + } else { + + // strip non allowed properties + notAllowedProperties.forEach(property => delete (object as any)[property]); + + } + } + } + stripEmptyErrors(errors: ValidationError[]) { return errors.filter(error => { if (error.children) { diff --git a/src/validation/ValidationTypes.ts b/src/validation/ValidationTypes.ts index e0ca39a3ab..b1caea6688 100644 --- a/src/validation/ValidationTypes.ts +++ b/src/validation/ValidationTypes.ts @@ -9,7 +9,7 @@ export class ValidationTypes { static CUSTOM_VALIDATION = "customValidation"; static NESTED_VALIDATION = "nestedValidation"; static CONDITIONAL_VALIDATION = "conditionalValidation"; - static ALLOWED = "allowedValidation"; + static ALLOW = "allowedValidation"; /* common checkers */ static IS_DEFINED = "isDefined"; diff --git a/src/validation/ValidatorOptions.ts b/src/validation/ValidatorOptions.ts index 5d3798006a..5cc5fbd828 100644 --- a/src/validation/ValidatorOptions.ts +++ b/src/validation/ValidatorOptions.ts @@ -8,6 +8,14 @@ export interface ValidatorOptions { */ skipMissingProperties?: boolean; + /** + * If set to true validator will throw an error if any of the properties are missing @Allow decorator. + * If set to false, all the properties that are missing @Allow decorator will be stripped. + * + * **If no properties have @Allow decorator no error will be thrown and no properties will be stripped** + */ + forbidNotAllowedProperties?: boolean; + /** * Groups to be used during validation of the object. */ diff --git a/test/functional/allowed-validation.spec.ts b/test/functional/allowed-validation.spec.ts index c4dda172bd..8f3b215bf4 100644 --- a/test/functional/allowed-validation.spec.ts +++ b/test/functional/allowed-validation.spec.ts @@ -1,5 +1,5 @@ import "es6-shim"; -import {Allowed, ValidateNested} from "../../src/decorator/decorators"; +import {Allow, ValidateNested} from "../../src/decorator/decorators"; import {Validator} from "../../src/validation/Validator"; import {expect} from "chai"; import {ValidationTypes} from "../../src/validation/ValidationTypes"; @@ -19,10 +19,10 @@ describe("allowed validation", function () { it("should fail if an object has both allowed and not allowed properties", function () { class MyClass { - @Allowed() + @Allow() title: string; - @Allowed() + @Allow() views: number; } @@ -30,11 +30,11 @@ describe("allowed validation", function () { model.title = "hello"; model.unallowedProperty = 42; - return validator.validate(model).then(errors => { + return validator.validate(model, { forbidNotAllowedProperties: true }).then(errors => { expect(errors.length).to.be.equal(1); errors[0].target.should.be.equal(model); errors[0].property.should.be.equal("unallowedProperty"); - errors[0].constraints.should.haveOwnProperty(ValidationTypes.ALLOWED); + errors[0].constraints.should.haveOwnProperty(ValidationTypes.ALLOW); }); }); @@ -57,9 +57,9 @@ describe("allowed validation", function () { it("should succeed if an object only allowed properties", function () { class MyClass { - @Allowed() + @Allow() title: string; - @Allowed() + @Allow() views: number; } @@ -74,7 +74,7 @@ describe("allowed validation", function () { it("should validate only nested object if parent has no allowed decorators", function () { class SubClass { - @Allowed() + @Allow() title: string; } @@ -91,12 +91,12 @@ describe("allowed validation", function () { model.sub = new SubClass(); model.sub.title = "world!\n"; model.sub.unallowedProperty = 42; - return validator.validate(model).then(errors => { + return validator.validate(model, { forbidNotAllowedProperties: true }).then(errors => { errors.should.have.lengthOf(1); errors[0].children.should.have.lengthOf(1); errors[0].children[0].target.should.be.equal(model.sub); errors[0].children[0].property.should.be.equal("unallowedProperty"); - errors[0].children[0].constraints.should.haveOwnProperty(ValidationTypes.ALLOWED); + errors[0].children[0].constraints.should.haveOwnProperty(ValidationTypes.ALLOW); }); }); From f2a940d30cfeac764357a83d73c117aaeaf314c1 Mon Sep 17 00:00:00 2001 From: aljazerzen Date: Fri, 8 Dec 2017 11:08:43 +0100 Subject: [PATCH 3/3] Refactored allowed to whitelisting (enabled in validator options) --- README.md | 35 ++++--- src/decorator/decorators.ts | 2 +- src/validation/ValidationExecutor.ts | 32 ++---- src/validation/ValidationTypes.ts | 2 +- src/validation/ValidatorOptions.ts | 12 ++- test/functional/allowed-validation.spec.ts | 104 ------------------- test/functional/whitelist-validation.spec.ts | 78 ++++++++++++++ 7 files changed, 122 insertions(+), 143 deletions(-) delete mode 100644 test/functional/allowed-validation.spec.ts create mode 100644 test/functional/whitelist-validation.spec.ts diff --git a/README.md b/README.md index 9abde6992c..94b4389411 100644 --- a/README.md +++ b/README.md @@ -296,44 +296,55 @@ In the example above, the validation rules applied to `example` won't be run unl Note that when the condition is false all validation decorators are ignored, including `isDefined`. -## Non-defined properties +## Whitelisting Even if your object is an instance of a validation class it can contain additional properties that are not defined. -If you do not want to have such properties on your object, add `@Allow()` decorator to all defined properties: +If you do not want to have such properties on your object, pass special flag to `validate` method: ```typescript -import {validate, Allow} from "class-validator"; +import {validate} from "class-validator"; +// ... +validate(post, { whitelist: true }); +``` + +This will strip all properties that don't have any decorators. If no other decorator is suitable for your property, +you can use @Allow decorator: + +```typescript +import {validate, Allow, Min} from "class-validator"; export class Post { @Allow() title: string; - @Allow() + @Min(0) views: number; + nonWhitelistedProperty: number; } let post = new Post(); post.title = 'Hello world!'; post.views = 420; -(post as any).unallowedProperty = 69; +post.nonWhitelistedProperty = 69; +(post as any).anotherNonWhitelistedProperty = "something"; validate(post).then(errors => { + // post.nonWhitelistedProperty is not defined + // (post as any).anotherNonWhitelistedProperty is not defined ... -}); // (post as any).unallowedProperty is not undefined -``` - -> If there none of the properties have `@Allow` decorator non-allowed properties will not be stripped. +}); +```` -If you would rather to have an error thrown when any un-allowed properties are present, pass special flag to `validate` -method: +If you would rather to have an error thrown when any non-whitelisted properties are present, pass another flag to +`validate` method: ```typescript import {validate} from "class-validator"; // ... -validate(post, { forbidNotAllowedProperties: true }); +validate(post, { whitelist: true, forbidNonWhitelisted: true }); ``` diff --git a/src/decorator/decorators.ts b/src/decorator/decorators.ts index bafa18bf94..a1a95e7e0b 100644 --- a/src/decorator/decorators.ts +++ b/src/decorator/decorators.ts @@ -69,7 +69,7 @@ export function ValidateNested(validationOptions?: ValidationOptions) { export function Allow(validationOptions?: ValidationOptions) { return function (object: Object, propertyName: string) { const args: ValidationMetadataArgs = { - type: ValidationTypes.ALLOW, + type: ValidationTypes.WHITELIST, target: object.constructor, propertyName: propertyName, validationOptions: validationOptions diff --git a/src/validation/ValidationExecutor.ts b/src/validation/ValidationExecutor.ts index 99f4d403c7..499df15f2d 100644 --- a/src/validation/ValidationExecutor.ts +++ b/src/validation/ValidationExecutor.ts @@ -44,14 +44,15 @@ export class ValidationExecutor { const targetMetadatas = this.metadataStorage.getTargetValidationMetadatas(object.constructor, targetSchema, groups); const groupedMetadatas = this.metadataStorage.groupByPropertyName(targetMetadatas); - this.executeAllowedValidation(object, groupedMetadatas, validationErrors); + if (this.validatorOptions && this.validatorOptions.whitelist) + this.whitelist(object, groupedMetadatas, validationErrors); // General validation Object.keys(groupedMetadatas).forEach(propertyName => { const value = (object as any)[propertyName]; 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.ALLOW); + 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); @@ -77,37 +78,26 @@ export class ValidationExecutor { }); } - executeAllowedValidation(object: any, - groupedMetadatas: { [propertyName: string]: ValidationMetadata[] }, - validationErrors: ValidationError[]) { - let performAllowedValidation = false; + whitelist(object: any, + groupedMetadatas: { [propertyName: string]: ValidationMetadata[] }, + validationErrors: ValidationError[]) { let notAllowedProperties: string[] = []; Object.keys(object).forEach(propertyName => { - let allowedMetadatas = 0; - - // count allowed decorators on this property - if (groupedMetadatas[propertyName]) - allowedMetadatas = groupedMetadatas[propertyName] - .filter(metadata => metadata.type === ValidationTypes.ALLOW).length ; - - // perform validation if any of the properties has more than zero allowed decorators - performAllowedValidation = performAllowedValidation || allowedMetadatas > 0; - - // is this property not allowed? - if (allowedMetadatas === 0) + // does this property have no metadata? + if (!groupedMetadatas[propertyName] || groupedMetadatas[propertyName].length === 0) notAllowedProperties.push(propertyName); }); - if (performAllowedValidation && notAllowedProperties.length > 0) { + if (notAllowedProperties.length > 0) { - if (this.validatorOptions.forbidNotAllowedProperties) { + if (this.validatorOptions && this.validatorOptions.forbidNonWhitelisted) { // throw errors notAllowedProperties.forEach(property => { validationErrors.push({ target: object, property, value: (object as any)[property], children: undefined, - constraints: { [ValidationTypes.ALLOW]: `property ${property} is not allowed` } + constraints: { [ValidationTypes.WHITELIST]: `property ${property} should not exist` } }); }); diff --git a/src/validation/ValidationTypes.ts b/src/validation/ValidationTypes.ts index b1caea6688..ac5f0654dd 100644 --- a/src/validation/ValidationTypes.ts +++ b/src/validation/ValidationTypes.ts @@ -9,7 +9,7 @@ export class ValidationTypes { static CUSTOM_VALIDATION = "customValidation"; static NESTED_VALIDATION = "nestedValidation"; static CONDITIONAL_VALIDATION = "conditionalValidation"; - static ALLOW = "allowedValidation"; + static WHITELIST = "whitelistValidation"; /* common checkers */ static IS_DEFINED = "isDefined"; diff --git a/src/validation/ValidatorOptions.ts b/src/validation/ValidatorOptions.ts index 5cc5fbd828..0cfb902c28 100644 --- a/src/validation/ValidatorOptions.ts +++ b/src/validation/ValidatorOptions.ts @@ -9,12 +9,16 @@ export interface ValidatorOptions { skipMissingProperties?: boolean; /** - * If set to true validator will throw an error if any of the properties are missing @Allow decorator. - * If set to false, all the properties that are missing @Allow decorator will be stripped. + * If set to true validator will strip validated object of any properties that do not have any decorators. * - * **If no properties have @Allow decorator no error will be thrown and no properties will be stripped** + * Tip: if no other decorator is suitable for your property use @Allow decorator. */ - forbidNotAllowedProperties?: boolean; + whitelist?: false; + + /** + * If set to true, instead of stripping non-whitelisted properties validator will throw an error + */ + forbidNonWhitelisted?: false; /** * Groups to be used during validation of the object. diff --git a/test/functional/allowed-validation.spec.ts b/test/functional/allowed-validation.spec.ts deleted file mode 100644 index 8f3b215bf4..0000000000 --- a/test/functional/allowed-validation.spec.ts +++ /dev/null @@ -1,104 +0,0 @@ -import "es6-shim"; -import {Allow, ValidateNested} from "../../src/decorator/decorators"; -import {Validator} from "../../src/validation/Validator"; -import {expect} from "chai"; -import {ValidationTypes} from "../../src/validation/ValidationTypes"; - -// ------------------------------------------------------------------------- -// Setup -// ------------------------------------------------------------------------- - -const validator = new Validator(); - -// ------------------------------------------------------------------------- -// Specifications: allowed validation -// ------------------------------------------------------------------------- - -describe("allowed validation", function () { - - it("should fail if an object has both allowed and not allowed properties", function () { - - class MyClass { - @Allow() - title: string; - - @Allow() - views: number; - } - - const model: any = new MyClass(); - - model.title = "hello"; - model.unallowedProperty = 42; - return validator.validate(model, { forbidNotAllowedProperties: true }).then(errors => { - expect(errors.length).to.be.equal(1); - errors[0].target.should.be.equal(model); - errors[0].property.should.be.equal("unallowedProperty"); - errors[0].constraints.should.haveOwnProperty(ValidationTypes.ALLOW); - }); - }); - - it("should succeed if an object only not allowed properties", function () { - - class MyClass { - title: string; - views: number; - } - - const model: any = new MyClass(); - - model.title = "hello"; - model.unallowedProperty = 42; - return validator.validate(model).then(errors => { - expect(errors.length).to.be.equal(0); - }); - }); - - it("should succeed if an object only allowed properties", function () { - - class MyClass { - @Allow() - title: string; - @Allow() - views: number; - } - - const model: any = new MyClass(); - - model.title = "hello"; - return validator.validate(model).then(errors => { - expect(errors.length).to.be.equal(0); - }); - }); - - it("should validate only nested object if parent has no allowed decorators", function () { - - class SubClass { - @Allow() - title: string; - } - - class MyClass { - title: string; - - @ValidateNested() - sub: SubClass; - } - - const model: any = new MyClass(); - - model.title = "hello"; - model.sub = new SubClass(); - model.sub.title = "world!\n"; - model.sub.unallowedProperty = 42; - return validator.validate(model, { forbidNotAllowedProperties: true }).then(errors => { - errors.should.have.lengthOf(1); - errors[0].children.should.have.lengthOf(1); - errors[0].children[0].target.should.be.equal(model.sub); - errors[0].children[0].property.should.be.equal("unallowedProperty"); - errors[0].children[0].constraints.should.haveOwnProperty(ValidationTypes.ALLOW); - }); - }); - - -}); diff --git a/test/functional/whitelist-validation.spec.ts b/test/functional/whitelist-validation.spec.ts new file mode 100644 index 0000000000..61268401d1 --- /dev/null +++ b/test/functional/whitelist-validation.spec.ts @@ -0,0 +1,78 @@ +import "es6-shim"; +import {Allow, IsDefined, Min, ValidateNested} from "../../src/decorator/decorators"; +import {Validator} from "../../src/validation/Validator"; +import {expect} from "chai"; +import {ValidationTypes} from "../../src/validation/ValidationTypes"; + +// ------------------------------------------------------------------------- +// Setup +// ------------------------------------------------------------------------- + +const validator = new Validator(); + +// ------------------------------------------------------------------------- +// Specifications: allowed validation +// ------------------------------------------------------------------------- + +describe("whitelist validation", function () { + + it("should strip non whitelisted properties, but leave whitelisted untouched", function () { + + class MyClass { + @IsDefined() + title: string; + + @Min(0) + views: number; + } + + const model: any = new MyClass(); + + model.title = "hello"; + model.views = 56; + model.unallowedProperty = 42; + return validator.validate(model, { whitelist: true }).then(errors => { + expect(errors.length).to.be.equal(0); + expect(model.unallowedProperty).be.undefined; + expect(model.title).to.equal("hello"); + expect(model.views).to.be.equal(56); + }); + }); + + it("should be able to whitelist with @Allow", function () { + + class MyClass { + @Allow() + views: number; + } + + const model: any = new MyClass(); + + model.views = 420; + model.unallowedProperty = "non-whitelisted"; + + return validator.validate(model, { whitelist: true }).then(errors => { + expect(errors.length).to.be.equal(0); + expect(model.unallowedProperty).be.undefined; + expect(model.views).to.be.equal(420); + }); + }); + + it("should throw an error when forbidNonWhitelisted flag is set", function () { + + class MyClass { + } + + const model: any = new MyClass(); + + model.unallowedProperty = "non-whitelisted"; + + return validator.validate(model, { whitelist: true, forbidNonWhitelisted: true }).then(errors => { + expect(errors.length).to.be.equal(1); + errors[0].target.should.be.equal(model); + errors[0].property.should.be.equal("unallowedProperty"); + errors[0].constraints.should.haveOwnProperty(ValidationTypes.WHITELIST); + }); + }); + +});