Reputation: 1843
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;
}
Upvotes: 7
Views: 1846
Reputation: 4472
You can chain the decorators by defining your own decorators. Note that I didn't use the class-validator
library before, so you need to test this. But this is how it can look like:
Here I defined a Decorator called IsCompanyName
and had it call the validation decorators in your example. I defined ValidationOptions
interface to pass to the decorator. It is up to you if you make this opts
parameter required or not. Also it is up to you to define the default behavior if the option is not specified. In my example I make it optional and made it behave that it is required by default. I call IsOptional
decorator only if opts
isdefined and required === false
. Otherwise I don't call it.
interface ValidationOptions {
required?: boolean
}
function IsCompanyName(opts?: ValidationOptions) {
return function (target: any, propertyKey: string) {
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);
IsDefined({ message: 'Must specify a receiver' })(target, propertyKey);
if (opts && opts.required === false) {
IsOptional()(target, propertyKey);
}
}
}
Upvotes: 3
Reputation: 2812
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);
}
}
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);
}
}
}
Upvotes: 8