|
|
|
@@ -59,74 +59,98 @@ async function hydrateMatch(row: typeof matches.$inferSelect) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isMissingTable(e: unknown): boolean {
|
|
|
|
|
const msg = e instanceof Error ? e.message : String(e)
|
|
|
|
|
return msg.includes('relation') && msg.includes('does not exist')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const resolvers = {
|
|
|
|
|
Query: {
|
|
|
|
|
async tournaments() {
|
|
|
|
|
const rows = await db.select().from(tournaments).orderBy(desc(tournaments.year))
|
|
|
|
|
return rows.map(r => ({ ...r, avgGoalsPerGame: r.avgGoalsPerGame ? parseFloat(r.avgGoalsPerGame) : null }))
|
|
|
|
|
try {
|
|
|
|
|
const rows = await db.select().from(tournaments).orderBy(desc(tournaments.year))
|
|
|
|
|
return rows.map(r => ({ ...r, avgGoalsPerGame: r.avgGoalsPerGame ? parseFloat(r.avgGoalsPerGame) : null }))
|
|
|
|
|
} catch (e) { if (isMissingTable(e)) return []; throw e }
|
|
|
|
|
},
|
|
|
|
|
async tournament(_: unknown, { year }: { year: number }) {
|
|
|
|
|
const rows = await db.select().from(tournaments).where(eq(tournaments.year, year)).limit(1)
|
|
|
|
|
if (!rows[0]) return null
|
|
|
|
|
return { ...rows[0], avgGoalsPerGame: rows[0].avgGoalsPerGame ? parseFloat(rows[0].avgGoalsPerGame) : null }
|
|
|
|
|
try {
|
|
|
|
|
const rows = await db.select().from(tournaments).where(eq(tournaments.year, year)).limit(1)
|
|
|
|
|
if (!rows[0]) return null
|
|
|
|
|
return { ...rows[0], avgGoalsPerGame: rows[0].avgGoalsPerGame ? parseFloat(rows[0].avgGoalsPerGame) : null }
|
|
|
|
|
} catch (e) { if (isMissingTable(e)) return null; throw e }
|
|
|
|
|
},
|
|
|
|
|
async matches(_: unknown, args: { year?: number; group?: string; round?: string; isQuali?: boolean }) {
|
|
|
|
|
const conditions = []
|
|
|
|
|
if (args.year) conditions.push(eq(matches.tournamentYear, args.year))
|
|
|
|
|
if (args.group) conditions.push(eq(matches.groupName, args.group))
|
|
|
|
|
if (args.round) conditions.push(eq(matches.round, args.round))
|
|
|
|
|
if (args.isQuali != null) conditions.push(eq(matches.isQualiPlayoff, args.isQuali))
|
|
|
|
|
const rows = await db.select().from(matches)
|
|
|
|
|
.where(conditions.length > 0 ? and(...conditions) : undefined)
|
|
|
|
|
.orderBy(asc(matches.date), asc(matches.id))
|
|
|
|
|
return Promise.all(rows.map(hydrateMatch))
|
|
|
|
|
try {
|
|
|
|
|
const conditions = []
|
|
|
|
|
if (args.year) conditions.push(eq(matches.tournamentYear, args.year))
|
|
|
|
|
if (args.group) conditions.push(eq(matches.groupName, args.group))
|
|
|
|
|
if (args.round) conditions.push(eq(matches.round, args.round))
|
|
|
|
|
if (args.isQuali != null) conditions.push(eq(matches.isQualiPlayoff, args.isQuali))
|
|
|
|
|
const rows = await db.select().from(matches)
|
|
|
|
|
.where(conditions.length > 0 ? and(...conditions) : undefined)
|
|
|
|
|
.orderBy(asc(matches.date), asc(matches.id))
|
|
|
|
|
return Promise.all(rows.map(hydrateMatch))
|
|
|
|
|
} catch (e) { if (isMissingTable(e)) return []; throw e }
|
|
|
|
|
},
|
|
|
|
|
async match(_: unknown, { id }: { id: number }) {
|
|
|
|
|
const rows = await db.select().from(matches).where(eq(matches.id, id)).limit(1)
|
|
|
|
|
return rows[0] ? hydrateMatch(rows[0]) : null
|
|
|
|
|
try {
|
|
|
|
|
const rows = await db.select().from(matches).where(eq(matches.id, id)).limit(1)
|
|
|
|
|
return rows[0] ? hydrateMatch(rows[0]) : null
|
|
|
|
|
} catch (e) { if (isMissingTable(e)) return null; throw e }
|
|
|
|
|
},
|
|
|
|
|
async liveMatches() {
|
|
|
|
|
const today = new Date().toISOString().slice(0, 10)
|
|
|
|
|
const rows = await db.select().from(matches)
|
|
|
|
|
.where(and(eq(matches.date, today), eq(matches.isQualiPlayoff, false)))
|
|
|
|
|
.orderBy(asc(matches.id))
|
|
|
|
|
const hydrated = await Promise.all(rows.map(hydrateMatch))
|
|
|
|
|
return hydrated.filter(m => m.isLive)
|
|
|
|
|
try {
|
|
|
|
|
const today = new Date().toISOString().slice(0, 10)
|
|
|
|
|
const rows = await db.select().from(matches)
|
|
|
|
|
.where(and(eq(matches.date, today), eq(matches.isQualiPlayoff, false)))
|
|
|
|
|
.orderBy(asc(matches.id))
|
|
|
|
|
const hydrated = await Promise.all(rows.map(hydrateMatch))
|
|
|
|
|
return hydrated.filter(m => m.isLive)
|
|
|
|
|
} catch (e) { if (isMissingTable(e)) return []; throw e }
|
|
|
|
|
},
|
|
|
|
|
async recentMatches(_: unknown, { limit = 10 }: { limit?: number }) {
|
|
|
|
|
const today = new Date().toISOString().slice(0, 10)
|
|
|
|
|
const rows = await db.select().from(matches)
|
|
|
|
|
.where(and(
|
|
|
|
|
lt(matches.date, today),
|
|
|
|
|
isNotNull(matches.scoreFtHome),
|
|
|
|
|
eq(matches.isQualiPlayoff, false),
|
|
|
|
|
))
|
|
|
|
|
.orderBy(desc(matches.date), desc(matches.id))
|
|
|
|
|
.limit(limit)
|
|
|
|
|
return Promise.all(rows.map(hydrateMatch))
|
|
|
|
|
try {
|
|
|
|
|
const today = new Date().toISOString().slice(0, 10)
|
|
|
|
|
const rows = await db.select().from(matches)
|
|
|
|
|
.where(and(
|
|
|
|
|
lt(matches.date, today),
|
|
|
|
|
isNotNull(matches.scoreFtHome),
|
|
|
|
|
eq(matches.isQualiPlayoff, false),
|
|
|
|
|
))
|
|
|
|
|
.orderBy(desc(matches.date), desc(matches.id))
|
|
|
|
|
.limit(limit)
|
|
|
|
|
return Promise.all(rows.map(hydrateMatch))
|
|
|
|
|
} catch (e) { if (isMissingTable(e)) return []; throw e }
|
|
|
|
|
},
|
|
|
|
|
async upcomingMatches(_: unknown, { limit = 10 }: { limit?: number }) {
|
|
|
|
|
const today = new Date().toISOString().slice(0, 10)
|
|
|
|
|
const rows = await db.select().from(matches)
|
|
|
|
|
.where(and(
|
|
|
|
|
gte(matches.date, today),
|
|
|
|
|
sql`${matches.scoreFtHome} IS NULL`,
|
|
|
|
|
eq(matches.isQualiPlayoff, false),
|
|
|
|
|
))
|
|
|
|
|
.orderBy(asc(matches.date), asc(matches.id))
|
|
|
|
|
.limit(limit)
|
|
|
|
|
return Promise.all(rows.map(hydrateMatch))
|
|
|
|
|
try {
|
|
|
|
|
const today = new Date().toISOString().slice(0, 10)
|
|
|
|
|
const rows = await db.select().from(matches)
|
|
|
|
|
.where(and(
|
|
|
|
|
gte(matches.date, today),
|
|
|
|
|
sql`${matches.scoreFtHome} IS NULL`,
|
|
|
|
|
eq(matches.isQualiPlayoff, false),
|
|
|
|
|
))
|
|
|
|
|
.orderBy(asc(matches.date), asc(matches.id))
|
|
|
|
|
.limit(limit)
|
|
|
|
|
return Promise.all(rows.map(hydrateMatch))
|
|
|
|
|
} catch (e) { if (isMissingTable(e)) return []; throw e }
|
|
|
|
|
},
|
|
|
|
|
async teams() {
|
|
|
|
|
const rows = await db.select().from(teams).orderBy(asc(teams.name))
|
|
|
|
|
return rows.map(teamWithSlug)
|
|
|
|
|
try {
|
|
|
|
|
const rows = await db.select().from(teams).orderBy(asc(teams.name))
|
|
|
|
|
return rows.map(teamWithSlug)
|
|
|
|
|
} catch (e) { if (isMissingTable(e)) return []; throw e }
|
|
|
|
|
},
|
|
|
|
|
async team(_: unknown, { slug }: { slug: string }) {
|
|
|
|
|
const rows = await db.select().from(teams)
|
|
|
|
|
const found = rows.find(r => slugify(r.name) === slug)
|
|
|
|
|
return found ? teamWithSlug(found) : null
|
|
|
|
|
try {
|
|
|
|
|
const rows = await db.select().from(teams)
|
|
|
|
|
const found = rows.find(r => slugify(r.name) === slug)
|
|
|
|
|
return found ? teamWithSlug(found) : null
|
|
|
|
|
} catch (e) { if (isMissingTable(e)) return null; throw e }
|
|
|
|
|
},
|
|
|
|
|
async topScorers(_: unknown, { year, limit = 20 }: { year?: number; limit?: number }) {
|
|
|
|
|
try {
|
|
|
|
|
const conditions = year
|
|
|
|
|
? sql`AND m.tournament_year = ${year} AND m.is_quali_playoff = false`
|
|
|
|
|
: sql`AND m.is_quali_playoff = false`
|
|
|
|
@@ -153,6 +177,7 @@ export const resolvers = {
|
|
|
|
|
tournaments: r.tournaments,
|
|
|
|
|
team: r.team_id ? await getTeamById(r.team_id as number) : null,
|
|
|
|
|
})))
|
|
|
|
|
} catch (e) { if (isMissingTable(e)) return []; throw e }
|
|
|
|
|
},
|
|
|
|
|
async player(_: unknown, { name }: { name: string }) {
|
|
|
|
|
const rows = await db.execute(sql`
|
|
|
|
@@ -210,6 +235,7 @@ export const resolvers = {
|
|
|
|
|
})))
|
|
|
|
|
},
|
|
|
|
|
async groupStandings(_: unknown, { year }: { year: number }) {
|
|
|
|
|
try {
|
|
|
|
|
const rows = await db.select({
|
|
|
|
|
groupName: groupStandings.groupName,
|
|
|
|
|
pos: groupStandings.pos,
|
|
|
|
@@ -237,6 +263,7 @@ export const resolvers = {
|
|
|
|
|
pts: r.pts ?? 0,
|
|
|
|
|
team: (await getTeamById(r.teamId))!,
|
|
|
|
|
})))
|
|
|
|
|
} catch (e) { if (isMissingTable(e)) return []; throw e }
|
|
|
|
|
},
|
|
|
|
|
async stadiums(_: unknown, { year }: { year?: number }) {
|
|
|
|
|
const rows = year
|
|
|
|
@@ -255,26 +282,20 @@ export const resolvers = {
|
|
|
|
|
}))
|
|
|
|
|
},
|
|
|
|
|
async tournamentStats() {
|
|
|
|
|
const [totals] = await db.execute(sql`
|
|
|
|
|
SELECT
|
|
|
|
|
COUNT(DISTINCT t.year)::int AS total_tournaments,
|
|
|
|
|
COUNT(DISTINCT m.id)::int AS total_matches,
|
|
|
|
|
COALESCE(SUM(m.score_ft_home + m.score_ft_away), 0)::int AS total_goals,
|
|
|
|
|
ROUND(COALESCE(SUM(m.score_ft_home + m.score_ft_away), 0)::numeric / NULLIF(COUNT(DISTINCT m.id), 0), 2)::float AS avg_goals
|
|
|
|
|
FROM tournaments t
|
|
|
|
|
LEFT JOIN matches m ON m.tournament_year = t.year AND m.is_quali_playoff = false AND m.score_ft_home IS NOT NULL
|
|
|
|
|
`)
|
|
|
|
|
const r = totals as Record<string, unknown>
|
|
|
|
|
return {
|
|
|
|
|
totalTournaments: r.total_tournaments,
|
|
|
|
|
totalMatches: r.total_matches,
|
|
|
|
|
totalGoals: r.total_goals,
|
|
|
|
|
avgGoalsPerGame: r.avg_goals,
|
|
|
|
|
mostGoalsInTournament: null,
|
|
|
|
|
highestScoringMatch: null,
|
|
|
|
|
biggestWin: null,
|
|
|
|
|
mostTitles: null,
|
|
|
|
|
}
|
|
|
|
|
const empty = { totalTournaments: 0, totalMatches: 0, totalGoals: 0, avgGoalsPerGame: null, mostGoalsInTournament: null, highestScoringMatch: null, biggestWin: null, mostTitles: null }
|
|
|
|
|
try {
|
|
|
|
|
const [totals] = await db.execute(sql`
|
|
|
|
|
SELECT
|
|
|
|
|
COUNT(DISTINCT t.year)::int AS total_tournaments,
|
|
|
|
|
COUNT(DISTINCT m.id)::int AS total_matches,
|
|
|
|
|
COALESCE(SUM(m.score_ft_home + m.score_ft_away), 0)::int AS total_goals,
|
|
|
|
|
ROUND(COALESCE(SUM(m.score_ft_home + m.score_ft_away), 0)::numeric / NULLIF(COUNT(DISTINCT m.id), 0), 2)::float AS avg_goals
|
|
|
|
|
FROM tournaments t
|
|
|
|
|
LEFT JOIN matches m ON m.tournament_year = t.year AND m.is_quali_playoff = false AND m.score_ft_home IS NOT NULL
|
|
|
|
|
`)
|
|
|
|
|
const r = totals as Record<string, unknown>
|
|
|
|
|
return { ...empty, totalTournaments: r.total_tournaments, totalMatches: r.total_matches, totalGoals: r.total_goals, avgGoalsPerGame: r.avg_goals }
|
|
|
|
|
} catch (e) { if (isMissingTable(e)) return empty; throw e }
|
|
|
|
|
},
|
|
|
|
|
async goalsByMinute() {
|
|
|
|
|
const rows = await db.execute(sql`
|
|
|
|
@@ -316,7 +337,12 @@ export const resolvers = {
|
|
|
|
|
GROUP BY t.confederation
|
|
|
|
|
ORDER BY appearances DESC
|
|
|
|
|
`)
|
|
|
|
|
return rows
|
|
|
|
|
return (rows as Record<string, unknown>[]).map(r => ({
|
|
|
|
|
confederation: r.confederation,
|
|
|
|
|
appearances: r.appearances,
|
|
|
|
|
titles: r.titles,
|
|
|
|
|
totalGoals: r.total_goals,
|
|
|
|
|
}))
|
|
|
|
|
},
|
|
|
|
|
async biggestWins(_: unknown, { limit = 10 }: { limit?: number }) {
|
|
|
|
|
const rows = await db.select().from(matches)
|
|
|
|
|