Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make a group of decorators easily reusable in Typescript

TL;DR

How to group decorators (from a library) together into 1 re-usable decorator

The Problem

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.

Example

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;
}

What I'm searching for

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;
}
like image 668
Michiel Avatar asked Oct 16 '22 00:10

Michiel


1 Answers

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

A small decorator factory factory

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

like image 197
Elias Schablowski Avatar answered Oct 19 '22 02:10

Elias Schablowski