Lifecycle Hooks
Execute custom logic before and after database operations
Available Hooks
Stabilize provides the following lifecycle hooks:
beforeCreate- Before inserting a new recordafterCreate- After inserting a new recordbeforeUpdate- Before updating a recordafterUpdate- After updating a recordbeforeSave- Before create or updateafterSave- After create or updatebeforeDelete- Before deleting a recordafterDelete- After deleting a record
Register Hooks
Hooks are registered using registerHooks() after defining your model. You can pass a single callback or an array of callbacks for each hook type:
import { defineModel, DataTypes } from "stabilize-orm";import { registerHooks } from "stabilize-orm";
const User = defineModel({ tableName: "users", columns: { id: { type: DataTypes.STRING, required: true, unique: true }, email: { type: DataTypes.STRING, length: 255, required: true }, password: { type: DataTypes.STRING, length: 255, required: true }, },});
// Register hooks after model definitionregisterHooks(User, { beforeCreate: async (entity) => { console.log("Creating user:", entity.email); entity.password = await hashPassword(entity.password); }, afterCreate: async (entity) => { console.log("User created with ID:", entity.id); }, beforeUpdate: async (entity) => { console.log("Updating user:", entity.id); }, beforeDelete: async (entity) => { console.log("Deleting user:", entity.id); },});
export { User };Multiple Callbacks
You can register multiple callbacks for the same hook type. They execute in order:
import { registerHooks } from "stabilize-orm";
registerHooks(User, { beforeCreate: [ async (entity) => { // First: validate if (!entity.email.includes("@")) { throw new Error("Invalid email"); } }, async (entity) => { // Second: hash password entity.password = await hashPassword(entity.password); }, async (entity) => { // Third: set defaults entity.isActive = true; }, ],});Hooks in defineModel
registerHooks() is not the only entry point. Hooks can also be declared inline on the model, which keeps the definition in one place:
const User = defineModel({ tableName: "users", columns: { /* ... */ }, hooks: { beforeCreate: async (entity) => { entity.password = await hashPassword(entity.password); }, },});Hooks as Class Methods
A third form: if the entity has a method whose name matches the hook, it is called. This is resolved on the instance, so it works only where the ORM holds a real entity — a hook declared this way on a row that arrived as a plain object will not be found.
class UserEntity { id!: string; email!: string;
async beforeCreate() { this.email = this.email.toLowerCase(); }}When more than one form is used, defineModel / registerHooks callbacks run first and class methods run second. Within a form, callbacks run in the order given.
Which Operations Fire Which Hook
| Method | Hooks | Notes |
|---|---|---|
create() | beforeCreate, beforeSave, afterCreate, afterSave | after* sees the row with its generated id |
update() | beforeUpdate, beforeSave, afterUpdate, afterSave | per call |
delete() | beforeDelete, afterDelete | Both fire on a soft delete too — the row is marked, not removed |
bulkCreate() | beforeCreate, beforeSave | All rows, then one insert — entirely inside a single transaction |
bulkUpdate() | afterUpdate, afterSave | Runs per matched row; no before* hook |
Note the asymmetry in bulkCreate(): the before hooks run for every row up front, but the insert is a single batched statement — so a hook that mutates the entity in place is reflected in the insert, while one that observes the database sees nothing yet.
Events Are a Separate Mechanism
Hooks are per model and per row. The orm.events emitter is ORM-wide and concerns the connection, not your data — the two are unrelated, and an event handler cannot observe a row change. See Events for the emitter and exactly which events fire today.