Validate array contains one of each object with specific properties and values

Viewed 1429

I looked in the class-validators samples and docs but cant find the validation I need.

I have an array of object literals each with specific properties and values.

const comboItems = [{itemType: 'Entree'}, {itemType: 'Side'}, {itemType: 'Beverage'}];

I want to validate that comboItems is not empty and contains a min and max of 3 objects AND that there is an itemType === 'Entree' AND an itemType==='Side' AND an itemType==='Beverage'

This is the class I've created which isnt giving the correct validation:

import {validate, ArrayContains, ArrayNotEmpty, ArrayMinSize, ArrayMaxSize} from 'class-validator';
import { plainToClass } from 'class-transformer';

export class MealMenuItem {
    @ArrayContains([{itemType: 'Entree'}, {itemType: 'Side'}, {itemType: 'Beverage'}])
    @ArrayNotEmpty()
    @ArrayMinSize(3)
    @ArrayMaxSize(3)
    comboItems: any[];
}

const mealMenuItemData: any = {comboItems: [{itemType: 'Entree'}, {itemType: 'Side'}, {itemType: 'Beverage'}]};

const mealMenuItemDataClassInstance = plainToClass(MealMenuItem, mealMenuItemData as MealMenuItem)

validate(mealMenuItemDataClassInstance).then(errors => {
    if (errors.length > 0) 
        console.log('validation failed. errors: ', JSON.stringify(errors));
    else 
        console.log('validation succeed');
});

Thanks for any help!

1 Answers

The ArrayContains decorator method just validate primitive types due to this line of class-validator module:

export function arrayContains(array: unknown, values: any[]): boolean {
  if (!(array instanceof Array)) return false;

  // Here return false for object items
  return values.every(value => array.indexOf(value) !== -1);
}

source code

So you can create a custom decorator for validation of your itemType field as bellow:

import {
    validate,
    ArrayMinSize,
    ArrayMaxSize,
    registerDecorator,
    ValidationOptions,
    ValidationArguments
} from 'class-validator';
import { plainToClass } from 'class-transformer';

function ContainSequenceOf(property: string, validationOptions?: ValidationOptions & { containThese: string[] }) {
    return function (object: Object, propertyName: string) {
        registerDecorator({
            name: 'containSequenceOf',
            target: object.constructor,
            propertyName: propertyName,
            constraints: [property],
            options: validationOptions,
            validator: {
                validate(value: any, args: ValidationArguments) {
                    const [relatedPropertyName] = args.constraints;
                    return value.every((item, i) => {
                        return item[relatedPropertyName] === validationOptions?.containThese[i]
                    })
                },
            },
        });
    };
}
export class MealMenuItem {
    @ContainSequenceOf('itemType', {
        message: "Is Not Valid!",
        containThese: ['Entree', 'Side', 'Beverage']
    })
    @ArrayMinSize(3)
    @ArrayMaxSize(3)
    comboItems: any[];
}

const mealMenuItemData: any = {comboItems: [{ itemType: 'Entree' }, { itemType: 'Side' }, { itemType: 'Beverage' }]};
const mealMenuItemDataClassInstance = plainToClass(MealMenuItem, mealMenuItemData as MealMenuItem)

validate(mealMenuItemDataClassInstance).then(errors => {
    if (errors.length > 0) 
        console.log('validation failed. errors: ', JSON.stringify(errors));
    else 
        console.log('validation succeed');
});
Related