简体   繁体   中英

How to properly set up serialization with NestJS?

I started to work in a new NestJs project but I'm facing an issue when I try to implement serialization. I want to implement serialization to transform objects before they gets sent in a network response. My project was working correctly but when I tried to implement ClassSerializerInterceptor in my controller I got the following error:

 [Nest] 27010 - 12/23/2019, 8:20:53 PM [ExceptionsHandler] Maximum call stack size exceeded +29851ms RangeError: Maximum call stack size exceeded at Object.Console.<computed> (internal/console/constructor.js:241:9) at Object.log (internal/console/constructor.js:282:26) at Object.consoleCall (<anonymous>) at _loop_1 (/path/to/my/project/node_modules/class-transformer/TransformOperationExecutor.js:146:47)

I changed the scope of ClassSerializerInterceptor to solve the problem but the error persists. According to the documentation , I need to use the interceptor in a controller and use the corresponding decorators in an entity to implement serialization. My implementation of serialization is the following:

billing-statement.controller.ts

 import { ClassSerializerInterceptor, Controller, Get, Query, UseInterceptors } from '@nestjs/common'; import { BillingStatementService } from './billing-statement.service'; import { BillingStatementDto } from './billing-statement.dto'; import { BillingStatement } from './billing-statement.entity'; @Controller('billing-statement') export class BillingStatementController { constructor(private readonly billingStatementService: BillingStatementService) {} @Get() @UseInterceptors(ClassSerializerInterceptor) async getBillingStatement( @Query() query: BillingStatementDto, ): Promise<BillingStatement> { return this.billingStatementService.findBillingStatementByUser(+query.id); } }

billing-statement.entity.ts

 import { AutoIncrement, BelongsTo, Column, ForeignKey, HasMany, Model, PrimaryKey, Table } from 'sequelize-typescript'; import { User } from '../users/user.entity'; import { Payment } from './payment.entity'; import { Exclude } from 'class-transformer'; @Table({ tableName: 'billing_statement_tbl', timestamps: false, }) export class BillingStatement extends Model<BillingStatement> { @AutoIncrement @PrimaryKey @Column({field: 'billing_statement_id_pk'}) id: number; @Column currency: string; @Column({field: 'total_amount'}) totalAmount: number; @Exclude() @Column({field: 'contract_start'}) contractStart: Date; @Exclude() @Column({field: 'contract_end'}) contractEnd: Date; @HasMany(() => Payment) payments: Payment[]; }

I don't know what I'm doing wrong or what is the source of the error.

From what I've seen so far, two things came to my mind.

  1. Extend use of class-transformer and make use of class-validator within entity class in order to exclude the whole class' properties and only expose the wanted ones in your resulting serialized object.

Code would like this:

billing-statement.entity.ts

import { AutoIncrement, BelongsTo, Column, ForeignKey, HasMany, Model, PrimaryKey, Table } from 'sequelize-typescript';
import { User } from '../users/user.entity';
import { Payment } from './payment.entity';
import { Exclude, Expose, Type } from 'class-transformer';
import { IsArray, IsNumber, IsString } from 'class-validator';

@Exclude()
@Table({
  tableName: 'billing_statement_tbl',
  timestamps: false,
})
export class BillingStatement extends Model<BillingStatement> {
  @AutoIncrement
  @PrimaryKey
  @Column({field: 'billing_statement_id_pk'})
  @Expose()
  @IsNumber()
  id: number;

  @Column
  @Expose()
  @IsString()
  currency: string;

  @Column({field: 'total_amount'})
  @Expose()
  @IsNumber()
  totalAmount: number;

  @Column({field: 'contract_start'})
  contractStart: Date;

  @Column({field: 'contract_end'})
  contractEnd: Date;

  @HasMany(() => Payment)
  @IsArray()
  @Expose()
  @Type(() => Payment)
  payments: Payment[];
}
  1. Another way would be to split your entity definition from the returned dto definition, in that way you can extend the definition of your entity, plus or minus wanted and unwanted properties in the returned dto. Eg, let's say you have named your response dto BillingStatementResponseDto , you would use this one in your controller response type. BillingStatementResponseDto could contain external api object's attributes (fetch from some external api for exampleà, some of your entity attributes and some of the incoming request dto's properties as well. You would also extend use of class-transformer and make use of class-validator like in 1st advice above in the BillingStatementResponseDto definition.

Code would look like this:

billing-statement.entity.ts (remains the same, without class-transformer stuff)

import { AutoIncrement, BelongsTo, Column, ForeignKey, HasMany, Model, PrimaryKey, Table } from 'sequelize-typescript';
import { User } from '../users/user.entity';
import { Payment } from './payment.entity';

@Table({
  tableName: 'billing_statement_tbl',
  timestamps: false,
})
export class BillingStatement extends Model<BillingStatement> {
  @AutoIncrement
  @PrimaryKey
  @Column({field: 'billing_statement_id_pk'})
  id: number;

  @Column
  currency: string;

  @Column({field: 'total_amount'})
  totalAmount: number;

  @Column({field: 'contract_start'})
  contractStart: Date;

  @Column({field: 'contract_end'})
  contractEnd: Date;

  @HasMany(() => Payment)
  payments: Payment[];
}

billing-statement-response.dto.ts (new file definition for your targeted returned object, with use of class-transformer and class-validator ) - to be imported and used in your controller

import { Exclude, Expose, Type } from 'class-transformer';
import { IsArray, IsNumber, IsString, ValidateNested } from 'class-validator';

@Exclude()
export class BillingStatementResponseDto {
  @Expose()
  @IsNumber()
  id: number;

  @Expose()
  @IsString()
  currency: string;

  @Expose()
  @IsNumber()
  totalAmount: number;

  @IsArray()
  @ValidateNested()
  @Expose()
  @Type(() => Payment)
  payments: Payment[];
}

billing-statement.controller.ts

import { ClassSerializerInterceptor, Controller, Get, Query, UseInterceptors } from '@nestjs/common';
import { BillingStatementService } from './billing-statement.service';
import { BillingStatementDto } from './billing-statement.dto';
import { BillingStatementResponseDto } from './billing-statement-response.dto'; // <= import your newly defined dto 

@Controller('billing-statement')
export class BillingStatementController {
  constructor(private readonly billingStatementService: BillingStatementService) {}

  @Get()
  @UseInterceptors(ClassSerializerInterceptor)
  async getBillingStatement(
    @Query() query: BillingStatementDto,
  ): Promise<BillingStatementResponseDto> { // <= here you go for the use of BillingStatementResponseDto
    return this.billingStatementService.findBillingStatementByUser(+query.id);
  }
}

IMHO, second solution would be better in terms of layers separation, flexibility, modularity and maintainability :)

Let me know if it helps ;)

Based on the error message, I think you have a circular reference issue. Just comment out the other objects that your billing_statement object is referring to , and then try again. If that is the reason you are getting this error, you should remove reference from child objects to the parent object or try not to serialize those references.

Best of luck.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM