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:
jamo.siu@gmail.com committed 2026-10-12 14:27:03 +08:00
1 parent 97ad09ad8a
commit 1e4f028685
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;
+3
View File
@@ -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"