import { database } from "~/database/context"; import * as schema from "~/database/schema"; import { eq, inArray } from "drizzle-orm"; import { calculatePickInfo } from "~/models/draft-utils"; import { getUserDisplayName } from "~/models/user"; import type { LoaderFunctionArgs } from "react-router"; const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; export async function loader({ params }: LoaderFunctionArgs) { const { seasonId } = params; // Fix #1: validate UUID format up front so malformed IDs return 400, not a DB error if (!seasonId || !UUID_RE.test(seasonId)) { return Response.json({ error: "Invalid season ID" }, { status: 400 }); } const db = database(); const season = await db.query.seasons.findFirst({ where: eq(schema.seasons.id, seasonId), }); if (!season) { return Response.json({ error: "Season not found" }, { status: 404 }); } const draftSlots = await db.query.draftSlots.findMany({ where: eq(schema.draftSlots.seasonId, seasonId), orderBy: schema.draftSlots.draftOrder, with: { team: true }, }); const totalTeams = draftSlots.length; const totalPicks = totalTeams * season.draftRounds; const currentPickNumber = season.currentPickNumber ?? 1; const isDraftComplete = season.status === "active" || season.status === "completed"; // Fix #4: guard against totalTeams === 0 before calling calculatePickInfo let onTheClockSlot: (typeof draftSlots)[number] | null = null; if (season.status === "draft" && totalTeams > 0) { const { pickInRound } = calculatePickInfo(currentPickNumber, totalTeams); onTheClockSlot = draftSlots.find((slot) => slot.draftOrder === pickInRound) ?? null; } // All picks with participant + sport + team owner info const picksRaw = await db .select({ pickNumber: schema.draftPicks.pickNumber, round: schema.draftPicks.round, teamName: schema.teams.name, teamOwnerId: schema.teams.ownerId, participantName: schema.participants.name, sport: schema.sports.name, }) .from(schema.draftPicks) .innerJoin(schema.teams, eq(schema.draftPicks.teamId, schema.teams.id)) .innerJoin(schema.participants, eq(schema.draftPicks.participantId, schema.participants.id)) .innerJoin(schema.sportsSeasons, eq(schema.participants.sportsSeasonId, schema.sportsSeasons.id)) .innerJoin(schema.sports, eq(schema.sportsSeasons.sportId, schema.sports.id)) .where(eq(schema.draftPicks.seasonId, seasonId)) .orderBy(schema.draftPicks.pickNumber); // Fix #2: include the on-the-clock owner in the batch so we never make a separate user query const picksOwnerIds = picksRaw.map((p) => p.teamOwnerId).filter(Boolean) as string[]; const clockOwnerId = onTheClockSlot?.team.ownerId ?? null; const allOwnerIds = [...new Set([...picksOwnerIds, ...(clockOwnerId ? [clockOwnerId] : [])])]; const usernameByUserId = new Map(); if (allOwnerIds.length > 0) { const owners = await db .select({ id: schema.users.id, username: schema.users.username, displayName: schema.users.displayName, }) .from(schema.users) .where(inArray(schema.users.id, allOwnerIds)); for (const owner of owners) { usernameByUserId.set(owner.id, getUserDisplayName(owner)); } } // Fix #3: single helper so onTheClock and picks use identical null-fallback logic const getUsername = (ownerId: string | null) => ownerId ? (usernameByUserId.get(ownerId) ?? null) : null; const onTheClock = onTheClockSlot ? { teamName: onTheClockSlot.team.name, username: getUsername(onTheClockSlot.team.ownerId) } : null; const picks = picksRaw.map((p) => ({ pickNumber: p.pickNumber, round: p.round, teamName: p.teamName, username: getUsername(p.teamOwnerId), participantName: p.participantName, sport: p.sport, })); return Response.json({ seasonId, status: season.status, currentPickNumber, totalPicks, isDraftComplete, isPaused: season.draftPaused, onTheClock, picks, }); }