import { sub } from "date-fns"; import { type Insertable, type NotNull, sql, type Transaction } from "kysely"; import { jsonArrayFrom, jsonObjectFrom } from "kysely/helpers/sqlite"; import { db } from "~/db/sql"; import type { CastedMatchesInfo, DB, PreparedMaps, Tables, TournamentSettings, } from "~/db/tables"; import * as Progression from "~/features/tournament-bracket/core/Progression"; import { Status } from "~/modules/brackets-model"; import { modesShort } from "~/modules/in-game-lists/modes"; import { nullFilledArray, nullifyingAvg } from "~/utils/arrays"; import { databaseTimestampNow, dateToDatabaseTimestamp } from "~/utils/dates"; import { shortNanoid } from "~/utils/id"; import { COMMON_USER_FIELDS, concatUserSubmittedImagePrefix, tournamentLogoWithDefault, } from "~/utils/kysely.server"; import type { Unwrapped } from "~/utils/types"; import type { TournamentTierNumber } from "./core/tiering"; import { updatedCastedMatchesInfo } from "./tournament-utils"; export type FindById = NonNullable>; export async function findById(id: number) { const isSetAsRanked = await db .selectFrom("Tournament") .select("settings") .where("id", "=", id) .executeTakeFirst() .then((row) => row?.settings.isRanked ?? false); const result = await db .selectFrom("Tournament") .innerJoin("CalendarEvent", "Tournament.id", "CalendarEvent.tournamentId") .innerJoin( "CalendarEventDate", "CalendarEvent.id", "CalendarEventDate.eventId", ) .select(({ eb }) => [ "Tournament.id", "CalendarEvent.id as eventId", "CalendarEvent.discordUrl", "CalendarEvent.tags", "Tournament.settings", "Tournament.castTwitchAccounts", "Tournament.castedMatchesInfo", "Tournament.mapPickingStyle", "Tournament.rules", "Tournament.parentTournamentId", "Tournament.tier", "CalendarEvent.name", "CalendarEvent.description", "CalendarEventDate.startTime", "Tournament.isFinalized", "Tournament.seedingSnapshot", jsonObjectFrom( eb .selectFrom("TournamentOrganization") .leftJoin( "UserSubmittedImage", "TournamentOrganization.avatarImgId", "UserSubmittedImage.id", ) .select(({ eb: innerEb }) => [ "TournamentOrganization.id", "TournamentOrganization.name", "TournamentOrganization.slug", concatUserSubmittedImagePrefix( innerEb.ref("UserSubmittedImage.url"), ).as("logoUrl"), jsonArrayFrom( innerEb .selectFrom("TournamentOrganizationMember") .innerJoin( "User", "TournamentOrganizationMember.userId", "User.id", ) .select([ "TournamentOrganizationMember.userId", "TournamentOrganizationMember.role", ...COMMON_USER_FIELDS, "User.pronouns", ]) .whereRef( "TournamentOrganizationMember.organizationId", "=", "TournamentOrganization.id", ), ).as("members"), ]) .whereRef( "TournamentOrganization.id", "=", "CalendarEvent.organizationId", ), ).as("organization"), tournamentLogoWithDefault(eb).as("logoUrl"), jsonObjectFrom( eb .selectFrom("User") .select([...COMMON_USER_FIELDS, "User.pronouns"]) .whereRef("User.id", "=", "CalendarEvent.authorId"), ).as("author"), jsonArrayFrom( eb .selectFrom("TournamentStaff") .innerJoin("User", "TournamentStaff.userId", "User.id") .select([ ...COMMON_USER_FIELDS, "User.pronouns", "TournamentStaff.role", ]) .where("TournamentStaff.tournamentId", "=", id), ).as("staff"), jsonArrayFrom( eb .selectFrom("TournamentBracketProgressionOverride") .select([ "TournamentBracketProgressionOverride.sourceBracketIdx", "TournamentBracketProgressionOverride.destinationBracketIdx", "TournamentBracketProgressionOverride.tournamentTeamId", ]) .whereRef( "TournamentBracketProgressionOverride.tournamentId", "=", "Tournament.id", ), ).as("bracketProgressionOverrides"), jsonArrayFrom( eb .selectFrom("TournamentTeam") .leftJoin( "UserSubmittedImage", "TournamentTeam.avatarImgId", "UserSubmittedImage.id", ) .select(({ eb: innerEb }) => [ "TournamentTeam.id", "TournamentTeam.name", "TournamentTeam.seed", "TournamentTeam.prefersNotToHost", "TournamentTeam.droppedOut", "TournamentTeam.inviteCode", "TournamentTeam.createdAt", "TournamentTeam.activeRosterUserIds", "TournamentTeam.startingBracketIdx", "TournamentTeam.abDivision", concatUserSubmittedImagePrefix( innerEb.ref("UserSubmittedImage.url"), ).as("pickupAvatarUrl"), jsonArrayFrom( innerEb .selectFrom("TournamentTeamMember") .innerJoin("User", "TournamentTeamMember.userId", "User.id") .leftJoin("SeedingSkill", (join) => join .onRef("User.id", "=", "SeedingSkill.userId") .on( "SeedingSkill.type", "=", isSetAsRanked ? "RANKED" : "UNRANKED", ), ) .leftJoin("PlusTier", "PlusTier.userId", "User.id") .leftJoin("LiveStream", "LiveStream.userId", "User.id") .select([ "User.id as userId", "User.username", "User.discordId", "User.discordAvatar", "User.customUrl", "User.country", "User.twitch", "SeedingSkill.ordinal", "PlusTier.tier as plusTier", "TournamentTeamMember.role", "TournamentTeamMember.createdAt", sql /*sql*/`coalesce( "TournamentTeamMember"."inGameName", "User"."inGameName" )`.as("inGameName"), "LiveStream.twitch as streamTwitch", "LiveStream.viewerCount as streamViewerCount", "LiveStream.thumbnailUrl as streamThumbnailUrl", ]) .whereRef( "TournamentTeamMember.tournamentTeamId", "=", "TournamentTeam.id", ) .orderBy("TournamentTeamMember.createdAt", "asc"), ).as("members"), jsonArrayFrom( innerEb .selectFrom("TournamentTeamCheckIn") .select([ "TournamentTeamCheckIn.bracketIdx", "TournamentTeamCheckIn.checkedInAt", "TournamentTeamCheckIn.isCheckOut", ]) .whereRef( "TournamentTeamCheckIn.tournamentTeamId", "=", "TournamentTeam.id", ), ).as("checkIns"), jsonArrayFrom( innerEb .selectFrom("MapPoolMap") .whereRef( "MapPoolMap.tournamentTeamId", "=", "TournamentTeam.id", ) .select(["MapPoolMap.stageId", "MapPoolMap.mode"]), ).as("mapPool"), jsonObjectFrom( innerEb .selectFrom("AllTeam") .leftJoin( "UserSubmittedImage", "AllTeam.avatarImgId", "UserSubmittedImage.id", ) .whereRef("AllTeam.id", "=", "TournamentTeam.teamId") .select((eb) => [ "AllTeam.id", "AllTeam.customUrl", concatUserSubmittedImagePrefix( eb.ref("UserSubmittedImage.url"), ).as("logoUrl"), "AllTeam.deletedAt", ]), ).as("team"), ]) .where("TournamentTeam.tournamentId", "=", id) .where("TournamentTeam.isPlaceholder", "=", 0) .orderBy("TournamentTeam.seed", "asc") .orderBy("TournamentTeam.createdAt", "asc") .orderBy("TournamentTeam.id", "asc"), ).as("teams"), jsonArrayFrom( eb .selectFrom("MapPoolMap") .select(["MapPoolMap.stageId", "MapPoolMap.mode"]) .whereRef( "MapPoolMap.tieBreakerCalendarEventId", "=", "CalendarEvent.id", ), ).as("tieBreakerMapPool"), jsonArrayFrom( eb .selectFrom("MapPoolMap") .select(["MapPoolMap.mode", "MapPoolMap.stageId"]) .whereRef("MapPoolMap.calendarEventId", "=", "CalendarEvent.id"), ).as("toSetMapPool"), jsonArrayFrom( eb .selectFrom("TournamentStage") .innerJoin( "TournamentMatch", "TournamentMatch.stageId", "TournamentStage.id", ) .innerJoin( "TournamentMatchGameResult", "TournamentMatch.id", "TournamentMatchGameResult.matchId", ) .innerJoin( "TournamentMatchGameResultParticipant", "TournamentMatchGameResult.id", "TournamentMatchGameResultParticipant.matchGameResultId", ) .select("TournamentMatchGameResultParticipant.userId") .groupBy("TournamentMatchGameResultParticipant.userId") .where("TournamentStage.tournamentId", "=", id), ).as("participatedUsers"), jsonArrayFrom( eb .selectFrom("LiveStream") .select([ "LiveStream.twitch", "LiveStream.viewerCount", "LiveStream.thumbnailUrl", ]) .where( sql`"LiveStream"."twitch" IN (SELECT value FROM json_each("Tournament"."castTwitchAccounts"))`, ), ).as("castStreams"), ]) .where("Tournament.id", "=", id) .$narrowType<{ author: NotNull }>() .executeTakeFirst(); if (!result) return null; return { ...result, teams: result.teams.map((team) => ({ ...team, members: team.members.map(({ ordinal, ...member }) => member), avgSeedingSkillOrdinal: nullifyingAvg( team.members .map((member) => member.ordinal) .filter((ordinal) => typeof ordinal === "number"), ), })), participatedUsers: result.participatedUsers.map((user) => user.userId), }; } export async function hasChildTournaments(parentTournamentId: number) { const row = await db .selectFrom("Tournament") .select("Tournament.id") .where("Tournament.parentTournamentId", "=", parentTournamentId) .limit(1) .executeTakeFirst(); return Boolean(row); } export async function findChildTournaments(parentTournamentId: number) { const rows = await db .selectFrom("Tournament") .innerJoin("CalendarEvent", "Tournament.id", "CalendarEvent.tournamentId") .select((eb) => [ "Tournament.id as tournamentId", "CalendarEvent.name", eb .selectFrom("TournamentTeam") .select(({ fn }) => [fn.countAll().as("teamsCount")]) .whereRef("TournamentTeam.tournamentId", "=", "Tournament.id") .where("TournamentTeam.isPlaceholder", "=", 0) .as("teamsCount"), jsonArrayFrom( eb .selectFrom("TournamentTeam") .innerJoin( "TournamentTeamMember", "TournamentTeamMember.tournamentTeamId", "TournamentTeam.id", ) .select(["TournamentTeamMember.userId"]) .whereRef("TournamentTeam.tournamentId", "=", "Tournament.id") .where("TournamentTeam.isPlaceholder", "=", 0), ).as("teamMembers"), ]) .where("Tournament.parentTournamentId", "=", parentTournamentId) .$narrowType<{ teamsCount: NotNull }>() .execute(); return rows.map((row) => ({ ...row, participantUserIds: new Set(row.teamMembers.map((member) => member.userId)), })); } export async function findTOSetMapPoolById(tournamentId: number) { return ( await db .selectFrom("CalendarEvent") .innerJoin("MapPoolMap", "CalendarEvent.id", "MapPoolMap.calendarEventId") .select(["MapPoolMap.mode", "MapPoolMap.stageId"]) .where("CalendarEvent.tournamentId", "=", tournamentId) .execute() ).sort((a, b) => { const modeAIndexOf = modesShort.indexOf(a.mode); const modeBIndexOf = modesShort.indexOf(b.mode); if (modeAIndexOf < modeBIndexOf) return -1; if (modeAIndexOf > modeBIndexOf) return 1; return a.stageId - b.stageId; }); } export async function findPreparedMapsById(tournamentId: number) { return ( ( await db .selectFrom("Tournament") .select("preparedMaps") .where("id", "=", tournamentId) .executeTakeFirst() )?.preparedMaps ?? undefined ); } export function relatedUsersByTournamentIds(tournamentIds: number[]) { return db .selectFrom("CalendarEventDate") .innerJoin("CalendarEvent", "CalendarEventDate.eventId", "CalendarEvent.id") .innerJoin("Tournament", "CalendarEvent.tournamentId", "Tournament.id") .select((eb) => [ "Tournament.id", "CalendarEvent.authorId", jsonArrayFrom( eb .selectFrom("TournamentStaff") .select(["TournamentStaff.userId"]) .whereRef("TournamentStaff.tournamentId", "=", "Tournament.id") .where("TournamentStaff.role", "=", "ORGANIZER"), ).as("staff"), jsonArrayFrom( eb .selectFrom("TournamentOrganization") .innerJoin( "TournamentOrganizationMember", "TournamentOrganization.id", "TournamentOrganizationMember.organizationId", ) .select(["TournamentOrganizationMember.userId"]) .whereRef( "TournamentOrganization.id", "=", "CalendarEvent.organizationId", ) .where("TournamentOrganizationMember.role", "in", [ "ADMIN", "ORGANIZER", ]), ).as("organizationMembers"), jsonArrayFrom( eb .selectFrom("TournamentTeam") .innerJoin( "TournamentTeamMember", "TournamentTeamMember.tournamentTeamId", "TournamentTeam.id", ) .select(["TournamentTeamMember.userId"]) .whereRef("TournamentTeam.tournamentId", "=", "Tournament.id"), ).as("teamMembers"), ]) .where("Tournament.id", "in", tournamentIds) .$narrowType<{ staff: NotNull; organizationMembers: NotNull; teamMembers: NotNull; }>() .execute(); } export type ForShowcase = Unwrapped; export function forShowcase() { return db .selectFrom("Tournament") .innerJoin("CalendarEvent", "Tournament.id", "CalendarEvent.tournamentId") .innerJoin( "CalendarEventDate", "CalendarEvent.id", "CalendarEventDate.eventId", ) .select((eb) => [ "Tournament.id", "Tournament.settings", "Tournament.tier", "Tournament.isFinalized", "CalendarEvent.authorId", "CalendarEvent.name", "CalendarEvent.organizationId", "CalendarEventDate.startTime", "CalendarEvent.hidden", eb .selectFrom("TournamentTeam") .leftJoin("TournamentTeamCheckIn", (join) => join .on("TournamentTeamCheckIn.bracketIdx", "is", null) .onRef( "TournamentTeamCheckIn.tournamentTeamId", "=", "TournamentTeam.id", ), ) .whereRef("TournamentTeam.tournamentId", "=", "Tournament.id") .where("TournamentTeam.isPlaceholder", "=", 0) .where((eb) => eb.or([ eb("TournamentTeamCheckIn.checkedInAt", "is not", null), eb("CalendarEventDate.startTime", ">", databaseTimestampNow()), ]), ) .select(({ fn }) => [ fn.count("TournamentTeam.id").distinct().as("teamsCount"), ]) .as("teamsCount"), tournamentLogoWithDefault(eb).as("logoUrl"), jsonObjectFrom( eb .selectFrom("TournamentOrganization") .select([ "TournamentOrganization.name", "TournamentOrganization.slug", ]) .whereRef( "TournamentOrganization.id", "=", "CalendarEvent.organizationId", ), ).as("organization"), jsonArrayFrom( eb .selectFrom("TournamentResult") .innerJoin("User", "TournamentResult.userId", "User.id") .innerJoin( "TournamentTeam", "TournamentResult.tournamentTeamId", "TournamentTeam.id", ) .leftJoin("AllTeam", "TournamentTeam.teamId", "AllTeam.id") .leftJoin( "UserSubmittedImage as TeamAvatar", "AllTeam.avatarImgId", "TeamAvatar.id", ) .leftJoin( "UserSubmittedImage as TournamentTeamAvatar", "TournamentTeam.avatarImgId", "TournamentTeamAvatar.id", ) .whereRef("TournamentResult.tournamentId", "=", "Tournament.id") .where("TournamentResult.placement", "=", 1) .select((eb) => [ ...COMMON_USER_FIELDS, "User.country", "TournamentResult.div", "TournamentTeam.name as teamName", concatUserSubmittedImagePrefix(eb.ref("TeamAvatar.url")).as( "teamLogoUrl", ), concatUserSubmittedImagePrefix( eb.ref("TournamentTeamAvatar.url"), ).as("pickupAvatarUrl"), ]), ).as("firstPlacers"), eb .selectFrom("TournamentMatchVod") .innerJoin( "TournamentMatch", "TournamentMatch.id", "TournamentMatchVod.matchId", ) .innerJoin( "TournamentStage", "TournamentStage.id", "TournamentMatch.stageId", ) .whereRef("TournamentStage.tournamentId", "=", "Tournament.id") .select(({ fn }) => [fn.countAll().as("count")]) .as("vodCount"), ]) .where("CalendarEventDate.startTime", ">", databaseTimestampWeekAgo()) .orderBy("CalendarEventDate.startTime", "asc") .$narrowType<{ teamsCount: NotNull }>() .execute(); } function databaseTimestampWeekAgo() { const now = new Date(); now.setDate(now.getDate() - 7); return dateToDatabaseTimestamp(now); } export function findAllBetweenTwoTimestamps({ startTime, endTime, }: { startTime: Date; endTime: Date; }) { return db .selectFrom("CalendarEvent") .innerJoin( "CalendarEventDate", "CalendarEvent.id", "CalendarEventDate.eventId", ) .innerJoin("Tournament", "CalendarEvent.tournamentId", "Tournament.id") .select(["Tournament.id as tournamentId"]) .where( "CalendarEventDate.startTime", ">", dateToDatabaseTimestamp(startTime), ) .where( "CalendarEventDate.startTime", "<=", dateToDatabaseTimestamp(endTime), ) .where("CalendarEvent.hidden", "=", 0) .execute(); } export function topThreeResultsByTournamentId(tournamentId: number) { return db .selectFrom("TournamentResult") .select(({ eb }) => [ "TournamentResult.placement", "TournamentResult.tournamentTeamId", jsonObjectFrom( eb .selectFrom("User") .select([...COMMON_USER_FIELDS]) .whereRef("User.id", "=", "TournamentResult.userId"), ).as("user"), ]) .where("tournamentId", "=", tournamentId) .where("TournamentResult.placement", "<=", 3) .$narrowType<{ user: NotNull }>() .execute(); } export async function friendCodesByTournamentId(tournamentId: number) { const values = await db .selectFrom("TournamentTeam") .innerJoin( "TournamentTeamMember", "TournamentTeam.id", "TournamentTeamMember.tournamentTeamId", ) .innerJoin( "UserFriendCode", "TournamentTeamMember.userId", "UserFriendCode.userId", ) .select(["TournamentTeamMember.userId", "UserFriendCode.friendCode"]) .orderBy("UserFriendCode.createdAt", "asc") .where("TournamentTeam.tournamentId", "=", tournamentId) .execute(); // later friend code overwrites earlier ones return values.reduce( (acc, cur) => { acc[cur.userId] = cur.friendCode; return acc; }, {} as Record, ); } export function updateProgression({ tournamentId, bracketProgression, }: { tournamentId: number; bracketProgression: TournamentSettings["bracketProgression"]; }) { return db.transaction().execute(async (trx) => { const { settings: existingSettings } = await trx .selectFrom("Tournament") .select("settings") .where("id", "=", tournamentId) .executeTakeFirstOrThrow(); if ( Progression.changedBracketProgressionFormat( existingSettings.bracketProgression, bracketProgression, ) ) { const allTournamentTeamsOfTournament = ( await trx .selectFrom("TournamentTeam") .select("id") .where("tournamentId", "=", tournamentId) .execute() ).map((t) => t.id); // delete all bracket check-ins await trx .deleteFrom("TournamentTeamCheckIn") .where("TournamentTeamCheckIn.bracketIdx", "is not", null) .where( "TournamentTeamCheckIn.tournamentTeamId", "in", allTournamentTeamsOfTournament, ) .execute(); await trx .updateTable("TournamentTeam") .set({ startingBracketIdx: null, }) .where("tournamentId", "=", tournamentId) .execute(); } const newSettings: Tables["Tournament"]["settings"] = { ...existingSettings, bracketProgression, }; await trx .updateTable("Tournament") .set({ settings: JSON.stringify(newSettings), preparedMaps: Progression.changedBracketProgressionFormat( existingSettings.bracketProgression, bracketProgression, ) ? null : undefined, }) .where("id", "=", tournamentId) .execute(); }); } export function overrideTeamBracketProgression({ tournamentId, tournamentTeamId, sourceBracketIdx, destinationBracketIdx, }: { tournamentId: number; tournamentTeamId: number; sourceBracketIdx: number; destinationBracketIdx: number; }) { // set in migration: unique("sourceBracketIdx", "tournamentTeamId") on conflict replace return db .insertInto("TournamentBracketProgressionOverride") .values({ tournamentId, tournamentTeamId, sourceBracketIdx, destinationBracketIdx, }) .execute(); } export function addStaff({ tournamentId, userId, role, }: { tournamentId: number; userId: number; role: Tables["TournamentStaff"]["role"]; }) { return db .insertInto("TournamentStaff") .values({ tournamentId, userId, role, }) .execute(); } export function removeStaff({ tournamentId, userId, }: { tournamentId: number; userId: number; }) { return db .deleteFrom("TournamentStaff") .where("tournamentId", "=", tournamentId) .where("userId", "=", userId) .execute(); } interface UpsertPreparedMapsArgs { tournamentId: number; maps: Omit; bracketIdx: number; } export function upsertPreparedMaps({ bracketIdx, maps, tournamentId, }: UpsertPreparedMapsArgs) { return db.transaction().execute(async (trx) => { const tournament = await trx .selectFrom("Tournament") .select(["Tournament.preparedMaps", "Tournament.settings"]) .where("Tournament.id", "=", tournamentId) .executeTakeFirstOrThrow(); const preparedMaps: Array = tournament.preparedMaps ?? nullFilledArray(tournament.settings.bracketProgression.length); preparedMaps[bracketIdx] = { ...maps, createdAt: databaseTimestampNow() }; await trx .updateTable("Tournament") .set({ preparedMaps: JSON.stringify(preparedMaps) }) .where("Tournament.id", "=", tournamentId) .execute(); }); } export function updateCastTwitchAccounts({ tournamentId, castTwitchAccounts, }: { tournamentId: number; castTwitchAccounts: string[]; }) { return db .updateTable("Tournament") .set({ castTwitchAccounts: JSON.stringify(castTwitchAccounts), }) .where("id", "=", tournamentId) .execute(); } const castedMatchesInfoByTournamentId = async ( trx: Transaction, tournamentId: number, ) => ( await trx .selectFrom("Tournament") .select("castedMatchesInfo") .where("id", "=", tournamentId) .executeTakeFirstOrThrow() ).castedMatchesInfo ?? ({ castedMatches: [], lockedMatches: [], } as CastedMatchesInfo); export function lockMatch({ matchId, tournamentId, twitchAccount, }: { matchId: number; tournamentId: number; twitchAccount: string; }) { return db.transaction().execute(async (trx) => { const castedMatchesInfo = await castedMatchesInfoByTournamentId( trx, tournamentId, ); if (!castedMatchesInfo.lockedMatches.some((lm) => lm.matchId === matchId)) { castedMatchesInfo.lockedMatches.push({ matchId, twitchAccount }); } await trx .updateTable("Tournament") .set({ castedMatchesInfo: JSON.stringify(castedMatchesInfo), }) .where("id", "=", tournamentId) .execute(); }); } export function unlockMatch({ matchId, tournamentId, }: { matchId: number; tournamentId: number; }) { return db.transaction().execute(async (trx) => { const castedMatchesInfo = await castedMatchesInfoByTournamentId( trx, tournamentId, ); castedMatchesInfo.lockedMatches = castedMatchesInfo.lockedMatches.filter( (lm) => lm.matchId !== matchId, ); await trx .updateTable("Tournament") .set({ castedMatchesInfo: JSON.stringify(castedMatchesInfo), }) .where("id", "=", tournamentId) .execute(); // Make sure that a match is not marked as started when it is unlocked // as we use this timestamp to determine the "deadline" for the match // so it doesn't make sense for that timer to run if players can't play yet await trx .updateTable("TournamentMatch") .set({ startedAt: databaseTimestampNow(), }) .where("id", "=", matchId) // ensure we don't set startedAt if it was never set before .where("TournamentMatch.startedAt", "is not", null) .execute(); }); } export function setMatchAsCasted({ matchId, tournamentId, twitchAccount, }: { matchId: number; tournamentId: number; twitchAccount: string | null; }) { return db.transaction().execute(async (trx) => { const castedMatchesInfo = await castedMatchesInfoByTournamentId( trx, tournamentId, ); const newCastedMatchesInfo = updatedCastedMatchesInfo(castedMatchesInfo, { matchId, twitchAccount, timestamp: databaseTimestampNow(), }); await trx .updateTable("Tournament") .set({ castedMatchesInfo: JSON.stringify(newCastedMatchesInfo), }) .where("id", "=", tournamentId) .execute(); }); } export function pickBanEventsByMatchId(matchId: number) { return db .selectFrom("TournamentMatchPickBanEvent") .select([ "TournamentMatchPickBanEvent.mode", "TournamentMatchPickBanEvent.stageId", "TournamentMatchPickBanEvent.type", "TournamentMatchPickBanEvent.number", "TournamentMatchPickBanEvent.createdAt", ]) .where("matchId", "=", matchId) .orderBy("TournamentMatchPickBanEvent.number", "asc") .execute(); } export function addPickBanEvent( values: Insertable, ) { return db.insertInto("TournamentMatchPickBanEvent").values(values).execute(); } export function resetBracket(tournamentStageId: number) { return db.transaction().execute(async (trx) => { await trx .deleteFrom("TournamentMatch") .where("stageId", "=", tournamentStageId) .execute(); await trx .deleteFrom("TournamentRound") .where("stageId", "=", tournamentStageId) .execute(); await trx .deleteFrom("TournamentGroup") .where("stageId", "=", tournamentStageId) .execute(); await trx .deleteFrom("TournamentStage") .where("id", "=", tournamentStageId) .execute(); }); } export function reopenTournament(tournamentId: number) { return db.transaction().execute(async (trx) => { await trx .deleteFrom("TournamentResult") .where("tournamentId", "=", tournamentId) .execute(); await trx .updateTable("Tournament") .set({ isFinalized: 0 }) .where("id", "=", tournamentId) .execute(); await trx .deleteFrom("Skill") .where("tournamentId", "=", tournamentId) .execute(); await trx .deleteFrom("TournamentBadgeOwner") .where("tournamentId", "=", tournamentId) .execute(); }); } export type TournamentRepositoryInsertableMatch = Omit< Insertable, "status" | "chatCode" >; export function insertSwissMatches( matches: TournamentRepositoryInsertableMatch[], ) { if (matches.length === 0) { throw new Error("No matches to insert"); } return db .insertInto("TournamentMatch") .values( matches.map((match) => ({ groupId: match.groupId, number: match.number, opponentOne: match.opponentOne, opponentTwo: match.opponentTwo, roundId: match.roundId, stageId: match.stageId, status: Status.Ready, chatCode: shortNanoid(), })), ) .execute(); } export function deleteSwissMatches({ groupId, roundId, }: { groupId: number; roundId: number; }) { return db .deleteFrom("TournamentMatch") .where("groupId", "=", groupId) .where("roundId", "=", roundId) .execute(); } export async function searchByName({ query, limit, minStartTime, }: { query: string; limit: number; minStartTime?: Date; }) { let sqlQuery = db .selectFrom("Tournament") .innerJoin("CalendarEvent", "Tournament.id", "CalendarEvent.tournamentId") .innerJoin( "CalendarEventDate", "CalendarEvent.id", "CalendarEventDate.eventId", ) .select((eb) => [ "Tournament.id", "CalendarEvent.name", "CalendarEventDate.startTime", tournamentLogoWithDefault(eb).as("logoUrl"), ]) .where("CalendarEvent.name", "like", `%${query}%`) .where("CalendarEvent.hidden", "=", 0) .orderBy("CalendarEventDate.startTime", "desc") .limit(limit); if (minStartTime) { sqlQuery = sqlQuery.where( "CalendarEventDate.startTime", ">=", dateToDatabaseTimestamp(minStartTime), ); } return sqlQuery.execute(); } export function updateTeamSeeds({ tournamentId, teamIds, teamsWithMembers, }: { tournamentId: number; teamIds: number[]; teamsWithMembers: Array<{ teamId: number; members: Array<{ userId: number; username: string }>; }>; }) { return db.transaction().execute(async (trx) => { await trx .updateTable("TournamentTeam") .set({ seed: null }) .where("tournamentId", "=", tournamentId) .execute(); for (const [i, teamId] of teamIds.entries()) { await trx .updateTable("TournamentTeam") .set({ seed: i + 1 }) .where("id", "=", teamId) .execute(); } const snapshot = JSON.stringify({ savedAt: databaseTimestampNow(), teams: teamsWithMembers, }); await trx .updateTable("Tournament") .set({ seedingSnapshot: snapshot }) .where("id", "=", tournamentId) .execute(); }); } export function updateTournamentTier({ tournamentId, tier, }: { tournamentId: number; tier: TournamentTierNumber; }) { return db .updateTable("Tournament") .set({ tier }) .where("id", "=", tournamentId) .execute(); } export async function findRunningTournamentIds() { const now = new Date(); const cutoff = sub(now, { days: 2 }); const rows = await db .selectFrom("Tournament") .innerJoin("CalendarEvent", "Tournament.id", "CalendarEvent.tournamentId") .innerJoin( "CalendarEventDate", "CalendarEvent.id", "CalendarEventDate.eventId", ) .select("Tournament.id") .where("Tournament.isFinalized", "=", 0) .where("CalendarEventDate.startTime", "<", dateToDatabaseTimestamp(now)) .where("CalendarEventDate.startTime", ">", dateToDatabaseTimestamp(cutoff)) .where((eb) => eb.exists( eb .selectFrom("TournamentStage") .select("TournamentStage.id") .whereRef("TournamentStage.tournamentId", "=", "Tournament.id"), ), ) .where( sql`json_extract("Tournament"."settings", '$.isTest')`, "is not", 1, ) .execute(); return rows.map((row) => row.id); }