MonoStack

@repo/db

Functions

overviewToQuery

Applies DataGrid filters, sorting, and pagination to a Drizzle select query. Put fixed constraints on the initial query; overview filters are combined with the existing where clause.

return overviewToQuery(query.where(eq(table.status, 'active')).$dynamic(), table, data)

Pass an explicit grouping allowlist to enable server-side grouping. Keys must match selected result fields. Values can reference base, joined, or computed SQL columns.

return overviewToQuery(query, usersTable, data, {
  groupingColumns: {
    status: usersTable.status,
    roleName: rolesTable.name,
  },
})

Grouped requests return one database-grouped hierarchy level at a time. The response contains { grouped: true, data, count, leafCount }; count paginates the current group level and leafCount is the full filtered leaf total. Each group includes a path. Send that path back as groupingPath to load its next group level or paginated leaf rows.

getOr404

This function simply takes a function and throws a 404 if the result is null or undefined

import { getOr404 } from '@repo/db'

const item = await getOr404(organizationRepository.get(session, params.id))

Audit Logs

@repo/db/audit — schema, hook, repository

import { auditLogsTable, createAuditHook, createAuditLog } from '@repo/db/audit'

export const auditTrail = createAuditHook({
  getChangedBy: () => getSession()?.entity.id,
  auditTable: auditLogsTable,
  logger,
})

@repo/db/audit/server — query builders

import { buildQueries } from '@repo/db/audit/server'

export const { getAuditLogQuery } = buildQueries(getGlobalDb)

@repo/db/client — UI components

<script lang="ts">
  import { AuditLogButton } from '@repo/db/client'
</script>

<!-- Via DetailHeader (preferred) -->
<DetailHeader title="User" {deleteForm} getAuditLogs={getAuditLogQuery} table="users" />

<!-- Standalone -->
<AuditLogButton query={getAuditLogQuery} id={params.id} table="example_models" />

Props:

  • query: (args: { id: string; table: string }) => Promise<AuditLogEntry[]> — query function
  • id: string — record ID
  • table: string — table name
  • hiddenKeys?: Set<string> — snapshot keys to hide (default: updatedBy, createdBy)

Nested snapshot objects and arrays are expandable. Object fields show scalar values first; arrays retain their original order. The dialog keeps a viewport gutter on small screens.

Same-Table Versioning

Use same-table versioning when the current row must keep a stable primary key while previous values stay queryable.

Current rows have versionOfId = null. Snapshot rows have versionOfId pointing to the current row's id.

import {
  createNextVersionValues,
  createVersionRelations,
  createVersionSnapshotValues,
  versionedModelColumns,
  versionedTableIndexes,
} from '@repo/db/versioning'
import { generalModelColumns, tenantSchema } from '@repo/auth/database'
import { and, eq, isNull, relations, sql } from 'drizzle-orm'

export const exampleTable = tenantSchema.table(
  'example',
  (t) => ({
    ...generalModelColumns,
    ...versionedModelColumns,
    name: t.text().notNull(),
  }),
  (t) => versionedTableIndexes('example', t),
)

export const exampleRelations = relations(exampleTable, (helpers) => ({
  ...createVersionRelations(exampleTable, helpers),
}))

await db.transaction(async (tx) => {
  const current = await tx.query.exampleTable.findFirst({
    where: and(eq(exampleTable.id, id), isNull(exampleTable.versionOfId)),
  })

  if (!current) {
    throw error(404, 'Not found')
  }

  await tx.insert(exampleTable).values(
    createVersionSnapshotValues(current, (row) => ({
      name: row.name,
    })),
  )

  await tx
    .update(exampleTable)
    .set({
      name: nextName,
      version: sql`${exampleTable.version} + 1`,
      ...createNextVersionValues(changedBy),
    })
    .where(and(eq(exampleTable.id, id), isNull(exampleTable.versionOfId)))
})

Each table explicitly chooses which domain columns are copied into snapshots. Do not blindly copy model columns like id, createdAt, updatedAt, or soft-delete fields.

On this page