Add a standard interceptor for the transformation of instance into POJO
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
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. 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
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 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.
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 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