nestjs icon indicating copy to clipboard operation
nestjs copied to clipboard

Add a standard interceptor for the transformation of instance into POJO

Open H6LS1S opened this issue 4 years ago • 6 comments

For most simple CRUDs don't often have to resort to mapping the entity via the response DTO, but often need to hide fields in the entity using mikro-orm or decorators from the class-transformer library. All this leads to writing similar code for the service (see below). This typing causes nestjs plugins working with Abstract Syntax Tree analysis, such as - @nestjs/swagger/plugin, to simply break, and fail to generate the API schema

...

  public async create(data: CreateUserDto): Promise<EntityData<UserEntity>> {
    const entity = this.sqlEntityRepository.create(data);
    await this.sqlEntityRepository.persistAndFlush(entity)
    return entity.toPOJO();
  }

...

The proposed version of serialization works before the serializer nestjs and gives the prepared class further down the call chain (the order of interceptor calls is important) without breaking service typing and does not require creating a DTO response

// mikro-orm-serializer.interceptor.ts
import { isObject } from '@nestjs/common/utils/shared.utils';
import { BaseEntity } from '@mikro-orm/core';
import { map, Observable } from 'rxjs';
import {
  Injectable,
  CallHandler,
  StreamableFile,
  NestInterceptor,
  ExecutionContext,
  PlainLiteralObject,
} from '@nestjs/common';

@Injectable()
export class MikroOrmSerializerInterceptor implements NestInterceptor {

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next
      .handle()
      .pipe(map((res: PlainLiteralObject | Array<PlainLiteralObject>) => this.serialize(res)));
  }

  /**
   * Serializes responses that are non-null objects nor streamable files.
   */
  serialize(
    response: PlainLiteralObject | Array<PlainLiteralObject>,
  ): PlainLiteralObject | Array<PlainLiteralObject> {
    if (!isObject(response) || response instanceof StreamableFile) return response;
    return Array.isArray(response)
      ? response.map((item) => this.transformToPOJO(item))
      : this.transformToPOJO(response);
  }

  /**
   * Transformation to POJO if argument is a BaseEntity instance
   */
  transformToPOJO(plainOrEntity: any): PlainLiteralObject {
    return plainOrEntity instanceof BaseEntity ? plainOrEntity.toPOJO() : plainOrEntity;
  }
}
  • Use as a global interceptor in conjunction with ClassSerializerInterceptor
//main.ts
   ...

  const classSerializerInterceptor = new ClassSerializerInterceptor(app.get(Reflector));
  const mikroOrmSerializerInterceptor = new MikroOrmSerializerInterceptor();

  return app
    .useGlobalInterceptors(classSerializerInterceptor, mikroOrmSerializerInterceptor)
    .listen(configService.get('PORT'), configService.get('HOST'));

Alternatively, can rewrite all logic related to metadata reflection using the class-transformer library

But I don't think that's a good option for upcoming releases

H6LS1S avatar Oct 14 '21 02:10 H6LS1S

I have no idea what you are trying to propose here, could you start with the motivation and examples of when this is needed (and what it solves)? I saw the PR too, and it also does not describe any motivation.

Is it about transforming API response? Because that should be done automatically as MikroORM entities implement toJSON method on the prototype. Calling toPOJO is actually wrong here, it won't respect populate hints, it's purpose is to allow caching (where you don't care about populate hints, it's the very opposite actually).

B4nan avatar Oct 14 '21 07:10 B4nan

@B4nan. It all started when I noticed fields in the API response that were marked as hidden: true in entity. After that I started trying different serialization methods, and only toPOJO was suitable for sharing with class-transformer. I use nestjs together with fastify, hardly fastify knows about toJSON. Maybe in my case it would be more correct to apply the interceptor ClassSerializerInterceptor first and then call toJSON in place of toPOJO in its my interceptor

H6LS1S avatar Oct 14 '21 23:10 H6LS1S

hardly fastify knows about toJSON

This is not about frameworks, the fact that you are sending the response as JSON is enough as there will be JSON.stringify call - and that is what calls the toJSON methods on entities.

Also note that all of toObject/toJSON/toPOJO are using the very same serialization technique, the only difference is how they work with populate hints. All of them will respect hidden: true on entity properties the same way.

I am more inclined to closing this, your PR just adds a new class (so it can live elsewhere too), and it apparently helps solving your problem, but it does not feel like something that should be included in the repository.

B4nan avatar Oct 15 '21 07:10 B4nan

@B4nan There is one problem with what you say, it does not call JSON.stringify anywhere. So I have no idea how the toJSON method can be called after calling classToPlain.

H6LS1S avatar Oct 15 '21 08:10 H6LS1S

it does not call JSON.stringify anywhere

It (fastify) definitely does call it (see https://github.com/fastify/fastify/blob/main/lib/schema-compilers.js and https://github.com/fastify/fast-json-stringify/blob/master/index.js#L900-L905, it uses its own helper to do this, but it works the very same way), at the very last stage, as JSON string is what you transfer over http, not objects. Your problem is that you want to work with POJO before it gets transformed to the actual response.

This really feels like something you should have in your own project.

B4nan avatar Oct 15 '21 08:10 B4nan

@B4nan Yes, but after calling classToPlain in the nest.js serializer to fastify the response already comes as a JSON without toJSON method or any metadata. I'm looking for a way to save the metadata of the mikro-orm and class-transformator and make it all work together. I think it's worth at least saying in the documentation that ClassSerializerInterceptor is not compatible, without the above proposed solution.

UPD: I disabled the nestjs serializer and yes, mikro-orm serialization worked, but the problem with working together with class-trasformer remained

H6LS1S avatar Oct 15 '21 09:10 H6LS1S