TypeORM / NestJS - @BeforeUpdate hook not working

8.5k views Asked by At

I'm having problems using TypeOrm hook "BeforeUpdate"

I'm trying to update the user entity password, by passing a simple string and calling the save method to trigger the beforeUpdate hook and then hash the password, but this hook is not working while calling the save method.

This is what i have

user.service.ts

async update(id: number, updateUserDto: UpdateUserDto) {
  const roles =
    updateUserDto.roles &&
    (await Promise.all(
      updateUserDto.roles.map((name) => this.preloadRoleByName(name))
    ));
  const user = await this.userRepository.findOneOrFail(id);
  if (!user) {
    throw new NotFoundException(`User with ID #${id} not found`);
  }
  const toSaveUser = {
    ...user,
    ...updateUserDto,
    roles,
  };
  return await this.userRepository.save(toSaveUser);
}

user.entity.ts

.
.
.
@Column()
@Exclude()
password: string;

@BeforeInsert()
@BeforeUpdate()
private async hashPassword() {
  const rounds = 10;
  const salt = await bcrypt.genSalt(rounds);
  this.password = await bcrypt.hash(this.password, salt);
}

user.controller.ts

@Patch(":id")
@UseInterceptors(ClassSerializerInterceptor)
async update(@Param("id") id: string, @Body() updateUserDto: UpdateUserDto) {
 return await this.usersService.update(+id, updateUserDto);
}

What I'm doing wrong?

BeforeInsert hook works or if I call userRepository.preload() method to update it works but it doesn't replace the relationship of the role, that's why I take this approach.

Any ideas?

3

There are 3 answers

0
Jonas Grønbek On BEST ANSWER

Something to be aware of when using the triggers is that they might require instances of the entity to run.

Problem:

const user = await this.userRepository.findOneOrFail(id); // entity instance
const toSaveUser = { ...user, ...updateUserDto, roles }; // plain object
return await this.userRepository.save(toSaveUser); // not running trigger

Solution:

const user = await this.userRepository.findOneOrFail(id); // entity instance
// still entity instance
const toSaveUser = this.userRepository.create({
  ...user,
  ...updateUserDto,
  roles,
});
return await this.userRepository.save(toSaveUser); // running trigger

And you should be good.

Since you are using repository.save() with an arbitrary object in your code, the trigger does not run. If we instead use repository.create() to create the instance, the trigger will now run.

The reason why your solution worked with repository.preload() is because it returns an instance. Repository api examples

0
Soham Lawar On

Two inputs from my side,

  1. Separate function for encrypting updated password -
  @BeforeUpdate()
  async hashPasswordBeforeUpdate() {
    this.password = await bcrypt.hash(this.password, 10);
  }
  1. Try creating PUT request instead of a PATCH request

I am able to generate a valid update query, sharing just for reference-

query: UPDATE `users` SET `levelId` = ?, `updatedAt` = ?, `password` = ? WHERE `id` IN (?) -- PARAMETERS: [null,"2021-05-07T07:27:47.198Z","$2b$10$uQOMNv57BZLB/W/9SWPbke6/OMdIDWxv3i25A8rUhA0/vEMloWb2W",1]
0
reza jafari On

You need to create DTO(Data Transfer Object) first and then update. this code is for update temporary password.

How create DTO:

this.<YOUR_REPOSITORY_NAME>.create(<INPUT_OBJECT>)

Example:

async updatePassword(id: number, tempPassword: string): Promise<boolean> {
    let newUser = { tempPassword: tempPassword };
    const userDto = this.userAccountRepository.create(newUser)
    const userAccount = this.userAccountRepository.update(
      {
        userAccountId: id,
      },
      userDto 
    );
    if (userAccount) {
      return true;
    } else {
      return false;
    }
  }

This is my entity:

import {
  BeforeInsert,
  BeforeUpdate,
  Column,
  Entity,
  JoinColumn,
  OneToMany,
  OneToOne,
  PrimaryColumn,
  } from 'typeorm';
import { Users } from '../users/users.entity';
const crypto = require('crypto');

    @Entity()
    export class UserAccount {
      @PrimaryColumn()
      userAccountId: number;
      @OneToOne(() => Users, { cascade: true })
      @JoinColumn({ name: 'userAccountId' })
      @Column({ nullable: true })
      tempPassword: string;
      @BeforeInsert()
      @BeforeUpdate()
      async hashPassword(): Promise<void> {
        if (!!this.password) {
          this.password = crypto.createHmac('sha256', this.password).digest('hex');
        }
        if (!!this.tempPassword) {
          this.tempPassword = crypto
            .createHmac('sha256', this.tempPassword)
            .digest('hex');
        }
      }
    }