Ensuring Sequelize Hooks Always Get Run

Last updated Invalid date
In a modular monolith, you can decouple business logic using Domain Events and Sequelize Hooks. For that to work, we need to make sure that our hooks are always getting called on every transaction.

Intro

In "Decoupling Logic with Domain Events [Guide] - Domain-Driven Design w/ TypeScript", we use Sequelize Hooks to decouple business logic, allowing the system to respond to events in a fashion similar to the Observer Pattern.

Sequelize hooks are places we can write callbacks that get invoked at key points in time like afterCreate, afterDestroy, afterUpdate, and more.

infra/sequelize/hooks/index.ts
import models from "../models"
import { DomainEvents } from "../../../core/domain/events/DomainEvents"
import { UniqueEntityID } from "../../../core/domain/UniqueEntityID"

const dispatchEventsCallback = (model: any, primaryKeyField: string) => {
  const aggregateId = new UniqueEntityID(model[primaryKeyField])
  DomainEvents.dispatchEventsForAggregate(aggregateId)
}

(async function createHooksForAggregateRoots() {
  const { User } = models

  User.addHook("afterCreate", (m: any) => dispatchEventsCallback(m, "user_id"))
  User.addHook("afterDestroy", (m: any) => dispatchEventsCallback(m, "user_id"))
  User.addHook("afterUpdate", (m: any) => dispatchEventsCallback(m, "user_id"))
  User.addHook("afterSave", (m: any) => dispatchEventsCallback(m, "user_id"))
  User.addHook("afterUpsert", (m: any) => dispatchEventsCallback(m, "user_id"))
})()

In Domain-Driven Design, after a transaction completes, we want to execute domain event handlers in order to decide whether we should invoke any follow up commands or not.

forum/subscriptions/afterUserCreated.ts
import { UserCreated } from "../../users/domain/events/userCreated"
import { IHandle } from "../../../shared/domain/events/IHandle"
import { CreateMember } from "../useCases/members/createMember/CreateMember"
import { DomainEvents } from "../../../shared/domain/events/DomainEvents"

export class AfterUserCreated implements IHandle<UserCreated> {
  private createMember: CreateMember

  constructor(createMember: CreateMember) {
    this.setupSubscriptions()
    this.createMember = createMember
  }

  setupSubscriptions(): void {
    // Register to the domain event
    DomainEvents.register(this.onUserCreated.bind(this), UserCreated.name)  }

  private async onUserCreated(event: UserCreated): Promise<void> {    const { user } = event

    try {
      await this.createMember.execute({ userId: user.userId.id.toString() })
      console.log(
        `[AfterUserCreated]: Successfully executed CreateMember use case AfterUserCreated`
      )
    } catch (err) {
      console.log(
        `[AfterUserCreated]: Failed to execute CreateMember use case AfterUserCreated.`
      )
    }
  }
}

In the Sequelize Repository, where we deal with persistence logic, I have noticed that the hook callbacks do not get called if no new rows were created and no columns were changed.

In the save method of a repository, you'll find code where we rely on a mapper to convert the domain object to the format necessary for Sequelize to save it. You'll also find code that determines if we're doing a create or an update based on the domain object's existence.

forum/repos/implementations/sequelizePostRepo.ts
export class SequelizePostRepo implements PostRepo {
  ...

  public async save (post: Post): Promise<void> {
    const PostModel = this.models.Post;
    const exists = await this.exists(post.postId);
    const isNewPost = !exists;
    const rawSequelizePost = await PostMap.toPersistence(post);
    
    if (isNewPost) {

      try {
        await PostModel.create(rawSequelizePost);
        await this.saveComments(post.comments);
        await this.savePostVotes(post.getVotes());
        
      } catch (err) {
        await this.delete(post.postId);
        throw new Error(err.toString())
      }

    } else {
      await this.saveComments(post.comments);
      await this.savePostVotes(post.getVotes());
      
      // Persist the post model to the database       await PostModel.update(rawSequelizePost, {         where: { post_id: post.postId.id.toString() }       });    }
  }
}

In the highlighted lines, I expect the afterUpdate hook to get called, though it will not in scenarios where there were no are differences.

To fix this, Sequelize's update method's second parameter configuration object allows you to pass in hooks: true.

forum/repos/implementations/sequelizePostRepo.ts
await PostModel.update(rawSequelizePost, { 
  where: { post_id: post.postId.id.toString() }
  // Be sure to include this to call hooks regardless
  // of anything changed or not.
  hooks: true,});

Doing this will ensure that the hooks get run everytime.



Discussion

Liked this? Sing it loud and proud 👨‍🎤.



Stay in touch!



About the author

Khalil Stemmler,
Software Essentialist ⚡

I'm Khalil. I turn code-first developers into confident crafters without having to buy, read & digest hundreds of complex programming books. Using Software Essentialism, my philosophy of software design, I coach developers through boredom, impostor syndrome, and a lack of direction to master software design and architecture. Mastery though, is not the end goal. It is merely a step towards your Inward Pull.



View more in Sequelize



You may also enjoy...

A few more related articles

How to Handle Updates on Aggregates - Domain-Driven Design w/ TypeScript
In this article, you'll learn approaches for handling aggregates on Aggregates in Domain-Driven Design.
Decoupling Logic with Domain Events [Guide] - Domain-Driven Design w/ TypeScript
In this article, we'll walk through the process of using Domain Events to clean up how we decouple complex domain logic across the...
How to Design & Persist Aggregates - Domain-Driven Design w/ TypeScript
In this article, you'll learn how identify the aggregate root and encapsulate a boundary around related entities. You'll also lear...
Junction Model Pattern: Many-to-Many - Sequelize
Many-to-many is a common modeling relationship between two entities. Here's one way to handle it with the Sequelize ORM.