import { Elysia, t } from "elysia" import { gameComments, user } from "@/lib/db/schema" import { db } from "@/lib/db/index" import { eq, and, desc, sql, isNull } from "drizzle-orm" import { requireRole } from "@/lib/auth/guard" export const commentsRoutes = new Elysia({ prefix: "/games/:gameId/comments", detail: { tags: ["Comments"] }, }) // LIST top-level comments for a game (paginated) .get( "/", async ({ params, query }) => { const limit = Math.min(Number(query.limit) || 20, 100) const offset = Number(query.offset) || 0 const conditions = [ eq(gameComments.gameId, params.gameId), eq(gameComments.isRemoved, false), isNull(gameComments.parentId), // top-level only ] const data = await db .select({ id: gameComments.id, gameId: gameComments.gameId, userId: gameComments.userId, parentId: gameComments.parentId, content: gameComments.content, upvotes: gameComments.upvotes, createdAt: gameComments.createdAt, updatedAt: gameComments.updatedAt, userName: user.name, userImage: user.image, }) .from(gameComments) .innerJoin(user, eq(gameComments.userId, user.id)) .where(and(...conditions)) .orderBy(desc(gameComments.createdAt)) .limit(limit) .offset(offset) const [{ count }] = await db .select({ count: sql`count(*)::int` }) .from(gameComments) .where(and(...conditions)) return { data, total: count, limit, offset } }, { params: t.Object({ gameId: t.String() }), query: t.Object({ limit: t.Optional(t.String()), offset: t.Optional(t.String()), }), }, ) // GET replies for a specific comment .get( "/:id/replies", async ({ params, query }) => { const limit = Math.min(Number(query.limit) || 20, 100) const offset = Number(query.offset) || 0 const data = await db .select({ id: gameComments.id, gameId: gameComments.gameId, userId: gameComments.userId, parentId: gameComments.parentId, content: gameComments.content, upvotes: gameComments.upvotes, createdAt: gameComments.createdAt, updatedAt: gameComments.updatedAt, userName: user.name, userImage: user.image, }) .from(gameComments) .innerJoin(user, eq(gameComments.userId, user.id)) .where( and( eq(gameComments.parentId, params.id), eq(gameComments.isRemoved, false), ), ) .orderBy(desc(gameComments.createdAt)) .limit(limit) .offset(offset) return data }, { params: t.Object({ gameId: t.String(), id: t.String() }), query: t.Object({ limit: t.Optional(t.String()), offset: t.Optional(t.String()), }), }, ) // CREATE comment .post( "/", async ({ params, body, request, set }) => { const guard = await requireRole(request.headers, [ "user", "contributor", "admin", ]) if (!guard.ok) { set.status = guard.status return { error: guard.error } } // ── Anti-spam: content length cap ────────────────────────── const contentStr = JSON.stringify(body.content) if (contentStr.length > 50000) { set.status = 413 return { error: "Comment content exceeds maximum size (50KB)" } } // If parentId is provided, verify it exists and belongs to the same game if (body.parentId) { const [parent] = await db .select({ id: gameComments.id }) .from(gameComments) .where( and( eq(gameComments.id, body.parentId), eq(gameComments.gameId, params.gameId), ), ) .limit(1) if (!parent) { set.status = 404 return { error: "Parent comment not found" } } } // ── Sanitize: strip