Как правильно настроить сериализацию с помощью 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 ответа
Из того, что я видел до сих пор, мне на ум пришли две вещи.
- Расширить использование
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[];
}
- Другой способ - отделить определение вашей сущности от возвращенного определения dto, таким образом вы можете расширить определение своей сущности, плюс или минус желаемые и нежелательные свойства в возвращенном dto. Например, допустим, вы назвали свой ответ dto
BillingStatementResponseDto
, вы должны использовать его в типе ответа вашего контроллера.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, а затем повторите попытку. Если это причина, по которой вы получаете эту ошибку, вам следует удалить ссылку с дочерних объектов на родительский объект или попытаться не сериализовать эти ссылки.
Удачи.