Как сериализовать ответ nest js с классом-преобразователем при получении данных с помощью Typegoose?

Я пытался проработать пример NestJs для раздела сериализации для Mongodb, используя Typegoose, используя библиотеку преобразователей классов. Пример, приведенный на https://docs.nestjs.com/techniques/serialization, показывает только, как использовать сериализацию в TypeORM. Я проделал тот же процесс для Typegoose. Вот что я пробовал до сих пор.

// cat.domain.ts

import { prop } from '@typegoose/typegoose';

export class Cat {
  @prop()
  name: string;

  @prop()
  age: number;

  @prop()
  breed: string;
}


// cats.service.ts

@Injectable()
export class CatsService {
  constructor(
    @InjectModel(Cat) private readonly catModel: ReturnModelType<typeof Cat>,
  ) {}

  findAll(): Observable<Cat[]> {
    return from(this.catModel.find().exec());
  }

  findOne(id: string): Observable<Cat> {
    return from(this.catModel.findById(id).exec());
  }
  ...
}

// cat.response.ts

import { ObjectId } from 'mongodb';
import { Exclude, Transform } from 'class-transformer';

export class CatResponse {
  @Transform(value => value.toString(), { toPlainOnly: true })
  _id?: ObjectId;

  name: string;

  age: number;

  @Exclude()
  breed: string;

  constructor(partial: Partial<CatResponse>) {
    Object.assign(this, partial);
  }
}

// cats.controller.ts

@Controller('cats')
@UseInterceptors(ClassSerializerInterceptor)
export class CatsController {
  constructor(private readonly catsService: CatsService) {}

  @Get()
  findAll(): Observable<CatResponse[]> {
    return this.catsService.findAll();
  }

  @Get(':id')
  findOne(@Param() params: FindOneParamsDto): Observable<CatResponse> {
    return this.catsService.findOne(params.id);
  }
  ...
}

Я попытался запустить вызов API для Get() с идентификатором, но вместо breed будучи исключенным из ответа, я получил следующий ответ.

{
    "$__": {
        "strictMode": true,
        "selected": {},
        "getters": {},
        "_id": {
            "_bsontype": "ObjectID",
            "id": {
                "type": "Buffer",
                "data": [
                    94,
                    93,
                    76,
                    66,
                    116,
                    204,
                    248,
                    112,
                    147,
                    216,
                    167,
                    205
                ]
            }
        },
        "wasPopulated": false,
        "activePaths": {
            "paths": {
                "_id": "init",
                "name": "init",
                "age": "init",
                "breed": "init",
                "__v": "init"
            },
            "states": {
                "ignore": {},
                "default": {},
                "init": {
                    "_id": true,
                    "name": true,
                    "age": true,
                    "breed": true,
                    "__v": true
                },
                "modify": {},
                "require": {}
            },
            "stateNames": [
                "require",
                "modify",
                "init",
                "default",
                "ignore"
            ]
        },
        "pathsToScopes": {},
        "cachedRequired": {},
        "$setCalled": [],
        "emitter": {
            "_events": {},
            "_eventsCount": 0,
            "_maxListeners": 0
        },
        "$options": {
            "skipId": true,
            "isNew": false,
            "willInit": true
        }
    },
    "isNew": false,
    "_doc": {
        "_id": {
            "_bsontype": "ObjectID",
            "id": {
                "type": "Buffer",
                "data": [
                    94,
                    93,
                    76,
                    66,
                    116,
                    204,
                    248,
                    112,
                    147,
                    216,
                    167,
                    205
                ]
            }
        },
        "name": "Sylver",
        "age": 14,
        "breed": "Persian Cat",
        "__v": 0
    },
    "$locals": {},
    "$op": null,
    "$init": true
}

Может ли кто-нибудь помочь мне с правильной сериализацией ответа?

3 ответа

это известная проблема (#108), typegoose (&mongoose) несовместим с class-transformer / class-
validator это потому, что typegoose должен преобразовать класс в схему, а mongoose скомпилирует его в модель (которая не является классом больше)

Для людей, пытающихся следовать документации nestjs и использовать mongoose, но ClassSerializerInterceptorне работает.

Публикация решения для использования class-transformer withe mongoose ниже, которое может быть полезно для других, использует собственный перехватчик, который вы можете увидеть в документации по гнезду. https://docs.nestjs.com/interceptors

      How the folder structure would be:  
  
src
└── cats
    ├── dto
    │   └── cats-response.dto.ts
    ├── interceptor
    │   └── cats.interceptor.ts
    ├── schemas
    │   └── cat.schema.ts
    ├── cats.controller.ts
    └── cats.service.ts
  1. Мы создадим dto для ответа кошки, который называется cats-response.dto.tsи в нем исключить breedсвойство из ответа с помощью декоратора Exclude(). Используя Dto для ответа, мы создадим экземпляр

  2. Мы создадим собственный перехватчик для ответов кошек, вызываемый в cats.interceptor.ts. Вы можете сгенерировать его с помощью Nest cli, команда nest g interceptor cats

cat-response.dto.ts
Создать CatsResponseDtoдля использования в нашем пользовательском перехватчике.

      import { Expose, Exclude } from 'class-transformer'

export class CatsResponseDto {

  @Expose()
  name: string;

  @Expose()
  age: number;

  // Exclude decorator to exclude it from our response.
  @Exclude()
  breed: string;

}
  

Cats.interceptor.ts
Создание пользовательского
Примечание: plainToClass устарел и теперь называется plainToInstance, я узнал об этом, только когда использовал его в своей Ide, и он подсказал. Официальная документация еще не обновлена. https://github.com/typestack/class-transformer#plaintoclass

В журнале изменений упоминается об этом.
https://github.com/typestack/class-transformer/blob/develop/CHANGELOG.md#041-breaking-change---2021-11-20

      import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { plainToInstance } from 'class-transformer'
import { map, Observable } from 'rxjs';
import { CatsResponseDto } from '../dto/cats-response.dto'

@Injectable()
export class CatsInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, handler: CallHandler): Observable<any> {
    
    return handler.handle().pipe(
      map((data: any) => {
        
        // run something before the response is sent out.
        // Please note that plainToClass is deprecated & is now called plainToInstance
        
        return plainToInstance(CatsResponseDto, data, {
        
        // By using excludeExtraneousValues we are ensuring that only properties decorated with Expose() decorator are included in response.
          
        excludeExtraneousValues: true,

        })
      })
    );

  }
}

Наша схема cat должна быть определена, как показано ниже (для справки) в cat.schema.tsили аналогичный согласно документации мангуста.

cat.schema.ts

      import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';

export type CatDocument = Cat & Document;

@Schema({ timestamps: true })
export class Cat {

  // no Id defined here as its automatically added by mongoose unless we explicitly provide option to turn it OFF in schema options.  
    
  @Prop({ required: true })
  name: string;

  @Prop({ required: true })
  age: number;

  @Prop({ required: true })
  breed: string;

}

export const CatSchema = SchemaFactory.createForClass(Cat);

Теперь привяжите наш пользовательский перехватчик CatsInterceptorв cats.controller.ts

коты.controller.ts

      import { Cat } from './schemas/cat.schema';
import { CatsInterceptor } from './interceptor/cats.interceptor';
import { CatsService } from './cats.service.ts'

@Controller('cats')
export class CatsController {
  constructor(private readonly catsService: CatsService) {}

  @Get()
  findAll(): Promise<Cat[]> {
    return this.catsService.findAll();
  }

  @UseInterceptors(CatsInterceptor)
  @Get(':id')
  findOne(@Param() params: FindOneParamsDto): Promise<Cat> {
    return this.catsService.findOne(params.id);
  }
  ...
}

РЕЗУЛЬТАТ : при вызове /cats/{id} ответ исключает породу.

связанная проблема: сериализация класса не работает вnesjs

Вот обходной путь:

// cats.controller.ts
...
import { classToPlain } from "class-transformer";
...

@Controller('cats')
@UseInterceptors(ClassSerializerInterceptor)
export class CatsController {
  constructor(private readonly catsService: CatsService) {}

  @Get()
  findAll(): Observable<CatResponse[]> {
    const cats = this.catsService.findAll();
    // transforming the Model to CatResponse class...
    const catResponses = cats.map(cat => classToPlain(new CatResponse(cat.toJSON())))
    return catResponses;
  }

  @Get(':id')
  findOne(@Param() params: FindOneParamsDto): Observable<CatResponse> {
    const cat = this.catsService.findOne(params.id);
    const catResponse = classToPlain(new CatResponse(cat.toJSON()));
    return 
  }
  ...
}

Надеюсь, это поможет.

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