typestack / class-validator

Decorator-based property validation for classes.
MIT License
10.89k stars 786 forks source link

feat: add option to define class level custom decorators #182

Open j opened 6 years ago

j commented 6 years ago

Is there support for validation on a class level and not just property level? I want to do DB lookups but issue a single query vs one per field.

NoNameProvided commented 6 years ago

Can you elaborate please, give me some theoretical example code?

j commented 6 years ago

Here's how Symfony does it in the PHP world:

https://symfony.com/doc/current/reference/constraints/UniqueEntity.html

j commented 6 years ago

In the class-validator world:

@IsUnique(WidgetRepository, ["name", "tag"])
export class Widget {
  @IsString()
  name: string;

  @IsString()
  @IsLowercase()
  tag: string;
}

@IsUnique would use something like typedi to get the repository from the container, and the array would be the fields that are to be unique.

I could think of a few more examples that would be nice to compare all fields at once. The main example is to do a single query utilizing the object after all field validations passed.

NoNameProvided commented 6 years ago

Have you seen TypeORM? It's exactly what you are looking for.

j commented 6 years ago

I'm not using TypeORM. I'm using MongoDB at the moment and they are pretty far behind in driver support. And not a lot of movement in MongoDB within TypeORM (issues grow, nothing getting done).

I'd like to push it to a validation library. The Symfony library I referenced above is a class validation library such as this one with support for validating DoctrineORM objects.

Edit: I'm also using https://github.com/19majkel94/type-graphql which has built-in support for this library.

NoNameProvided commented 6 years ago

Then you can create such functionality here via

j commented 6 years ago

@NoNameProvided I created a decorator already, but it's bound to the field and not the class, so I lack access to multiple fields.

Edit: I might be able to access multiple fields, but if so, it wouldn't be clear what the validation is trying to do.

NoNameProvided commented 6 years ago

I created a decorator already, but it's bound to the field and not the class, so I lack access to multiple fields.

You can create multi-field validations. Here is an example: https://github.com/typestack/class-validator/issues/145#issuecomment-373949845

I might be able to access multiple fields, but if so, it wouldn't be clear what the validation is trying to do.

In your example, it totally makes sense to put it on the unique property instead of the class.

j commented 6 years ago

Yes, in my fake example use-case, perhaps it can be solved "cleanishly" (although the two fields are not related at all, so IMO it's not clean and is actually more confusing).

Another example where it makes more sense on a class level:

@IsValidLocation(['address1', 'address2', 'city', 'state', 'zip'])
export class User {
  address1: string;
  address2: string;
  city: string;
  state: string;
  zip: string;
}

or

@IsValidLogin()
export class Auth {
  username: string;
  password: string;
}
// This may do a true lookup on the image, verify image size, etc, etc... I don't see this being clean on a field level.

@IsImageValid(({ baseUrl, name, extension }) => `${baseUrl)${name}${extension}`)
export class Image {
  @IsURL()
  baseURL: string;

  @IsString()
  name: string;

  @IsValidExtension()
  extension: string;
}

Others do it:

I see where if you're comparing a field to another field, then field level validation works and makes sense, but if the fields aren't being compared and are being used as a final validation constraint, then it'd make more sense on the class level. It can also be an area where you may do heavier validations. You can run your simple field level validations first, then do things like DB lookups, external requests, etc, on a class level so that simple validations can fail first.

As of now, I'd never do any of these examples with class-validator and am forced to do these types of validations hardcoded in my app. It'd be awesome to have it all in one place.

tonyxiao commented 6 years ago

This makes a lot of sense to me - I was thinking of exactly the same thing (specifically wanting to do Joi's with and or validators.

zveljkovic commented 6 years ago

Anything on this issue? Class level validators are very useful.

ambroiseRabier commented 5 years ago

Up, for example I have this case:

// Having at least one of the two set.
@ClassLevelValidator(post => post.articleId != null || post.externalLink != null)
export class PostDto {
  @IsNotEmpty()
  @IsString()
  title: string;

  @IsDefined()
  @IsString()
  shortDescription: string;

  @IsOptional()
  @IsInt()
  articleId: number; // if set I know this post relate to an article entity, that I can retrieve using the id

  @IsOptional()
  @IsUrl()
  externalLink: string; // if set I know this post relate to an external link.
}

// if both are set or none are set then this is a wrong entry.

It do exist in Java https://stackoverflow.com/questions/2781771/how-can-i-validate-two-or-more-fields-in-combination

ajaygangarde commented 4 years ago

Generic Unique Constrain For any Column I hope we can Use below code like

File Name:- isUnique.ts @ValidatorConstraint({ async: true }) export class IsUniqueConstraint implements ValidatorConstraintInterface { validate(columnNameValue: any, args: ValidationArguments) { let columnNameKey = args.property; let tableName = args.targetName; return getManager().query("SELECT * FROM " + tableName + " WHERE " + columnNameKey + " = ?", [columnNameValue]).then(user => { if (user[0]) return false; return true; }); } } export function IsUnique(validationOptions?: ValidationOptions) { return function (object: Object, propertyName: string) { registerDecorator({ target: object.constructor, propertyName: propertyName, options: validationOptions, constraints: [], validator: IsUniqueConstraint }); }; }

Can be use in Typeorm Model Users as

@Column() @IsEmail({}, { message: 'Invalid email Address.' }) @IsUnique({ message: "Email $value already exists. Choose another name." }) email: string;

@Column() @Length(4, 20) @IsUnique({ message: "User Name $value already exists. Choose another name." }) username: string;

So it will be common for any model table.

fullofcaffeine commented 4 years ago

I was also hoping for a class-level validator, any news about this?

LendaVadym commented 4 years ago

Voting up for class level decorators. Would be a useful feature.

j commented 4 years ago

@NoNameProvided this is pretty wanted.

vlapo commented 4 years ago

Sure. We are open to proposals how this may works. But for now this is future request.

astahmer commented 4 years ago

Hey, I made a tiny package that solves this exact problem, I tried to remain as close as possible to class-validator API https://github.com/astahmer/entity-validator/

Beej126 commented 3 years ago

my need for "class level" is to validate across multiple properties at the same time versus just one... i was concerned when i saw this issue still open... yet issue #759 referencing the docs Custom validation decorators - @IsLongerThan() example shows how to reference the any of the current objects properties inside the validator so i'm hopeful i can adapt that to my needs... just wanted to post in case it helps others

royi-frontegg commented 3 years ago

Any updates on this issue?

ModestinoAndre commented 3 years ago

I have created a workaround for this issue, but I don't know if it is a good approach.

It works by creating a property named _classValidationProperty on target prototype and registering a property decorator to it. Inside this property decorator, you have access to object been validated, so you can do any validations using multiple properties of the object. I also created a helper function to create the decorators.

import { registerDecorator, ValidationArguments } from 'class-validator';

@ValidateFoo()
export class Foo {
  a: number;
  b: number;
  c: number;
}

interface Class {
  new(...args: any[]): {};
}

export function ValidateFoo() {
  return createClassValidationDecorator('Foo', (value: any, args: ValidationArguments) => {
    // Do whatever validation you need
    const foo = args.object as Foo;
    return (foo.a === foo.b) && (foo.a === foo.c);
  });
}

// Another Example
export function ValidateBar() {
  // validateBarFunction is defined somewhere else
  return createClassValidationDecorator('bar', validateBarFunction);
}

export function createClassValidationDecorator(validatorName: string, validateFunc: (value: any, args: ValidationArguments) => any) {
  return function decorateClass<T extends Class>(target: T) {
    console.log('>>> Decorating class:', target.name);
    let _classValidatorRegistered = false;

    return class extends target {
      private readonly _classValidationProperty;

      constructor(...args: any[]) {
        super(...args);
        console.log(`Executing ${target.name}.constructor`);
        if (!_classValidatorRegistered) {
          _classValidatorRegistered = true;
          console.log(`Registering property validator for ${target.name}._classValidationProperty`);
          registerDecorator({
            name: validatorName,
            target: this.constructor,
            propertyName: '_classValidationProperty',
            // constraints: ['...'],
            // options: validationOptions,
            validator: {
              validate: validateFunc
            },
          });
        }
      }
    };
  }
}
lucasltv commented 3 years ago

up!

DustinJSilk commented 3 years ago

That was very useful @ModestinoAndre, thanks.

I ended up with a similar solution quite close to yours if anyone needs it some day, which works with Nestjs InputType / ObjectType decorators as well:

import {
  registerDecorator,
  ValidatorConstraintInterface,
} from 'class-validator';
import { MarkRequired } from 'ts-essentials';

type AnyClass = { new (...args: any[]): any };

type PublicConstructor = new (...args: any[]) => any;

/** The response type for registerClassValidator() */
export type ClassValidationDecorator = <T extends AnyClass>(
  target: T,
) => PublicConstructor;

/** A helper method to create a new Class-level validation decorator. */
export function registerClassValidator(options: {
  name: string;
  validator: new () => MarkRequired<
    ValidatorConstraintInterface,
    'defaultMessage'
  >;
  constraints: any[];
}): ClassValidationDecorator {
  return function decorateClass<T extends AnyClass>(
    target: T,
  ): PublicConstructor {
    const { name, validator, constraints } = options;

    registerDecorator({
      name,
      target,
      propertyName: target.name,
      constraints,
      validator,
      options: {
        always: true,
      },
    });

    return target;
  };
}

And then I can use it like this:

/** The class-validator constraint for the RequiredTogether() class decorator */
@ValidatorConstraint()
class RequiredTogetherConstraint implements ValidatorConstraintInterface {
  validate(value: undefined, args: ValidationArguments): boolean {
    const [requiredFields] = args.constraints;

    return (
      requiredFields.every((field: string) => field in args.object) ||
      requiredFields.every((field: string) => !(field in args.object))
    );
  }

  defaultMessage(args: ValidationArguments): string {
    const [fields] = args.constraints;

    return 'All fields must exist together or not at all: ' + fields.join(', ');
  }
}

/**
 * A class level decorator to check that all or none of the fields exist.
 * This is useful when using optional fields but must all exist together.
 */
export function RequiredTogether(fields: string[]): ClassValidationDecorator {
  return registerClassValidator({
    name: 'RequiredTogether',
    validator: RequiredTogetherConstraint,
    constraints: [fields],
  });
}
Char2sGu commented 3 years ago

Need this feature too!!

juliusiv commented 3 years ago

Marshmallow (a schema validator for Python) also has a feature like this: @validates_schema.

Some use-cases where this is useful:

hanchchch commented 1 year ago

any updates about this?

slukes commented 6 months ago

Hello any news on this please?