Events
Observe the ORM from a single subscription point — connections, queries, errors, transactions and migrations.
For row-level changes — before create, after update — use Lifecycle Hooks instead; events are ORM-wide, hooks are per model.
Subscribing
Every Stabilize instance exposes an events emitter. Handlers are plain functions; register with on() and remove with off().
import { orm } from "./db";
orm.events.on("connection:open", (type) => { console.log("Connected to " + type); // "postgres" | "mysql" | "sqlite" | "mssql"});
orm.events.on("connection:close", () => { console.log("Connection closed");});API
on(event: StabilizeEvent, handler: (...args: any[]) => void): voidoff(event: StabilizeEvent, handler: (...args: any[]) => void): voidemit(event: StabilizeEvent, ...args: any[]): voidoff() matches by exact function reference, so keep a named reference rather than passing an inline arrow if you intend to unsubscribe:
const onOpen = (type) => console.log("Connected to " + type);
orm.events.on("connection:open", onOpen);orm.events.off("connection:open", onOpen); // removed
// This does NOT work - the second arrow is a different functionorm.events.on("connection:open", () => console.log("hi"));orm.events.off("connection:open", () => console.log("hi"));The Events
Nine names are declared, and all nine are emitted. Each carries a single payload object — except connection:open, which carries the backend name on its own.
"connection:open" // DBType — "postgres" | "mysql" | "sqlite" | "mssql" | "mongodb""connection:close" // no payload
"query" // { dbType, query, params, executionTime }"error" // { dbType, phase, error, ... } see below
"transaction:start" // { dbType }"transaction:complete" // { dbType }"transaction:error" // { dbType, phase: "transaction", error }
"migration:start" // { dbType, name, index, total }"migration:complete" // { dbType, name, index, total }The error payload
error is the one event whose shape depends on where it came from. phase tells you which, and is always present, so a listener filtering on it sees every error:
// phase: "query" - thrown by the retry loop, on every attempt, not only the last{ dbType, phase: "query", query, params, attempt, attempts, error }
// phase: "migration" - a migration step failed{ dbType, phase: "migration", error }
// phase: "transaction" - the callback threw, and the transaction rolled back{ dbType, phase: "transaction", error }Because error fires on every attempt, a query that fails twice and then succeeds produces two error events and no failed call. Compare attempt against attempts to tell a transient failure from the one that ended it.
migration:start carries a position
Both migration events fire once per unit of work rather than once per run, and report where that unit sits in the list. A run of forty migrations gives forty pairs, so progress can be shown against a real total instead of a single start and end you cannot attribute to anything.
orm.events.on("migration:complete", ({ name, index, total }) => { console.log(`[${index + 1}/${total}] ${name}`);});name is the migration's name for migrate(), and the table or collection being reconciled for autoMigrate() — which a caller who never wrote a migration could not otherwise attribute.
connection:open Fires Before You Can Subscribe
The connection opens inside the Stabilize constructor, so it is announced before the instance is returned to you. An on() call afterwards is simply too late:
const orm = new Stabilize(dbConfig);orm.events.on("connection:open", handler); // never called - already firedBuild the emitter yourself, subscribe, then pass it in. The fifth constructor argument is the only way to hear that first event:
import { Stabilize, StabilizeEmitter } from "stabilize-orm";
const events = new StabilizeEmitter();
events.on("connection:open", (type) => { console.log("Connected to " + type);});
const orm = new Stabilize(dbConfig, cacheConfig, loggerConfig, undefined, events);Passing the same emitter to several instances is supported — the client adopts it rather than building its own, so every query, error and transaction:* reaches the handlers you registered.
Handler Errors Are Swallowed
Each handler is invoked inside a try/catch that discards the error. A throwing handler cannot break the ORM call it fired from — but it also fails silently, so wrap your own logic if a failure there matters.
orm.events.on("connection:open", (type) => { try { metrics.increment("db.connect." + type); } catch (error) { // Without this catch, the failure would vanish entirely logger.error("metrics failed", error); }});