Various tournament queries migrated to Kysely (#2978)

This commit is contained in:
Kalle
2026-04-14 20:26:48 +03:00
committed by GitHub
parent ef4fb6423e
commit f3e660917d
27 changed files with 325 additions and 515 deletions

View File

@@ -1,4 +1,78 @@
import { sql } from "kysely";
import { jsonArrayFrom } from "kysely/helpers/sqlite";
import { db } from "~/db/sql";
import type { Unwrapped } from "~/utils/types";
export type FindMatchById = NonNullable<Unwrapped<typeof findMatchById>>;
export async function findMatchById(id: number) {
const row = await db
.selectFrom("TournamentMatch")
.innerJoin(
"TournamentStage",
"TournamentStage.id",
"TournamentMatch.stageId",
)
.innerJoin(
"TournamentRound",
"TournamentRound.id",
"TournamentMatch.roundId",
)
.innerJoin("Tournament", "Tournament.id", "TournamentStage.tournamentId")
.select(({ eb }) => [
"TournamentMatch.id",
"TournamentMatch.groupId",
"TournamentMatch.opponentOne",
"TournamentMatch.opponentTwo",
"TournamentMatch.chatCode",
"TournamentMatch.startedAt",
"TournamentMatch.status",
"Tournament.mapPickingStyle",
"TournamentRound.id as roundId",
"TournamentRound.maps as roundMaps",
jsonArrayFrom(
eb
.selectFrom("TournamentTeamMember")
.innerJoin("User", "User.id", "TournamentTeamMember.userId")
.select([
"User.id",
"User.username",
"TournamentTeamMember.tournamentTeamId",
sql<
string | null
>`coalesce("TournamentTeamMember"."inGameName", "User"."inGameName")`.as(
"inGameName",
),
"User.discordId",
"User.customUrl",
"User.discordAvatar",
"User.pronouns",
])
.where(({ or, eb: innerEb }) =>
or([
innerEb(
"TournamentTeamMember.tournamentTeamId",
"=",
sql<number>`"TournamentMatch"."opponentOne" ->> '$.id'`,
),
innerEb(
"TournamentTeamMember.tournamentTeamId",
"=",
sql<number>`"TournamentMatch"."opponentTwo" ->> '$.id'`,
),
]),
),
).as("players"),
])
.where("TournamentMatch.id", "=", id)
.executeTakeFirst();
if (!row) return;
return {
...row,
bestOf: row.roundMaps.count,
};
}
export function findResultById(id: number) {
return db

View File

@@ -288,9 +288,8 @@ export const action: ActionFunction = async ({ params, request }) => {
`Checking in (bracket try): tournament team id: ${teamMemberOf.id} - user id: ${user.id} - tournament id: ${tournament.ctx.id} - bracket idx: ${data.bracketIdx}`,
);
await TournamentTeamRepository.checkIn({
await TournamentTeamRepository.checkIn(teamMemberOf.id, {
bracketIdx: data.bracketIdx,
tournamentTeamId: teamMemberOf.id,
});
logger.info(

View File

@@ -29,14 +29,11 @@ import { deleteMatchPickBanEvents } from "../queries/deleteMatchPickBanEvents.se
import { deleteParticipantsByMatchGameResultId } from "../queries/deleteParticipantsByMatchGameResultId.server";
import { deletePickBanEvent } from "../queries/deletePickBanEvent.server";
import { deleteTournamentMatchGameResultById } from "../queries/deleteTournamentMatchGameResultById.server";
import {
type FindMatchById,
findMatchById,
} from "../queries/findMatchById.server";
import { findResultsByMatchId } from "../queries/findResultsByMatchId.server";
import { insertTournamentMatchGameResult } from "../queries/insertTournamentMatchGameResult.server";
import { insertTournamentMatchGameResultParticipant } from "../queries/insertTournamentMatchGameResultParticipant.server";
import { updateMatchGameResultPoints } from "../queries/updateMatchGameResultPoints.server";
import type { FindMatchById } from "../TournamentMatchRepository.server";
import {
matchPageParamsSchema,
matchSchema,
@@ -56,7 +53,9 @@ export const action: ActionFunction = async ({ params, request }) => {
params,
schema: matchPageParamsSchema,
});
const match = notFoundIfFalsy(findMatchById(matchId));
const match = notFoundIfFalsy(
await TournamentMatchRepository.findMatchById(matchId),
);
const data = await parseRequestPayload({
request,
schema: matchSchema,

View File

@@ -15,8 +15,8 @@ import { executeRoll } from "../core/executeRoll.server";
import { mapListFromResults, resolveMapList } from "../core/mapList.server";
import * as PickBan from "../core/PickBan";
import { tournamentFromDBCached } from "../core/Tournament.server";
import { findMatchById } from "../queries/findMatchById.server";
import { findResultsByMatchId } from "../queries/findResultsByMatchId.server";
import * as TournamentMatchRepository from "../TournamentMatchRepository.server";
import { matchPageParamsSchema } from "../tournament-bracket-schemas.server";
import { matchEndedEarly } from "../tournament-bracket-utils";
@@ -33,7 +33,9 @@ export const loader = async ({ params }: LoaderFunctionArgs) => {
user: undefined,
});
const match = notFoundIfFalsy(findMatchById(matchId));
const match = notFoundIfFalsy(
await TournamentMatchRepository.findMatchById(matchId),
);
const isBye = !match.opponentOne || !match.opponentTwo;
if (isBye) {

View File

@@ -1,91 +0,0 @@
import { sql } from "~/db/sql";
import type { Tables, TournamentRoundMaps } from "~/db/tables";
import type { Match } from "~/modules/brackets-model";
import { parseDBArray } from "~/utils/sql";
const stm = sql.prepare(/* sql */ `
select
"TournamentMatch"."id",
"TournamentMatch"."groupId",
"TournamentMatch"."opponentOne",
"TournamentMatch"."opponentTwo",
"TournamentMatch"."chatCode",
"TournamentMatch"."startedAt",
"TournamentMatch"."status",
"Tournament"."mapPickingStyle",
"TournamentRound"."id" as "roundId",
"TournamentRound"."maps" as "roundMaps",
json_group_array(
json_object(
'id',
"User"."id",
'username',
"User"."username",
'tournamentTeamId',
"TournamentTeamMember"."tournamentTeamId",
'inGameName',
COALESCE("TournamentTeamMember"."inGameName", "User"."inGameName"),
'discordId',
"User"."discordId",
'customUrl',
"User"."customUrl",
'discordAvatar',
"User"."discordAvatar",
'pronouns', json("User"."pronouns")
)
) as "players"
from "TournamentMatch"
left join "TournamentStage" on "TournamentStage"."id" = "TournamentMatch"."stageId"
left join "TournamentRound" on "TournamentRound"."id" = "TournamentMatch"."roundId"
left join "Tournament" on "Tournament"."id" = "TournamentStage"."tournamentId"
left join "TournamentTeamMember" on
"TournamentTeamMember"."tournamentTeamId" = "TournamentMatch"."opponentOne" ->> '$.id'
or
"TournamentTeamMember"."tournamentTeamId" = "TournamentMatch"."opponentTwo" ->> '$.id'
left join "User" on "User"."id" = "TournamentTeamMember"."userId"
where "TournamentMatch"."id" = @id
group by "TournamentMatch"."id"
`);
export type FindMatchById = ReturnType<typeof findMatchById>;
export const findMatchById = (id: number) => {
const row = stm.get({ id }) as
| ((Pick<
Tables["TournamentMatch"],
"id" | "groupId" | "chatCode" | "startedAt" | "status"
> &
Pick<Tables["Tournament"], "mapPickingStyle"> & { players: string }) & {
opponentOne: string;
opponentTwo: string;
roundId: number;
roundMaps: string;
})
| undefined;
if (!row) return;
const roundMaps = JSON.parse(row.roundMaps) as TournamentRoundMaps;
return {
...row,
bestOf: roundMaps.count,
roundId: row.roundId,
roundMaps,
opponentOne: JSON.parse(row.opponentOne) as Match["opponent1"],
opponentTwo: JSON.parse(row.opponentTwo) as Match["opponent2"],
status: row.status,
players: (
parseDBArray(row.players) as Array<{
id: Tables["User"]["id"];
username: Tables["User"]["username"];
tournamentTeamId: Tables["TournamentTeamMember"]["tournamentTeamId"];
inGameName: Tables["User"]["inGameName"];
discordId: Tables["User"]["discordId"];
customUrl: Tables["User"]["customUrl"];
discordAvatar: Tables["User"]["discordAvatar"];
pronouns: Tables["User"]["pronouns"];
}>
).filter((player) => player.id),
};
};

View File

@@ -2,6 +2,7 @@ import type { Transaction } from "kysely";
import { sql } from "kysely";
import { db } from "~/db/sql";
import type { DB, Tables } from "~/db/tables";
import type { MapPool } from "~/features/map-list-generator/core/map-pool";
import type { ModeShort, StageId } from "~/modules/in-game-lists/types";
import { flatZip } from "~/utils/arrays";
import { databaseTimestampNow, dateToDatabaseTimestamp } from "~/utils/dates";
@@ -111,13 +112,11 @@ export function create({
avatarFileName,
userId,
tournamentId,
ownerInGameName,
}: {
team: Pick<Tables["TournamentTeam"], "name" | "prefersNotToHost" | "teamId">;
avatarFileName?: string;
userId: number;
tournamentId: number;
ownerInGameName: string | null;
}) {
return db.transaction().execute(async (trx) => {
const avatarImgId = avatarFileName
@@ -141,13 +140,15 @@ export function create({
.returning("id")
.executeTakeFirstOrThrow();
const inGameName = await resolveInGameName(trx, tournamentId, userId);
await trx
.insertInto("TournamentTeamMember")
.values({
tournamentTeamId: tournamentTeam.id,
userId,
role: "OWNER",
inGameName: ownerInGameName,
inGameName,
})
.execute();
@@ -155,6 +156,28 @@ export function create({
});
}
async function resolveInGameName(
trx: Transaction<DB>,
tournamentId: number,
userId: number,
) {
const tournament = await trx
.selectFrom("Tournament")
.select("Tournament.settings")
.where("Tournament.id", "=", tournamentId)
.executeTakeFirstOrThrow();
if (!tournament.settings.requireInGameNames) return null;
const user = await trx
.selectFrom("User")
.select("User.inGameName")
.where("User.id", "=", userId)
.executeTakeFirstOrThrow();
return user.inGameName;
}
export function copyFromAnotherTournament({
tournamentTeamId,
destinationTournamentId,
@@ -355,13 +378,17 @@ export function updateStartingBrackets(
});
}
export function checkIn({
tournamentTeamId,
bracketIdx,
}: {
tournamentTeamId: number;
bracketIdx: number | null;
}) {
/**
* Checks in a tournament team. Clears any existing check-out records before inserting the check-in.
* When called without `bracketIdx`, checks in for the whole tournament.
* When called with `bracketIdx`, checks in for a specific bracket (e.g. after progression).
*/
export function checkIn(
tournamentTeamId: number,
options?: { bracketIdx: number },
) {
const bracketIdx = options?.bracketIdx ?? null;
return db.transaction().execute(async (trx) => {
let query = trx
.deleteFrom("TournamentTeamCheckIn")
@@ -467,6 +494,139 @@ export function undoDropOut(tournamentTeamId: number) {
.execute();
}
export function join({
previousTeamId,
whatToDoWithPreviousTeam,
newTeamId,
userId,
checkOutTeam = false,
}: {
previousTeamId?: number;
whatToDoWithPreviousTeam?: "LEAVE" | "DELETE";
newTeamId: number;
userId: number;
checkOutTeam?: boolean;
}) {
return db.transaction().execute(async (trx) => {
if (whatToDoWithPreviousTeam === "DELETE") {
await trx
.deleteFrom("TournamentTeam")
.where("TournamentTeam.id", "=", previousTeamId!)
.execute();
} else if (whatToDoWithPreviousTeam === "LEAVE") {
await trx
.deleteFrom("TournamentTeamMember")
.where("TournamentTeamMember.tournamentTeamId", "=", previousTeamId!)
.where("TournamentTeamMember.userId", "=", userId)
.execute();
}
if (checkOutTeam) {
invariant(
previousTeamId,
"previousTeamId is required when checking out team",
);
await trx
.deleteFrom("TournamentTeamCheckIn")
.where("TournamentTeamCheckIn.tournamentTeamId", "=", previousTeamId)
.execute();
}
const tournamentId = (
await trx
.selectFrom("TournamentTeam")
.select("TournamentTeam.tournamentId")
.where("TournamentTeam.id", "=", newTeamId)
.executeTakeFirstOrThrow()
).tournamentId;
const inGameName = await resolveInGameName(trx, tournamentId, userId);
await trx
.insertInto("TournamentTeamMember")
.values({
tournamentTeamId: newTeamId,
userId,
inGameName,
})
.execute();
});
}
export function del(tournamentTeamId: number) {
return db.transaction().execute(async (trx) => {
await trx
.deleteFrom("MapPoolMap")
.where("MapPoolMap.tournamentTeamId", "=", tournamentTeamId)
.execute();
await trx
.deleteFrom("TournamentTeam")
.where("TournamentTeam.id", "=", tournamentTeamId)
.execute();
});
}
export function leave({ teamId, userId }: { teamId: number; userId: number }) {
return db
.deleteFrom("TournamentTeamMember")
.where("TournamentTeamMember.tournamentTeamId", "=", teamId)
.where("TournamentTeamMember.userId", "=", userId)
.execute();
}
export function transferOwnership(
tournamentTeamId: number,
{
oldCaptainId,
newCaptainId,
}: { oldCaptainId: number; newCaptainId: number },
) {
return db.transaction().execute(async (trx) => {
await trx
.updateTable("TournamentTeamMember")
.set({ role: "REGULAR" })
.where("TournamentTeamMember.tournamentTeamId", "=", tournamentTeamId)
.where("TournamentTeamMember.userId", "=", oldCaptainId)
.execute();
await trx
.updateTable("TournamentTeamMember")
.set({ role: "OWNER" })
.where("TournamentTeamMember.tournamentTeamId", "=", tournamentTeamId)
.where("TournamentTeamMember.userId", "=", newCaptainId)
.execute();
});
}
export function upsertCounterpickMaps({
tournamentTeamId,
mapPool,
}: {
tournamentTeamId: Tables["TournamentTeam"]["id"];
mapPool: MapPool;
}) {
return db.transaction().execute(async (trx) => {
await trx
.deleteFrom("MapPoolMap")
.where("MapPoolMap.tournamentTeamId", "=", tournamentTeamId)
.execute();
if (mapPool.stageModePairs.length > 0) {
await trx
.insertInto("MapPoolMap")
.values(
mapPool.stageModePairs.map(({ stageId, mode }) => ({
tournamentTeamId,
stageId,
mode,
})),
)
.execute();
}
});
}
async function findTeamRecentMaps(teamId: number, limit: number) {
return db
.selectFrom("TournamentMatchGameResult")
@@ -485,6 +645,14 @@ async function findTeamRecentMaps(teamId: number, limit: number) {
.execute();
}
export function findByInviteCode(inviteCode: string) {
return db
.selectFrom("TournamentTeam")
.select(["TournamentTeam.id", "TournamentTeam.tournamentId"])
.where("TournamentTeam.inviteCode", "=", inviteCode)
.executeTakeFirst();
}
export async function findRecentlyPlayedMapsByIds({
teamIds,
limit = 5,

View File

@@ -30,15 +30,9 @@ import {
} from "~/utils/remix.server";
import { assertUnreachable } from "~/utils/types";
import { idObject } from "../../../utils/zod";
import { changeTeamOwner } from "../queries/changeTeamOwner.server";
import { deleteTeam } from "../queries/deleteTeam.server";
import { joinTeam, leaveTeam } from "../queries/joinLeaveTeam.server";
import * as TournamentRepository from "../TournamentRepository.server";
import { adminActionSchema } from "../tournament-schemas.server";
import {
endDroppedTeamMatches,
inGameNameIfNeeded,
} from "../tournament-utils.server";
import { endDroppedTeamMatches } from "../tournament-utils.server";
export const action: ActionFunction = async ({ request, params }) => {
const user = requireUser();
@@ -70,16 +64,14 @@ export const action: ActionFunction = async ({ request, params }) => {
!tournament.teamMemberOfByUser({ id: data.userId }),
"User already on a team",
);
const addTeamUser = await UserRepository.findLeanById(data.userId);
errorToastIfFalsy(addTeamUser?.friendCode, "User has no friend code set");
errorToastIfFalsy(
(await UserRepository.findLeanById(data.userId))?.friendCode,
"User has no friend code set",
!tournament.ctx.settings.requireInGameNames || addTeamUser?.inGameName,
"User has no in-game name set",
);
await TournamentTeamRepository.create({
ownerInGameName: await inGameNameIfNeeded({
tournament,
userId: data.userId,
}),
team: {
name: data.teamName,
prefersNotToHost: 0,
@@ -112,10 +104,9 @@ export const action: ActionFunction = async ({ request, params }) => {
const newCaptain = team.members.find((m) => m.userId === data.memberId);
errorToastIfFalsy(newCaptain, "Invalid member id");
changeTeamOwner({
newCaptainId: data.memberId,
await TournamentTeamRepository.transferOwnership(data.teamId, {
oldCaptainId: oldCaptain.userId,
tournamentTeamId: data.teamId,
newCaptainId: data.memberId,
});
message = "Team owner changed";
@@ -152,11 +143,11 @@ export const action: ActionFunction = async ({ request, params }) => {
invariant(bracket, "Invalid bracket idx");
errorToastIfFalsy(bracket.preview, "Bracket has been started");
await TournamentTeamRepository.checkIn({
tournamentTeamId: data.teamId,
await TournamentTeamRepository.checkIn(
data.teamId,
// no sources = regular check in
bracketIdx: !bracket.sources ? null : data.bracketIdx,
});
bracket.sources ? { bracketIdx: data.bracketIdx } : undefined,
);
message = "Checked team in";
break;
@@ -214,7 +205,7 @@ export const action: ActionFunction = async ({ request, params }) => {
});
}
leaveTeam({
await TournamentTeamRepository.leave({
userId: data.memberId,
teamId: team.id,
});
@@ -250,16 +241,22 @@ export const action: ActionFunction = async ({ request, params }) => {
"User trying to be added currently has an active ban from sendou.ink",
);
const addMemberUser = await UserRepository.findLeanById(data.userId);
errorToastIfFalsy(
(await UserRepository.findLeanById(data.userId))?.friendCode,
addMemberUser?.friendCode,
"User has no friend code set",
);
errorToastIfFalsy(
!tournament.ctx.settings.requireInGameNames ||
addMemberUser?.inGameName,
"User has no in-game name set",
);
await TournamentLFGRepository.leaveLfg({
userId: data.userId,
tournamentId,
});
joinTeam({
await TournamentTeamRepository.join({
userId: data.userId,
newTeamId: team.id,
previousTeamId: previousTeam?.id,
@@ -270,11 +267,6 @@ export const action: ActionFunction = async ({ request, params }) => {
tournament.hasStarted
? "DELETE"
: undefined,
tournamentId,
inGameName: await inGameNameIfNeeded({
tournament,
userId: data.userId,
}),
});
ShowcaseTournaments.addToCached({
@@ -310,7 +302,7 @@ export const action: ActionFunction = async ({ request, params }) => {
errorToastIfFalsy(team, "Invalid team id");
errorToastIfFalsy(!tournament.hasStarted, "Tournament has started");
deleteTeam(team.id);
await TournamentTeamRepository.del(team.id);
for (const member of team.members) {
ShowcaseTournaments.removeFromCached({

View File

@@ -2,6 +2,7 @@ import type { ActionFunction } from "react-router";
import { redirect } from "react-router";
import { requireUser } from "~/features/auth/core/user.server";
import * as ShowcaseTournaments from "~/features/front-page/core/ShowcaseTournaments.server";
import * as TournamentTeamRepository from "~/features/tournament/TournamentTeamRepository.server";
import {
clearTournamentDataCache,
tournamentFromDB,
@@ -16,11 +17,8 @@ import {
} from "~/utils/remix.server";
import { tournamentPage } from "~/utils/urls";
import { idObject } from "~/utils/zod";
import { findByInviteCode } from "../queries/findTeamByInviteCode.server";
import { joinTeam } from "../queries/joinLeaveTeam.server";
import { validateCanJoinTeam } from "../tournament-utils";
import {
inGameNameIfNeeded,
requireNotBannedByOrganization,
requireSendouQParticipationIfNeeded,
} from "../tournament-utils.server";
@@ -35,7 +33,9 @@ export const action: ActionFunction = async ({ request, params }) => {
const inviteCode = url.searchParams.get("code");
invariant(inviteCode, "code is missing");
const leanTeam = notFoundIfFalsy(findByInviteCode(inviteCode));
const leanTeam = notFoundIfFalsy(
await TournamentTeamRepository.findByInviteCode(inviteCode),
);
const tournament = await tournamentFromDB({ tournamentId, user });
@@ -88,7 +88,7 @@ export const action: ActionFunction = async ({ request, params }) => {
: "LEAVE";
await TournamentLFGRepository.leaveLfg({ userId: user.id, tournamentId });
joinTeam({
await TournamentTeamRepository.join({
userId: user.id,
newTeamId: teamToJoin.id,
previousTeamId: previousTeam?.id,
@@ -99,11 +99,6 @@ export const action: ActionFunction = async ({ request, params }) => {
previousTeam &&
previousTeam.members.length <= tournament.minMembersPerTeam,
whatToDoWithPreviousTeam,
tournamentId,
inGameName: await inGameNameIfNeeded({
tournament,
userId: user.id,
}),
});
ShowcaseTournaments.addToCached({

View File

@@ -22,12 +22,6 @@ import {
} from "~/utils/remix.server";
import { assertUnreachable } from "~/utils/types";
import { idObject } from "~/utils/zod";
import { checkIn } from "../queries/checkIn.server";
import { deleteTeam } from "../queries/deleteTeam.server";
import deleteTeamMember from "../queries/deleteTeamMember.server";
import { findOwnTournamentTeam } from "../queries/findOwnTournamentTeam.server";
import { joinTeam } from "../queries/joinLeaveTeam.server";
import { upsertCounterpickMaps } from "../queries/upsertCounterpickMaps.server";
import { TOURNAMENT } from "../tournament-constants";
import { registerSchema } from "../tournament-schemas.server";
import {
@@ -35,7 +29,6 @@ import {
validateCounterPickMapPool,
} from "../tournament-utils";
import {
inGameNameIfNeeded,
requireNotBannedByOrganization,
requireSendouQParticipationIfNeeded,
} from "../tournament-utils.server";
@@ -130,10 +123,6 @@ export const action: ActionFunction = async ({ request, params }) => {
tournamentId,
});
await TournamentTeamRepository.create({
ownerInGameName: await inGameNameIfNeeded({
tournament,
userId: user.id,
}),
team: {
name: data.teamName,
prefersNotToHost: Number(data.prefersNotToHost),
@@ -165,20 +154,18 @@ export const action: ActionFunction = async ({ request, params }) => {
);
errorToastIfFalsy(data.userId !== user.id, "Can't kick yourself");
const detailedOwnTeam = findOwnTournamentTeam({
tournamentId,
userId: user.id,
});
// making sure they aren't unfilling one checking in condition i.e. having full roster
// and then having members kicked without it affecting the checking in status
errorToastIfFalsy(
detailedOwnTeam &&
(!detailedOwnTeam.checkedInAt ||
ownTeam.members.length > tournament.minMembersPerTeam),
!ownTeamCheckedIn ||
ownTeam.members.length > tournament.minMembersPerTeam,
"Can't kick a member after checking in",
);
deleteTeamMember({ tournamentTeamId: ownTeam.id, userId: data.userId });
await TournamentTeamRepository.leave({
teamId: ownTeam.id,
userId: data.userId,
});
ShowcaseTournaments.removeFromCached({
tournamentId,
@@ -197,8 +184,8 @@ export const action: ActionFunction = async ({ request, params }) => {
"You cannot leave after checking in",
);
deleteTeamMember({
tournamentTeamId: teamMemberOf.id,
await TournamentTeamRepository.leave({
teamId: teamMemberOf.id,
userId: user.id,
});
@@ -225,7 +212,7 @@ export const action: ActionFunction = async ({ request, params }) => {
"Invalid map pool",
);
upsertCounterpickMaps({
await TournamentTeamRepository.upsertCounterpickMaps({
tournamentTeamId: ownTeam.id,
mapPool: new MapPool(data.mapPool),
});
@@ -253,7 +240,7 @@ export const action: ActionFunction = async ({ request, params }) => {
`Can't check-in - ${tournament.checkInConditionsFulfilledByTeamId(teamMemberOf.id).reason}`,
);
checkIn(teamMemberOf.id);
await TournamentTeamRepository.checkIn(teamMemberOf.id);
logger.info(
`Checking in (success): tournament team id: ${teamMemberOf.id} - user id: ${user.id} - tournament id: ${tournamentId}`,
);
@@ -293,14 +280,9 @@ export const action: ActionFunction = async ({ request, params }) => {
userId: data.userId,
tournamentId,
});
joinTeam({
await TournamentTeamRepository.join({
userId: data.userId,
newTeamId: ownTeam.id,
tournamentId,
inGameName: await inGameNameIfNeeded({
tournament,
userId: data.userId,
}),
});
await SavedCalendarEventRepository.unsave({
@@ -344,7 +326,7 @@ export const action: ActionFunction = async ({ request, params }) => {
"Unregistering from leagues is not possible after registration has closed",
);
deleteTeam(ownTeam.id);
await TournamentTeamRepository.del(ownTeam.id);
for (const member of ownTeam.members) {
ShowcaseTournaments.removeFromCached({

View File

@@ -1,12 +1,16 @@
import type { LoaderFunctionArgs } from "react-router";
import { findByInviteCode } from "../queries/findTeamByInviteCode.server";
import * as TournamentTeamRepository from "~/features/tournament/TournamentTeamRepository.server";
export const loader = ({ request }: LoaderFunctionArgs) => {
export const loader = async ({ request }: LoaderFunctionArgs) => {
const url = new URL(request.url);
const inviteCode = url.searchParams.get("code");
const team = inviteCode
? await TournamentTeamRepository.findByInviteCode(inviteCode)
: null;
return {
teamId: inviteCode ? findByInviteCode(inviteCode)?.id : null,
teamId: team?.id ?? null,
inviteCode,
};
};

View File

@@ -3,10 +3,10 @@ import { getUser } from "~/features/auth/core/user.server";
import * as SQGroupRepository from "~/features/sendouq/SQGroupRepository.server";
import * as TeamRepository from "~/features/team/TeamRepository.server";
import * as SavedCalendarEventRepository from "~/features/tournament/SavedCalendarEventRepository.server";
import { tournamentFromDBCached } from "~/features/tournament-bracket/core/Tournament.server";
import { findMapPoolByTeamId } from "~/features/tournament-bracket/queries/findMapPoolByTeamId.server";
import { parseParams } from "~/utils/remix.server";
import { idObject } from "~/utils/zod";
import { findOwnTournamentTeam } from "../queries/findOwnTournamentTeam.server";
export const loader = async ({ params }: LoaderFunctionArgs) => {
const user = getUser();
@@ -17,11 +17,10 @@ export const loader = async ({ params }: LoaderFunctionArgs) => {
schema: idObject,
});
const ownTournamentTeam = findOwnTournamentTeam({
tournamentId,
userId: user.id,
});
if (!ownTournamentTeam) {
const tournament = await tournamentFromDBCached({ tournamentId, user });
const ownTeam = tournament.ownedTeamByUser(user);
if (!ownTeam) {
return {
mapPool: null,
friendPlayers: null,
@@ -34,7 +33,7 @@ export const loader = async ({ params }: LoaderFunctionArgs) => {
}
return {
mapPool: findMapPoolByTeamId(ownTournamentTeam.id),
mapPool: findMapPoolByTeamId(ownTeam.id),
friendPlayers: await SQGroupRepository.friendsAndTeammates(user.id),
teams: await TeamRepository.findAllMemberOfByUserId(user.id),
isSaved: false,

View File

@@ -1,30 +0,0 @@
import { sql } from "~/db/sql";
import type { Tables } from "~/db/tables";
const stm = sql.prepare(/* sql */ `
update TournamentTeamMember
set "role" = @role
where
"tournamentTeamId" = @tournamentTeamId and
"userId" = @userId
`);
export const changeTeamOwner = sql.transaction(
(args: {
tournamentTeamId: Tables["TournamentTeam"]["id"];
oldCaptainId: Tables["User"]["id"];
newCaptainId: Tables["User"]["id"];
}) => {
stm.run({
tournamentTeamId: args.tournamentTeamId,
userId: args.oldCaptainId,
role: "REGULAR",
});
stm.run({
tournamentTeamId: args.tournamentTeamId,
userId: args.newCaptainId,
role: "OWNER",
});
},
);

View File

@@ -1,12 +0,0 @@
import { sql } from "~/db/sql";
const stm = sql.prepare(/* sql */ `
insert into "TournamentTeamCheckIn"
("tournamentTeamId", "checkedInAt")
values
(@tournamentTeamId, strftime('%s', 'now'))
`);
export function checkIn(tournamentTeamId: number) {
stm.run({ tournamentTeamId });
}

View File

@@ -1,10 +0,0 @@
import { sql } from "~/db/sql";
const stm = sql.prepare(/* sql */ `
delete from "TournamentTeamCheckIn"
where "tournamentTeamId" = @tournamentTeamId
`);
export function checkOut(tournamentTeamId: number) {
stm.run({ tournamentTeamId });
}

View File

@@ -1,16 +0,0 @@
import { sql } from "~/db/sql";
const deleteTeamStm = sql.prepare(/*sql*/ `
delete from "TournamentTeam"
where "id" = @tournamentTeamId
`);
const deleteMapPoolStm = sql.prepare(/*sql*/ `
delete from "MapPoolMap"
where "tournamentTeamId" = @tournamentTeamId
`);
export const deleteTeam = sql.transaction((tournamentTeamId: number) => {
deleteMapPoolStm.run({ tournamentTeamId });
deleteTeamStm.run({ tournamentTeamId });
});

View File

@@ -1,17 +0,0 @@
import { sql } from "~/db/sql";
const stm = sql.prepare(/*sql*/ `
delete from "TournamentTeamMember"
where userId = @userId
and tournamentTeamId = @tournamentTeamId
`);
export default function deleteTeamMember({
userId,
tournamentTeamId,
}: {
userId: number;
tournamentTeamId: number;
}) {
stm.run({ userId, tournamentTeamId });
}

View File

@@ -1,39 +0,0 @@
import { sql } from "~/db/sql";
import type { Tables } from "~/db/tables";
const stm = sql.prepare(/*sql*/ `
select
"TournamentTeam"."id",
"TournamentTeam"."name",
"TournamentTeamCheckIn"."checkedInAt",
"TournamentTeam"."inviteCode"
from
"TournamentTeam"
left join "TournamentTeamCheckIn" on
"TournamentTeamCheckIn"."tournamentTeamId" = "TournamentTeam"."id"
left join "TournamentTeamMember" on
"TournamentTeamMember"."tournamentTeamId" = "TournamentTeam"."id"
and "TournamentTeamMember"."role" = 'OWNER'
where
"TournamentTeam"."tournamentId" = @tournamentId
and "TournamentTeam"."isPlaceholder" = 0
and "TournamentTeamMember"."userId" = @userId
`);
type FindOwnTeam =
| (Pick<Tables["TournamentTeam"], "id" | "name" | "inviteCode"> &
Pick<Tables["TournamentTeamCheckIn"], "checkedInAt">)
| null;
export function findOwnTournamentTeam({
tournamentId,
userId,
}: {
tournamentId: number;
userId: number;
}) {
return stm.get({
tournamentId,
userId,
}) as FindOwnTeam;
}

View File

@@ -1,16 +0,0 @@
import { sql } from "~/db/sql";
const stm = sql.prepare(/*sql */ `
select
"TournamentTeam"."id",
"TournamentTeam"."tournamentId"
from "TournamentTeam"
where "TournamentTeam"."inviteCode" = @inviteCode
`);
export function findByInviteCode(inviteCode: string) {
return stm.get({ inviteCode }) as {
id: number;
tournamentId: number;
} | null;
}

View File

@@ -1,14 +0,0 @@
import { sql } from "~/db/sql";
import type { Tables } from "~/db/tables";
const stm = sql.prepare(/*sql*/ `
select 1
from "TournamentResult"
where "TournamentResult"."tournamentId" = @tournamentId
`);
export default function hasTournamentFinalized(
tournamentId: Tables["Tournament"]["id"],
) {
return Boolean(stm.get({ tournamentId }));
}

View File

@@ -1,14 +0,0 @@
import { sql } from "~/db/sql";
import type { Tables } from "~/db/tables";
const stm = sql.prepare(/*sql*/ `
select 1
from "TournamentStage"
where "TournamentStage"."tournamentId" = @tournamentId
`);
export default function hasTournamentStarted(
tournamentId: Tables["Tournament"]["id"],
) {
return Boolean(stm.get({ tournamentId }));
}

View File

@@ -1,76 +0,0 @@
import { sql } from "~/db/sql";
import invariant from "~/utils/invariant";
import { checkOut } from "./checkOut.server";
const createTeamMemberStm = sql.prepare(/*sql*/ `
insert into "TournamentTeamMember" (
"tournamentTeamId",
"inGameName",
"userId"
) values (
@tournamentTeamId,
@inGameName,
@userId
)
`);
const deleteTeamStm = sql.prepare(/*sql*/ `
delete from "TournamentTeam"
where "id" = @tournamentTeamId
`);
const deleteMemberStm = sql.prepare(/*sql*/ `
delete from "TournamentTeamMember"
where "tournamentTeamId" = @tournamentTeamId
and "userId" = @userId
`);
export const joinTeam = sql.transaction(
({
previousTeamId,
whatToDoWithPreviousTeam,
newTeamId,
userId,
inGameName,
tournamentId: _tournamentId,
checkOutTeam = false,
}: {
previousTeamId?: number;
whatToDoWithPreviousTeam?: "LEAVE" | "DELETE";
newTeamId: number;
userId: number;
inGameName: string | null;
tournamentId: number;
checkOutTeam?: boolean;
}) => {
if (whatToDoWithPreviousTeam === "DELETE") {
deleteTeamStm.run({ tournamentTeamId: previousTeamId ?? null });
} else if (whatToDoWithPreviousTeam === "LEAVE") {
deleteMemberStm.run({ tournamentTeamId: previousTeamId ?? null, userId });
}
if (checkOutTeam) {
invariant(
previousTeamId,
"previousTeamId is required when checking out team",
);
checkOut(previousTeamId);
}
createTeamMemberStm.run({
tournamentTeamId: newTeamId,
userId,
inGameName,
});
},
);
export const leaveTeam = ({
teamId,
userId,
}: {
teamId: number;
userId: number;
}) => {
deleteMemberStm.run({ tournamentTeamId: teamId, userId });
};

View File

@@ -1,37 +0,0 @@
import { sql } from "~/db/sql";
import type { Tables } from "~/db/tables";
import type { MapPool } from "~/features/map-list-generator/core/map-pool";
const deleteCounterpickMapsByTeamIdStm = sql.prepare(/* sql */ `
delete from
"MapPoolMap"
where
"tournamentTeamId" = @tournamentTeamId
`);
const addCounterpickMapStm = sql.prepare(/* sql */ `
insert into
"MapPoolMap" ("tournamentTeamId", "stageId", "mode")
values
(@tournamentTeamId, @stageId, @mode)
`);
export const upsertCounterpickMaps = sql.transaction(
({
tournamentTeamId,
mapPool,
}: {
tournamentTeamId: Tables["TournamentTeam"]["id"];
mapPool: MapPool;
}) => {
deleteCounterpickMapsByTeamIdStm.run({ tournamentTeamId });
for (const { stageId, mode } of mapPool.stageModePairs) {
addCounterpickMapStm.run({
tournamentTeamId,
stageId,
mode,
});
}
},
);

View File

@@ -1,4 +1,5 @@
import { type LoaderFunctionArgs, redirect } from "react-router";
import { tournamentFromDBCached } from "~/features/tournament-bracket/core/Tournament.server";
import { parseParams } from "~/utils/remix.server";
import {
tournamentBracketsPage,
@@ -6,20 +7,23 @@ import {
tournamentResultsPage,
} from "~/utils/urls";
import { idObject } from "~/utils/zod";
import hasTournamentFinalized from "../queries/hasTournamentFinalized.server";
import hasTournamentStarted from "../queries/hasTournamentStarted.server";
export const loader = ({ params }: LoaderFunctionArgs) => {
export const loader = async ({ params }: LoaderFunctionArgs) => {
const { id: tournamentId } = parseParams({
params,
schema: idObject,
});
if (!hasTournamentStarted(tournamentId)) {
const tournament = await tournamentFromDBCached({
tournamentId,
user: undefined,
});
if (!tournament.hasStarted) {
return redirect(tournamentRegisterPage(tournamentId));
}
if (!hasTournamentFinalized(tournamentId)) {
if (!tournament.ctx.isFinalized) {
return redirect(tournamentBracketsPage({ tournamentId }));
}

View File

@@ -3,7 +3,6 @@ import { databaseTimestampNow } from "~/utils/dates";
import invariant from "~/utils/invariant";
import { getServerTournamentManager } from "../tournament-bracket/core/brackets-manager/manager.server";
import { tournamentFromDB } from "../tournament-bracket/core/Tournament.server";
import { joinTeam } from "./queries/joinLeaveTeam.server";
import { updateRoundMaps } from "./queries/updateRoundMaps.server";
import * as TournamentTeamRepository from "./TournamentTeamRepository.server";
@@ -57,7 +56,6 @@ export async function dbInsertTournamentTeam({
tournamentId?: number;
}) {
const tournamentTeam = await TournamentTeamRepository.create({
ownerInGameName: null,
team: {
name: `Test Team ${ownerId}`,
prefersNotToHost: 0,
@@ -70,19 +68,13 @@ export async function dbInsertTournamentTeam({
for (let i = 1; i < membersCount; i++) {
const memberId = ownerId + i;
joinTeam({
await TournamentTeamRepository.join({
userId: memberId,
newTeamId: tournamentTeam.id,
tournamentId,
inGameName: null,
});
}
await TournamentTeamRepository.checkIn({
tournamentTeamId: tournamentTeam.id,
// no sources = regular check in
bracketIdx: null,
});
await TournamentTeamRepository.checkIn(tournamentTeam.id);
}
/**

View File

@@ -1,28 +1,11 @@
import * as LeaderboardRepository from "~/features/leaderboards/LeaderboardRepository.server";
import type { getServerTournamentManager } from "~/features/tournament-bracket/core/brackets-manager/manager.server";
import * as TournamentOrganizationRepository from "~/features/tournament-organization/TournamentOrganizationRepository.server";
import * as UserRepository from "~/features/user-page/UserRepository.server";
import { logger } from "~/utils/logger";
import { errorToast, errorToastIfFalsy } from "~/utils/remix.server";
import { MATCHES_COUNT_NEEDED_FOR_LEADERBOARD } from "../leaderboards/leaderboards-constants";
import type { Tournament } from "../tournament-bracket/core/Tournament";
export const inGameNameIfNeeded = async ({
tournament,
userId,
}: {
tournament: Tournament;
userId: number;
}) => {
if (!tournament.ctx.settings.requireInGameNames) return null;
const inGameName = await UserRepository.inGameNameByUserId(userId);
errorToastIfFalsy(inGameName, "No in-game name");
return inGameName;
};
export async function requireNotBannedByOrganization({
tournament,
user,

View File

@@ -480,6 +480,7 @@ test.describe("Tournament bracket", () => {
test("shows tournament results on user profile after finalized tournament", async ({
page,
}) => {
test.slow();
const tournamentId = 4;
await seed(page, "SMALL_SOS");

View File

@@ -1,7 +1,6 @@
import "dotenv/config";
import * as LeaderboardRepository from "~/features/leaderboards/LeaderboardRepository.server";
import * as Seasons from "~/features/mmr/core/Seasons";
import { joinTeam } from "~/features/tournament/queries/joinLeaveTeam.server";
import * as TournamentTeamRepository from "~/features/tournament/TournamentTeamRepository.server";
import { tournamentFromDB } from "~/features/tournament-bracket/core/Tournament.server";
import * as UserRepository from "~/features/user-page/UserRepository.server";
@@ -101,10 +100,6 @@ async function main() {
const teamName = resolvedNames[i];
const owner = entry.members[0];
const ownerInGameName = tournament.ctx.settings.requireInGameNames
? await UserRepository.inGameNameByUserId(owner.id)
: null;
const tournamentTeam = await TournamentTeamRepository.create({
team: {
name: teamName,
@@ -113,19 +108,12 @@ async function main() {
},
userId: owner.id,
tournamentId,
ownerInGameName: ownerInGameName ?? null,
});
for (const member of entry.members.slice(1)) {
const memberInGameName = tournament.ctx.settings.requireInGameNames
? await UserRepository.inGameNameByUserId(member.id)
: null;
joinTeam({
await TournamentTeamRepository.join({
newTeamId: tournamentTeam.id,
userId: member.id,
inGameName: memberInGameName ?? null,
tournamentId,
});
}