Search code examples
typescriptgraphqlmikro-orm

Use EventSubscriber to register Database History in mikroorm


I am fairly new to the concept of LifcycleHooks and EventSubscribers. I am trying to populate a history table that records the changes done on any other table. I have been trying to use EventSubscriber for this: The idea is that on the onFlush event trigger of my eventSubscriber, I am extracting all the changed values and fields from the changeset and creating an object of the history table and trying to persist it but I see the following error:

This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason: ValidationError: You cannot call em.flush() from inside lifecycle hook handlers

My subscriber looks like this:

import { HistoryLog } from 'src/entities/history-log.entity;
export class EntityChangeSubscriber implements EventSubscriber<AnyEntity>  {

  async afterCreate(args: EventArgs<AnyEntity>): Promise<void> {
    console.log('===========1111 AfterCreateHook');
    console.log(args.entity);
  }
  async afterUpdate(args: EventArgs<AnyEntity>): Promise<void> {
    console.log('===========2222 AfterUpdateHook');
    console.log(args.entity);
  }

  async onFlush(args: FlushEventArgs): Promise<void> {
    console.log('===========3333 onFlushHook');
    console.log(args);
    const changeSets = args.uow.getChangeSets();
    const entities = changeSets.map((cs) => cs.entity);
    console.log('Affected entities', entities);
  }

  async afterFlush(args: FlushEventArgs): Promise<void> {
    console.log('===========4444 AfterFlushHook');

    const changeSets = args.uow.getChangeSets();
    console.log('change sets', changeSets);
    changeSets?.map(async (cs) => {
      if (cs.persisted) {
        console.log('print changeset');
        let nextValues = cs.payload ? cs.payload : {};
        let tableName = cs.name;
        let operation = cs.type;
        let rowId = cs.getPrimaryKey()?.toString();
        let modifiedFields = Object.keys(cs.payload);
        let previousValues = cs.originalEntity ? cs.originalEntity : {};
       for (let key in previousValues) {
          if (
            !previousValues.hasOwnProperty(key) ||
            !modifiedFields.includes(key)
          ) {
            delete previousValues[key];
          }
        }
       const historyLog=new HistoryLog({
          operation,
          tableName,
          rowId,
          previousValues,
          nextValues
        });
        console.log(historyLog);
     
        let historyLog1= await args.em.persistAndFlush(historyLog);
        console.log('historyLog1>>> ',historyLog1);
        return historyLog1;
      }
    });
 }
}

My mikro-orm-postgres.config.ts registers the subscriber and I can see changeset being loaded subscribers: [new EntityChangeSubscriber()]

How can I implement this? Is there a way that I can return the results of EventSubscriber to a service class and do a persist operation there?


Solution

  • You should not flush from inside any hook or event handler. Using flush events is correct, but instead of afterFlush you should use onFlush and append the newly added entities to current unit of work. That is exactly what the example in the docs shows.

    https://mikro-orm.io/docs/lifecycle-hooks/#using-onflush-event

    If you want to issue the query in parallel (not in the same transaction), you could either fork the EM to get around the validation error, or use em.nativeInsert() instead of UoW to create the new record.