Files
jamo.siu@gmail.com 1e4f028685 refactor(db): switch records layer to Prisma
- Replace hand-written node:sqlite queries with a Prisma client using
  the better-sqlite3 driver adapter
- Version the schema with migrations: baseline table, timestamp
  normalisation, and WAL journal mode
- Store timestamps with an explicit UTC offset so they stop being parsed
  as local time
- Show created/updated times on the edit page, formatted with dayjs
- Move the root redirect into next.config to avoid a prerender error
- Run migrations from the container entrypoint before starting the server

BREAKING CHANGE: DNSMASQ_ADMIN_DB_PATH is replaced by DATABASE_URL, which
must be a Prisma connection URL (file:./data/dnsmasq-admin.db). Existing
database files are migrated in place and need no conversion.
2026-10-12 14:27:03 +08:00

69 lines
2.5 KiB
TypeScript

import { PrismaBetterSqlite3 } from "@prisma/adapter-better-sqlite3"
import { PrismaClient } from "./generated/prisma/client"
import type { DnsRecordModel } from "./generated/prisma/models"
import { env } from "./env"
export type RecordType = "A" | "CNAME"
/**
* The row Prisma hands back, with `type` narrowed to the union this app actually allows.
*
* SQLite has no enum support, so the generated field is a plain string even though every
* path that writes it goes through lib/validate.ts. Narrowing here keeps the rest of the app
* type-safe without pretending the database enforces it.
*/
export type DnsRecord = Omit<DnsRecordModel, "type" | "createdAt" | "updatedAt"> & {
type: RecordType
createdAt: string
updatedAt: string
}
/** Narrow a database string into RecordType, defaulting to `A` like the old mapper did. */
export function toRecordType(value: string): RecordType {
return value === "CNAME" ? "CNAME" : "A"
}
/**
* Timestamps travel through the app as ISO strings rather than `Date`.
*
* They are stored UTC with an explicit `+00:00` offset (see prisma/migrations/0002), so an
* ISO string round-trips unambiguously, whereas a `Date` crossing the Server Component /
* Client Component boundary invites accidental local-time formatting in the wrong place.
* Use lib/datetime.ts to render one.
*/
export function toTimestamp(value: Date): string {
return value.toISOString()
}
export function toDnsRecord(row: DnsRecordModel): DnsRecord {
return {
id: row.id,
domain: row.domain,
type: toRecordType(row.type),
value: row.value,
enabled: row.enabled,
comment: row.comment,
createdAt: toTimestamp(row.createdAt),
updatedAt: toTimestamp(row.updatedAt),
}
}
function createClient(): PrismaClient {
// The adapter strips the `file:` prefix before handing the path to better-sqlite3.
// `timeout` maps to busy_timeout and already defaults to 5000, which is what this app used
// before; listed explicitly so the behaviour survives a driver default change.
const adapter = new PrismaBetterSqlite3({ url: env.databaseUrl, timeout: 5000 })
return new PrismaClient({ adapter })
}
// Cached on globalThis so Next.js dev HMR does not open a new handle per reload; see the
// journal_mode migration note below for why one long-lived handle matters here.
const globalForDb = globalThis as typeof globalThis & {
__dnsmasqAdminPrisma?: PrismaClient
}
export function getDb(): PrismaClient {
globalForDb.__dnsmasqAdminPrisma ??= createClient()
return globalForDb.__dnsmasqAdminPrisma
}