Reputation: 593
I have a query parameter in my REST API, which values should be restricted by an enum type. I'm looking for a way to throw a "Bad Request" error, when a client gives something different.
My enum looks like this:
export enum Precision {
S = 's',
MS = 'ms',
U = 'u',
NS = 'ns',
}
My controller function looks like this:
@Get(':deviceId/:datapoint/last')
@ApiOkResponse()
@ApiQuery({name: 'precision', enum: Precision})
getLastMeasurement(
@Param('deviceId') deviceId: string,
@Param('datapoint') datapoint: string,
@Query('precision') precision: Precision = Precision.S,
@Res() response: Response,
) {
console.log(precision);
....
response.status(HttpStatus.OK).send(body);
}
My problem here is that the function accepts other values, too (for example I can send an f as the query parameter's value). The Function won't return an error to the client, but I want to without writing an if else block at the beginning of each controller function.
I guess there is a rather simple solution to this, but when I try to look it up on the internet I always get results for class validation in DTOs, but not for a simple enum validation directly in the query param/REST controller.
Thanks for your time,
J
Upvotes: 9
Views: 10970
Reputation: 1783
Building on everyone's answers, here's a pipe that will
import { BadRequestException, Injectable, PipeTransform } from '@nestjs/common';
import { isDefined, isEnum } from 'class-validator';
type EnumKey = string | number | symbol;
/**
* A pipe that validates an enum value and sets a default value if none is provided.
*/
@Injectable()
export class EnumValidationPipe<T extends Record<EnumKey, unknown>> implements PipeTransform<unknown, T[keyof T]> {
constructor(
private enumEntity: T,
private defaultValue?: T[keyof T],
) {}
transform(value: unknown): T[keyof T] {
if (!isDefined(value) && isDefined(this.defaultValue)) {
return this.defaultValue;
}
if (isDefined(value) && isEnum(value, this.enumEntity)) {
return value as T[keyof T];
} else {
const errorMessage = `The value ${value} is not valid. See the acceptable values: ${Object.values(this.enumEntity).join(', ')}`;
throw new BadRequestException(errorMessage);
}
}
}
And you can use it like this
getLastMeasurement(
... // some other params
@Query('precision', new EnumValidationPipe(Precision, Precision.S)) precision: Precision
) {
... // do something
}
Upvotes: 0
Reputation: 1
Just improved a bug in Raphael's answer, it was returning the key of the enum instead of the value.
Also, used the metadata param to outpput on the error message the name of the field being validated and also changed
${Object.keys(this.enumEntity).map(key => this.enumEntity[key])}
to
${Object.values(this.enumEntity)}
There is the final code of the validator:
@Injectable()
export class EnumValidationPipe implements PipeTransform<string, Promise<any>> {
constructor(private enumEntity: any) {}
transform(value: string, metadata: ArgumentMetadata): Promise<any> {
if (isDefined(value) && isEnum(value, this.enumEntity)) {
return Promise.resolve(value);
} else {
const errorMessage = `the value ${value} from field ${
metadata.data
} is not valid. Acceptable values: ${Object.values(this.enumEntity)}`;
throw new BadRequestException(errorMessage);
}
}
}
Upvotes: 0
Reputation: 645
There is 2 issues here.
The first one is that you are passing the default value of precision
params in the wrong way. You must use the DefaultValuePipe
like this:
getLastMeasurement(
... // some other params
@Query('precision', new DefaultValuePipe(Precision.S)) precision: Precision
) {
... // do something
}
The second one is the enum validation. NestJS comes with only 6 types of validationPipes and none of them validates an enum, so you must create your own custom validation pipe to validate enums.
There is 2 possible ways that you can do that:
Based on this https://docs.nestjs.com/pipes#custom-pipes, it would be something like:
import { BadRequestException, PipeTransform } from '@nestjs/common';
import { isDefined, isEnum } from 'class-validator';
export class PrecisionValidationPipe implements PipeTransform<string, Promise<Precision>> {
transform(value: string): Promise<Precision> {
if (isDefined(value) && isEnum(value, Precision)) {
return Precision[value];
} else {
const errorMessage = `the value ${value} is not valid. See the acceptable values: ${Object.keys(
Precision
).map(key => Precision[key])}`;
throw new BadRequestException(errorMessage);
}
}
}
and then in your request, it would be like
getLastMeasurement(
@Query('precision', PrecisionValidationPipe, new DefaultValuePipe(Precision.S)) precision: Precision,
) {
console.log(precision);
....
response.status(HttpStatus.OK).send(body);
}
import { BadRequestException, Injectable, PipeTransform } from '@nestjs/common';
import { isDefined, isEnum } from 'class-validator';
@Injectable()
export class EnumValidationPipe implements PipeTransform<string, Promise<any>> {
constructor(private enumEntity: any) {}
transform(value: string): Promise<any> {
if (isDefined(value) && isEnum(value, this.enumEntity)) {
return this.enumEntity[value];
} else {
const errorMessage = `the value ${value} is not valid. See the acceptable values: ${Object.keys(this.enumEntity).map(key => this.enumEntity[key])}`;
throw new BadRequestException(errorMessage);
}
}
}
and then in your request, it would be like
getLastMeasurement(
@Query('precision', new EnumValidationPipe(Precision), new DefaultValuePipe(Precision.S)) precision: Precision,
) {
console.log(precision);
....
response.status(HttpStatus.OK).send(body);
}
Upvotes: 16
Reputation: 70131
You should be able to create a class like LastMeasurementQueryParams
that makes use of class-validator decorators, and use the built-in ValidationPipe to check and make sure that one of the expected values are sent in.
The class could look something like this:
export class LastMeasurementQueryParams {
@IsEnum(Precision)
precision: Precision;
}
And then your controller can look like this:
@Get(':deviceId/:datapoint/last')
@ApiOkResponse()
@ApiQuery({name: 'precision', enum: Precision})
getLastMeasurement(
@Param('deviceId') deviceId: string,
@Param('datapoint') datapoint: string,
@Query('precision') precision: LastMeasurementQueryParams = { precision: Precision.S },
@Res() response: Response,
) {
console.log(precision);
....
response.status(HttpStatus.OK).send(body);
}
Upvotes: 8