brackt/app/models/tournament.ts
Chris Parsons 3a4fe41eb5
feat(admin): canonical tournament list + result entry routes
New routes /admin/tournaments and /admin/tournaments/:id. The detail
page lets admins enter tournament results once via paste-and-parse,
then calls syncTournamentResults to fan out to every linked window.

Phase 3 Task 4 of canonical tournament layer migration.
2026-05-01 22:03:39 +00:00

138 lines
3.3 KiB
TypeScript

import { eq, and, asc, desc } from "drizzle-orm";
import { database } from "~/database/context";
import * as schema from "~/database/schema";
export type Tournament = typeof schema.tournaments.$inferSelect;
export type NewTournament = typeof schema.tournaments.$inferInsert;
export type TournamentStatus = Tournament["status"];
export async function createTournament(
data: NewTournament
): Promise<Tournament> {
const db = database();
const [tournament] = await db
.insert(schema.tournaments)
.values(data)
.returning();
return tournament;
}
export async function getTournamentById(
id: string
): Promise<Tournament | null> {
const db = database();
const results = await db
.select()
.from(schema.tournaments)
.where(eq(schema.tournaments.id, id))
.limit(1);
return results[0] ?? null;
}
export async function findTournamentsBySport(
sportId: string
): Promise<Tournament[]> {
const db = database();
return await db
.select()
.from(schema.tournaments)
.where(eq(schema.tournaments.sportId, sportId))
.orderBy(asc(schema.tournaments.year), asc(schema.tournaments.startsAt));
}
export async function findTournamentBySportNameYear(
sportId: string,
name: string,
year: number
): Promise<Tournament | null> {
const db = database();
const results = await db
.select()
.from(schema.tournaments)
.where(
and(
eq(schema.tournaments.sportId, sportId),
eq(schema.tournaments.name, name),
eq(schema.tournaments.year, year)
)
)
.limit(1);
return results[0] ?? null;
}
export async function upsertTournament(
data: NewTournament
): Promise<Tournament> {
if (!data.sportId || !data.name || !data.year) {
throw new Error("sportId, name, and year are required for upsert");
}
const existing = await findTournamentBySportNameYear(
data.sportId,
data.name,
data.year
);
if (existing) {
return existing;
}
return await createTournament(data);
}
export async function findAllTournamentsGroupedBySport(): Promise<
Array<{
sport: { id: string; name: string };
tournaments: Tournament[];
}>
> {
const db = database();
const rows = await db
.select({
tournament: schema.tournaments,
sport: {
id: schema.sports.id,
name: schema.sports.name,
},
})
.from(schema.tournaments)
.innerJoin(schema.sports, eq(schema.tournaments.sportId, schema.sports.id))
.orderBy(
asc(schema.sports.name),
desc(schema.tournaments.year),
asc(schema.tournaments.startsAt)
);
const groupMap = new Map<
string,
{ sport: { id: string; name: string }; tournaments: Tournament[] }
>();
for (const row of rows) {
const existing = groupMap.get(row.sport.id);
if (existing) {
existing.tournaments.push(row.tournament);
} else {
groupMap.set(row.sport.id, {
sport: row.sport,
tournaments: [row.tournament],
});
}
}
return Array.from(groupMap.values());
}
export async function updateTournamentStatus(
id: string,
status: TournamentStatus
): Promise<Tournament> {
const db = database();
const [tournament] = await db
.update(schema.tournaments)
.set({ status, updatedAt: new Date() })
.where(eq(schema.tournaments.id, id))
.returning();
return tournament;
}