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
7 changes: 6 additions & 1 deletion src/metadata/MetadataStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ConstraintMetadata } from './ConstraintMetadata';
import { ValidationSchema } from '../validation-schema/ValidationSchema';
import { ValidationSchemaToMetadataTransformer } from '../validation-schema/ValidationSchemaToMetadataTransformer';
import { getGlobal } from '../utils';
import { ValidationTypes } from '../validation/ValidationTypes';

/**
* Storage all metadatas.
Expand Down Expand Up @@ -140,7 +141,11 @@ export class MetadataStorage {
return !originalMetadatas.find(originalMetadata => {
return (
originalMetadata.propertyName === inheritedMetadata.propertyName &&
originalMetadata.type === inheritedMetadata.type
originalMetadata.type === inheritedMetadata.type &&
(originalMetadata.type !== ValidationTypes.CUSTOM_VALIDATION ||
(originalMetadata.name !== undefined || inheritedMetadata.name !== undefined
? originalMetadata.name === inheritedMetadata.name
: originalMetadata.constraintCls === inheritedMetadata.constraintCls))
);
});
});
Expand Down
137 changes: 136 additions & 1 deletion test/functional/inherited-validation.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,144 @@
import { Contains, MinLength } from '../../src/decorator/decorators';
import {
Contains,
IsNotEmpty,
IsString,
MinLength,
Validate,
ValidatorConstraint,
} from '../../src/decorator/decorators';
import { Validator } from '../../src/validation/Validator';
import { ValidationArguments } from '../../src/validation/ValidationArguments';

const validator = new Validator();

describe('inherited validation', () => {
it('should retain different validators on a redeclared property across inheritance levels', async () => {
class Base {
@IsString()
@MinLength(3)
title: string;
}
class Child extends Base {
@Contains('hello')
title: string;
}
class Grandchild extends Child {
@IsNotEmpty()
title: string;
}

const model = new Grandchild();
model.title = 'a';
const errors = await validator.validate(model);
expect(errors).toHaveLength(1);
expect(errors[0].constraints).toEqual({
contains: 'title must contain a hello string',
minLength: 'title must be longer than or equal to 3 characters',
});
model.title = '';
const emptyErrors = await validator.validate(model);
expect(emptyErrors).toHaveLength(1);
expect(emptyErrors[0].constraints).toEqual({
isNotEmpty: 'title should not be empty',
contains: 'title must contain a hello string',
minLength: 'title must be longer than or equal to 3 characters',
});
model.title = 'hello';
expect(await validator.validate(model)).toEqual([]);
});

it('should prefer the child constraints for the same named validator', async () => {
class Base {
@MinLength(10)
title: string;
}
class Child extends Base {
@MinLength(3)
title: string;
}

const model = new Child();
model.title = 'hello';
expect(await validator.validate(model)).toEqual([]);
model.title = 'a';
const errors = await validator.validate(model);
expect(errors).toHaveLength(1);
expect(errors[0].constraints).toEqual({ minLength: 'title must be longer than or equal to 3 characters' });
});

it('should retain different custom constraint classes and prefer child constraints for the same class', async () => {
@ValidatorConstraint({ name: 'startsWith' })
class StartsWith {
validate(value: string, args: ValidationArguments): boolean {
return value.startsWith(args.constraints[0]);
}
}
@ValidatorConstraint({ name: 'endsWith' })
class EndsWith {
validate(value: string, args: ValidationArguments): boolean {
return value.endsWith(args.constraints[0]);
}
}
class Base {
@Validate(StartsWith, ['base'])
@Validate(EndsWith, ['!'])
title: string;
}
class Child extends Base {
@Validate(StartsWith, ['child'])
title: string;
}

const model = new Child();
model.title = 'child';
const errors = await validator.validate(model);
expect(errors).toHaveLength(1);
expect(errors[0].constraints).toEqual({ endsWith: '' });
model.title = 'child!';
expect(await validator.validate(model)).toEqual([]);
});

it('should retain inherited custom constraints when the child adds a named validator', async () => {
@ValidatorConstraint({ name: 'hasHello' })
class HasHello {
validate(value: string): boolean {
return value.includes('hello');
}
}
class Base {
@Validate(HasHello)
title: string;
}
class Child extends Base {
@IsNotEmpty()
title: string;
}

const model = new Child();
model.title = 'world';
const errors = await validator.validate(model);
expect(errors).toHaveLength(1);
expect(errors[0].constraints).toEqual({ hasHello: '' });
});

it('should only override inherited validators in the selected group', async () => {
class Base {
@MinLength(10, { groups: ['base'] })
title: string;
}
class Child extends Base {
@MinLength(3, { groups: ['child'] })
title: string;
}

const model = new Child();
model.title = 'hello';
const errors = await validator.validate(model, { groups: ['base'] });
expect(errors).toHaveLength(1);
expect(errors[0].constraints).toEqual({ minLength: 'title must be longer than or equal to 10 characters' });
expect(await validator.validate(model, { groups: ['child'] })).toEqual([]);
});

it('should validate inherited properties', () => {
expect.assertions(9);

Expand Down