feat(admin): DB CRUD panel + /api/admin endpoints (#38)
CI / build + unit + e2e (pull_request) Successful in 3m47s
CI / build + unit + e2e (pull_request) Successful in 3m47s
- ADMIN button in the mission-control header opens a table picker + row grid with add/edit/delete (components/admin-panel.tsx), deliberately unauthenticated by explicit request
- /api/admin/tables and /api/admin/tables/{name}/rows driven off a name→table registry built from lib/db/schema.ts (lib/db/admin.ts) — no per-table code; 404 unknown table, 400 malformed payload, 409 constraint violation
- OpenAPI spec updated for all four admin operations + Admin* schemas
- unit: lib/db/admin.test.ts (registry, CRUD round-trips, composite PKs, error mapping); e2e: read-only admin.spec.ts
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
import { getGameDb } from '@/lib/db/client'
|
||||
import { AdminError, deleteAdminRow, insertAdminRow, readAdminRows, updateAdminRow } from '@/lib/db/admin'
|
||||
|
||||
// #38 — rows of one admin table: GET (capped list), POST (insert), PATCH ({key, values}), DELETE ({key}).
|
||||
async function jsonBody(req: Request): Promise<Record<string, unknown>> {
|
||||
let body: unknown
|
||||
try {
|
||||
body = await req.json()
|
||||
} catch {
|
||||
throw new AdminError(400, 'Invalid JSON body')
|
||||
}
|
||||
if (typeof body !== 'object' || body === null || Array.isArray(body)) throw new AdminError(400, 'Body must be a JSON object')
|
||||
return body as Record<string, unknown>
|
||||
}
|
||||
|
||||
async function run<T>(fn: () => Promise<T>): Promise<Response> {
|
||||
try {
|
||||
return Response.json(await fn())
|
||||
} catch (err) {
|
||||
if (err instanceof AdminError) return Response.json({ error: err.message }, { status: err.status })
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(req: Request, ctx: { params: Promise<{ name: string }> }) {
|
||||
const db = await getGameDb()
|
||||
const { name } = await ctx.params
|
||||
const url = new URL(req.url)
|
||||
const limitRaw = url.searchParams.get('limit')
|
||||
const offsetRaw = url.searchParams.get('offset')
|
||||
const limit = limitRaw === null ? 50 : Number(limitRaw)
|
||||
const offset = offsetRaw === null ? 0 : Number(offsetRaw)
|
||||
return run(() => readAdminRows(db, name, limit, offset))
|
||||
}
|
||||
|
||||
export async function POST(req: Request, ctx: { params: Promise<{ name: string }> }) {
|
||||
const db = await getGameDb()
|
||||
const { name } = await ctx.params
|
||||
return run(async () => insertAdminRow(db, name, await jsonBody(req)))
|
||||
}
|
||||
|
||||
export async function PATCH(req: Request, ctx: { params: Promise<{ name: string }> }) {
|
||||
const db = await getGameDb()
|
||||
const { name } = await ctx.params
|
||||
return run(async () => {
|
||||
const body = await jsonBody(req)
|
||||
return updateAdminRow(db, name, body.key, body.values)
|
||||
})
|
||||
}
|
||||
|
||||
export async function DELETE(req: Request, ctx: { params: Promise<{ name: string }> }) {
|
||||
const db = await getGameDb()
|
||||
const { name } = await ctx.params
|
||||
return run(async () => deleteAdminRow(db, name, (await jsonBody(req)).key))
|
||||
}
|
||||
Reference in New Issue
Block a user