Skip to content

Commit 5fc23be

Browse files
authored
feat: add always & strictGroups to ValidationOptions (#680)
1 parent d6d9868 commit 5fc23be

File tree

5 files changed

+159
-4
lines changed

5 files changed

+159
-4
lines changed

src/metadata/MetadataStorage.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,38 @@ export class MetadataStorage {
6363
getTargetValidationMetadatas(
6464
targetConstructor: Function,
6565
targetSchema: string,
66+
always: boolean,
67+
strictGroups: boolean,
6668
groups?: string[]
6769
): ValidationMetadata[] {
70+
const includeMetadataBecauseOfAlwaysOption = (metadata: ValidationMetadata): boolean => {
71+
// `metadata.always` overrides global default.
72+
if (typeof metadata.always !== 'undefined') return metadata.always;
73+
74+
// `metadata.groups` overrides global default.
75+
if (metadata.groups && metadata.groups.length) return false;
76+
77+
// Use global default.
78+
return always;
79+
};
80+
81+
const excludeMetadataBecauseOfStrictGroupsOption = (metadata: ValidationMetadata): boolean => {
82+
if (strictGroups) {
83+
// Validation is not using groups.
84+
if (!groups || !groups.length) {
85+
// `metadata.groups` has at least one group.
86+
if (metadata.groups && metadata.groups.length) return true;
87+
}
88+
}
89+
90+
return false;
91+
};
92+
6893
// get directly related to a target metadatas
6994
const originalMetadatas = this.validationMetadatas.filter(metadata => {
7095
if (metadata.target !== targetConstructor && metadata.target !== targetSchema) return false;
71-
if (metadata.always) return true;
96+
if (includeMetadataBecauseOfAlwaysOption(metadata)) return true;
97+
if (excludeMetadataBecauseOfStrictGroupsOption(metadata)) return false;
7298
if (groups && groups.length > 0)
7399
return metadata.groups && !!metadata.groups.find(group => groups.indexOf(group) !== -1);
74100

@@ -82,7 +108,8 @@ export class MetadataStorage {
82108
if (metadata.target === targetConstructor) return false;
83109
if (metadata.target instanceof Function && !(targetConstructor.prototype instanceof metadata.target))
84110
return false;
85-
if (metadata.always) return true;
111+
if (includeMetadataBecauseOfAlwaysOption(metadata)) return true;
112+
if (excludeMetadataBecauseOfStrictGroupsOption(metadata)) return false;
86113
if (groups && groups.length > 0)
87114
return metadata.groups && !!metadata.groups.find(group => groups.indexOf(group) !== -1);
88115

src/metadata/ValidationMetadata.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export class ValidationMetadata {
4747
/**
4848
* Indicates if validation must be performed always, no matter of validation groups used.
4949
*/
50-
always: boolean = false;
50+
always?: boolean;
5151

5252
/**
5353
* Specifies if validated value is an array and each of its item must be validated.

src/validation/ValidationExecutor.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,16 @@ export class ValidationExecutor {
5050
}
5151

5252
const groups = this.validatorOptions ? this.validatorOptions.groups : undefined;
53-
const targetMetadatas = this.metadataStorage.getTargetValidationMetadatas(object.constructor, targetSchema, groups);
53+
const strictGroups = (this.validatorOptions && this.validatorOptions.strictGroups) || false;
54+
const always = (this.validatorOptions && this.validatorOptions.always) || false;
55+
56+
const targetMetadatas = this.metadataStorage.getTargetValidationMetadatas(
57+
object.constructor,
58+
targetSchema,
59+
always,
60+
strictGroups,
61+
groups
62+
);
5463
const groupedMetadatas = this.metadataStorage.groupByPropertyName(targetMetadatas);
5564

5665
if (this.validatorOptions && this.validatorOptions.forbidUnknownValues && !targetMetadatas.length) {

src/validation/ValidatorOptions.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,17 @@ export interface ValidatorOptions {
3434
*/
3535
groups?: string[];
3636

37+
/**
38+
* Set default for `always` option of decorators. Default can be overridden in decorator options.
39+
*/
40+
always?: boolean;
41+
42+
/**
43+
* If [groups]{@link ValidatorOptions#groups} is not given or is empty,
44+
* ignore decorators with at least one group.
45+
*/
46+
strictGroups?: boolean;
47+
3748
/**
3849
* If set to true, the validation will not use default messages.
3950
* Error message always will be undefined if its not explicitly set.

test/functional/validation-options.spec.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {
66
Validate,
77
ValidateNested,
88
ValidatorConstraint,
9+
IsOptional,
10+
IsNotEmpty,
911
} from '../../src/decorator/decorators';
1012
import { Validator } from '../../src/validation/Validator';
1113
import {
@@ -863,6 +865,112 @@ describe('groups', () => {
863865
});
864866
});
865867

868+
describe('ValidationOptions.always', function () {
869+
class MyClass {
870+
@Contains('noOptions')
871+
noOptions: string;
872+
873+
@Contains('groupA', {
874+
groups: ['A'],
875+
})
876+
groupA: string;
877+
878+
@Contains('alwaysFalse', {
879+
always: false,
880+
})
881+
alwaysFalse: string;
882+
883+
@Contains('alwaysTrue', {
884+
always: true,
885+
})
886+
alwaysTrue: string;
887+
}
888+
889+
const model1 = new MyClass();
890+
model1.noOptions = 'XXX';
891+
model1.groupA = 'groupA';
892+
model1.alwaysFalse = 'alwaysFalse';
893+
model1.alwaysTrue = 'alwaysTrue';
894+
895+
const model2 = new MyClass();
896+
model2.noOptions = 'noOptions';
897+
model2.groupA = 'XXX';
898+
model2.alwaysFalse = 'alwaysFalse';
899+
model2.alwaysTrue = 'alwaysTrue';
900+
901+
const model3 = new MyClass();
902+
model3.noOptions = 'noOptions';
903+
model3.groupA = 'groupA';
904+
model3.alwaysFalse = 'XXX';
905+
model3.alwaysTrue = 'alwaysTrue';
906+
907+
const model4 = new MyClass();
908+
model4.noOptions = 'noOptions';
909+
model4.groupA = 'groupA';
910+
model4.alwaysFalse = 'alwaysFalse';
911+
model4.alwaysTrue = 'XXX';
912+
913+
it('should validate decorator without options', function () {
914+
return validator.validate(model1, { always: true, groups: ['A'] }).then(errors => {
915+
expect(errors).toHaveLength(1);
916+
});
917+
});
918+
919+
it('should not validate decorator with groups if validating without matching groups', function () {
920+
return validator.validate(model2, { always: true, groups: ['B'] }).then(errors => {
921+
expect(errors).toHaveLength(0);
922+
});
923+
});
924+
925+
it('should not validate decorator with always set to false', function () {
926+
return validator.validate(model3, { always: true, groups: ['A'] }).then(errors => {
927+
expect(errors).toHaveLength(0);
928+
});
929+
});
930+
931+
it('should validate decorator with always set to true', function () {
932+
return validator.validate(model4, { always: true, groups: ['A'] }).then(errors => {
933+
expect(errors).toHaveLength(1);
934+
});
935+
});
936+
});
937+
938+
describe('strictGroups', function () {
939+
class MyClass {
940+
@Contains('hello', {
941+
groups: ['A'],
942+
})
943+
title: string;
944+
}
945+
946+
const model1 = new MyClass();
947+
948+
it('should ignore decorators with groups if validating without groups', function () {
949+
return validator.validate(model1, { strictGroups: true }).then(errors => {
950+
expect(errors).toHaveLength(0);
951+
});
952+
});
953+
954+
it('should ignore decorators with groups if validating with empty groups array', function () {
955+
return validator.validate(model1, { strictGroups: true, groups: [] }).then(errors => {
956+
expect(errors).toHaveLength(0);
957+
});
958+
});
959+
960+
it('should include decorators with groups if validating with matching groups', function () {
961+
return validator.validate(model1, { strictGroups: true, groups: ['A'] }).then(errors => {
962+
expect(errors).toHaveLength(1);
963+
expectTitleContains(errors[0]);
964+
});
965+
});
966+
967+
it('should not include decorators with groups if validating with different groups', function () {
968+
return validator.validate(model1, { strictGroups: true, groups: ['B'] }).then(errors => {
969+
expect(errors).toHaveLength(0);
970+
});
971+
});
972+
});
973+
866974
describe('always', () => {
867975
class MyClass {
868976
@Contains('hello', {

0 commit comments

Comments
 (0)