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.
This commit is contained in:
25 files changed
+1796
-141
No files matched your search
@@ -0,0 +1,31 @@
|
||||
-- Baseline: the `records` table as Prisma itself generates it from prisma/schema.prisma.
|
||||
--
|
||||
-- This SQL is kept byte-identical to the output of
|
||||
-- `prisma migrate diff --from-empty --to-schema prisma/schema.prisma --script`
|
||||
-- on purpose. The alternative — adding `COLLATE NOCASE` on `domain` and a
|
||||
-- `CHECK (type IN ('A','CNAME'))` by hand — makes Prisma see permanent drift and propose a
|
||||
-- DROP TABLE + rebuild migration the next time anyone runs `migrate dev`, which would silently
|
||||
-- drop those very constraints. A clean diff is worth more here than the extra constraints,
|
||||
-- because both properties are enforced elsewhere:
|
||||
--
|
||||
-- - case-insensitive uniqueness: lib/validate.ts lowercases every domain before it reaches
|
||||
-- the database, so `FOO.com` and `foo.com` can never both be inserted;
|
||||
-- - case-insensitive ordering: lib/records.ts sorts with localeCompare after fetching,
|
||||
-- since SQLite would otherwise order by binary collation ('Zoo' before 'apple');
|
||||
-- - the type whitelist: again lib/validate.ts, plus the generated union in lib/db.ts.
|
||||
--
|
||||
-- Existing databases installed before this migration already ran the earlier hand-written
|
||||
-- version and keep their column-level COLLATE NOCASE; nothing about them changes.
|
||||
CREATE TABLE "records" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"domain" TEXT NOT NULL,
|
||||
"type" TEXT NOT NULL,
|
||||
"value" TEXT NOT NULL,
|
||||
"enabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"comment" TEXT NOT NULL DEFAULT '',
|
||||
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" DATETIME NOT NULL
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "records_domain_key" ON "records"("domain");
|
||||
@@ -0,0 +1,18 @@
|
||||
-- Rewrite timestamps written by the v0.1.0 schema into the format Prisma expects.
|
||||
--
|
||||
-- Background: the old column default was `datetime('now')`, producing values like
|
||||
-- `2026-10-09 08:55:10`. SQLite stores that UTC, but JavaScript parses the space-separated
|
||||
-- form as *local* time — on a UTC+8 machine `new Date('2026-10-09 08:55:10')` yields
|
||||
-- 00:55:10Z, silently off by eight hours. Prisma 7's better-sqlite3 adapter writes
|
||||
-- `2026-10-09T08:55:10.000+00:00` instead, which is unambiguous.
|
||||
--
|
||||
-- Leaving old rows in the legacy format would give one column two different parse rules
|
||||
-- depending on when the row was created, so every existing row is normalised here.
|
||||
--
|
||||
-- Idempotent: the WHERE clause skips anything already in ISO shape, so this is a no-op on
|
||||
-- freshly migrated databases and safe to re-run.
|
||||
UPDATE "records"
|
||||
SET
|
||||
"created_at" = strftime('%Y-%m-%dT%H:%M:%S.000+00:00', "created_at"),
|
||||
"updated_at" = strftime('%Y-%m-%dT%H:%M:%S.000+00:00', "updated_at")
|
||||
WHERE "created_at" NOT LIKE '____-__-__T%';
|
||||
@@ -0,0 +1,11 @@
|
||||
-- Set journal_mode to WAL, exactly as the pre-Prisma code did on every connection open.
|
||||
--
|
||||
-- WAL lets readers proceed while a write is in flight, which matters here: the SQLite file is
|
||||
-- frequently opened by something else at the same time — the sqlite3 CLI, or a database
|
||||
-- console in an IDE — and the UI answers from it on every page load.
|
||||
--
|
||||
-- Unlike busy_timeout, this cannot be set from the app: the better-sqlite3 adapter takes only
|
||||
-- a connection URL and driver options, with no hook for PRAGMAs. It is safe to do here
|
||||
-- because `PRAGMA journal_mode` is a persistent property of the database file, so it only has
|
||||
-- to be set once rather than on every connection. A no-op on databases already in WAL.
|
||||
PRAGMA journal_mode = WAL;
|
||||
@@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "sqlite"
|
||||
@@ -0,0 +1,37 @@
|
||||
// dnsmasq-admin — single-table schema mapping the `records` table that shipped with v0.1.0,
|
||||
// so existing databases keep working untouched.
|
||||
//
|
||||
// Keep this file as the single source of truth for the table shape, and keep migrations
|
||||
// byte-identical to what `prisma migrate diff` produces from it. Hand-editing a migration to
|
||||
// add something this file cannot express — such as column-level COLLATE NOCASE on `domain`, or
|
||||
// a CHECK constraint on `type` — leaves Prisma reporting permanent drift, and the next
|
||||
// `migrate dev` then proposes a DROP TABLE + rebuild migration that silently drops it. Those
|
||||
// two safeguards therefore live in application code; see
|
||||
// prisma/migrations/0001_baseline/migration.sql for the details.
|
||||
//
|
||||
// The `type` field is a plain String because SQLite has no enum support; the union is enforced
|
||||
// by lib/validate.ts and by the `RecordType` type in lib/db.ts.
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client"
|
||||
output = "../lib/generated/prisma"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
}
|
||||
|
||||
/// Named `DnsRecord` (table `records`) rather than `Record` to avoid colliding with
|
||||
/// TypeScript's built-in `Record<K, V>` utility type.
|
||||
model DnsRecord {
|
||||
id Int @id @default(autoincrement())
|
||||
domain String @unique
|
||||
type String
|
||||
value String
|
||||
enabled Boolean @default(true)
|
||||
comment String @default("")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
@@map("records")
|
||||
}
|
||||
Reference in new issue
Block a user