sendou.ink/app/features/scrims/ScrimMapListRepository.server.ts
2026-05-25 17:35:44 +03:00

124 lines
3.7 KiB
TypeScript

import type { Transaction } from "kysely";
import { jsonArrayFrom } from "kysely/helpers/sqlite";
import { db } from "~/db/sql";
import type { DB, TablesInsertable } from "~/db/tables";
import { MapPool } from "~/features/map-list-generator/core/map-pool";
import type { ModeShort, StageId } from "~/modules/in-game-lists/types";
import { databaseTimestampNow } from "~/utils/dates";
import * as ScrimMapRepository from "./ScrimMapRepository.server";
import type { ScrimSide } from "./scrims-types";
type SubmitMapListArgs = Omit<TablesInsertable["ScrimMapList"], "updatedAt">;
/**
* Inserts a map list row for the given side, replacing any existing row for
* the same `(scrimPostId, side)` pair, and (atomically) generates and inserts
* the next map for the scrim if no unreported map is currently waiting.
*/
export async function submitMapListAndGenerateIfNeeded(
args: SubmitMapListArgs,
): Promise<void> {
const now = databaseTimestampNow();
await db.transaction().execute(async (trx) => {
await trx
.insertInto("ScrimMapList")
.values({
scrimPostId: args.scrimPostId,
side: args.side,
source: args.source,
tournamentId: args.tournamentId ?? null,
serializedPool: args.serializedPool ?? null,
updatedAt: now,
})
.onConflict((oc) =>
oc.columns(["scrimPostId", "side"]).doUpdateSet({
source: args.source,
tournamentId: args.tournamentId ?? null,
serializedPool: args.serializedPool ?? null,
updatedAt: now,
}),
)
.execute();
await ScrimMapRepository.tryGenerateAndInsertNextMapInTrx(
trx,
args.scrimPostId,
);
});
}
/** Deletes a side's map list, if one exists. */
export async function deleteMapList(
scrimPostId: number,
side: ScrimSide,
): Promise<void> {
await db
.deleteFrom("ScrimMapList")
.where("scrimPostId", "=", scrimPostId)
.where("side", "=", side)
.execute();
}
export type ResolvedScrimMapList = {
side: ScrimSide;
mapList: Array<{ mode: ModeShort; stageId: StageId }>;
tournament?: { id: number; name: string };
updatedAt: number;
};
/**
* Returns all submitted map lists for the scrim with the pool resolved into
* concrete `(mode, stageId)` pairs. Tournament-sourced rows additionally carry
* the tournament's id and name for display. Pass a transaction as `executor`
* to read within an existing transaction.
*/
export async function findMapListsByScrimPostId(
scrimPostId: number,
executor: typeof db | Transaction<DB> = db,
): Promise<ResolvedScrimMapList[]> {
const rows = await executor
.selectFrom("ScrimMapList")
.leftJoin(
"CalendarEvent",
"ScrimMapList.tournamentId",
"CalendarEvent.tournamentId",
)
.select((eb) => [
"ScrimMapList.side",
"ScrimMapList.source",
"ScrimMapList.tournamentId",
"ScrimMapList.serializedPool",
"ScrimMapList.updatedAt",
eb.ref("CalendarEvent.name").as("tournamentName"),
jsonArrayFrom(
eb
.selectFrom("MapPoolMap")
.select(["MapPoolMap.mode", "MapPoolMap.stageId"])
.whereRef("MapPoolMap.calendarEventId", "=", "CalendarEvent.id"),
).as("tournamentMapPool"),
])
.where("ScrimMapList.scrimPostId", "=", scrimPostId)
.execute();
return rows.map((row) => ({
side: row.side,
mapList: resolveMapList(row),
tournament:
row.source === "TOURNAMENT" && row.tournamentId !== null
? { id: row.tournamentId, name: row.tournamentName ?? "" }
: undefined,
updatedAt: row.updatedAt,
}));
}
function resolveMapList(row: {
source: "TOURNAMENT" | "POOL";
serializedPool: string | null;
tournamentMapPool: Array<{ mode: ModeShort; stageId: StageId }>;
}): Array<{ mode: ModeShort; stageId: StageId }> {
if (row.source === "TOURNAMENT") return row.tournamentMapPool;
if (!row.serializedPool) return [];
return new MapPool(row.serializedPool).stageModePairs;
}