JCC Express

JCC Eloquent ORM

Observer

Introduction

JCC-Eloquent models can fire lifecycle events whenever records are created, updated, deleted, or restored. You register hooks for those events in two main ways:

  1. static boot() on the model — register this.creating(...), this.saved(...), and other callbacks directly on the model class (most common for app-specific logic).
  2. Observer classes — group hooks in a separate class and attach with @Observer(...) (useful when you want a dedicated, testable listener or constructor injection).

Both approaches use the same event names and run at the same points in the save/delete cycle.


Available events

MethodWhen it runs
savingBefore insert or update
creatingBefore insert (new record)
updatingBefore update (existing record)
createdAfter insert
updatedAfter update
savedAfter insert or update
deletingBefore delete
deletedAfter delete
restoringBefore a soft-deleted row is restored
restoredAfter a soft-deleted row is restored

Typical create order: savingcreatingcreatedsaved.

Typical update order: savingupdatingupdatedsaved.

Hook callbacks may be async; the framework awaits them during dispatch.


Register hooks on the model (boot())

Override static boot() on your model and call this.creating(...), this.created(...), and the other registrars. The framework calls boot() automatically the first time the model participates in an event (via Event.bootIfNotBooted).

TypeScript
// app/Models/User.ts
import { Model } from "jcc-express-mvc/Eloquent";
import { bcrypt } from "jcc-express-mvc";

export class User extends Model {
  protected hidden = ["password"];
  declare id?: number;
  declare name?: string;
  declare email?: string;
  declare password?: string;

  static boot(): void {
    this.creating(async (user: User) => {
      if (user.password) {
        user.password = await bcrypt(user.password);
      }
    });

    this.created(async (user: User) => {
      console.log("User was created:", user.id);
    });

    this.saving(async (user: User) => {
      console.log("User is being saved:", user.id);
    });

    this.saved(async (user: User) => {
      console.log("User was saved:", user.id);
    });

    this.updating(async (user: User) => {
      // before update
    });

    this.updated(async (user: User) => {
      // after update
    });

    this.deleting(async (user: User) => {
      // before delete
    });

    this.deleted(async (user: User) => {
      // after delete
    });
  }
}

Use this.<event>(callback) inside boot() — the same helpers exist on the model class (User.creating(...)) if you prefer registering outside the class (for example in a service provider).

Optional static booted() runs after boot() if you need a second setup phase.


Observer classes (alternative)

For larger apps, move hooks into a dedicated observer class and register it with @Observer on the model.

Generate an observer

Bash
bun artisanNode make:observer UserObserver
bun artisanNode make:observer UserObserver model=User

This creates app/Observers/UserObserver.ts with stub methods for every lifecycle hook. When you pass model=User, the scaffold imports your User model and types the method parameters.


Define an observer

TypeScript
// app/Observers/UserObserver.ts
import { User } from "@/Models/User";
import { bcrypt } from "jcc-express-mvc";

export class UserObserver {
  async creating(user: User) {
    if (user.password) {
      user.password = await bcrypt(user.password);
    }
  }

  async created(user: User) {
    // Welcome email, default related records, etc.
  }

  async updating(user: User) {
    if (user.isDirty("password")) {
      user.password = await bcrypt(user.password!);
    }
  }

  async updated(user: User) {
    // Invalidate cache, sync search index, etc.
  }

  async deleting(user: User) {
    // Guard or cleanup before delete
  }

  async deleted(user: User) {
    // Cleanup after delete
  }
}

Only implement the methods you need — empty stubs can be removed.

The observer is resolved from the service container (app.make(...)), so you can use constructor injection if the class is registered in a provider.


Register on a model

Attach the observer with the @Observer decorator on your model class:

TypeScript
import { Model, Observer } from "jcc-express-mvc/Eloquent";
import { UserObserver } from "@/Observers/UserObserver";

@Observer(UserObserver)
export class User extends Model {
  protected hidden = ["password"];
  declare id?: number;
  declare name?: string;
  declare email?: string;
  declare password?: string;
}

Registration runs when the model class is loaded. The framework wires each implemented observer method to the matching lifecycle event.


Example: password hashing and cache busting

TypeScript
// app/Observers/UserObserver.ts
import { User } from "@/Models/User";
import { bcrypt } from "jcc-express-mvc";
import { Cache } from "jcc-express-mvc/Core/Cache";

export class UserObserver {
  async saving(user: User) {
    if (user.isDirty("password") && user.password) {
      user.password = await bcrypt(user.password);
    }
  }

  async updated(user: User) {
    await Cache.forget(`user:${user.id}`);
  }

  async deleted(user: User) {
    await Cache.forget(`user:${user.id}`);
  }
}
TypeScript
// app/Models/User.ts
import { Model, Observer } from "jcc-express-mvc/Eloquent";
import { UserObserver } from "@/Observers/UserObserver";

@Observer(UserObserver)
export class User extends Model {
  protected hidden = ["password"];
}

Register outside boot() (alternative)

You can attach the same callbacks without overriding boot():

TypeScript
import { User } from "@/Models/User";

User.creating(async (user) => {
  // runs before insert
});

User.created(async (user) => {
  // runs after insert
});

Prefer static boot() on the model when the hooks belong to that model; use a service provider when wiring events from elsewhere.


Soft deletes

When protected softDeletes = true on the model, restore operations fire restoring and restored. See Soft deletes.


Disable events for specific operations

Skip observers and other model events when you need a silent write (seeders, imports, admin fixes):

TypeScript
// One-off block — no events for anything inside
await User.executeWithoutEvents(async () => {
  await User.create({ name: "Bulk import" });
});

// Instance helpers
await user.saveQuietly();
await user.updateQuietly({ name: "Silent" });
await user.deleteQuietly();

// Query builder
await User.where("id", 1).updateQuietly({ active: false });
await User.where("id", 1).deleteQuietly();

See Retrieving models for more detail.


Summary

  • Lifecycle events: saving, creating, updating, created, updated, saved, deleting, deleted, restoring, restored.
  • Register on the model with static boot() and this.creating(...) / this.saved(...) / etc.
  • Or use @Observer(YourObserver) and make:observer for a separate listener class.
  • Use saveQuietly(), executeWithoutEvents(), and related helpers to skip hooks when needed.