Skip to content

Instantly share code, notes, and snippets.

@MatiasDuhalde
Last active December 10, 2024 00:27
Show Gist options
  • Save MatiasDuhalde/a84614f922a57d7ca635acb8f22f448f to your computer and use it in GitHub Desktop.
Save MatiasDuhalde/a84614f922a57d7ca635acb8f22f448f to your computer and use it in GitHub Desktop.
class-validator Mutually Exclusive decorator
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(
', ',
)}`;
},
},
});
};
}
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);
}
}
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);
// []
@Helveg
Copy link

Helveg commented Dec 9, 2024

nice! thanks :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment