mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-05-10 04:40:46 -05:00
529 lines
15 KiB
TypeScript
529 lines
15 KiB
TypeScript
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<Tournament, "id" | "showMapListGenerator">
|
|
) => {
|
|
return createTournamentStm.get(args) as Tournament;
|
|
};
|
|
|
|
export type CreateArgs = Pick<
|
|
CalendarEvent,
|
|
| "name"
|
|
| "authorId"
|
|
| "tags"
|
|
| "description"
|
|
| "discordInviteCode"
|
|
| "bracketUrl"
|
|
> & {
|
|
startTimes: Array<CalendarEventDate["startTime"]>;
|
|
badges: Array<CalendarEventBadge["badgeId"]>;
|
|
mapPoolMaps?: Array<Pick<MapPoolMap, "mode" | "stageId">>;
|
|
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<Update["mapPoolMaps"]>;
|
|
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<CalendarEventTag>;
|
|
|
|
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<CalendarEventDate, "eventId" | "startTime"> & {
|
|
eventDateId: CalendarEventDate["id"];
|
|
} & Pick<User, "discordName" | "discordDiscriminator"> & {
|
|
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<Tournament, "mapPickingStyle"> &
|
|
Pick<CalendarEventDate, "startTime" | "eventId"> &
|
|
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<Pick<CalendarEventDate, "startTime">>
|
|
).map(({ startTime }) => startTime);
|
|
}
|
|
|
|
const findBadgesByEventIdStm = sql.prepare(findBadgesByEventIdSql);
|
|
export function findBadgesByEventId(eventId: CalendarEvent["id"]) {
|
|
return findBadgesByEventIdStm.all({ eventId }) as Array<
|
|
Pick<Badge, "id" | "code" | "hue" | "displayName">
|
|
>;
|
|
}
|
|
|
|
export function findMapPoolByEventId(calendarEventId: CalendarEvent["id"]) {
|
|
const rows = findMapPoolByEventIdStm.all({ calendarEventId }) as Array<
|
|
Pick<MapPoolMap, "stageId" | "mode">
|
|
>;
|
|
|
|
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<Pick<CalendarEvent, "id" | "name">>
|
|
).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<CalendarEvent, "id" | "name"> & {
|
|
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<CalendarEvent, "id" | "name"> & {
|
|
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<MapPoolMap, "mode" | "stageId">
|
|
>;
|
|
}
|
|
|
|
export const deleteById = sql.transaction(
|
|
({
|
|
eventId,
|
|
tournamentId,
|
|
}: {
|
|
eventId: number;
|
|
tournamentId: number | null;
|
|
}) => {
|
|
deleteByIdStm.run({ id: eventId });
|
|
if (tournamentId) deleteTournamentByIdStm.run({ id: tournamentId });
|
|
}
|
|
);
|