@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 functionid: string— record IDtable: string— table namehiddenKeys?: 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.