sendou.ink/app/features/tournament-bracket/core/brackets-manager/crud-db.server.ts
Kalle eae3d529e2
Bracket component rewrite (#1653)
* Remove old code

* Add prefetching

* Elim bracket initial

* Hide rounds with only byes

* Round hiding logic

* Align stuff

* Add TODO

* Adjustments

* Deadline

* Compactify button

* Simulations

* Round robin bracket initial

* eventId -> tournamentId

* seedByTeamId removed

* Couple more TODOs

* RR placements table

* Locking matches

* Extract TournamentStream component

* Bracket streams

* Remove extras for tournament-manager, misc

* Fix E2E tests

* Fix SKALOP_SYSTEM_MESSAGE_URL in env.example

* TODOs

* TODO moved to GitHub

* Handle team changing in match cache invalidation

* Fix streamer seeing undo last score button

* Show "Sub" badge on team roster page

* Show who didn't play yet on match teams preview

* Ranked/unranked badge

* Bracket hover show roster

* Add lock/unlock match test

* Fix score reporting
2024-02-11 10:49:12 +02:00

506 lines
12 KiB
TypeScript

// this file offers database functions specifically for the crud.server.ts file
import type {
Participant,
Stage as StageType,
Group as GroupType,
Round as RoundType,
Match as MatchType,
} from "~/modules/brackets-model";
import { sql } from "~/db/sql";
import type {
Tournament,
TournamentGroup,
TournamentMatch,
TournamentRound,
TournamentStage,
TournamentTeam,
} from "~/db/types";
import { nanoid } from "nanoid";
import { dateToDatabaseTimestamp } from "~/utils/dates";
const team_getByTournamentIdStm = sql.prepare(/*sql*/ `
select
*
from
"TournamentTeam"
where
"TournamentTeam"."tournamentId" = @tournamentId
`);
export class Team {
static #convertTeam(rawTeam: TournamentTeam): Participant {
return {
id: rawTeam.id,
name: rawTeam.name,
tournament_id: rawTeam.tournamentId,
};
}
static getByTournamentId(tournamentId: Tournament["id"]): Participant[] {
return (team_getByTournamentIdStm.all({ tournamentId }) as any[]).map(
this.#convertTeam,
);
}
}
const stage_getByIdStm = sql.prepare(/*sql*/ `
select
*
from
"TournamentStage"
where
"TournamentStage"."id" = @id
`);
const stage_getByTournamentIdStm = sql.prepare(/*sql*/ `
select
*
from
"TournamentStage"
where
"TournamentStage"."tournamentId" = @tournamentId
`);
const stage_insertStm = sql.prepare(/*sql*/ `
insert into
"TournamentStage"
("tournamentId", "number", "name", "type", "settings", "createdAt")
values
(@tournamentId, @number, @name, @type, @settings, @createdAt)
returning *
`);
const stage_updateSettingsStm = sql.prepare(/*sql*/ `
update
"TournamentStage"
set
"settings" = @settings
where
"TournamentStage"."id" = @id
`);
export class Stage {
id?: TournamentStage["id"];
tournamentId: TournamentStage["tournamentId"];
number: TournamentStage["number"];
name: TournamentStage["name"];
type: StageType["type"];
settings: TournamentStage["settings"];
constructor(
id: TournamentStage["id"] | undefined,
tournamentId: TournamentStage["tournamentId"],
number: TournamentStage["number"],
name: TournamentStage["name"],
type: StageType["type"],
settings: TournamentStage["settings"],
) {
this.id = id;
this.tournamentId = tournamentId;
this.number = number;
this.name = name;
this.type = type;
this.settings = settings;
}
insert() {
const stage = stage_insertStm.get({
tournamentId: this.tournamentId,
number: this.number,
name: this.name,
type: this.type,
settings: this.settings,
createdAt: dateToDatabaseTimestamp(new Date()),
}) as any;
this.id = stage.id;
return true;
}
static #convertStage(rawStage: TournamentStage): StageType {
return {
id: rawStage.id,
name: rawStage.name,
number: rawStage.number,
settings: JSON.parse(rawStage.settings),
tournament_id: rawStage.tournamentId,
type: rawStage.type,
};
}
static getById(id: TournamentStage["id"]): StageType {
const stage = stage_getByIdStm.get({ id }) as any;
if (!stage) return stage;
return this.#convertStage(stage);
}
static getByTournamentId(tournamentId: Tournament["id"]): Participant[] {
return (stage_getByTournamentIdStm.all({ tournamentId }) as any[]).map(
this.#convertStage,
);
}
static updateSettings(
id: TournamentStage["id"],
settings: TournamentStage["settings"],
) {
stage_updateSettingsStm.run({ id, settings });
return true;
}
}
const group_getByIdStm = sql.prepare(/*sql*/ `
select *
from "TournamentGroup"
where "TournamentGroup"."id" = @id
`);
const group_getByStageIdStm = sql.prepare(/*sql*/ `
select *
from "TournamentGroup"
where "TournamentGroup"."stageId" = @stageId
`);
const group_getByStageAndNumberStm = sql.prepare(/*sql*/ `
select *
from "TournamentGroup"
where "TournamentGroup"."stageId" = @stageId
and "TournamentGroup"."number" = @number
`);
const group_insertStm = sql.prepare(/*sql*/ `
insert into
"TournamentGroup"
("stageId", "number")
values
(@stageId, @number)
returning *
`);
export class Group {
id?: TournamentGroup["id"];
stageId: TournamentGroup["stageId"];
number: TournamentGroup["number"];
constructor(
id: TournamentGroup["id"] | undefined,
stageId: TournamentGroup["stageId"],
number: TournamentGroup["number"],
) {
this.id = id;
this.stageId = stageId;
this.number = number;
}
static #convertGroup(rawGroup: TournamentGroup): GroupType {
return {
id: rawGroup.id,
number: rawGroup.number,
stage_id: rawGroup.stageId,
};
}
static getById(id: TournamentGroup["id"]): GroupType {
const group = group_getByIdStm.get({ id }) as any;
if (!group) return group;
return this.#convertGroup(group);
}
static getByStageId(stageId: TournamentStage["id"]): GroupType[] {
return (group_getByStageIdStm.all({ stageId }) as any[]).map(
this.#convertGroup,
);
}
static getByStageAndNumber(
stageId: TournamentStage["id"],
number: TournamentGroup["number"],
): GroupType {
const group = group_getByStageAndNumberStm.get({ stageId, number }) as any;
if (!group) return group;
return this.#convertGroup(group_getByStageAndNumberStm.get(group) as any);
}
insert() {
const group = group_insertStm.get({
stageId: this.stageId,
number: this.number,
}) as any;
this.id = group.id;
return true;
}
}
const round_getByIdStm = sql.prepare(/*sql*/ `
select *
from "TournamentRound"
where "TournamentRound"."id" = @id
`);
const round_getByGroupIdStm = sql.prepare(/*sql*/ `
select *
from "TournamentRound"
where "TournamentRound"."groupId" = @groupId
`);
const round_getByStageIdStm = sql.prepare(/*sql*/ `
select *
from "TournamentRound"
where "TournamentRound"."stageId" = @stageId
`);
const round_getByGroupAndNumberStm = sql.prepare(/*sql*/ `
select *
from "TournamentRound"
where "TournamentRound"."groupId" = @groupId
and "TournamentRound"."number" = @number
`);
const round_insertStm = sql.prepare(/*sql*/ `
insert into
"TournamentRound"
("stageId", "groupId", "number")
values
(@stageId, @groupId, @number)
returning *
`);
export class Round {
id?: TournamentRound["id"];
stageId: TournamentRound["stageId"];
groupId: TournamentRound["groupId"];
number: TournamentRound["number"];
constructor(
id: TournamentRound["id"] | undefined,
stageId: TournamentRound["stageId"],
groupId: TournamentRound["groupId"],
number: TournamentRound["number"],
) {
this.id = id;
this.stageId = stageId;
this.groupId = groupId;
this.number = number;
}
insert() {
const round = round_insertStm.get({
stageId: this.stageId,
groupId: this.groupId,
number: this.number,
}) as any;
this.id = round.id;
return true;
}
static #convertRound(rawRound: TournamentRound): RoundType {
return {
id: rawRound.id,
group_id: rawRound.groupId,
number: rawRound.number,
stage_id: rawRound.stageId,
};
}
static getByStageId(stageId: TournamentStage["id"]): RoundType[] {
return (round_getByStageIdStm.all({ stageId }) as any[]).map(
this.#convertRound,
);
}
static getByGroupId(groupId: TournamentGroup["id"]): RoundType[] {
return (round_getByGroupIdStm.all({ groupId }) as any[]).map(
this.#convertRound,
);
}
static getByGroupAndNumber(
groupId: TournamentGroup["id"],
number: TournamentRound["number"],
): RoundType {
const round = round_getByGroupAndNumberStm.get({ groupId, number }) as any;
if (!round) return round;
return this.#convertRound(round);
}
static getById(id: TournamentRound["id"]): RoundType {
const round = round_getByIdStm.get({ id }) as any;
if (!round) return round;
return this.#convertRound(round);
}
}
const match_getByIdStm = sql.prepare(/*sql*/ `
select *
from "TournamentMatch"
where "TournamentMatch"."id" = @id
`);
const match_getByStageIdStm = sql.prepare(/*sql*/ `
select
"TournamentMatch".*,
sum("TournamentMatchGameResult"."opponentOnePoints") as "opponentOnePointsTotal",
sum("TournamentMatchGameResult"."opponentTwoPoints") as "opponentTwoPointsTotal",
max("TournamentMatchGameResult"."createdAt") as "lastGameFinishedAt"
from "TournamentMatch"
left join "TournamentMatchGameResult" on "TournamentMatch"."id" = "TournamentMatchGameResult"."matchId"
where "TournamentMatch"."stageId" = @stageId
group by "TournamentMatch"."id"
`);
const match_getByRoundAndNumberStm = sql.prepare(/*sql*/ `
select *
from "TournamentMatch"
where "TournamentMatch"."roundId" = @roundId
and "TournamentMatch"."number" = @number
`);
const match_insertStm = sql.prepare(/*sql*/ `
insert into
"TournamentMatch"
("roundId", "stageId", "groupId", "number", "opponentOne", "opponentTwo", "status", "chatCode")
values
(@roundId, @stageId, @groupId, @number, @opponentOne, @opponentTwo, @status, @chatCode)
returning *
`);
const match_updateStm = sql.prepare(/*sql*/ `
update "TournamentMatch"
set
"roundId" = @roundId,
"stageId" = @stageId,
"groupId" = @groupId,
"number" = @number,
"opponentOne" = @opponentOne,
"opponentTwo" = @opponentTwo,
"status" = @status
where
"TournamentMatch"."id" = @id
`);
export class Match {
id?: TournamentMatch["id"];
roundId: TournamentMatch["roundId"];
stageId: TournamentMatch["stageId"];
groupId: TournamentMatch["groupId"];
number: TournamentMatch["number"];
opponentOne: TournamentMatch["opponentOne"];
opponentTwo: TournamentMatch["opponentTwo"];
status: TournamentMatch["status"];
constructor(
id: TournamentMatch["id"] | undefined,
status: TournamentMatch["status"],
stageId: TournamentMatch["stageId"],
groupId: TournamentMatch["groupId"],
roundId: TournamentMatch["roundId"],
number: TournamentMatch["number"],
_unknown1: null,
_unknown2: null,
_unknown3: null,
opponentOne: TournamentMatch["opponentOne"],
opponentTwo: TournamentMatch["opponentTwo"],
) {
this.id = id;
this.roundId = roundId;
this.stageId = stageId;
this.groupId = groupId;
this.number = number;
this.opponentOne = opponentOne;
this.opponentTwo = opponentTwo;
this.status = status;
}
static #convertMatch(
rawMatch: TournamentMatch & {
opponentOnePointsTotal: number | null;
opponentTwoPointsTotal: number | null;
lastGameFinishedAt: number | null;
},
): MatchType {
return {
id: rawMatch.id,
group_id: rawMatch.groupId,
number: rawMatch.number,
opponent1:
rawMatch.opponentOne === "null"
? null
: {
...JSON.parse(rawMatch.opponentOne),
totalPoints: rawMatch.opponentOnePointsTotal ?? undefined,
},
opponent2:
rawMatch.opponentTwo === "null"
? null
: {
...JSON.parse(rawMatch.opponentTwo),
totalPoints: rawMatch.opponentTwoPointsTotal ?? undefined,
},
round_id: rawMatch.roundId,
stage_id: rawMatch.stageId,
status: rawMatch.status,
lastGameFinishedAt: rawMatch.lastGameFinishedAt,
};
}
static getById(id: TournamentMatch["id"]): MatchType {
const match = match_getByIdStm.get({ id }) as any;
if (!match) return match;
return this.#convertMatch(match);
}
static getByStageId(stageId: TournamentStage["id"]): MatchType[] {
return (match_getByStageIdStm.all({ stageId }) as any[]).map(
this.#convertMatch,
);
}
static getByRoundAndNumber(
roundId: TournamentRound["id"],
number: TournamentMatch["number"],
): MatchType {
const match = match_getByRoundAndNumberStm.get({ roundId, number }) as any;
if (!match) return match;
return this.#convertMatch(match);
}
insert() {
const match = match_insertStm.get({
roundId: this.roundId,
stageId: this.stageId,
groupId: this.groupId,
number: this.number,
opponentOne: this.opponentOne ?? "null",
opponentTwo: this.opponentTwo ?? "null",
status: this.status,
chatCode: nanoid(10),
}) as any;
this.id = match.id;
return true;
}
update() {
match_updateStm.run({
id: this.id,
roundId: this.roundId,
stageId: this.stageId,
groupId: this.groupId,
number: this.number,
opponentOne: this.opponentOne ?? "null",
opponentTwo: this.opponentTwo ?? "null",
status: this.status,
});
return true;
}
}