import type { SupabaseClient } from "@supabase/supabase-js"; import type { Database, Tables } from "@/lib/database.types"; import type { ScoringInput } from "@/lib/scoring"; type Client = SupabaseClient; export type EventData = { event: Tables<"events">; teams: Tables<"teams">[]; games: Tables<"games">[]; criteria: Tables<"criteria">[]; judges: Tables<"judges">[]; scores: Tables<"scores">[]; adjustments: Tables<"adjustments">[]; }; const PAGE_SIZE = 1000; // `games!inner()` joins through games purely to filter by event, so child rows load in // the same round trip as the games themselves instead of waiting for their ids. const VIA_GAME = "*, games!inner()"; /** Supabase returns at most 1000 rows per request; a busy night has more scores than that. */ async function loadAllScores(supabase: Client, eventId: string): Promise[]> { const rows: Tables<"scores">[] = []; for (let from = 0; ; from += PAGE_SIZE) { const { data, error } = await supabase .from("scores") .select(VIA_GAME) .eq("games.event_id", eventId) .order("id") .range(from, from + PAGE_SIZE - 1) .overrideTypes[], { merge: false }>(); if (error) throw new Error(`Could not load scores: ${error.message}`); rows.push(...data); if (data.length < PAGE_SIZE) return rows; } } /** * Everything the scoring engine needs for one event, in one parallel round trip. * Shared by the admin pages (RLS client) and the public/judge APIs (service client, * after the token has been validated). */ export async function loadEventData(supabase: Client, event: Tables<"events">): Promise { const [teams, games, adjustments, criteria, judges, scores] = await Promise.all([ supabase.from("teams").select("*").eq("event_id", event.id).order("sort_order").order("created_at"), supabase.from("games").select("*").eq("event_id", event.id).order("sort_order").order("created_at"), supabase.from("adjustments").select("*").eq("event_id", event.id).order("created_at"), supabase .from("criteria") .select(VIA_GAME) .eq("games.event_id", event.id) .order("sort_order") .overrideTypes[], { merge: false }>(), supabase .from("judges") .select(VIA_GAME) .eq("games.event_id", event.id) .order("created_at") .overrideTypes[], { merge: false }>(), loadAllScores(supabase, event.id), ]); const error = teams.error ?? games.error ?? adjustments.error ?? criteria.error ?? judges.error; if (error) throw new Error(`Could not load the event: ${error.message}`); return { event, teams: teams.data ?? [], games: games.data ?? [], adjustments: adjustments.data ?? [], criteria: criteria.data ?? [], judges: judges.data ?? [], scores, }; } /** Maps DB rows to the scoring engine's plain input. */ export function toScoringInput(data: EventData): ScoringInput { return { teams: data.teams.map((team) => ({ id: team.id, sortOrder: team.sort_order })), games: data.games.map((game) => ({ id: game.id, scoringMode: game.scoring_mode, pointsMode: game.points_mode, multiplier: game.multiplier, placementPoints: game.placement_points, status: game.status, })), criteria: data.criteria.map((criterion) => ({ id: criterion.id, gameId: criterion.game_id, weight: criterion.weight, })), judges: data.judges.map((judge) => ({ id: judge.id, gameId: judge.game_id, revoked: judge.revoked })), scores: data.scores.map((score) => ({ gameId: score.game_id, teamId: score.team_id, criterionId: score.criterion_id, judgeId: score.judge_id, value: score.value, })), adjustments: data.adjustments.map((adjustment) => ({ teamId: adjustment.team_id, points: adjustment.points, })), }; }