import { dateToDatabaseTimestamp } from "~/utils/dates"; import { sql } from "../../sql"; import type { CalendarEvent, CalendarEventDate, User, Badge, CalendarEventTag, CalendarEventBadge, CalendarEventResultTeam, CalendarEventResultPlayer, MapPoolMap, Tournament, } from "../../types"; import { MapPool } from "~/modules/map-pool-serializer"; import createSql from "./create.sql"; import updateSql from "./update.sql"; import createDateSql from "./createDate.sql"; import deleteDatesByEventIdSql from "./deleteDatesByEventId.sql"; import createBadgeSql from "./createBadge.sql"; import deleteBadgesByEventIdSql from "./deleteBadgesByEventId.sql"; import updateParticipantsCountSql from "./updateParticipantsCount.sql"; import deleteResultTeamsByEventIdSql from "./deleteResultTeamsByEventId.sql"; import insertResultTeamSql from "./insertResultTeam.sql"; import insertResultPlayerSql from "./insertResultPlayer.sql"; import findWinnersByEventIdSql from "./findWinnersByEventId.sql"; import findResultsByUserIdSql from "./findResultsByUserId.sql"; import findMatesByResultTeamIdSql from "./findMatesByResultTeamId.sql"; import findAllBetweenTwoTimestampsSql from "./findAllBetweenTwoTimestamps.sql"; import findByIdSql from "./findById.sql"; import startTimesOfRangeSql from "./startTimesOfRange.sql"; import findBadgesByEventIdSql from "./findBadgesByEventId.sql"; import eventsToReportSql from "./eventsToReport.sql"; import createMapPoolMapSql from "./createMapPoolMap.sql"; import deleteMapPoolMapsSql from "./deleteMapPoolMaps.sql"; import createTieBreakerMapPoolMapSql from "./createTieBreakerMapPoolMap.sql"; import findMapPoolByEventIdSql from "./findMapPoolByEventId.sql"; import findRecentMapPoolsByAuthorIdSql from "./findRecentMapPoolsByAuthorId.sql"; import findAllEventsWithMapPoolsSql from "./findAllEventsWithMapPools.sql"; import findTieBreakerMapPoolByEventIdSql from "./findTieBreakerMapPoolByEventId.sql"; import deleteByIdSql from "./deleteById.sql"; import createTournamentSql from "./createTournament.sql"; import deleteTournamentByIdSql from "./deleteTournamentById.sql"; const createStm = sql.prepare(createSql); const updateStm = sql.prepare(updateSql); const createDateStm = sql.prepare(createDateSql); const deleteDatesByEventIdStm = sql.prepare(deleteDatesByEventIdSql); const createBadgeStm = sql.prepare(createBadgeSql); const deleteBadgesByEventIdStm = sql.prepare(deleteBadgesByEventIdSql); const createMapPoolMapStm = sql.prepare(createMapPoolMapSql); const deleteMapPoolMapsStm = sql.prepare(deleteMapPoolMapsSql); const createTieBreakerMapPoolMapStm = sql.prepare( createTieBreakerMapPoolMapSql ); const findMapPoolByEventIdStm = sql.prepare(findMapPoolByEventIdSql); const findTieBreakerMapPoolByEventIdtm = sql.prepare( findTieBreakerMapPoolByEventIdSql ); const deleteByIdStm = sql.prepare(deleteByIdSql); const deleteTournamentByIdStm = sql.prepare(deleteTournamentByIdSql); const createTournamentStm = sql.prepare(createTournamentSql); const createTournament = ( args: Omit ) => { return createTournamentStm.get(args) as Tournament; }; export type CreateArgs = Pick< CalendarEvent, | "name" | "authorId" | "tags" | "description" | "discordInviteCode" | "bracketUrl" > & { startTimes: Array; badges: Array; mapPoolMaps?: Array>; createTournament: boolean; mapPickingStyle: Tournament["mapPickingStyle"]; }; export const create = sql.transaction( ({ startTimes, badges, mapPoolMaps = [], ...calendarEventArgs }: CreateArgs) => { let tournamentId; if (calendarEventArgs.createTournament) { tournamentId = createTournament({ // TODO: format picking format: "DE", mapPickingStyle: calendarEventArgs.mapPickingStyle, }).id; } const createdEvent = createStm.get({ ...calendarEventArgs, tournamentId, }) as CalendarEvent; for (const startTime of startTimes) { createDateStm.run({ eventId: createdEvent.id, startTime, }); } for (const badgeId of badges) { createBadgeStm.run({ eventId: createdEvent.id, badgeId, }); } upsertMapPool({ eventId: createdEvent.id, mapPoolMaps, isFullTournament: calendarEventArgs.createTournament, }); return createdEvent.id; } ); export type Update = Omit< CreateArgs, "authorId" | "createTournament" | "mapPickingStyle" > & { eventId: CalendarEvent["id"]; }; export const update = sql.transaction( ({ startTimes, badges, mapPoolMaps = [], eventId, ...calendarEventArgs }: Update) => { const event = updateStm.get({ ...calendarEventArgs, eventId, }) as CalendarEvent; deleteDatesByEventIdStm.run({ eventId }); for (const startTime of startTimes) { createDateStm.run({ eventId, startTime, }); } deleteBadgesByEventIdStm.run({ eventId }); for (const badgeId of badges) { createBadgeStm.run({ eventId, badgeId, }); } // can't edit tournament specific info after creation if (!event.tournamentId) { upsertMapPool({ eventId, mapPoolMaps, isFullTournament: false, }); } } ); function upsertMapPool({ eventId, mapPoolMaps, isFullTournament, }: { eventId: Update["eventId"]; mapPoolMaps: NonNullable; isFullTournament: boolean; }) { deleteMapPoolMapsStm.run({ calendarEventId: eventId }); if (isFullTournament) { for (const mapPoolArgs of mapPoolMaps) { createTieBreakerMapPoolMapStm.run({ calendarEventId: eventId, ...mapPoolArgs, }); } } else { for (const mapPoolArgs of mapPoolMaps) { createMapPoolMapStm.run({ calendarEventId: eventId, ...mapPoolArgs, }); } } } const updateParticipantsCountStm = sql.prepare(updateParticipantsCountSql); const deleteResultTeamsByEventIdStm = sql.prepare( deleteResultTeamsByEventIdSql ); const insertResultTeamStm = sql.prepare(insertResultTeamSql); const insertResultPlayerStm = sql.prepare(insertResultPlayerSql); export const upsertReportedScores = sql.transaction( ({ eventId, participantCount, results, }: { eventId: CalendarEvent["id"]; participantCount: CalendarEvent["participantCount"]; results: Array<{ teamName: CalendarEventResultTeam["name"]; placement: CalendarEventResultTeam["placement"]; players: Array<{ userId: CalendarEventResultPlayer["userId"]; name: CalendarEventResultPlayer["name"]; }>; }>; }) => { updateParticipantsCountStm.run({ eventId, participantCount }); deleteResultTeamsByEventIdStm.run({ eventId }); for (const { players, ...teamArgs } of results) { const teamInDb = insertResultTeamStm.get({ eventId, name: teamArgs.teamName, placement: teamArgs.placement, }) as CalendarEventResultTeam; for (const playerArgs of players) { insertResultPlayerStm.run({ teamId: teamInDb.id, userId: playerArgs.userId, name: playerArgs.name, }); } } } ); const findWinnersByEventIdStm = sql.prepare(findWinnersByEventIdSql); export function findResultsByEventId(eventId: CalendarEvent["id"]) { const rows = findWinnersByEventIdStm.all({ eventId }) as Array<{ id: CalendarEventResultTeam["id"]; teamName: CalendarEventResultTeam["name"]; placement: CalendarEventResultTeam["placement"]; playerId: CalendarEventResultPlayer["userId"]; playerName: CalendarEventResultPlayer["name"] | null; playerDiscordName: User["discordName"] | null; playerDiscordDiscriminator: User["discordDiscriminator"] | null; playerDiscordId: User["discordId"] | null; playerDiscordAvatar: User["discordAvatar"]; }>; const result: Array<{ teamName: CalendarEventResultTeam["name"]; placement: CalendarEventResultTeam["placement"]; players: Array< | string | Pick< User, | "id" | "discordId" | "discordName" | "discordDiscriminator" | "discordAvatar" > >; }> = []; for (const row of rows) { const team = result.find((team) => team.teamName === row.teamName); const player = row.playerName ?? { // player name and user id are mutually exclusive // also if user id exists we know a joined user also has to exist id: row.playerId!, discordId: row.playerDiscordId!, discordName: row.playerDiscordName!, discordDiscriminator: row.playerDiscordDiscriminator!, discordAvatar: row.playerDiscordAvatar, }; if (team) { team.players.push(player); } else { result.push({ teamName: row.teamName, placement: row.placement, players: [player], }); } } return result; } const findResultsByUserIdStm = sql.prepare(findResultsByUserIdSql); const findMatesByResultTeamIdStm = sql.prepare(findMatesByResultTeamIdSql); export function findResultsByUserId(userId: User["id"]) { return ( findResultsByUserIdStm.all({ userId }) as Array<{ eventId: CalendarEvent["id"]; teamId: CalendarEventResultTeam["id"]; eventName: CalendarEvent["name"]; teamName: CalendarEventResultTeam["name"]; placement: CalendarEventResultTeam["placement"]; participantCount: CalendarEvent["participantCount"]; startTime: CalendarEventDate["startTime"]; isHighlight: number; }> ).map((row) => ({ ...row, isHighlight: Boolean(row.isHighlight), mates: ( findMatesByResultTeamIdStm.all({ teamId: row.teamId, userId, }) as Array<{ name: CalendarEventResultPlayer["name"]; id: User["id"]; discordName: User["discordName"]; discordDiscriminator: User["discordDiscriminator"]; discordId: User["discordId"]; discordAvatar: User["discordAvatar"]; }> ).map(({ name, ...mate }) => name ?? mate), })); } const findAllBetweenTwoTimestampsStm = sql.prepare( findAllBetweenTwoTimestampsSql ); function addTagArray< T extends { hasBadge: number; tags?: CalendarEvent["tags"]; tournamentId: CalendarEvent["tournamentId"]; } >(arg: T) { const { hasBadge, ...row } = arg; const tags = (row.tags ? row.tags.split(",") : []) as Array; if (hasBadge) tags.unshift("BADGE"); if (row.tournamentId) tags.unshift("FULL_TOURNAMENT"); return { ...row, tags }; } export function findAllBetweenTwoTimestamps({ startTime, endTime, }: { startTime: Date; endTime: Date; }) { const rows = findAllBetweenTwoTimestampsStm.all({ startTime: dateToDatabaseTimestamp(startTime), endTime: dateToDatabaseTimestamp(endTime), }) as Array< Pick< CalendarEvent, "name" | "discordUrl" | "bracketUrl" | "tags" | "tournamentId" > & Pick & { eventDateId: CalendarEventDate["id"]; } & Pick & { nthAppearance: number; } & { hasBadge: number } >; return rows.map(addTagArray).map(addBadges); } const findByIdStm = sql.prepare(findByIdSql); export function findById(id: CalendarEvent["id"]) { const [firstRow, ...rest] = findByIdStm.all({ id }) as Array< Pick< CalendarEvent, | "name" | "description" | "discordUrl" | "discordInviteCode" | "bracketUrl" | "tags" | "authorId" | "participantCount" | "tournamentId" > & Pick & Pick & Pick< User, "discordName" | "discordDiscriminator" | "discordId" | "discordAvatar" > & { hasBadge: number } >; if (!firstRow) return null; return addTagArray({ ...firstRow, startTimes: [firstRow, ...rest].map((row) => row.startTime), startTime: undefined, }); } function addBadges< T extends { eventId: CalendarEvent["id"]; tags?: CalendarEvent["tags"] } >(arg: T) { return { ...arg, badgePrizes: arg.tags?.includes("BADGE") ? findBadgesByEventId(arg.eventId) : [], }; } const startTimesOfRangeStm = sql.prepare(startTimesOfRangeSql); export function startTimesOfRange({ startTime, endTime, }: { startTime: Date; endTime: Date; }) { return ( startTimesOfRangeStm.all({ startTime: dateToDatabaseTimestamp(startTime), endTime: dateToDatabaseTimestamp(endTime), }) as Array> ).map(({ startTime }) => startTime); } const findBadgesByEventIdStm = sql.prepare(findBadgesByEventIdSql); export function findBadgesByEventId(eventId: CalendarEvent["id"]) { return findBadgesByEventIdStm.all({ eventId }) as Array< Pick >; } export function findMapPoolByEventId(calendarEventId: CalendarEvent["id"]) { const rows = findMapPoolByEventIdStm.all({ calendarEventId }) as Array< Pick >; if (rows.length === 0) return; return MapPool.parse(rows); } const eventsToReportStm = sql.prepare(eventsToReportSql); export function eventsToReport(authorId?: CalendarEvent["authorId"]) { if (!authorId) return []; const oneMonthAgo = new Date(); oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1); return ( eventsToReportStm.all({ authorId, upperLimitTime: dateToDatabaseTimestamp(new Date()), lowerLimitTime: dateToDatabaseTimestamp(oneMonthAgo), }) as Array> ).map((row) => ({ id: row.id, name: row.name })); } const findRecentMapPoolsByAuthorIdStm = sql.prepare( findRecentMapPoolsByAuthorIdSql ); export function findRecentMapPoolsByAuthorId( authorId: CalendarEvent["authorId"] ) { return ( findRecentMapPoolsByAuthorIdStm.all({ authorId }) as Array< Pick & { mapPool: string; } > ).map((row) => ({ id: row.id, name: row.name, serializedMapPool: MapPool.serialize(JSON.parse(row.mapPool)), })); } const findAllEventsWithMapPoolsStm = sql.prepare(findAllEventsWithMapPoolsSql); export function findAllEventsWithMapPools() { return ( findAllEventsWithMapPoolsStm.all() as Array< Pick & { mapPool: string; } > ).map((row) => ({ id: row.id, name: row.name, serializedMapPool: MapPool.serialize(JSON.parse(row.mapPool)), })); } export function findTieBreakerMapPoolByEventId( calendarEventId: string | number ) { return findTieBreakerMapPoolByEventIdtm.all({ calendarEventId }) as Array< Pick >; } export const deleteById = sql.transaction( ({ eventId, tournamentId, }: { eventId: number; tournamentId: number | null; }) => { deleteByIdStm.run({ id: eventId }); if (tournamentId) deleteTournamentByIdStm.run({ id: tournamentId }); } );