How to group decorators (from a library) together into 1 re-usable decorator
Every time my REST API receives a request, it will validate the provided body properties (using the class-validator library). Every route has its own dedicated validation class (in the code they are called Dtos) (see example)
Every provided body property has a couple of validation rules, these can sometimes get really complex, other engineers should be able to re-use these validation rules easily.
Route 1: Company Creation
POST - /api/company
     >> Parameters: name, domain, size, contact
class CreateCompanyDto implements Dto {
    @IsString({message: 'Must be text format'})
    @MinLength(2, { message: "Must have at least 2 characters" })
    @MaxLength(20, { message: "Can't be longer than 20 characters" })
    @IsDefined({ message: 'Must specify a receiver' })
    public name!: string;
    @MaxLength(253, { message: "Can't be longer than 253 characters" })
    @IsFQDN({}, {message: 'Must be a valid domain name'})
    @IsDefined({ message: 'Must specify a domain' })
    public domain!: string;
    @MaxLength(30, { message: "Can't be longer than 30 characters" })
    @IsString({message: 'Must be text format'})
    @IsDefined({ message: 'Must specify a company size' })
    public size!: string;
    @IsPhoneNumber(null, {message: 'Must be a valid phone number'})
    @IsDefined({ message: 'Must specify a phone number' })
    public contact!: string;
}
Route 2: Company Update
PUT - /api/company
    >> Parameters: id, name, domain, size, contact
class UpdateCompanyDto implements Dto {
    @IsUUID()
    @IsDefined({ message: 'Must be defined' })
    public id!: string;
    @IsString({ message: 'Must be text format' })
    @MinLength(2, { message: "Must have at least 2 characters" })
    @MaxLength(20, { message: "Can't be longer than 20 characters" })
    @IsOptional()
    public name!: string;
    @MaxLength(253, { message: "Can't be longer than 253 characters" })
    @IsFQDN({}, { message: 'Must be a valid domain name' })
    @IsOptional()
    public domain!: string;
    @MaxLength(30, { message: "Can't be longer than 30 characters" })
    @IsString({ message: 'Must be text format' })
    @IsOptional()
    public size!: string;
    @IsPhoneNumber(null, { message: 'Must be a valid phone number' })
    @IsOptional()
    public contact!: string;
}
Like you can see in the example, it's not uncommon that one validation class need to use properties from another validation class.
The problem is that if an engineer adds 1 validation rule to a property inside a random validation class, the other validation classes won't dynamically update.
Question: What is the best way to make sure that once a decorator gets changed/added other validation classes know about the update.
Is there some way to group them together into a variable/decorator? Any help from any Typescript guru is appreciated!
Acceptable outcome:
class CreateCompanyDto implements Dto {
    @IsCompanyName({required: true})
    public name!: string;
    @IsCompanyDomain({required: true})
    public domain!: string;
    @isCompanySize({required: true})
    public size!: string;
    @isCompanyContact({required: true})
    public contact!: string;
}
class UpdateCompanyDto implements Dto {
    @IsCompanyId({required: true})
    public id!: string;
    @IsCompanyName({required: false})
    public name!: string;
    @IsCompanyDomain({required: false})
    public domain!: string;
    @isCompanySize({required: false})
    public size!: string;
    @isCompanyContact({required: false})
    public contact!: string;
}
                Due to the function nature of decorators, you can easily define your own decorator factory to just call all the required validators:
export function IsCompanyName({ required }: { required: boolean }): PropertyDecorator {
  return function (target: any,
    propertyKey: string | symbol): void {
    IsString({ message: 'Must be text format' })(target, propertyKey);
    MinLength(2, { message: "Must have at least 2 characters" })(target, propertyKey);
    MaxLength(20, { message: "Can't be longer than 20 characters" })(target, propertyKey);
    if (required)
      IsDefined({ message: 'Must specify a receiver' })(target, propertyKey);
    else
      IsOptional()(target, propertyKey);
  }
}
Playground
export function ValidatorComposer(validators: PropertyDecorator[], name: string): (options: { required: boolean }) => PropertyDecorator {
  return function ({ required }: { required: boolean }) {
    return function (target: any,
      propertyKey: string | symbol): void {
      validators.forEach((validator) => validator(target, propertyKey));
      if (required)
        IsDefined({ message: 'Must specify a ' + name })(target, propertyKey);
      else
        IsOptional()(target, propertyKey);
    }
  }
}
Playground
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With