It may be a bit delayed, but I have come up with a solution using a customized validator to achieve the desired outcome.
import {
ValidationOptions,
registerDecorator,
ValidationArguments
} from 'class-validator';
class GenericModel {}
/**
* Introducing a customizable custom validation method
*/
export const createCustomValidator = <Model extends GenericModel>(
validatorName: string,
validatorFunc: (a: unknown, b: unknown) => boolean,
modelTypeGuard: (obj: unknown) => obj is Model
) => {
return (
otherPropertyName: keyof Model,
validationOptions?: ValidationOptions
) => {
return (obj: Model, propertyName: string) => {
registerDecorator({
name: validatorName,
target: obj.constructor,
propertyName,
constraints: [otherPropertyName],
options: validationOptions,
validator: {
validate(value: string, args: ValidationArguments) {
const model = args.object;
if (!modelTypeGuard(model)) {
return false;
}
return validatorFunc(value, model[otherPropertyName]);
}
}
});
};
};
};
An illustration of how this can be applied:
const isLessThanOrEqualTo = (a: unknown, b: unknown): boolean => {
if (typeof a !== 'number' || typeof b !== 'number') {
return false;
}
return a <= b;
};
const isSomeModel = (obj: unknown): obj is SomeModel => {
return obj instanceof SomeModel;
};
/**
* __Is Less Than Or Equal To__ validation function
*/
const IsLte = createCustomValidator<SomeModel>(
'isLte',
isLessThanOrEqualTo,
isSomeModel
);
export class SomeModel {
@IsNumber()
@Min(1, { message: 'minimum points should be positive nonzero number' })
totalPoints!: number;
@IsNumber()
@Min(1, { message: 'minimum points should be positive nonzero number' })
@IsLte('totalPoints', {
message: 'Obtained points must be less than or equal to total points'
})
obtainedPoints!: number;
}
In addition, here is a basic test scenario for validation purposes:
import 'reflect-metadata';
import { validate, ValidationError } from 'class-validator';
describe('obtainedPoints', () => {
it('should throw an error when the obtainedPoints exceed the total points', async () => {
const model = new SomeModel();
model.totalPoints = 10;
model.obtainedPoints = 20;
const result = await validate(model, {
forbidUnknownValues: true
});
expect(result.length).toBe(1);
expect(result[0].property).toBe('obtainedPoints');
expect(result[0].constraints?.isLte).toBeTruthy();
});
});