Files
dnsmasq-admin/app/records/actions.ts
T
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

116 lines
3.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use server"
import { revalidatePath } from "next/cache"
import { redirect } from "next/navigation"
import type { ActionState } from "@/lib/action-state"
import { writeManagedConfig } from "@/lib/dnsmasq-config"
import { restartDnsmasqContainer } from "@/lib/docker"
import {
DuplicateDomainError,
createRecord,
listEnabledRecords,
removeRecord,
setRecordEnabled,
updateRecord,
} from "@/lib/records"
import { parseRecordForm } from "@/lib/validate"
const RECORDS_PATH = "/records"
function readId(formData: FormData): number | null {
const raw = formData.get("id")
const id = typeof raw === "string" ? Number(raw) : NaN
return Number.isInteger(id) && id > 0 ? id : null
}
function asDuplicateState(error: unknown): ActionState | null {
if (error instanceof DuplicateDomainError) {
return { ok: false, fieldErrors: { domain: `域名 ${error.domain} 已存在` } }
}
return null
}
export async function createRecordAction(
_prev: ActionState,
formData: FormData
): Promise<ActionState> {
const parsed = parseRecordForm(formData)
if (!parsed.ok) return { ok: false, fieldErrors: parsed.fieldErrors }
try {
await createRecord(parsed.input)
} catch (error) {
const duplicate = asDuplicateState(error)
if (duplicate) return duplicate
throw error
}
revalidatePath(RECORDS_PATH)
redirect(RECORDS_PATH)
}
export async function updateRecordAction(
_prev: ActionState,
formData: FormData
): Promise<ActionState> {
const id = readId(formData)
if (id === null) return { ok: false, message: "记录不存在,可能已被删除" }
const parsed = parseRecordForm(formData)
if (!parsed.ok) return { ok: false, fieldErrors: parsed.fieldErrors }
try {
await updateRecord(id, parsed.input)
} catch (error) {
const duplicate = asDuplicateState(error)
if (duplicate) return duplicate
throw error
}
revalidatePath(RECORDS_PATH)
redirect(RECORDS_PATH)
}
export async function deleteRecordAction(formData: FormData): Promise<void> {
const id = readId(formData)
if (id === null) return
await removeRecord(id)
revalidatePath(RECORDS_PATH)
}
export async function toggleRecordAction(formData: FormData): Promise<void> {
const id = readId(formData)
if (id === null) return
// The hidden field carries the state this record should move *to*.
await setRecordEnabled(id, formData.get("enabled") === "1")
revalidatePath(RECORDS_PATH)
}
/**
* Regenerate the managed dnsmasq config from SQLite and restart the container.
*
* Deliberately manual rather than automatic, so several edits can be batched into one
* restart (see the plan: 手动应用并重启).
*/
export async function applyConfigAction(
_prev: ActionState,
_formData: FormData
): Promise<ActionState> {
const written = await writeManagedConfig(await listEnabledRecords())
const reload = await restartDnsmasqContainer()
revalidatePath(RECORDS_PATH)
const writtenMessage = `已写入 ${written.recordCount} 条记录到 ${written.path}`
if (reload.status === "restarted") {
return { ok: true, message: `${writtenMessage},${reload.detail}` }
}
if (reload.status === "skipped") {
return { ok: true, message: writtenMessage, detail: reload.detail }
}
return { ok: false, message: `${writtenMessage},但重启失败`, detail: reload.detail }
}