import { Elysia, t } from "elysia" import { db } from "@/lib/db/index" import { games, gameVersions, gamePlatformSupport } from "@/lib/db/schema" import { ilike, eq } from "drizzle-orm" import { requireRole } from "@/lib/auth/guard" import { generateSlug } from "@/lib/utils/slug" export const gamesManualRoutes = new Elysia({ prefix: "/games", detail: { tags: ["Games"] } }) .post( "/manual", async ({ request, body, set }) => { const guard = await requireRole(request.headers, ["user", "contributor", "admin"]) if (!guard.ok) { set.status = guard.status return { error: guard.error } } // Duplicate check: exact title match (case-insensitive) const existing = await db .select({ id: games.id, title: games.title, source: games.source }) .from(games) .where(ilike(games.title, body.title)) .limit(5) if (existing.length > 0) { const exactMatch = existing.find( (g) => g.title.toLowerCase() === body.title.toLowerCase() ) if (exactMatch) { set.status = 409 return { error: "Game already exists", existingGame: exactMatch } } } // Generate slug for non-Steam games const slug = generateSlug(body.title) let finalSlug: string | null = slug if (slug) { let suffix = 2 while (true) { const existingSlug = await db .select({ id: games.id }) .from(games) .where(eq(games.slug, finalSlug)) .limit(1) if (existingSlug.length === 0) break finalSlug = `${slug}-${suffix}` suffix++ } } else { finalSlug = null } // Create the game const [game] = await db .insert(games) .values({ title: body.title, developer: body.developer || null, publisher: body.publisher || null, description: body.description || null, source: body.source || "manual", slug: finalSlug, headerImage: body.headerImage || null, capsuleImage: body.capsuleImage || null, storeUrl: body.storeUrl || null, genres: body.genres || null, releaseDate: body.releaseDate || null, createdBy: guard.user.id, }) .returning() // Create platform support entries if (body.platformSupport && body.platformSupport.length > 0) { await db.insert(gamePlatformSupport).values( body.platformSupport.map((ps) => ({ gameId: game.id, hardwareSlug: ps.hardwareSlug, isSupported: ps.isSupported, protonStatus: ps.protonStatus as "native" | "proton" | "unsupported" | "unknown", })) ) } // Create initial game version await db.insert(gameVersions).values({ gameId: game.id, isLatest: true, }) set.status = 201 return { game } }, { body: t.Object({ title: t.String({ minLength: 1, maxLength: 200 }), developer: t.Optional(t.String()), publisher: t.Optional(t.String()), description: t.Optional(t.String()), source: t.Optional(t.Union([t.Literal("manual"), t.Literal("gog"), t.Literal("epic")])), headerImage: t.Optional(t.String()), capsuleImage: t.Optional(t.String()), storeUrl: t.Optional(t.String()), genres: t.Optional(t.Array(t.String())), releaseDate: t.Optional(t.String()), platformSupport: t.Optional( t.Array( t.Object({ hardwareSlug: t.String(), isSupported: t.Boolean(), protonStatus: t.String(), }) ) ), }), } ) .put( "/:gameId/manual", async ({ request, params, body, set }) => { const guard = await requireRole(request.headers, ["user", "contributor", "admin"]) if (!guard.ok) { set.status = guard.status return { error: guard.error } } const [game] = await db .select() .from(games) .where(eq(games.id, params.gameId)) .limit(1) if (!game) { set.status = 404 return { error: "Game not found" } } if (game.source === "steam") { set.status = 403 return { error: "Steam games are synced automatically and cannot be manually edited" } } // Only creator or admin can edit if (game.createdBy && game.createdBy !== guard.user.id && guard.user.role !== "admin") { set.status = 403 return { error: "Only the creator or an admin can edit this game" } } const [updated] = await db .update(games) .set({ title: body.title ?? game.title, developer: body.developer ?? game.developer, publisher: body.publisher ?? game.publisher, description: body.description ?? game.description, headerImage: body.headerImage ?? game.headerImage, capsuleImage: body.capsuleImage ?? game.capsuleImage, storeUrl: body.storeUrl ?? game.storeUrl, genres: body.genres ?? game.genres, releaseDate: body.releaseDate ?? game.releaseDate, updatedAt: new Date(), }) .where(eq(games.id, params.gameId)) .returning() return { game: updated } }, { params: t.Object({ gameId: t.String() }), body: t.Object({ title: t.Optional(t.String()), developer: t.Optional(t.String()), publisher: t.Optional(t.String()), description: t.Optional(t.String()), headerImage: t.Optional(t.String()), capsuleImage: t.Optional(t.String()), storeUrl: t.Optional(t.String()), genres: t.Optional(t.Array(t.String())), releaseDate: t.Optional(t.String()), }), } )