From 128e668e012b84ea4fd3f37efc8fe365c7f4d70b Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Sat, 25 Apr 2026 18:28:36 +0800 Subject: [PATCH] feat(api): add performance entries CRUD with verify and stats endpoints --- lib/api/performance.ts | 115 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 lib/api/performance.ts diff --git a/lib/api/performance.ts b/lib/api/performance.ts new file mode 100644 index 0000000..329b6a7 --- /dev/null +++ b/lib/api/performance.ts @@ -0,0 +1,115 @@ +import { Elysia, t } from "elysia" +import { createCrudRoutes } from "./crud-builder" +import { performanceEntries, games, gameVersions } from "@/lib/db/schema" +import { db } from "@/lib/db/index" +import { eq, and, desc, sql } from "drizzle-orm" +import { requireRole } from "@/lib/auth/guard" + +// ── Performance Entries CRUD ────────────────────────────────────── +export const performanceRoutes = createCrudRoutes(performanceEntries, { + prefix: "/performance", + name: "Performance Entry", + auth: { read: "public", write: "user", delete: "admin" }, + softDelete: true, + search: { fields: ["userNotes"] }, + filter: { fields: ["hardwareSlug", "fsrVersion", "frameGenMethod"] }, +}) + +// ── Verify endpoint (admin/mod) ─────────────────────────────────── +export const performanceVerifyRoutes = new Elysia({ + prefix: "/performance", +}) + .post( + "/:id/verify", + async ({ params, request, set }) => { + const guard = await requireRole(request.headers, ["contributor", "admin"]) + if (!guard.ok) { + set.status = guard.status + return { error: guard.error } + } + + const [entry] = await db + .select() + .from(performanceEntries) + .where(eq(performanceEntries.id, params.id)) + .limit(1) + + if (!entry) { + set.status = 404 + return { error: "Performance entry not found" } + } + + if (entry.verifiedAt) { + set.status = 409 + return { error: "Entry already verified" } + } + + const [updated] = await db + .update(performanceEntries) + .set({ + verifiedAt: new Date(), + verifiedBy: guard.user.id, + updatedAt: new Date(), + }) + .where(eq(performanceEntries.id, params.id)) + .returning() + + return updated + }, + { + params: t.Object({ id: t.String() }), + }, + ) + // ── Stats endpoint: aggregated performance for a game+hardware combo ── + .get( + "/stats", + async ({ query, set }) => { + const { gameId, hardwareSlug, fsrVersion } = query as { + gameId?: string + hardwareSlug?: string + fsrVersion?: string + } + + if (!gameId) { + set.status = 400 + return { error: "gameId query parameter is required" } + } + + const conditions = [ + eq(games.id, gameId), + eq(performanceEntries.isRemoved, false), + ] + + if (hardwareSlug) { + conditions.push(eq(performanceEntries.hardwareSlug, hardwareSlug)) + } + if (fsrVersion) { + conditions.push(eq(performanceEntries.fsrVersion, fsrVersion as any)) + } + + // Join through gameVersions to get to games + const stats = await db + .select({ + count: sql`count(*)::int`, + fpsAvg: sql`avg(${performanceEntries.fpsAvg})::real`, + fpsLow: sql`percentile_cont(0.1) within group (order by ${performanceEntries.fpsAvg})::real`, + fpsHigh: sql`percentile_cont(0.9) within group (order by ${performanceEntries.fpsAvg})::real`, + }) + .from(performanceEntries) + .innerJoin( + gameVersions, + eq(performanceEntries.versionId, gameVersions.id), + ) + .innerJoin(games, eq(gameVersions.gameId, games.id)) + .where(and(...conditions)) + + return stats[0] ?? { count: 0, fpsAvg: null, fpsLow: null, fpsHigh: null } + }, + { + query: t.Object({ + gameId: t.String(), + hardwareSlug: t.Optional(t.String()), + fsrVersion: t.Optional(t.String()), + }), + }, + )