There is a nest.js project, where in the request body we expect an object, one property of this object contains stringified JSON value. The idea is to convert this string to an object, validate it and pass to controller as an object
ValidationPipe set up:
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
transform: true,
}),
);
DTO:
#Transform(parseJson, { toClassOnly: true })
#Type(() => AdditionalInfo)
#IsNotEmptyObject()
#ValidateNested()
additionalInfo: AdditionalInfo;
parseJson function
export function parseJson(options: {
key: string;
value: string;
obj: string | Record<string, any>;
}): Record<string, any> {
try {
return JSON.parse(options.value);
} catch (e) {
throw new BadRequestException(`${options.key} contains invalid JSON `);
}
}
For some reason in the controller the parsed value gets lost, and we receive an empty object.
Looks like #Transform works well with primitives only.
Decided to create ParseJsonPipe and use it instead.
Usage (in the controller):
#Body('additionalInfo', new ParseJsonPipe(), new ValidationPipe(AdditionalInfoDto)) additionalInfo: AdditionalInfo,
ParseJsonPipe:
import { ArgumentMetadata, BadRequestException, Injectable, PipeTransform } from '#nestjs/common';
#Injectable()
export class ParseJsonPipe implements PipeTransform<string, Record<string, any>> {
transform(value: string, metadata: ArgumentMetadata): Record<string, any> {
const propertyName = metadata.data;
try {
return JSON.parse(value);
} catch (e) {
throw new BadRequestException(`${propertyName} contains invalid JSON `);
}
}
}
ValidationPipe implements PipeTransform from #nestjs/common, transform function looks like that:
async transform(value: any): Promise<any> {
if (!this.metaType) { // AdditionalInfoDto
return value;
}
const object = plainToClass(this.metaType, value);
const errors = await validate(object);
if (errors.length > 0) {
const message = this.getErrorMessages(errors);
throw new BadRequestException({ message });
}
return value;
}
Related
I have a NestJS TS application that has an xml endpoint. I want to validate an xml body. Here's how I went with parsing xml to js object:
#Injectable()
export class XmlToJsInterceptor implements NestInterceptor {
constructor(private parser: CxmlParserService) {}
public intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
const req: XmlRequest<unknown> = context.switchToHttp().getRequest();
if (req.rawBody) {
req.xml = this.parser.convertXMLToObject(req.rawBody) as unknown;
}
return next.handle();
}
}
export const XmlBody = createParamDecorator((data: unknown, ctx: ExecutionContext): unknown => {
const request: XmlRequest<unknown> = ctx.switchToHttp().getRequest();
return request.xml;
});
And I use it like this:
#UseInterceptors(XmlToJsInterceptor)
#Controller('/endpoint')
export class MyController {
#Post('/')
#HttpCode(HttpStatus.OK)
async handleEndpoint(
#XmlBody() body: MyClassValidator,
): Promise<void> {
Now I want to use class-validator to check if xml request has a proper structure. I thought to extend XmlBody nestjs param decorator to include validation and manually call class-validator like this:
export const XmlBody = createParamDecorator((data: unknown, ctx: ExecutionContext): unknown => {
const request: XmlRequest<unknown> = ctx.switchToHttp().getRequest();
const validatedConfig = plainToClass(HOWDO_I_GET_PARAM_CLASS, request.xml, {
enableImplicitConversion: true,
});
const errors = validateSync(validatedConfig, { skipMissingProperties: false });
if (errors.length > 0) {
throw new Error(errors.toString());
}
return request.xml;
});
But I don't know to get typescript class from annotated parameter.
I have an issue with repetitive requests for checking an Order id, if it is valid ObjectId or not. I got this error:
CastError: Cast to ObjectId failed for value "629b9fbd620dbc419a52e8" (type string) at path "_id" for model "Order"
After a lot of Googling, I found two approaches to tackle the problem, however I'll have to duplicate these codes for each service, which isn't a good idea.
First approach:
if (!mongoose.Types.ObjectId.isValid(req.params.id)) {
throw new HttpException('Not a valid ObjectId!', HttpStatus.NOT_FOUND);
} else {
return id;
}
Second approach:
if (!mongoose.isValidObjectId(req.params.id)) {
throw new BadRequestException('Not a valid ObjectId');
} else {
return id;
}
I used below codes for making and using a middleware, thus I could check ID whenever a service using an id parameter.
validateMongoID.ts
import {
BadRequestException,
Injectable,
NestMiddleware,
} from '#nestjs/common';
import { Request, Response, NextFunction } from 'express';
import mongoose from 'mongoose';
#Injectable()
export class IsValidObjectId implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
// Validate Mongo ID
if (req.params.id) {
if (!mongoose.isValidObjectId(req.params.id)) {
throw new BadRequestException('Not a valid ObjectId');
}
}
next();
}
}
orders.module.ts
export class OrdersModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(IsValidObjectId).forRoutes('/');
}
}
After trying as a middleware in the orders.modules.ts, I got the same error mentioned above. So, any idea to use it as a middleware?
I had to do this exact thing a couple of weeks ago.
Here is my solution. Works perfectly fine. Not a middleware, though.
id-param.decorator.ts
import { ArgumentMetadata, BadRequestException, Param, PipeTransform } from '#nestjs/common';
import { Types } from 'mongoose';
class ValidateMongoIdPipe implements PipeTransform<string> {
transform(value: string, metadata: ArgumentMetadata) {
if (!Types.ObjectId.isValid(value)) {
throw new BadRequestException(`${metadata.data} must be a valid MongoDB ObjectId`);
}
return value;
}
}
export const IdParam = (param = '_id'): ParameterDecorator => (
Param(param, new ValidateMongoIdPipe())
);
Usage
// If param is called _id then the argument is optional
#Get('/:_id')
getObjectById(#IdParam() _id: string) {
return this.objectsService.getById(_id);
}
#Get('/:object_id/some-relation/:nested_id')
getNestedObjectById(
#IdParam('object_id') objectId: string,
#IdParam('nested_id') nestedId: string,
) {
return this.objectsService.getNestedById(objectId, nestedId);
}
How it works
When using the #Param decorator you can give it transform pipes that will validate and mutate incoming value.
#IdParam decorator is just a #Param with the ValidateMongoIdPipe provided as a second argument.
I have found another way to solve it with the help of Lhon (tagged in comments).
create a file (I named it globalErrorHandler.ts) as follows:
import {
ArgumentsHost,
ExceptionFilter,
HttpException,
HttpStatus,
InternalServerErrorException,
} from '#nestjs/common';
export class AllExceptionsFilter implements ExceptionFilter {
catch(exception: InternalServerErrorException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
const status =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
/**
* #description Exception json response
* #param message
*/
const responseMessage = (type, message) => {
response.status(status).json({
statusCode: status,
path: request.url,
errorType: type,
errorMessage: message,
});
};
// Throw an exceptions for either
// MongoError, ValidationError, TypeError, CastError and Error
if (exception.message) {
const newmsg: any = exception;
responseMessage(
'Error',
newmsg.response?.message ? newmsg.response.message : exception.message,
);
} else {
responseMessage(exception.name, exception.message);
}
}
}
add below line to main.ts
app.useGlobalFilters(new AllExceptionsFilter());
create another file (I named it validateMongoID.ts) as follows:
import {
BadRequestException,
Injectable,
NestMiddleware,
} from '#nestjs/common';
import { Request, Response, NextFunction } from 'express';
#Injectable()
export class IsValidObjectId implements NestMiddleware {
async use(req: Request, res: Response, next: NextFunction) {
// Validate Mongo ID
if (req.params.id) {
if (!/^[a-fA-F0-9]{24}$/.test(req.params.id)) {
throw new BadRequestException('Not a valid ObjectId');
}
}
next();
}
}
last step: import it as a middleware in app.module.ts
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(IsValidObjectId).forRoutes('*');
}
}
I would like to trim (empty white spaces at the beggining / end of a input field) of all body values. I don't want to have to loop all body elements
for each API request to clean the fields up.
I was wondering if I can overwrite the #body() annotation, and put the code in there, or if there's a input formatter or pipe that does that.
At the moment, I'm doing this:
createAccount(#Body() body: any) {
this.account.create(body.map(s => s.trim()))
}
Thanks
Thank you Uroš Anđelić for your advise. I created a PipeTransform to take care of this:
import { Injectable, PipeTransform,
ArgumentMetadata, BadRequestException } from '#nestjs/common'
#Injectable()
export class TrimPipe implements PipeTransform {
private isObj(obj: any): boolean {
return typeof obj === 'object' && obj !== null
}
private trim(values) {
Object.keys(values).forEach(key => {
if (key !== 'password') {
if (this.isObj(values[key])) {
values[key] = this.trim(values[key])
} else {
if (typeof values[key] === 'string') {
values[key] = values[key].trim()
}
}
}
})
return values
}
transform(values: any, metadata: ArgumentMetadata) {
const { type } = metadata
if (this.isObj(values) && type === 'body') {
return this.trim(values)
}
throw new BadRequestException('Validation failed')
}
}
And this is how to use it
#UsePipes(new TrimPipe())
createAccount(#Body() body: any) {
this.account.create(body)
}
You can also set it up as a global pipe:
app.useGlobalPipes(new TrimPipe());
I made an interceptor just for that. I use it globally but you can use it wherever you want with #UseInterceptors decorator. Here is the base class that can be extended for other body transformations also:
import { CallHandler, ExecutionContext, NestInterceptor } from '#nestjs/common'
import { Observable } from 'rxjs'
export abstract class TransformRequest implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
this.cleanRequest(context.switchToHttp().getRequest())
return next.handle()
}
cleanRequest(req: any): void {
req.query = this.cleanObject(req.query)
req.params = this.cleanObject(req.params)
// If you use express adapter you will have
// req.method
// If you use fastify adapter you will have
// req.raw.method
if (req.raw.method !== 'GET') {
req.body = this.cleanObject(req.body)
}
}
cleanObject(obj: object | null | undefined) {
if (!obj) {
return obj
}
for (const key in obj) {
// Prototype of obj is null
// if (!obj.hasOwnProperty(key)) {
// continue
// }
const value = obj[key]
// If the value is another nested object we need to recursively
// clean it too. This will work for both array and object.
if (value instanceof Object) {
this.cleanObject(value)
} else {
// If the value is not an object then it's a scalar
// so we just let it be transformed.
obj[key] = this.transform(key, value)
}
}
return obj
}
abstract transform(key: string | number, value: boolean | number | string | null | undefined): any
}
And here is the trim strings class:
import { Injectable } from '#nestjs/common'
import { TransformRequest } from './transform.request'
#Injectable()
export class TrimStrings extends TransformRequest {
private except = ['password']
transform(key: string | number, value: any) {
if (this.isString(value) && this.isString(key) && !this.except.includes(key)) {
return value.trim()
}
return value
}
isString(value: any): value is string {
return typeof value === 'string' || value instanceof String
}
}
You can also find it in this repository.
I actually think it's a better idea to trim the body in the front-end side if that is an option.
You can extend NestMiddleware class for that also:
import { NestMiddleware } from '#nestjs/common';
import { Request, Response, NextFunction } from 'express';
export class TrimMiddleware implements NestMiddleware {
use(req: Request, _res: Response, next: NextFunction) {
const requestBody = req.body;
if (this.isObj(requestBody)) {
req.body = this.trim(requestBody);
}
next();
}
private isObj(obj: any): boolean {
return typeof obj === 'object' && obj !== null;
}
private trim(value: unknown) {
if (typeof value === 'string') {
return value.trim();
}
if (Array.isArray(value)) {
value.forEach((element, index) => {
value[index] = this.trim(element);
});
return value;
}
if (this.isObj(value)) {
Object.keys(value).forEach(key => {
value[key] = this.trim(value[key]);
});
return value;
}
return value;
}
}
And then in NestModule you can use apply and forRoutes methods to decide on which endpoint apply middleware.
export class MyDto extends Base{
#ApiModelProperty()
#Expose()
#MyCustomModifier()
readonly code: string = "";
MyCustomModifier(){
// modify
code = someUpdateOnCode()
}
Can we do something like this, so we can update dto properties
#Injectable()
export class JoiValidationPipe implements PipeTransform {
constructor(private readonly schema) {}
transform(value: any, metadata: ArgumentMetadata) {
const { error } = this.schema.validate(value);
if (error) {
console.log(error, 'error');
throw new BadRequestException(error.message);
}
// some changing value.code = someUpdateOnCode()
return value;
}
}
and use your pipe like this
import * as Joi from '#hapi/joi';
#Put('')
#UsePipes(
new JoiValidationPipe(
Joi.object().keys({
code: Joi.string()
.min(3)
.max(250)
.allow('')
.optional()
)
})
async someControler(){}
I want to validate body payload using class-validator in a nest.js controller. My currency.dto.ts file is like this:
import {
IsNotEmpty,
IsString,
ValidateNested,
IsNumber,
IsDefined,
} from 'class-validator';
class Data {
#IsNotEmpty()
#IsString()
type: string;
#IsNotEmpty()
#IsNumber()
id: number;
}
export class CurrencyDTO {
#ValidateNested({ each: true })
#IsDefined()
data: Data[];
}
and in my nest.js controller, I use it like this.
#Post()
#UseGuards(new AuthTokenGuard())
#UsePipes(new ValidationPipe())
addNewCurrency(#Req() req, #Body() data: CurrencyDTO) {
console.log('data', data);
}
my validation pipe class is like this:
import {
PipeTransform,
Injectable,
ArgumentMetadata,
BadRequestException,
HttpException,
HttpStatus,
} from '#nestjs/common';
import { validate, IsInstance } from 'class-validator';
import { plainToClass, Exclude } from 'class-transformer';
#Injectable()
export class ValidationPipe implements PipeTransform<any> {
async transform(value: any, metadata: ArgumentMetadata) {
if (value instanceof Object && this.isEmpty(value)) {
throw new HttpException(
`Validation failed: No Body provided`,
HttpStatus.BAD_REQUEST,
);
}
const { metatype } = metadata;
if (!metatype || !this.toValidate(metatype)) {
return value;
}
const object = plainToClass(metatype, value);
const errorsList = await validate(object);
if (errorsList.length > 0) {
const errors = [];
for (const error of errorsList) {
const errorsObject = error.constraints;
const { isNotEmpty } = errorsObject;
if (isNotEmpty) {
const parameter = isNotEmpty.split(' ')[0];
errors.push({
title: `The ${parameter} parameter is required.`,
parameter: `${parameter}`,
});
}
}
if (errors.length > 0) {
throw new HttpException({ errors }, HttpStatus.BAD_REQUEST);
}
}
return value;
}
private toValidate(metatype): boolean {
const types = [String, Boolean, Number, Array, Object];
return !types.find(type => metatype === type);
}
private isEmpty(value: any) {
if (Object.keys(value).length > 0) {
return false;
}
return true;
}
}
This validation pipe works fine for all except for nested objects. Any idea what am I doing wrong here?
My body payload is like this:
{
"data": [{
"id": 1,
"type": "a"
}]
}
Try specifying the nested type with #Type:
import { Type } from 'class-transformer';
export class CurrencyDTO {
#ValidateNested({ each: true })
#Type(() => Data)
data: Data[];
}
For a nested type to be validated, it needs to be an instance of a class not just a plain data object. With the #Type decorator you tell class-transformer to instantiate a class for the given property when plainToClass is called in your VaildationPipe.
If you are using the built-in ValidationPipe make sure you have set the option transform: true.
At least in my case, the accepted answer needed some more info. As is, the validation will not run if the key data does not exist on the request. To get full validation try:
#IsDefined()
#IsNotEmptyObject()
#ValidateNested()
#Type(() => CreateOrganizationDto)
#ApiProperty()
organization: CreateOrganizationDto;