TypeORM AfterSave() triggers after creation but when queried, it returns NULL

Viewed 14717

I have an Entity called Trip. The structure is: db structure

What I want is whenever a new trip is created, the room column should be populated with ${tripId}_${someRandomStringHere}. So for example, I just created a new trip using this body:

trip_body

The response should be: tr]ip response

The newly created trip has the id of 15. So, the response has the room valued at 15_4gupvdo0ea408c25ia0qsbh because again: ${tripId}_${someRandomStringHere}.

This is working as expected whenever I POST the request and create the trip. BUT whenever I query all the trips created, the room property of each trip objects shows null!

Look at the /api/trips:

trips array

room property is NULL. So what the heck I dont understand what is happening.

My Trip Entity code is:

import { PrimaryGeneratedColumn, Column, CreateDateColumn, 
UpdateDateColumn, Entity, Unique, ManyToOne, AfterInsert, JoinColumn, getConnection } from 'typeorm'
import { DriverEntity } from 'src/driver/driver.entity';

@Entity('trips')
export class TripEntity {
  @PrimaryGeneratedColumn()
  id: number

  @Column()
  destination: string

  @Column('decimal')
  destination_lat: number

  @Column('decimal')
  destination_long: number

  @Column()
  maxPassenger: number

  @Column()
  totalPassenger: number

  @Column({ nullable: true })
  room: string

  @CreateDateColumn()
  created_at: Date

  @UpdateDateColumn()
  updated_at: Date
// --------->HERE: The after insert 
  @AfterInsert()
  async createSocketRoom(): Promise<void> {
    const randomString = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15)
    this.room =  `${this.id}_${randomString}`
  }

  // Trip belongs to driver
  // Adds driver_id to trips table
  @ManyToOne(type => DriverEntity, driver => driver.trips)
  @JoinColumn({ name: 'driver_id' })
  driver: DriverEntity
}

My Trip Service Code is:

async create(data: CreateTripDTO) {
    const { driver_id } = data
    const driver = await this.driverRepository.findOne({ where: { id: driver_id } })
    const trip = await this.tripRepository.create(data)
    trip.driver = driver

    await this.tripRepository.save(trip)
    return trip
  }

I dont think I need to include the Trip Controller code but anyway.. I don't know why it is happening because I have my User Entity with @BeforeUpdate and works fine...

After reading alot of similar github issues, watched youtube tutorials [Hi Ben Awad! :D], I found a somewhat fix.. by using Subscribers

Actually, I don't know what is the difference of the Listener/Subscriber. Maybe I am doing the wrong usage. Can someone enlighten me please? For example the difference of AfterSave of Entity Listener vs AfterSave of Entity Subscriber. When/Best case to use? something like that. Anyway back with the "fix..."

I created a Trip Subscriber:

import { EventSubscriber, EntitySubscriberInterface, InsertEvent } from "typeorm";
import { TripEntity } from "src/trip/trip.entity";

@EventSubscriber()
export class TripSubsriber implements EntitySubscriberInterface<TripEntity> {
    // Denotes that this subscriber only listens to Trip Entity
    listenTo() {
      return TripEntity
    }

    // Called after entity insertion
    async afterInsert(event: InsertEvent<any>) {
        console.log(`AFTER ENTITY INSERTED: `, event.entity);
        const randomString = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15)
        // query the trip with given event.entity
        const trip = await event.manager.getRepository(TripEntity).findOne(event.entity.id)
        // populate the room with desired format
        trip.room = `${trip.id}_${randomString}`
        // save it!
        await event.manager.getRepository(TripEntity).save(trip)
    }

}

At first it is not working but after digging for hours again, I need to add a subscriber property with the value of the path of my subscribers at the ormconfig.json for it to work! e.g: "subscribers": [ "src/subscriber/*.ts" ]

Again, the Trip Subscriber code seems spaghetti to me because I already have the event.entity object but I do not know how to update it without the need of querying and updating it using event.manager.getRepository(). Please can someone fix this code for me? the proper way of doing it?

NOW, It is working! the request body: the request body

the /api/trips res: res

My questions are:

  1. Why whenever I use that method methoud subscriber, it is not working. Is it not the proper way to do it? The why is it in the docs? Or for other use case?
  2. Do I really have to use subscriber for it to achieve? Thats so many steps.

I came from Rails. So having to create files/subscribers just to do it somewhat tiring. Unlike ActiveRecord's after_save callback it is very easy..

PS. I'm new to nest-js and typeorm

3 Answers

@AfterInsert method will just modify your JS object after inserting into DB is done. So thats reason why is your code not working. You have to use @BeforeInsert decorator. BeforeInsert will modify your JS entity/object before inserting/saving into DB.

What it looks like is happening with your AfterInsert is that you are creating the random room string just fine, but you are not saving the value to the database, only using the return of the id so that you can create the string. What you could do in your AfterInsert is run the save() function from the EntityManager or RepositoryManger once more and commit the value to the database, similar to what you have happening in you Subscriber. I haven't dived too deep into the Subscriber/Listener vs Before-/AfterInsert decorators, so I can't give a deeper answer to your questions.

If you'd rather not make two commits to the database, you can always do a query for the most recent id and increment it by 1 (thus, matching what the new objects id should be) with something like

const maxId = await this.tripRepository.findOne({select: ['id'], order: {id: "DESC"} });
const trip = await this.tripRepository.create(data);
const randomString = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15)
this.room =  `${maxId + 1}_${randomString}`
trip.driver = driver
await this.tripRepository.save(trip)

It's a little clunky to look at, but it doesn't require two writes to the database (though you'll definitely need to ensure that after creation room and trip have the same id).

Your last option would be to create a Trigger in your database that does the same thing as your JavaScript code.

Related