Как правильно настроить сериализацию с помощью NestJS?

Я начал работать в новом проекте NestJs, но сталкиваюсь с проблемой, когда пытаюсь реализовать сериализацию. Я хочу реализовать сериализацию для преобразования объектов до того, как они будут отправлены в сетевом ответе. Мой проект работал правильно, но когда я попытался реализовать ClassSerializerInterceptor в своем контроллере, я получил следующую ошибку:

[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)

Я изменил область действия ClassSerializerInterceptor, чтобы решить проблему, но ошибка не исчезла. Согласно документации, мне нужно использовать перехватчик в контроллере и использовать соответствующие декораторы в сущности для реализации сериализации. Моя реализация сериализации следующая:

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[];
}

Я не знаю, что делаю не так и в чем причина ошибки.

2 ответа

Из того, что я видел до сих пор, мне на ум пришли две вещи.

  1. Расширить использование class-transformer и использовать class-validator внутри класса сущности, чтобы исключить свойства всего класса и раскрыть только нужные в полученном сериализованном объекте.

Код хотел бы этого:

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. Другой способ - отделить определение вашей сущности от возвращенного определения dto, таким образом вы можете расширить определение своей сущности, плюс или минус желаемые и нежелательные свойства в возвращенном dto. Например, допустим, вы назвали свой ответ dtoBillingStatementResponseDto, вы должны использовать его в типе ответа вашего контроллера. BillingStatementResponseDto может содержать внешние атрибуты объекта api (например, выборка из некоторого внешнего API, некоторые из ваших атрибутов сущности, а также некоторые свойства входящего запроса dto. Вы также можете расширить использование class-transformer и использовать class-validator как в 1-м совете выше в BillingStatementResponseDto определение.

Код будет выглядеть так:

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';

@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 (новое определение файла для вашего целевого возвращенного объекта с использованиемclass-transformer а также class-validator) - для импорта и использования в вашем контроллере

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);
  }
}

ИМХО, второе решение было бы лучше с точки зрения разделения слоев, гибкости, модульности и ремонтопригодности:)

Дайте мне знать, если это поможет;)

Основываясь на сообщении об ошибке, я думаю, что у вас проблема с циклической справкой. Просто закомментируйте другие объекты, на которые ссылается ваш объект billing_statement, а затем повторите попытку. Если это причина, по которой вы получаете эту ошибку, вам следует удалить ссылку с дочерних объектов на родительский объект или попытаться не сериализовать эти ссылки.

Удачи.

Другие вопросы по тегам