Last active
December 10, 2024 00:27
-
-
Save MatiasDuhalde/a84614f922a57d7ca635acb8f22f448f to your computer and use it in GitHub Desktop.
class-validator Mutually Exclusive decorator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
ValidationArguments, | |
ValidationOptions, | |
ValidationTypes, | |
registerDecorator, | |
} from 'class-validator'; | |
import 'reflect-metadata'; | |
// Must not conflict with other existing properties in the application | |
const MUTUALLY_EXCLUSIVE_KEY = (tag: string) => | |
`custom:__@rst/validator_mutually_exclusive_${tag}__`; | |
export function MutuallyExclusive( | |
tag = 'default', | |
validationOptions?: ValidationOptions, | |
) { | |
return function (object: object, propertyName: string) { | |
const key = MUTUALLY_EXCLUSIVE_KEY(tag); | |
const existing = Reflect.getMetadata(key, object) || []; | |
// We add the decorated property to the list of mutually exclusive properties | |
Reflect.defineMetadata(key, [...existing, propertyName], object); | |
// This decorator is used to cut the validation chain when we only have one property defined | |
registerDecorator({ | |
name: ValidationTypes.CONDITIONAL_VALIDATION, | |
target: object.constructor, | |
propertyName: propertyName, | |
constraints: [ | |
(object: any, value: any) => { | |
const mutuallyExclusiveProps: string[] = Reflect.getMetadata( | |
key, | |
object, | |
); | |
const definedCount = mutuallyExclusiveProps.reduce( | |
(p, c) => (object[c as keyof object] !== undefined ? ++p : p), | |
0, | |
); | |
return !(definedCount === 1 && value === undefined); | |
}, | |
], | |
options: validationOptions, | |
validator: () => true, | |
}); | |
// This decorator is used to display a custom error message | |
registerDecorator({ | |
name: 'MutuallyExclusive', | |
target: object.constructor, | |
propertyName: propertyName, | |
constraints: [tag], | |
options: validationOptions, | |
validator: { | |
validate(value: any, args: ValidationArguments) { | |
const mutuallyExclusiveProps: string[] = Reflect.getMetadata( | |
key, | |
args.object, | |
); | |
return ( | |
mutuallyExclusiveProps.reduce( | |
(p, c) => | |
args.object[c as keyof object] !== undefined ? ++p : p, | |
0, | |
) === 1 | |
); | |
}, | |
defaultMessage(validationArguments?: ValidationArguments) { | |
if (!validationArguments) { | |
return 'Mutually exclusive validation failed'; | |
} | |
const mutuallyExclusiveProps: string[] = Reflect.getMetadata( | |
key, | |
validationArguments.object, | |
); | |
return `Exactly one of the following properties must be defined: ${mutuallyExclusiveProps.join( | |
', ', | |
)}`; | |
}, | |
}, | |
}); | |
}; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { MutuallyExclusive } from './mutually-exclusive.decorator'; | |
import { | |
IsCreditCard, | |
IsEmail, | |
IsNotEmpty, | |
IsPositive, | |
IsUUID, | |
} from 'class-validator'; | |
export class Example { | |
@MutuallyExclusive('set-one') | |
@IsUUID() | |
propertyOne: string; | |
@MutuallyExclusive('set-one') | |
@IsNotEmpty() | |
propertyTwo: string; | |
@MutuallyExclusive('set-two') | |
@IsPositive() | |
propertyThree: number; | |
@MutuallyExclusive('set-two') | |
propertyFour: string; | |
@MutuallyExclusive('set-two') | |
@IsCreditCard() | |
propertyFive: string; | |
constructor(params: Partial<Example>) { | |
Object.assign(this, params); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { MutuallyExclusive } from './mutually-exclusive.decorator'; | |
import { Example } from './sample.class'; | |
// This should fail because none of set-one nor set-two are defined | |
const test1 = new Example({}); | |
validate(test1, {}).then(console.log); | |
/** | |
[ | |
ValidationError { | |
target: Example {}, | |
value: undefined, | |
property: 'propertyOne', | |
children: [], | |
constraints: { | |
isUuid: 'propertyOne must be a UUID', | |
MutuallyExclusive: 'Exactly one of the following properties must be defined: propertyOne, propertyTwo' | |
} | |
}, | |
ValidationError { | |
target: Example {}, | |
value: undefined, | |
property: 'propertyTwo', | |
children: [], | |
constraints: { | |
isNotEmpty: 'propertyTwo should not be empty', | |
MutuallyExclusive: 'Exactly one of the following properties must be defined: propertyOne, propertyTwo' | |
} | |
}, | |
ValidationError { | |
target: Example {}, | |
value: undefined, | |
property: 'propertyThree', | |
children: [], | |
constraints: { | |
isPositive: 'propertyThree must be a positive number', | |
MutuallyExclusive: 'Exactly one of the following properties must be defined: propertyThree, propertyFour, propertyFive' | |
} | |
}, | |
ValidationError { | |
target: Example {}, | |
value: undefined, | |
property: 'propertyFour', | |
children: [], | |
constraints: { | |
MutuallyExclusive: 'Exactly one of the following properties must be defined: propertyThree, propertyFour, propertyFive' | |
} | |
}, | |
ValidationError { | |
target: Example {}, | |
value: undefined, | |
property: 'propertyFive', | |
children: [], | |
constraints: { | |
isCreditCard: 'propertyFive must be a credit card', | |
MutuallyExclusive: 'Exactly one of the following properties must be defined: propertyThree, propertyFour, propertyFive' | |
} | |
} | |
] | |
*/ | |
// This should fail because two properties of set-one are defined | |
const test2 = new Example({ | |
propertyOne: '123e4567-e89b-12d3-a456-426614174000', | |
propertyTwo: 'test', | |
propertyThree: 1, | |
}); | |
validate(test2, {}).then(console.log); | |
/** | |
[ | |
ValidationError { | |
target: Example { | |
propertyOne: '123e4567-e89b-12d3-a456-426614174000', | |
propertyTwo: 'test', | |
propertyThree: 1 | |
}, | |
value: '123e4567-e89b-12d3-a456-426614174000', | |
property: 'propertyOne', | |
children: [], | |
constraints: { | |
MutuallyExclusive: 'Exactly one of the following properties must be defined: propertyOne, propertyTwo' | |
} | |
}, | |
ValidationError { | |
target: Example { | |
propertyOne: '123e4567-e89b-12d3-a456-426614174000', | |
propertyTwo: 'test', | |
propertyThree: 1 | |
}, | |
value: 'test', | |
property: 'propertyTwo', | |
children: [], | |
constraints: { | |
MutuallyExclusive: 'Exactly one of the following properties must be defined: propertyOne, propertyTwo' | |
} | |
} | |
] | |
*/ | |
// This should fail because two properties of set-two are defined | |
const test3 = new Example({ | |
propertyOne: '123e4567-e89b-12d3-a456-426614174000', | |
propertyThree: 1, | |
propertyFive: '1234-1234-1234-1234', | |
}); | |
validate(test3, {}).then(console.log); | |
/** | |
[ | |
ValidationError { | |
target: Example { | |
propertyOne: '123e4567-e89b-12d3-a456-426614174000', | |
propertyThree: 1, | |
propertyFive: '1234-1234-1234-1234' | |
}, | |
value: 1, | |
property: 'propertyThree', | |
children: [], | |
constraints: { | |
MutuallyExclusive: 'Exactly one of the following properties must be defined: propertyThree, propertyFour, propertyFive' | |
} | |
}, | |
ValidationError { | |
target: Example { | |
propertyOne: '123e4567-e89b-12d3-a456-426614174000', | |
propertyThree: 1, | |
propertyFive: '1234-1234-1234-1234' | |
}, | |
value: undefined, | |
property: 'propertyFour', | |
children: [], | |
constraints: { | |
MutuallyExclusive: 'Exactly one of the following properties must be defined: propertyThree, propertyFour, propertyFive' | |
} | |
}, | |
ValidationError { | |
target: Example { | |
propertyOne: '123e4567-e89b-12d3-a456-426614174000', | |
propertyThree: 1, | |
propertyFive: '1234-1234-1234-1234' | |
}, | |
value: '1234-1234-1234-1234', | |
property: 'propertyFive', | |
children: [], | |
constraints: { | |
isCreditCard: 'propertyFive must be a credit card', | |
MutuallyExclusive: 'Exactly one of the following properties must be defined: propertyThree, propertyFour, propertyFive' | |
} | |
} | |
] | |
*/ | |
// This should fail because no properties of set-one are defined | |
const test4 = new Example({ | |
propertyThree: 1, | |
}); | |
validate(test4, {}).then(console.log); | |
/** | |
[ | |
ValidationError { | |
target: Example { propertyThree: 1 }, | |
value: undefined, | |
property: 'propertyOne', | |
children: [], | |
constraints: { | |
isUuid: 'propertyOne must be a UUID', | |
MutuallyExclusive: 'Exactly one of the following properties must be defined: propertyOne, propertyTwo' | |
} | |
}, | |
ValidationError { | |
target: Example { propertyThree: 1 }, | |
value: undefined, | |
property: 'propertyTwo', | |
children: [], | |
constraints: { | |
isNotEmpty: 'propertyTwo should not be empty', | |
MutuallyExclusive: 'Exactly one of the following properties must be defined: propertyOne, propertyTwo' | |
} | |
} | |
] | |
*/ | |
// This should fail because propertyOne does not pass the other validation checks | |
const test5 = new Example({ | |
propertyOne: 'not a uuid', | |
propertyThree: 1, | |
}); | |
validate(test5, {}).then(console.log); | |
/** | |
[ | |
ValidationError { | |
target: Example { propertyOne: 'not a uuid', propertyThree: 1 }, | |
value: 'not a uuid', | |
property: 'propertyOne', | |
children: [], | |
constraints: { isUuid: 'propertyOne must be a UUID' } | |
} | |
] | |
*/ | |
// This should pass | |
const test6 = new Example({ | |
propertyTwo: 'test', | |
propertyThree: 1, | |
}); | |
validate(test6, {}).then(console.log); | |
// [] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
nice! thanks :)