Сериализация ответа Nestjs с массивом объектов
Я хочу сериализовать ответ контроллера с помощью техники сериализации nestjs. Я не нашел никакого подхода, и мое решение заключается в следующем:
Пользовательский объект
export type UserRoleType = "admin" | "editor" | "ghost";
@Entity()
export class User {
@PrimaryGeneratedColumn() id: number;
@Column('text')
username: string;
@Column('text')
password: string;
@Column({
type: "enum",
enum: ["admin", "editor", "ghost"],
default: "ghost"
})
roles: UserRoleType;
@Column({ nullable: true })
profileId: number;
}
Классы ответов пользователей
import { Exclude } from 'class-transformer';
export class UserResponse {
id: number;
username: string;
@Exclude()
roles: string;
@Exclude()
password: string;
@Exclude()
profileId: number;
constructor(partial: Partial<UserResponse>) {
Object.assign(this, partial);
}
}
import { Exclude, Type } from 'class-transformer';
import { User } from 'src/_entities/user.entity';
import { UserResponse } from './user.response';
export class UsersResponse {
@Type(() => UserResponse)
users: User[]
constructor() { }
}
контроллер
@Controller('user')
export class UsersController {
constructor(
private readonly userService: UserService
) {
}
@UseInterceptors(ClassSerializerInterceptor)
@Get('all')
async findAll(
): Promise<UsersResponse> {
let users = await this.userService.findAll().catch(e => { throw new NotAcceptableException(e) })
let rsp =new UsersResponse()
rsp.users = users
return rsp
}
Это работает, но я должен явно назначить результат запроса базы данных члену пользователя ответа. Есть ли способ лучше? большое спасибо
Здесь фактический ответ и желаемый результат, для лучшего объяснения.
Результат в этом подходе
{
"users": [
{
"id": 1,
"username": "a"
},
{
"id": 2,
"username": "bbbbbb"
}
]
}
Требуются результаты
{
{
"id": 1,
"username": "a"
},
{
"id": 2,
"username": "bbbbbb"
}
}
4 ответа
Я бы порекомендовал прямо поставить @Exclude
декораторы на вашем классе сущности User
вместо дублирования свойств в UserResponse
, Следующий ответ предполагает, что вы сделали это.
Плоский ответ
Если вы посмотрите на код ClassSerializerInterceptor
Вы можете видеть, что он автоматически обрабатывает массивы:
return isArray
? (response as PlainLiteralObject[]).map(item =>
this.transformToPlain(item, options),
)
: this.transformToPlain(response, options);
Однако он преобразует их только в том случае, если вы непосредственно вернете массив, поэтому return users
вместо return {users: users}
:
@UseInterceptors(ClassSerializerInterceptor)
@Get('all')
async findAll(): Promise<User> {
return this.userService.findAll()
}
Вложенный ответ
Если вам нужен вложенный ответ, то ваш путь - хорошее решение. Кроме того, вы можете вызвать класс-трансформер serialize
непосредственно вместо использования ClassSerializerInterceptor
, Он также обрабатывает массивы автоматически:
import { serialize } from 'class-transformer';
@Get('all')
async findAll(): Promise<UsersResponse> {
const users: User[] = await this.userService.findAll();
return {users: serialize(users)};
}
Ваш подход рекомендуется nestjs, но в нем есть ошибка. Вы исключаете некоторые свойства из показа клиенту. но что, если вы работаете в проекте, в котором администратор и администратор хотят видеть все данные о пользователях или продуктах. Если вы исключите поля в объектах, ваш администратор тоже не увидит эти поля. Вместо этого оставьте объекты как есть и напишите dto для каждого контроллера или для каждого обработчика запросов, а в этом dto просто перечислите свойства, которые вы хотите раскрыть.
Затем напишите собственный перехватчик и создайте конкретный dto для объекта ecah. Например, в вашем примере вы создаете userDto:
import { Expose } from 'class-transformer';
// this is a serizalization dto
export class UserDto {
@Expose()
id: number;
@Expose()
roles: UserRoleType;
@Expose()
albums: Album[];
// Basically you list what you wanna expose here
}
Пользовательский перехватчик немного запутан:
import {
UseInterceptors,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { plainToClass } from 'class-transformer';
// Normally user entity goes into the interceptor and nestjs turns it into the JSON. But we we ill turn it to User DTO which will have all the serialization rules.then nest will take dto and turn it to the json and send it back as response
export class SerializerInterceptor implements NestInterceptor {
// dto is the variable. so you can use this class for different entities
constructor(private dto:any){
}
intercept(context: ExecutionContext, handler: CallHandler): Observable<any> {
// you can write some code to run before request is handled
return handler.handle().pipe(
// data is the incoming user entity
map((data: any) => {
return plainToClass(this.dto, data, {
// this takes care of everything. this will expose things that are set in the UserDto
excludeExtraneousValues: true,
});
}),
);
}
}
Теперь вы используете это в контроллере:
// See we passed UserDto. for different entities, we would just write a new dto for that entity and our custom interceptor would stay reusable
@UseInterceptors(new SerializerInterceptor(UserDto))
@Get('all')
async findAll(
): Promise<UsersResponse> {
let users = await this.userService.findAll().catch(e => { throw new NotAcceptableException(e) })
let rsp =new UsersResponse()
rsp.users = users
return rsp
}
Вау, как легко, если я знаю! Отлично, это решает мою проблему. Также ваша рекомендация для User Entity с декоратором класса-преобразователя @Exclue().
И я знаю, что мне не нужен пользовательский класс UsersResponse в этом случае использования. Это решение было то, что я искал, но я преодолел этот довольно простой способ
Большое спасибо за ваш быстрый ответ и решение проблемы.
Привет в Берлин из Ростока:)
Вот мой последний подход:
контроллер
@UseInterceptors(ClassSerializerInterceptor)
@Get('all')
async findAll(
): Promise<User> {
return await this.userService.findAll().catch(e => { throw new NotAcceptableException(e) })
}
Пользователь Entitiy
import { Entity, Column, PrimaryGeneratedColumn, OneToOne, JoinColumn, OneToMany } from 'typeorm';
import { Profile } from './profile.entity';
import { Photo } from './photo.entity';
import { Album } from './album.entity';
import { Exclude } from 'class-transformer';
export type UserRoleType = "admin" | "editor" | "ghost";
@Entity()
export class User {
@PrimaryGeneratedColumn() id: number;
@Column('text')
username: string;
@Exclude()
@Column('text')
password: string;
@Column({
type: "enum",
enum: ["admin", "editor", "ghost"],
default: "ghost"
})
roles: UserRoleType;
@Exclude()
@Column({ nullable: true })
profileId: number;
@OneToMany(type => Photo, photo => photo.user)
photos: Photo[];
@OneToMany(type => Album, albums => albums.user)
albums: Album[];
@OneToOne(type => Profile, profile => profile.user)
@JoinColumn()
profile: Profile;
}
Ответ Результат
[
{
"id": 1,
"username": "a",
"roles": "admin"
},
{
"id": 2,
"username": "bbbbbb",
"roles": "ghost"
}
]
У меня есть альтернативный способ решения вашей проблемы. вы можете удалить @UseInterceptors(ClassSerializerInterceptor) из вашего контроллера. Вместо этого используйте функцию сериализации и десериализации .
import { serialize, deserialize } from 'class-transformer';
import { User } from './users.entity';
@Get('all')
async findAll() {
const users = serialize(await this.userService.findAll());
return {
status: 200,
message: 'ok',
users: deserialize(User, users)
};
}
это тоже работает для отдельных данных
import { Param } from '@nestjs/common';
import { serialize, deserialize } from 'class-transformer';
import { User } from './users.entity';
@Get(':id')
async findById(@Param('id') id: number) {
const user = serialize(await this.userService.findById(id));
return {
status: 200,
message: 'ok',
user: deserialize(User, user)
};
}