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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ export class Post {

## 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 from both its own class and the base class.
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 from both its own class and the base class. If a property-decorator pair is defined in both the subclass and parent-class, the decorator from the subclass will be used instead of the parent-class. For this purpose, "@IsDefined()" and "@IsOptional()" are considered the same.

```typescript
import { validate } from 'class-validator';
Expand Down Expand Up @@ -733,6 +733,7 @@ Lets create another custom validation decorator called `IsUserAlreadyExist`:
export function IsUserAlreadyExist(validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) {
registerDecorator({
name: 'isUserAlreadyExist',
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
Expand Down
3 changes: 3 additions & 0 deletions src/decorator/common/Allow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ import { ValidationTypes } from '../../validation/ValidationTypes';
import { ValidationMetadata } from '../../metadata/ValidationMetadata';
import { getMetadataStorage } from '../../metadata/MetadataStorage';

export const ALLOW = 'allow';

/**
* If object has both allowed and not allowed properties a validation error will be thrown.
*/
export function Allow(validationOptions?: ValidationOptions): PropertyDecorator {
return function (object: object, propertyName: string): void {
const args: ValidationMetadataArgs = {
name: ALLOW,
type: ValidationTypes.WHITELIST,
target: object.constructor,
propertyName: propertyName,
Expand Down
3 changes: 3 additions & 0 deletions src/decorator/common/Validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { getMetadataStorage } from '../../metadata/MetadataStorage';
import { ValidationTypes } from '../../validation/ValidationTypes';
import { ConstraintMetadata } from '../../metadata/ConstraintMetadata';

export const VALIDATE = 'validate';

/**
* Registers custom validator class.
*/
Expand Down Expand Up @@ -40,6 +42,7 @@ export function Validate(
): PropertyDecorator {
return function (object: object, propertyName: string): void {
const args: ValidationMetadataArgs = {
name: VALIDATE,
type: ValidationTypes.CUSTOM_VALIDATION,
target: object.constructor,
propertyName: propertyName,
Expand Down
3 changes: 3 additions & 0 deletions src/decorator/common/ValidateIf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { ValidationTypes } from '../../validation/ValidationTypes';
import { ValidationMetadata } from '../../metadata/ValidationMetadata';
import { getMetadataStorage } from '../../metadata/MetadataStorage';

const VALIDATE_IF = 'validateIf';

/**
* Ignores the other validators on a property when the provided condition function returns false.
*/
Expand All @@ -13,6 +15,7 @@ export function ValidateIf(
): PropertyDecorator {
return function (object: object, propertyName: string): void {
const args: ValidationMetadataArgs = {
name: VALIDATE_IF,
type: ValidationTypes.CONDITIONAL_VALIDATION,
target: object.constructor,
propertyName: propertyName,
Expand Down
2 changes: 2 additions & 0 deletions src/decorator/common/ValidateNested.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ValidationTypes } from '../../validation/ValidationTypes';
import { ValidationMetadata } from '../../metadata/ValidationMetadata';
import { getMetadataStorage } from '../../metadata/MetadataStorage';

const VALIDATE_NESTED = 'validateNested';
/**
* Objects / object arrays marked with this decorator will also be validated.
*/
Expand All @@ -14,6 +15,7 @@ export function ValidateNested(validationOptions?: ValidationOptions): PropertyD

return function (object: object, propertyName: string): void {
const args: ValidationMetadataArgs = {
name: VALIDATE_NESTED,
type: ValidationTypes.NESTED_VALIDATION,
target: object.constructor,
propertyName: propertyName,
Expand Down
3 changes: 3 additions & 0 deletions src/decorator/common/ValidatePromise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ import { ValidationTypes } from '../../validation/ValidationTypes';
import { ValidationMetadata } from '../../metadata/ValidationMetadata';
import { getMetadataStorage } from '../../metadata/MetadataStorage';

export const VALIDATE_PROMISE = 'validatePromise';

/**
* Resolve promise before validation
*/
export function ValidatePromise(validationOptions?: ValidationOptions): PropertyDecorator {
return function (object: object, propertyName: string): void {
const args: ValidationMetadataArgs = {
name: VALIDATE_PROMISE,
type: ValidationTypes.PROMISE_VALIDATION,
target: object.constructor,
propertyName: propertyName,
Expand Down
13 changes: 9 additions & 4 deletions src/metadata/MetadataStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { ConstraintMetadata } from './ConstraintMetadata';
import { ValidationSchema } from '../validation-schema/ValidationSchema';
import { ValidationSchemaToMetadataTransformer } from '../validation-schema/ValidationSchemaToMetadataTransformer';
import { getGlobal } from '../utils';
import { IS_DEFINED } from '../decorator/common/IsDefined';
import { IS_OPTIONAL } from '../decorator/common/IsOptional';

/**
* Storage all metadatas.
Expand Down Expand Up @@ -138,10 +140,13 @@ export class MetadataStorage {
// filter out duplicate metadatas, prefer original metadatas instead of inherited metadatas
const uniqueInheritedMetadatas = inheritedMetadatas.filter(inheritedMetadata => {
return !originalMetadatas.find(originalMetadata => {
return (
originalMetadata.propertyName === inheritedMetadata.propertyName &&
originalMetadata.type === inheritedMetadata.type
);
const isSameProperty = originalMetadata.propertyName === inheritedMetadata.propertyName;
const isSameValidator =
originalMetadata.name === inheritedMetadata.name ||
(originalMetadata.name === IS_DEFINED && inheritedMetadata.name === IS_OPTIONAL) ||
(originalMetadata.name === IS_OPTIONAL && inheritedMetadata.name === IS_DEFINED);

return isSameProperty && isSameValidator;
});
});

Expand Down
6 changes: 3 additions & 3 deletions src/metadata/ValidationMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ export class ValidationMetadata {
// -------------------------------------------------------------------------

/**
* Validation type.
* Validation type. Should be one of the ValidationTypes values.
*/
type: string;

/**
* Validator name.
* Validation name. Used to uniquely identify this validator.
*/
name?: string;
name: string;

/**
* Target class to which this validation is applied.
Expand Down
6 changes: 3 additions & 3 deletions src/metadata/ValidationMetadataArgs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import { ValidationOptions } from '../decorator/ValidationOptions';
*/
export interface ValidationMetadataArgs {
/**
* Validation type.
* Validation type. Should be one of the ValidationTypes values.
*/
type: string;

/**
* Validator name.
* Validation name. Used to uniquely identify this validator.
*/
name?: string;
name: string;

/**
* Object that is used to be validated.
Expand Down
6 changes: 3 additions & 3 deletions src/validation-schema/ValidationSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ export interface ValidationSchema {
*/
[propertyName: string]: {
/**
* Validation type. Should be one of the ValidationTypes value.
* Validation type. Should be one of the ValidationTypes values.
*/
type: string;

/**
* Validator name.
* Validation name. Used to uniquely identify this validator.
*/
name?: string;
name: string;

/**
* Constraints set by validation type.
Expand Down
174 changes: 173 additions & 1 deletion test/functional/inherited-validation.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Contains, MinLength } from '../../src/decorator/decorators';
import { Contains, MinLength, Equals, Min, IsOptional, IsDefined } from '../../src/decorator/decorators';
import { Validator } from '../../src/validation/Validator';

const validator = new Validator();
Expand Down Expand Up @@ -34,4 +34,176 @@ describe('inherited validation', () => {
expect(errors[1].value).toEqual('helo world');
});
});

it('should use validators from parent and child classes', () => {
expect.assertions(5);

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

class MySubClass extends MyClass {
@MinLength(5)
title: string;
}

const model = new MySubClass();
model.title = 'helo';
return validator.validate(model).then(errors => {
expect(errors.length).toEqual(1);
expect(errors[0].target).toEqual(model);
expect(errors[0].property).toEqual('title');
expect(errors[0].constraints).toEqual({
minLength: 'title must be longer than or equal to 5 characters',
contains: 'title must contain a hello string',
});
expect(errors[0].value).toEqual('helo');
});
});

it('should override inherited validators in sub classes', () => {
expect.assertions(9);

class MyClass {
@Min(30)
age: number;

@Equals('validator')
first_name: string;

@Equals('class')
last_name: string;
}

class MySubClass extends MyClass {
@Min(40)
age: number;

@Equals('class')
first_name: string;

@Equals('validator')
last_name: string;
}

const model = new MySubClass();
model.age = 20; // fail validation (using sub classes constraint)
model.first_name = 'class'; // pass validation (overriding fail from parent)
model.last_name = 'class'; // fail validation (overriding pass from parent)

return validator.validate(model).then(errors => {
expect(errors.length).toEqual(2);
expect(errors[0].target).toEqual(model);
expect(errors[0].property).toEqual('age');
expect(errors[0].constraints).toEqual({
min: 'age must not be less than 40',
});
expect(errors[0].value).toEqual(20);

expect(errors[1].target).toEqual(model);
expect(errors[1].property).toEqual('last_name');
expect(errors[1].constraints).toEqual({
equals: 'last_name must be equal to validator',
});
expect(errors[1].value).toEqual('class');
});
});

it('should not override different validators of inherited properties in the parent class', () => {
expect.assertions(4);

class MyClass {
@Contains('parent-class')
title: string;
}

class MySubClass extends MyClass {
@Equals('sub-class')
title: string;
}

const model = new MySubClass();
model.title = 'sub-class';

return validator.validate(model).then(errors => {
expect(errors.length).toEqual(1);
expect(errors[0].target).toEqual(model);
expect(errors[0].property).toEqual('title');
expect(errors[0].constraints).toEqual({
contains: 'title must contain a parent-class string',
});
});
});

it('should not override different validators of inherited properties in the sub class', () => {
expect.assertions(4);

class MyClass {
@Contains('parent-class')
title: string;
}

class MySubClass extends MyClass {
@Equals('sub-class')
title: string;
}

const model = new MySubClass();
model.title = 'parent-class';

return validator.validate(model).then(errors => {
expect(errors.length).toEqual(1);
expect(errors[0].target).toEqual(model);
expect(errors[0].property).toEqual('title');
expect(errors[0].constraints).toEqual({
equals: 'title must be equal to sub-class',
});
});
});

it('should override isOptional validator in the parent class', () => {
expect.assertions(4);

class MyClass {
@IsOptional()
title?: string;
}

class MySubClass extends MyClass {
@IsDefined()
title: string;
}

const model = new MySubClass();

return validator.validate(model).then(errors => {
expect(errors.length).toEqual(1);
expect(errors[0].target).toEqual(model);
expect(errors[0].property).toEqual('title');
expect(errors[0].constraints).toEqual({
isDefined: 'title should not be null or undefined',
});
});
});

it('should override isDefined validator in the parent class', () => {
expect.assertions(1);

class MyClass {
@IsDefined()
title?: string;
}

class MySubClass extends MyClass {
@IsOptional()
title: string;
}

const model = new MySubClass();

return validator.validate(model).then(errors => {
expect(errors.length).toEqual(0);
});
});
});