MonoStack
Concepts

Database Hooks

Hooks let you intercept database mutations (insert, update, delete) without modifying repository code. The system uses a Proxy to wrap the Drizzle database instance.

wrapDbWithHooks

Wraps a getDbContext function so every db instance it returns is proxied with hooks.

import { wrapDbWithHooks } from '@repo/db'
import type { DatabaseHooks } from '@repo/db'

const hooks: DatabaseHooks = {
  onMutation: async (context, execute) => {
    console.log(`${context.operation} on ${context.table}`)
    return execute()
  },
}

const getDbContextWithHooks = wrapDbWithHooks(getDbContext, hooks)

The returned function has the same signature as the original getDbContext. The db property on its return value is proxied.

DatabaseHooks

interface DatabaseHooks {
  onMutation?: MutationHook
  onSetUpdate?: SetUpdateHook
  onSetInsert?: SetInsertHook
}

onMutation

Called before execution of any insert, update, or delete. Receives a HooksContext and an execute function. You must call execute() to proceed.

import type { MutationHook } from '@repo/db'

const onMutation: MutationHook = async (context, execute) => {
  // context.db        -- the database instance
  // context.table     -- the PgTable being operated on
  // context.operation -- 'insert' | 'update' | 'delete'
  // context.values    -- values for insert/update (if applicable)
  // context.where     -- SQL where clause (if applicable)

  const result = await execute()
  return result
}

onSetUpdate

Intercepts the .set(values) call on an update query. Return the (possibly modified) values.

import type { SetUpdateHook } from '@repo/db'

const onSetUpdate: SetUpdateHook = (values, context) => {
  // context.db, context.table, context.values available
  return { ...values, updatedAt: new Date() }
}

onSetInsert

Intercepts the .values(values) call on an insert query. Return the (possibly modified) values.

import type { SetInsertHook } from '@repo/db'

const onSetInsert: SetInsertHook = (values, context) => {
  return { ...values, createdAt: new Date() }
}

HooksContext

interface HooksContext<TTable, TDb> {
  db: TDb
  table: TTable
  operation: 'insert' | 'update' | 'delete'
  values?: TTable['$inferInsert'] | TTable['$inferInsert'][]
  where?: SQL
}

Transaction Support

The proxy also wraps transactions. Any db.transaction(...) call will proxy the tx object with the same hooks:

await db.transaction(async (tx) => {
  // tx is also proxied -- hooks fire for operations inside the transaction
  await tx.insert(users).values({ name: 'Alice' })
})

On this page