There are several DTO that need to be used dynamically based on the http parameter that comes in the request. For example POST /api/tasks?dto_name=dto_1, you need to substitute the required DTO (dto_1) into the controller, taking into account its validation. Is there a way to do this in Nest.js?
I tried to implement via generics, I can't pass the parameter from http.
It's possible, by implementing the validation pipe yourself.
This is a simple pipe that does what you want:
class Dto1 {
@IsNumber()
n: number;
}
class Dto2 {
@IsBoolean()
b: boolean;
}
const DTO_DICT = {
dto_1: Dto1,
dto_2: Dto2,
};
/* A type alias to dynamically get a union of all classes from the dictionary */
export type DtosUnion = InstanceType<ValuesOf<typeof DTO_DICT>>;
const mappedPipe = async (
payload: Record<string, any>,
dtoKey: keyof typeof DTO_DICT,
validationPipeOptions: ValidationPipeOptions = { transformOptions: undefined }, // You might want to pass global pipe options or override transform behavior
): Promise<DtosUnion> => {
/* Validate payload, make sure it exists in your DTO dictionary */
if (!payload) throw new BadRequestException(`Payload is not an object`);
const constructor = DTO_DICT[dtoKey];
if (!constructor) throw new BadRequestException(`Dto doesn't exists: ${dtoKey}`);
/* Use the constructor that you found to instantiate the relevant Dto */
const transformed = plainToInstance<any, any>(
constructor,
payload,
validationPipeOptions.transformOptions,
);
/* Run the validations */
const validation = await validate(transformed);
if (!validation.length) return transformed;
/* This is what Nest does out of the box behind the scenes when validation fails */
const validationPipe = new ValidationPipe(validationPipeOptions);
const exceptionFactory = validationPipe.createExceptionFactory();
const exception = exceptionFactory(validation);
throw exception;
};
This is how it's used:
@Post()
async create(@Body() dto: DtosUnion, @Query('dto_name') dtoName: string): Promise<any> {
const validatedDto = mappedPipe(dto, dtoName);
/* Do what you need with the `validatedDto` */
/* ... */
}
validatedDto
is still of type DtosUnion
, since after the pipe it can be any of the DTOs(or it can fail validation and throw). If you'll print validatedDto
in runtime - you'll get the specific DTO you expect.
Do note that if the provided DTO name matches a DTO that has loose validation, it might pass validation for payloads that shouldn't have been passing.