From 8560439628b72cf6399764adebddc4939f3da816 Mon Sep 17 00:00:00 2001 From: Alexis GEORGES Date: Tue, 12 Mar 2024 12:23:57 +0100 Subject: [PATCH] feat(stark-core): add new `StarkArrayIsValid` validation decorator This new validator validates each value contained in the array. This replaces the not properly working `ValidateNested()` decorator. --- .../metadata/application-metadata.entity.ts | 5 +- .../stark-core/src/validation/decorators.ts | 1 + .../validation/decorators/array-is-valid.ts | 1 + ...array-is-valid.validator.decorator.spec.ts | 58 ++++++++++++++ .../array-is-valid.validator.decorator.ts | 79 +++++++++++++++++++ 5 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 packages/stark-core/src/validation/decorators/array-is-valid.ts create mode 100644 packages/stark-core/src/validation/decorators/array-is-valid/array-is-valid.validator.decorator.spec.ts create mode 100644 packages/stark-core/src/validation/decorators/array-is-valid/array-is-valid.validator.decorator.ts diff --git a/packages/stark-core/src/configuration/entities/metadata/application-metadata.entity.ts b/packages/stark-core/src/configuration/entities/metadata/application-metadata.entity.ts index e600501c51..3ac6fd5522 100644 --- a/packages/stark-core/src/configuration/entities/metadata/application-metadata.entity.ts +++ b/packages/stark-core/src/configuration/entities/metadata/application-metadata.entity.ts @@ -1,10 +1,11 @@ -import { ArrayNotEmpty, IsDefined, IsNotEmpty, IsString, ValidateNested } from "class-validator"; +import { ArrayNotEmpty, IsDefined, IsNotEmpty, IsString } from "class-validator"; import { autoserialize } from "cerialize"; import { StarkApplicationMetadata } from "./application-metadata.entity.intf"; import { StarkLanguageImpl } from "../language"; import { StarkLanguage } from "../language/language.entity.intf"; import { StarkLanguages } from "../language/language.constants"; +import { StarkArrayIsValid } from "../../../validation/decorators"; /** * This class is only for serialization purposes @@ -45,7 +46,7 @@ export class StarkApplicationMetadataImpl implements StarkApplicationMetadata { @IsDefined({ groups: ["settings"] }) @ArrayNotEmpty({ groups: ["settings"] }) - @ValidateNested({ groups: ["settings"], each: true }) // validate each item of the array + @StarkArrayIsValid({ groups: ["settings"] }) public supportedLanguages: StarkLanguageImpl[] = []; /** diff --git a/packages/stark-core/src/validation/decorators.ts b/packages/stark-core/src/validation/decorators.ts index 555d54b7c5..d6e8e22aa3 100644 --- a/packages/stark-core/src/validation/decorators.ts +++ b/packages/stark-core/src/validation/decorators.ts @@ -1,3 +1,4 @@ +export * from "./decorators/array-is-valid"; export * from "./decorators/is-bban"; export * from "./decorators/is-bic"; export * from "./decorators/is-company-number"; diff --git a/packages/stark-core/src/validation/decorators/array-is-valid.ts b/packages/stark-core/src/validation/decorators/array-is-valid.ts new file mode 100644 index 0000000000..ff0453f3e1 --- /dev/null +++ b/packages/stark-core/src/validation/decorators/array-is-valid.ts @@ -0,0 +1 @@ +export * from "./array-is-valid/array-is-valid.validator.decorator"; diff --git a/packages/stark-core/src/validation/decorators/array-is-valid/array-is-valid.validator.decorator.spec.ts b/packages/stark-core/src/validation/decorators/array-is-valid/array-is-valid.validator.decorator.spec.ts new file mode 100644 index 0000000000..dcb2b8d094 --- /dev/null +++ b/packages/stark-core/src/validation/decorators/array-is-valid/array-is-valid.validator.decorator.spec.ts @@ -0,0 +1,58 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { MaxLength, validateSync, ValidationError } from "class-validator"; +import { StarkArrayIsValid, starkArrayIsValidValidatorName } from "./array-is-valid.validator.decorator"; + +class ValueClass { + @MaxLength(10) + public description!: string; +} + +class MyClass { + @StarkArrayIsValid() + public dummyArray: (ValueClass | string)[] = []; +} + +class SimpleClass { + @StarkArrayIsValid() + public name!: string; +} + +describe("ValidatorDecorator: StarkArrayIsValid", () => { + let myClass: MyClass; + let simpleClass: SimpleClass; + let valueClass: ValueClass; + const validatorConstraintName: string = starkArrayIsValidValidatorName; + + beforeEach(() => { + myClass = new MyClass(); + simpleClass = new SimpleClass(); + valueClass = new ValueClass(); + }); + + it("should fail if the object to validate is not an Array", () => { + const errors: ValidationError[] = validateSync(simpleClass); + + expect(errors.length).toBe(1); + expect(errors[0].constraints).toBeDefined(); + expect(errors[0].constraints![validatorConstraintName]).toBeDefined(); + }); + + it("should fail if Array contains invalid values", () => { + valueClass.description = "this is not a valid length"; + myClass.dummyArray.push(valueClass); + const errors: ValidationError[] = validateSync(myClass); + + expect(errors.length).toBe(1); + expect(errors[0].constraints).toBeDefined(); + expect(errors[0].constraints![validatorConstraintName]).toBeDefined(); + expect(errors[0].constraints![validatorConstraintName]).toContain("array values"); + }); + + it("should NOT fail if Array contains valid values", () => { + valueClass.description = "length ok"; + myClass.dummyArray.push("stringValue"); + const errors: ValidationError[] = validateSync(myClass); + + expect(errors.length).toBe(0); + }); +}); diff --git a/packages/stark-core/src/validation/decorators/array-is-valid/array-is-valid.validator.decorator.ts b/packages/stark-core/src/validation/decorators/array-is-valid/array-is-valid.validator.decorator.ts new file mode 100644 index 0000000000..d111e5cefd --- /dev/null +++ b/packages/stark-core/src/validation/decorators/array-is-valid/array-is-valid.validator.decorator.ts @@ -0,0 +1,79 @@ +import { + registerDecorator, + validateSync, + ValidationError, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface +} from "class-validator"; +import { StarkValidationErrorsUtil } from "../../../util/validation-errors.util"; + +/** + * Name of the validator, in case injection is needed. + */ +export const starkArrayIsValidValidatorName = "starkArrayIsValid"; + +/** + * StarkArrayIsValid validator constraint + * Validates that all entries in the Array are valid (validateSync is called in every value) + */ +@ValidatorConstraint({ name: starkArrayIsValidValidatorName, async: false }) +class StarkArrayIsValidConstraint implements ValidatorConstraintInterface { + /** + * an Array of validation errors' values + */ + public valuesValidationErrors: ValidationError[] = []; + + /** + * Validates that a give Array is valid + * @param array - the array to validate + * @returns `true` if the array is valid + */ + public validate(array: any[]): boolean { + if (!array || !Array.isArray(array) || array.length === 0) { + return false; + } + + this.valuesValidationErrors = []; + + array.forEach((value: any) => { + // skipping primitive types + if (typeof value === "object") { + this.valuesValidationErrors = [...this.valuesValidationErrors, ...validateSync(value)]; + } + }); + + return this.valuesValidationErrors.length === 0; + } + + /** + * Default message displayed when the array contains invalid entries + * @returns A default message + */ + public defaultMessage(): string { + let valuesValidationMessage: string = StarkValidationErrorsUtil.toString(this.valuesValidationErrors); + + if (valuesValidationMessage.length) { + valuesValidationMessage = "\nValidation errors in $property array values:\n\n" + valuesValidationMessage; + } + + return "$property array contains invalid entries.\n" + valuesValidationMessage; + } +} + +/** + * Validator decorator that uses the StarkArrayIsValid validator constraint + * @param validationOptions - options to determine if the array is valid + * @returns Function + */ +export function StarkArrayIsValid(validationOptions?: ValidationOptions): Function { + return (object: object, propertyName: string): void => { + registerDecorator({ + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + constraints: [], + validator: StarkArrayIsValidConstraint + }); + }; +}