Modify the request body of a multipart/form-data, then apply the validationPipe

I am attempting to convert a formData request from string to a JSON object using transformation and then validate it with the validationPipe (class-validator). However, I encountered an issue:

Maximum call stack size exceeded
    at cloneObject (E:\projectos\Gitlab\latineo\latineo-apirest\node_modules\mongoose\lib\utils.js:290:21)
    at clone (E:\projectos\Gitlab\latineo\latineo-apirest\node_modules\mongoose\lib\utils.js:204:16)

Upon debugging, I noticed that my controller is being entered 3 times and the object is saved in the database without validation. The transformJSONToObject function is called 9 times...

This is my main.ts file:

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe({ transform: true }));
  app.use(helmet());
  app.enableCors();
  app.use(
    rateLimit({
      windowMs: 15 * 60 * 1000, // 15 minutes
      max: 4000, // limit each IP to 100 requests per windowMs
    }),
  );
  app.use(compression());
  app.use('/upload', express.static(join(__dirname, '..', 'upload')));
  const options = new DocumentBuilder()
    .setTitle('XXX')
    .setDescription('XXX')
    .setVersion('1.0')
    .addBearerAuth()
    .build();
  const document = SwaggerModule.createDocument(app, options);
  SwaggerModule.setup('api', app, document);
  await app.listen(3000);
}
bootstrap();

This is my nestjs DTO:

export class CreateRestaurantDto {
  @IsString()
  @IsNotEmpty()
  @ApiModelProperty({ type: String })
  @Length(3, 100)
  readonly name: string;
  @IsString()
  @IsNotEmpty()
  @ApiModelProperty({ type: String })
  @Length(3, 500)
  readonly description: string;
  @Transform(transformJSONToObject, { toClassOnly: true })
  @ValidateNested()
  @ApiModelProperty({ type: [RestaurantsMenu] })
  readonly menu: RestaurantsMenu[];
  @Transform(transformJSONToObject, { toClassOnly: true })
  @IsString({
    each: true,
  })
  @IsNotEmpty({
    each: true,
  })
  @Length(3, 50, { each: true })
  @ApiModelProperty({ type: [String] })
  readonly type: string[];
  @Transform(transformJSONToObject, { toClassOnly: true })
  @ValidateNested()
  @ApiModelProperty({ type: [RestaurantsLocation] })
  readonly location: RestaurantsLocation[];
}

This is my Controller:

 @ApiBearerAuth()
  @UseGuards(JwtAuthGuard)
  @UseInterceptors(FilesInterceptor('imageUrls'))
  @ApiConsumes('multipart/form-data')
  @ApiImplicitFile({
    name: 'imageUrls',
    required: true,
    description: 'List of restaurants',
  })
  @Post()
  async createRestaurant(
    @Body() createRestaurantDto: CreateRestaurantDto,
    @UploadedFiles() imageUrls,
    @Req() request: any,
  ): Promise<RestaurantDocument> {
    const userId = request.payload.userId;
    const user = await this.usersService.findUserById(userId);
    const mapUrls = imageUrls.map(element => {
      return element.path;
    });
    const restaurant = {
      ...createRestaurantDto,
      imagesUrls: mapUrls,
      creator: user,
    };
    const createdRestaurant = await this.restaurantsService.addRestaurant(
      restaurant,
    );
    user.restaurants.push(createdRestaurant);
    user.save();
    return createdRestaurant;
  }

Answer №1

Although it may seem like it's too late now, but I believe this information could still be beneficial to someone out there.

By utilizing the deep-parse-json library, my approach involves creating a custom ParseFormDataJsonPipe and incorporating it just before the ValidationPipe. This pipe allows for passing a specified list of form-data properties in the constructor to maintain certain elements such as images, binaries, JSON arrays, etc.

import { PipeTransform, ArgumentMetadata } from '@nestjs/common';
import { deepParseJson } from 'deep-parse-json';
import * as _ from 'lodash';

type TParseFormDataJsonOptions = {
  except?: string[];
};

export class ParseFormDataJsonPipe implements PipeTransform {
  constructor(private options?: TParseFormDataJsonOptions) {}

  transform(value: any, _metadata: ArgumentMetadata) {
    const { except } = this.options;
    const serializedValue = value;
    const originProperties = {};
    if (except?.length) {
      _.merge(originProperties, _.pick(serializedValue, ...except));
    }
    const deserializedValue = deepParseJson(value);
    console.log(`deserializedValue`, deserializedValue);
    return { ...deserializedValue, ...originProperties };
  }
}

Below is an illustration of how this can be utilized within a controller:

@Post()
@ApiBearerAuth('admin')
@ApiConsumes('multipart/form-data')
@UseGuards(JwtAuthGuard, RoleGuard)
@Roles(Role.Admin)
@UseInterceptors(
FileInterceptor('image', {
limits: { fileSize: imgurConfig.fileSizeLimit },
}),
)
async createProductByAdmin(
@Body(
new ParseFormDataJsonPipe({ except: ['image', 'categoryIds'] }),
new ValidationPipe(),
)
createDto: CreateProductDto,
@UploadedFile() image: Express.Multer.File,
) {
console.log(`createDto`, createDto);
}

Answer №2

If you're looking to streamline your DTOs, consider leveraging the capabilities of the class-transformer library.

import { Transform, Type } from 'class-transformer';
  1. When dealing with objects:

  @Transform(({ value }) => JSON.parse(value))
  name: { small:string, middle:string, big:string };
    
  1. For handling numbers:

  @Type(() => Number)
  count number;

Answer №3

After facing a similar challenge for some time, I recently stumbled upon a solution that struck me as both simple and effective. Despite the age of this post, I wanted to share it in case it could assist someone else.

The approach involves creating an interceptor to extract a specific field from multipart formdata containing JSON, parsing it, and then assigning it to the request body. Assuming this field is named body, the interceptor would be structured like this:

@Injectable()
export class BodyInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest<Request>();

    try {
      const body = JSON.parse(request.body.body);
      request.body = body;
    } catch (err) {
      throw new BadRequestException(err.message);
    }

    return next.handle();
  }
}

I've enclosed the JSON.parse() code within a try/catch block to handle cases where invalid JSON syntax is passed, triggering an error akin to Nest's default response for such scenarios.

To implement this, you would simply call the interceptor after FileInterceptor:

@UseInterceptors(FilesInterceptor('files'), BodyInterceptor)

Subsequently, you can structure your body based on your DTO by utilizing @Body(). From my testing, this method appears to be functioning correctly, but please don't hesitate to notify me if otherwise. Wishing you success with implementing this solution.

Answer №4

If someone needs assistance, I recently encountered a similar issue involving sending a form with a file along with other data types such as dates and numbers.

Here are two ways to effectively handle validation:

  • 1. Ensure you explicitly cast to string before setting the value in formData.

    for (let i in value) {
      formData.append(i, value[i] === null ? "" : value[i]);
    }
    
  • 2. Use this transformation function:

    const transformDateFn = (params: TransformFnParams) =>
      params.obj[params.key] === "" || params.obj[params.key] === "null"
        ? null
        : params.value;
    

In addition:

    @Transform(transformDateFn)
    @IsOptional()
    @Type(() => Date)
    @IsDate()
    myFieldName: Date;

To break it down:

1. @Transform will alter the value

2. @IsOptional() will skip validation for null properties (remember to apply the ignore option in pipes first!)

 app.useGlobalPipes(
    new ValidationPipe({
      enableDebugMessages: true,
      skipNullProperties: true,
      transform: true,
      validationError: {
        value: true,
        target: true,
      },
   }),
 );

3. @Type will enforce correct type casting if the value is not null.

4. @IsDate allows validation to proceed.

That's all. Feel free to ask questions if needed.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

What causes the typings for Partial Record to be undefined when using Object.values?

When I retrieve Object.values from a Partial Record, the values consist of a combination of what I anticipated and undefined. const example: Partial<Record<string, number>> = {} const values = Object.values(example) // The type for values is u ...

Combine values from 2 distinct arrays nested within an array of objects and present them as a unified array

In my current array, the structure looks like this: const books[{ name: 'book1', id: '1', fromStatus: [{ name: 'Available', id: 1 }, { name: 'Free', id: 2 }], t ...

How can I achieve the same functionality as C# LINQ's GroupBy in Typescript?

Currently, I am working with Angular using Typescript. My situation involves having an array of objects with multiple properties which have been grouped in the server-side code and a duplicate property has been set. The challenge arises when the user updat ...

Different methods to prompt TypeScript to deduce the type

Consider the following code snippet: function Foo(num: number) { switch (num) { case 0: return { type: "Quz", str: 'string', } as const; case 1: return { type: "Bar", 1: 'value' } as const; default: thr ...

Only object types can be used to create rest types. Error: ts(2700)

As I work on developing a custom input component for my project, I am encountering an issue unlike the smooth creation of the custom button component: Button Component (smooth operation) export type ButtonProps = { color: 'default' | 'pr ...

What are the steps to enable full functionality of the strict option in TypeScript?

Despite enforcing strict options, TypeScript is not flagging the absence of defined types for port, req, and res in this code snippet. I am using Vscode and wondering how to fully enforce type checking. import express from 'express'; const app ...

Display a complete inventory of installed typings using the tsd command

How can I display a list of all installed tsd typings in the terminal? Perhaps using the following command: $ tsd list ...

Unable to associate with 'paint' as it is not a recognized attribute of 'mgl-layer' while using mapbox in Angular 9

I am currently working on an Angular 9 project with the latest version of mapbox integrated. My goal is to toggle between displaying contours and museums on the map. To achieve this, I have installed the package: "@types/mapbox-gl": "^1.12. ...

Converting an integer into a String Enum in TypeScript can result in an undefined value being returned

Issue with Mapping Integer to Enum in TypeScript export enum MyEnum { Unknown = 'Unknown', SomeValue = 'SomeValue', SomeOtherValue = 'SomeOtherValue', } Recently, I encountered a problem with mapping integer val ...

Tips for refreshing the apollo cache

I have been pondering why updating data within the Apollo Client cache seems more challenging compared to similar libraries such as react-query. For instance, when dealing with a query involving pagination (offset and limit) and receiving an array of item ...

What is the most efficient way to check for the presence and truthiness of a nested boolean in Node.js?

There are instances where I must verify the deeply nested boolean value of an object to determine its existence and whether it is set to true or false. For instance, I need to ascertain if payload.options.save is assigned a value of false, yet I am uncert ...

Contrast between employing typeof for a type parameter in a generic function and not using it

Can you explain the difference between using InstanceType<typeof UserManager> and InstanceType<UserManager> I'm trying to understand TypeScript better. I noticed in TS' typeof documentation that SomeGeneric<typeof UserManager> ...

Is there a way to change a typescript enum value into a string format?

I'm working with enums in TypeScript and I need to convert the enum value to a string for passing it to an API endpoint. Can someone please guide me on how to achieve this? Thanks. enum RecordStatus { CancelledClosed = 102830004, Completed = ...

Angular 12: An issue has occurred due to a TypeError where properties of undefined cannot be read, specifically pertaining to the element's 'nativeElement

Within my phone number input field, I have integrated a prefixes dropdown. Below is the code snippet for this feature. HTML code in modal <div class="phone" [ngClass]="{ 'error_border': submitted && f.phoneNumber.er ...

When it comes to Typescript interfaces, subsequent fields are not overloaded or merged

So I've been exploring interfaces in TypeScript and their ability to merge different types, but I ran into a hiccup while transpiling my script. Here's where things went wrong in my interface: export interface StoreConfig extends Document, TimeS ...

Effective Ways to Transfer Data from Angular to CSS in an HTML Table

I am working on an Angular web app that includes an HTML table. The data for this table is retrieved from a database via a service and displayed as text. One of the columns in the table represents a Status, which I want to visually represent as a colored ...

How to format a Date object as yyyy-MM-dd when serializing to JSON in Angular?

I have a situation where I need to display an object's 'birthDate' property in the format DD/MM/YYYY on screen. The data is stored in the database as YYYY-MM-DD, which is a string type. I am currently using bootstrap bsDatePicker for this ta ...

Tips on utilizing storage.set() within google.maps.events.addListener(marker, 'dragend', function() { }); in Ionic 3

google.maps.event.addListener(Marker, 'click', (function(Marker) { return function() { this.storage.set('mylocation', this.Marker.getPosition()); } })(Marker)); polyfills.js:3 Uncaught TypeError: Cannot read property 'set ...

Mastering the art of theming components using styled-components and Material-UI

Can you integrate the Material-UI "theme"-prop with styled-components using TypeScript? Here is an example of Material-UI code: const useStyles = makeStyles((theme: Theme) => ({ root: { background: theme.palette.primary.main, }, })); I attemp ...

Using createStyles in TypeScript to align content with justifyContent

Within my toolbar, I have two icons positioned on the left end. At the moment, I am applying this specific styling approach: const useStyles = makeStyles((theme: Theme) => createStyles({ root: { display: 'flex', }, appBar: ...