Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,37 @@ validate(post).then(errors => {
});
```

If you would want a property conditionally whitelisted you can use the @AllowIf decorator:

```typescript
import {validate, Allow, AllowIf, Min} from "class-validator";

export class Post {

@Allow()
title: string;

@Min(0)
views: number;

@AllowIf(post => post.views > 10)
whitelistedProperty: number;
}

let post = new Post();
post.title = 'Hello world!';
post.views = 420;

post.whitelistedProperty = 69;
(post as any).nonWhitelistedProperty = "something";

validate(post).then(errors => {
// post.whitelistedProperty is defined
// (post as any).nonWhitelistedProperty is not defined
...
});
```

If you would rather to have an error thrown when any non-whitelisted properties are present, pass another flag to
`validate` method:

Expand Down
1 change: 1 addition & 0 deletions src/decorator/common/Allow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export function Allow(validationOptions?: ValidationOptions): PropertyDecorator
type: ValidationTypes.WHITELIST,
target: object.constructor,
propertyName: propertyName,
constraints: [],
validationOptions: validationOptions,
};
getMetadataStorage().addValidationMetadata(new ValidationMetadata(args));
Expand Down
24 changes: 24 additions & 0 deletions src/decorator/common/AllowIf.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ValidationOptions } from '../ValidationOptions';
import { ValidationMetadataArgs } from '../../metadata/ValidationMetadataArgs';
import { ValidationTypes } from '../../validation/ValidationTypes';
import { ValidationMetadata } from '../../metadata/ValidationMetadata';
import { getMetadataStorage } from '../../metadata/MetadataStorage';

/**
* If object has both allowed and not allowed properties a validation error will be thrown.
*/
export function AllowIf(
condition: (object: any) => boolean,
validationOptions?: ValidationOptions
): PropertyDecorator {
return function (object: object, propertyName: string): void {
const args: ValidationMetadataArgs = {
type: ValidationTypes.WHITELIST,
target: object.constructor,
propertyName: propertyName,
constraints: [condition],
validationOptions: validationOptions,
};
getMetadataStorage().addValidationMetadata(new ValidationMetadata(args));
};
}
1 change: 1 addition & 0 deletions src/decorator/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
// -------------------------------------------------------------------------

export * from './common/Allow';
export * from './common/AllowIf';
export * from './common/IsDefined';
export * from './common/IsOptional';
export * from './common/Validate';
Expand Down
19 changes: 18 additions & 1 deletion src/validation/ValidationExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,18 @@ export class ValidationExecutor {
const notAllowedProperties: string[] = [];

Object.keys(object).forEach(propertyName => {
const metadatas = groupedMetadatas[propertyName];
// does this property have no metadata?
if (!groupedMetadatas[propertyName] || groupedMetadatas[propertyName].length === 0)
if (!metadatas || metadatas.length === 0) {
notAllowedProperties.push(propertyName);
return;
}
// does this property has condition to allow?
const conditionalWhitelistMetadatas = metadatas.filter(metadata => metadata.type === ValidationTypes.WHITELIST && metadata.constraints.length > 0);
const canAllow = this.conditionalWhitelist(object, propertyName, conditionalWhitelistMetadatas);
if (!canAllow) {
notAllowedProperties.push(propertyName);
}
});

if (notAllowedProperties.length > 0) {
Expand Down Expand Up @@ -242,6 +251,14 @@ export class ValidationExecutor {
return validationError;
}

private conditionalWhitelist(object: any, propertyName: string, metadatas: ValidationMetadata[]): ValidationMetadata[] {
return !Object.keys(object).includes(propertyName) ||
object[propertyName] === undefined ||
metadatas
.map(metadata => metadata.constraints[0](object))
.reduce((resultA, resultB) => resultA && resultB, true);
}

private conditionalValidations(object: object, value: any, metadatas: ValidationMetadata[]): ValidationMetadata[] {
return metadatas
.map(metadata => metadata.constraints[0](object, value))
Expand Down
116 changes: 115 additions & 1 deletion test/functional/whitelist-validation.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Allow, IsDefined, IsOptional, Min } from '../../src/decorator/decorators';
import { Allow, AllowIf, IsDefined, IsOptional, Min } from '../../src/decorator/decorators';
import { Validator } from '../../src/validation/Validator';
import { ValidationTypes } from '../../src';

Expand Down Expand Up @@ -45,6 +45,120 @@ describe('whitelist validation', () => {
});
});

it("should'n be able to whitelist with @AllowIf when condition return false", () => {
class MyClass {
@AllowIf(o => false)
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).toEqual(0);
expect(model.unallowedProperty).toBeUndefined();
expect(model.views).toBeUndefined();
});
});

it('should be able to whitelist with @AllowIf when condition return true', () => {
class MyClass {
@AllowIf(o => true)
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).toEqual(0);
expect(model.unallowedProperty).toBeUndefined();
expect(model.views).toEqual(420);
});
});

it('should not throw an error when forbidNonWhitelisted flag is set and @AllowIf is true', () => {
class MyPayload {
/**
* Since forbidUnknownValues defaults to true, we must add a property to
* register the class in the metadata storage. Otherwise the unknown value check
* would take priority (first check) and exit without running the whitelist logic.
*/
@IsOptional()
propertyToRegisterClass: string;

@AllowIf(o => true)
conditionalDecorated: string;

constructor(conditionalDecorated: string) {
this.conditionalDecorated = conditionalDecorated;
}
}

const instance = new MyPayload('conditional-whitelisted');

return validator.validate(instance, { whitelist: true, forbidNonWhitelisted: true }).then(errors => {
expect(errors.length).toEqual(0);
expect(instance.conditionalDecorated).toEqual('conditional-whitelisted')
});
});

it('should throw an error when forbidNonWhitelisted flag is set and @AllowIf is false', () => {
class MyPayload {
/**
* Since forbidUnknownValues defaults to true, we must add a property to
* register the class in the metadata storage. Otherwise the unknown value check
* would take priority (first check) and exit without running the whitelist logic.
*/
@IsOptional()
propertyToRegisterClass: string;

@AllowIf(o => false)
nonDecorated: string;

constructor(nonDecorated: string) {
this.nonDecorated = nonDecorated;
}
}

const instance = new MyPayload('non-whitelisted');

return validator.validate(instance, { whitelist: true, forbidNonWhitelisted: true }).then(errors => {
expect(errors.length).toEqual(1);
expect(errors[0].target).toEqual(instance);
expect(errors[0].property).toEqual('nonDecorated');
expect(errors[0].constraints).toHaveProperty(ValidationTypes.WHITELIST);
expect(() => errors[0].toString()).not.toThrow();
});
});

it('should not throw an error when forbidNonWhitelisted flag is set and @AllowIf is false and property is not set', () => {
class MyPayload {
/**
* Since forbidUnknownValues defaults to true, we must add a property to
* register the class in the metadata storage. Otherwise the unknown value check
* would take priority (first check) and exit without running the whitelist logic.
*/
@IsOptional()
propertyToRegisterClass: string;

@AllowIf(o => false)
conditionalDecorated: string;

}

const instance = new MyPayload();

return validator.validate(instance, { whitelist: true, forbidNonWhitelisted: true }).then(errors => {
expect(errors.length).toEqual(0);
expect(instance.conditionalDecorated).toBeUndefined();
});
});

it('should throw an error when forbidNonWhitelisted flag is set', () => {
class MyPayload {
/**
Expand Down