Сериализация классов не работает в nestjs

У меня простая модель пользователя, и я хочу исключить из нее пароль. Используя официальные документы и ответьте здесь, я попытался заставить его работать, но это не работает, поскольку я получаю ответ примерно так.

[
  {
    "$__": {
      "strictMode": true,
      "selected": {},
      "getters": {},
      "_id": {
        "_bsontype": "ObjectID",
        "id": {
          "type": "Buffer",
          "data": [
            94,
            19,
            73,
            179,
            3,
            138,
            216,
            246,
            182,
            234,
            62,
            37
          ]
        }
      },
      "wasPopulated": false,
      "activePaths": {
        "paths": {
          "password": "init",
          "email": "init",
          "name": "init",
          "_id": "init",
          "__v": "init"
        },
        "states": {
          "ignore": {},
          "default": {},
          "init": {
            "_id": true,
            "name": true,
            "email": true,
            "password": true,
            "__v": true
          },
          "modify": {},
          "require": {}
        },
        "stateNames": [
          "require",
          "modify",
          "init",
          "default",
          "ignore"
        ]
      },
      "pathsToScopes": {},
      "cachedRequired": {},
      "session": null,
      "$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,
            19,
            73,
            179,
            3,
            138,
            216,
            246,
            182,
            234,
            62,
            37
          ]
        }
      },
      "name": "Kamran",
      "email": "kamran@example.com",
      "password": "Pass1234",
      "__v": 0
    },
    "$locals": {},
    "$init": true
  }
]

Вот моя модель. я используюTypegoose но то же самое и с Mongoose также.

export class User extends Typegoose {
  @Transform((value) => value.toString(), { toPlainOnly: true })
  _id: string;

  @prop({ required: true })
  public name!: string;

  @prop({ required: true })
  public email!: string;

  @Exclude({ toPlainOnly: true })
  @prop({ required: true })
  public password!: string;
}

Моя пользовательская служба

@Injectable()
export class UserService {
  constructor(@InjectModel(User) private readonly user: ReturnModelType<typeof User>) {}

  async getUsers() {
    return this.user.find().exec();
  }
}

и пользовательский контроллер

@Controller('users')
@UseInterceptors(ClassSerializerInterceptor)
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Get()
  async index() : Promise<User[] | []> {
    return this.userService.getUsers();
  }
}

Я попытался использовать свой собственный перехватчик, как описано здесь, но это не сработало, поэтому я изменил его на код ниже, как указано здесь

@Injectable()
export class TransformInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(map(data => classToPlain(this.transform(data))));
  }

  transform(data) {
    const transformObject = (obj) => {
      const result = obj.toObject();
      const classProto = Object.getPrototypeOf(new User());
      Object.setPrototypeOf(result, classProto);
      return result;
    }

    return Array.isArray(data) ? data.map(obj => transformObject(obj)) : transformObject(data);
  }
}

Теперь он работает, но код не является универсальным. Есть ли способ сделать его общим?

6 ответов

Решение

Я думаю, что обнаружил проблему, но пока не понимаю, почему это происходит. Итак, вот проблема, если я возвращаю экземпляр класса, тогда сериализация работает, но если я просто возвращаю простой ответ db, возникает вышеупомянутая проблема. Я обновил прототип объектов ответа вtransform метод toObjectв мой пользовательский класс. Вот код.

Модель пользователя

@modelOptions({
  schemaOptions: {
    toObject: {
      transform: function(doc, ret, options) {
        Object.setPrototypeOf(ret, Object.getPrototypeOf(new User()));
      }
    },
  },
})
export class User {
  @Transform((value) => value.toString(), { toPlainOnly: true })
  public _id: string;

  @prop({ required: true })
  public name!: string;

  @prop({ required: true })
  public email!: string;

  @Exclude({ toPlainOnly: true })
  @prop({ required: true })
  public password!: string;
}

TransformInterceptor

@Injectable()
export class TransformInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(map(data => classToPlain(this.transform(data))));
  }

  transform(data) {
    return Array.isArray(data) ? data.map(obj => obj.toObject()) : data.toObject();
  }
}

А теперь, если вы просто украсите свой контроллер или метод @UseInterceptors(TransformInterceptor)он будет работать отлично. Этоtypegoose решение, но оно будет работать так же с mongoose также.

Ответ @kamran-arshad помог мне найти подходящий способ достичь ожидаемого результата с помощью typegoose. Вы можете использовать декоратор@modelOptions() и передайте ему объект с функцией для генерации JSON.

@modelOptions({
  toJSON: {
    transform: function(doc, ret, options) {
      delete ret.password;
      return ret;
    }
  }
})
export class User extends Typegoose {
@prop({required: true})
name!: string;

@prop({required: true})
password!: string;
}

Он не идеален, поскольку декораторы из class-transformне работают должным образом, но они выполняют свою работу. Кроме того, вам следует избегать использованияClassSerializerInterceptor потому что это даст тот же результат, о котором упоминал OP.

Чтобы избежать болей в спине и головных болей с Mongoose, я бы предложил использовать plainToClass чтобы иметь полную совместимость мангуста / преобразования классов и избежать необходимости делать пользовательские переопределения для преодоления этой проблемы.

Например, добавьте это в свой сервис:

      async validateUser(email: string, password: string): Promise<UserWithoutPassword | null> {
    const user = await this.usersService.findOne({ email });

    if (user && await compare(password, user.password))
    {
        return plainToClass(UserWithoutPassword, user.toObject());
    }

    return null;
}

Таким образом, вы можете использовать @Exclude() и другие декораторы

Источник: ответ Stackoverflow

Решение, которое сработало для меня:@Transform(({value}) => value.toHexString(), {toPlainOnly: true})

Пример полного кода:

      export class User {
    @ObjectIdColumn()
    @Transform(({value}) => value.toHexString(), {toPlainOnly: true})
    _id: ObjectID

    @Column({unique: true })
    username: string

    @Column({ unique: true })
    email: string

    @Exclude({ toPlainOnly: true })
    @Column()
    password: string
}

Вот моя реализация, все декораторы будут работать без необходимости ClassSerializerInterceptor

      PersonSchema.methods.toJSON = function () {
  return plainToClass(Person, this.toObject());
};
Другие вопросы по тегам