Merge remote-tracking branch 'origin/main' into ingest

# Conflicts:
#	app/components/match-page/MatchTimeline.tsx
#	app/db/tables.ts
#	app/features/tournament-match/components/TournamentMatchTabs.tsx
#	app/features/tournament-match/loaders/to.$id.matches.$mid.server.ts
#	app/features/vods/loaders/vods.new.server.ts
#	db-test.sqlite3
#	e2e/seeds/db-seed-AB_RR.sqlite3
#	e2e/seeds/db-seed-DEFAULT.sqlite3
#	e2e/seeds/db-seed-FINALIZED_BRACKET.sqlite3
#	e2e/seeds/db-seed-IN_SQ_MATCH.sqlite3
#	e2e/seeds/db-seed-NO_SCRIMS.sqlite3
#	e2e/seeds/db-seed-NO_SQ_GROUPS.sqlite3
#	e2e/seeds/db-seed-NO_TOURNAMENT_TEAMS.sqlite3
#	e2e/seeds/db-seed-NZAP_IN_TEAM.sqlite3
#	e2e/seeds/db-seed-REG_OPEN.sqlite3
#	e2e/seeds/db-seed-SMALL_SOS.sqlite3
#	e2e/seeds/db-seed-TEAM_MAP_PREFS.sqlite3
#	knip.ts
#	package.json
#	pnpm-lock.yaml
#	vite.config.ts
This commit is contained in:
Kalle
2026-08-04 20:15:52 +03:00
1574 changed files with 70501 additions and 46458 deletions

View File

@@ -1,4 +1,4 @@
import { sql } from "~/db/sql";
import { db } from "~/db/sql";
import invariant from "~/utils/invariant";
import { logger } from "~/utils/logger";
@@ -12,23 +12,24 @@ invariant(
"discordIds must be a comma separated list of discord ids",
);
const stm = sql.prepare(
/* sql */ `insert into "TournamentBadgeOwner" ("badgeId", "userId") values (@badgeId, (select "id" from "User" where "discordId" = @userId))`,
);
const userStm = sql.prepare(
/* sql */ `select "id" from "User" where "discordId" = @discordId`,
);
const users = discordIds.split(",");
for (const userId of users) {
const user = userStm.get({ discordId: userId });
for (const discordId of users) {
const user = await db
.selectFrom("User")
.select("id")
.where("discordId", "=", discordId)
.executeTakeFirst();
if (!user) {
logger.info(`User with discord id ${userId} not found`);
logger.info(`User with discord id ${discordId} not found`);
continue;
}
stm.run({ badgeId: Number(badgeId), userId });
await db
.insertInto("TournamentBadgeOwner")
.values({ badgeId: Number(badgeId), userId: user.id })
.execute();
}
logger.info(`Added ${users.length} owners to the badge`);

View File

@@ -1,4 +1,4 @@
import { sql } from "~/db/sql";
import * as BadgeRepository from "~/features/badges/BadgeRepository.server";
import invariant from "~/utils/invariant";
import { logger } from "~/utils/logger";
@@ -20,10 +20,11 @@ invariant(
"hue must be between -360 and 360",
);
sql
.prepare(
"insert into badge (code, displayName, hue) values ($code, $displayName, $hue)",
)
.run({ code, displayName, hue: parsedHue ?? null });
await BadgeRepository.insert({
code,
displayName,
hue: parsedHue ?? null,
authorId: null,
});
logger.info(`Added new badge: ${displayName}`);

View File

@@ -50,7 +50,7 @@ async function main() {
const season = Seasons.currentOrPrevious();
invariant(season, "No current or previous season found");
const leaderboard = await LeaderboardRepository.teamLeaderboardBySeason({
const leaderboard = await LeaderboardRepository.findTeamLeaderboardBySeason({
season: season.nth,
onlyOneEntryPerUser: true,
});
@@ -84,7 +84,9 @@ async function main() {
invariant(user.friendCode, `User ${member.username} has no friend code`);
if (tournament.ctx.settings.requireInGameNames) {
const inGameName = await UserRepository.inGameNameByUserId(member.id);
const inGameName = await UserRepository.findInGameNameByUserId(
member.id,
);
invariant(
inGameName,
`User ${member.username} has no in-game name (required by tournament)`,
@@ -107,7 +109,7 @@ async function main() {
const tournamentTeam = await userAsyncLocalStorage.run(
{ user: adminUser },
() =>
TournamentTeamRepository.create({
TournamentTeamRepository.insert({
team: {
name: teamName,
prefersNotToHost: 0,

View File

@@ -6,6 +6,12 @@ import { type BenchmarkCase, buildCases } from "./benchmark-db/cases";
import { resolveFixtures } from "./benchmark-db/fixtures";
const DEFAULT_ITERATIONS = 10;
const SUMMARY_BUCKETS = [
{ label: ">=100ms", minMs: 100 },
{ label: "10-100ms", minMs: 10 },
{ label: "1-10ms", minMs: 1 },
{ label: "<1ms", minMs: 0 },
];
interface CaseResult {
name: string;
@@ -55,6 +61,7 @@ async function main() {
);
printResults(results, skipped);
printSummary(results, skipped);
}
main()
@@ -131,10 +138,23 @@ function erroredResult(name: string, error: unknown): CaseResult {
function stats(durations: number[]) {
const sorted = [...durations].sort((a, b) => a - b);
const mean = sorted.reduce((acc, cur) => acc + cur, 0) / sorted.length;
const p95 =
sorted[Math.min(sorted.length - 1, Math.ceil(sorted.length * 0.95) - 1)];
return { min: sorted[0], mean, p95 };
return { min: sorted[0], mean, p95: percentile(sorted, 0.95) };
}
function percentile(sortedValues: number[], fraction: number) {
const index = Math.ceil(sortedValues.length * fraction) - 1;
return sortedValues[Math.min(sortedValues.length - 1, Math.max(0, index))];
}
function geometricMean(values: number[]) {
const logSum = values.reduce(
(acc, cur) => acc + Math.log(Math.max(cur, Number.EPSILON)),
0,
);
return Math.exp(logSum / values.length);
}
function printResults(results: CaseResult[], skipped: string[]) {
@@ -176,6 +196,45 @@ function printResults(results: CaseResult[], skipped: string[]) {
}
}
function printSummary(results: CaseResult[], skipped: string[]) {
const timed = results.filter((result) => !result.error);
if (timed.length === 0) return;
const means = timed.map((result) => result.mean).sort((a, b) => a - b);
const total = means.reduce((acc, cur) => acc + cur, 0);
const slowest = timed.reduce((acc, cur) => (cur.mean > acc.mean ? cur : acc));
const rows: Array<[string, string]> = [
[
"cases timed",
`${timed.length} (${skipped.length} skipped, ${results.length - timed.length} errored)`,
],
["total", formatMs(total)],
["geometric mean", formatMs(geometricMean(means))],
["median", formatMs(percentile(means, 0.5))],
["p95", formatMs(percentile(means, 0.95))],
["slowest", `${formatMs(slowest.mean)} (${slowest.name})`],
];
let unbucketed = means;
for (const bucket of SUMMARY_BUCKETS) {
const inBucket = unbucketed.filter((mean) => mean >= bucket.minMs);
rows.push([bucket.label, `${inBucket.length} cases`]);
unbucketed = unbucketed.filter((mean) => mean < bucket.minMs);
}
const labelWidth = Math.max(...rows.map(([label]) => label.length));
logger.info("");
logger.info("Summary (mean per case)");
for (const [label, value] of rows) {
logger.info(`${label.padEnd(labelWidth)} ${value}`);
}
logger.info(
"total is dominated by the slowest cases, geometric mean weighs every case equally",
);
}
function formatMs(ms: number) {
return `${roundToNDecimalPlaces(ms)}ms`;
}

View File

@@ -13,7 +13,7 @@ import * as LeaderboardRepository from "~/features/leaderboards/LeaderboardRepos
import * as LFGRepository from "~/features/lfg/LFGRepository.server";
import * as LiveStreamRepository from "~/features/live-streams/LiveStreamRepository.server";
import * as MatchProfileRepository from "~/features/match-profile/MatchProfileRepository.server";
import * as MmrSkillRepository from "~/features/mmr/SkillRepository.server";
import * as SkillRepository from "~/features/mmr/SkillRepository.server";
import * as NotificationRepository from "~/features/notifications/NotificationRepository.server";
import * as PlusSuggestionRepository from "~/features/plus-suggestions/PlusSuggestionRepository.server";
import * as PlusVotingRepository from "~/features/plus-voting/PlusVotingRepository.server";
@@ -38,6 +38,7 @@ import * as TournamentMatchVodRepository from "~/features/tournament-bracket/Tou
import * as TournamentLFGRepository from "~/features/tournament-lfg/TournamentLFGRepository.server";
import * as TournamentMatchRepository from "~/features/tournament-match/TournamentMatchRepository.server";
import * as TournamentOrganizationRepository from "~/features/tournament-organization/TournamentOrganizationRepository.server";
import * as TrophyRepository from "~/features/trophies/TrophyRepository.server";
import * as UserCardRepository from "~/features/user-card/UserCardRepository.server";
import * as UserRepository from "~/features/user-page/UserRepository.server";
import * as VodRepository from "~/features/vods/VodRepository.server";
@@ -79,26 +80,28 @@ export function buildCases(fx: Fixtures): {
}
// AdminRepository
addStatic("AdminRepository.allBannedUsers", () =>
AdminRepository.allBannedUsers(),
addStatic("AdminRepository.findAllBannedUsers", () =>
AdminRepository.findAllBannedUsers(),
);
add("AdminRepository.findModeNoteById", fx.modNoteId, (id) =>
AdminRepository.findModeNoteById(id),
add("AdminRepository.findModNoteById", fx.modNoteId, (id) =>
AdminRepository.findModNoteById(id),
);
// ExternalStreamRepository
addStatic("ExternalStreamRepository.all", () =>
ExternalStreamRepository.all(),
addStatic("ExternalStreamRepository.findAll", () =>
ExternalStreamRepository.findAll(),
);
addStatic("ExternalStreamRepository.forSidebar", () =>
ExternalStreamRepository.forSidebar(),
addStatic("ExternalStreamRepository.findAllForSidebar", () =>
ExternalStreamRepository.findAllForSidebar(),
);
// ApiRepository
add("ApiRepository.findTokenByUserId", fx.apiTokenUserId, (userId) =>
ApiRepository.findTokenByUserId(userId, "read"),
);
addStatic("ApiRepository.allApiTokens", () => ApiRepository.allApiTokens());
addStatic("ApiRepository.findAllApiTokens", () =>
ApiRepository.findAllApiTokens(),
);
// ArtRepository
addStatic("ArtRepository.findShowcaseArts", () =>
@@ -147,7 +150,7 @@ export function buildCases(fx: Fixtures): {
);
// BadgeRepository
addStatic("BadgeRepository.all", () => BadgeRepository.all());
addStatic("BadgeRepository.findAll", () => BadgeRepository.findAll());
add("BadgeRepository.findById", fx.heavyBadgeId, (badgeId) =>
BadgeRepository.findById(badgeId),
);
@@ -165,30 +168,31 @@ export function buildCases(fx: Fixtures): {
);
// BuildRepository
add("BuildRepository.allByUserId", fx.heavyBuildUserId, (userId) =>
BuildRepository.allByUserId(userId, {
add("BuildRepository.findAllByUserId", fx.heavyBuildUserId, (userId) =>
BuildRepository.findAllByUserId(userId, {
showPrivate: true,
sortAbilities: true,
}),
);
add("BuildRepository.ownerIdById", fx.buildId, (buildId) =>
BuildRepository.ownerIdById(buildId),
add("BuildRepository.findOwnerIdById", fx.buildId, (buildId) =>
BuildRepository.findOwnerIdById(buildId),
);
addStatic("BuildRepository.abilityPointAverages.all", () =>
BuildRepository.abilityPointAverages(),
addStatic("BuildRepository.findAllAbilityPointAverages.all", () =>
BuildRepository.findAllAbilityPointAverages(),
);
add(
"BuildRepository.abilityPointAverages.byWeapon",
"BuildRepository.findAllAbilityPointAverages.byWeapon",
fx.heavyWeaponSplId,
(weaponSplId) => BuildRepository.abilityPointAverages(weaponSplId),
(weaponSplId) => BuildRepository.findAllAbilityPointAverages(weaponSplId),
);
add(
"BuildRepository.popularAbilitiesByWeaponId",
"BuildRepository.findAllPopularAbilitiesByWeaponId",
fx.heavyWeaponSplId,
(weaponSplId) => BuildRepository.popularAbilitiesByWeaponId(weaponSplId),
(weaponSplId) =>
BuildRepository.findAllPopularAbilitiesByWeaponId(weaponSplId),
);
add("BuildRepository.allByWeaponId", fx.heavyWeaponSplId, (weaponSplId) =>
BuildRepository.allByWeaponId(weaponSplId, {
add("BuildRepository.findAllByWeaponId", fx.heavyWeaponSplId, (weaponSplId) =>
BuildRepository.findAllByWeaponId(weaponSplId, {
limit: 60,
sortAbilities: true,
}),
@@ -276,8 +280,8 @@ export function buildCases(fx: Fixtures): {
addStatic("ImageRepository.countAllUnvalidated", () =>
ImageRepository.countAllUnvalidated(),
);
addStatic("ImageRepository.unvalidatedImages", () =>
ImageRepository.unvalidatedImages(),
addStatic("ImageRepository.findAllUnvalidated", () =>
ImageRepository.findAllUnvalidated(),
);
add(
"ImageRepository.countUnvalidatedBySubmitterUserId",
@@ -286,40 +290,42 @@ export function buildCases(fx: Fixtures): {
);
// LeaderboardRepository
add("LeaderboardRepository.teamLeaderboardBySeason", fx.sq, (sq) =>
LeaderboardRepository.teamLeaderboardBySeason({
add("LeaderboardRepository.findTeamLeaderboardBySeason", fx.sq, (sq) =>
LeaderboardRepository.findTeamLeaderboardBySeason({
season: sq.season,
onlyOneEntryPerUser: true,
}),
);
add("LeaderboardRepository.userHasEnoughSqMatches", fx.sq, (sq) =>
LeaderboardRepository.userHasEnoughSqMatches(sq.userId),
add("LeaderboardRepository.hasEnoughSqMatchesByUserId", fx.sq, (sq) =>
LeaderboardRepository.hasEnoughSqMatchesByUserId(sq.userId),
);
add("LeaderboardRepository.seasonsParticipatedInByUserId", fx.sq, (sq) =>
LeaderboardRepository.seasonsParticipatedInByUserId(sq.userId),
add("LeaderboardRepository.findSeasonsParticipatedInByUserId", fx.sq, (sq) =>
LeaderboardRepository.findSeasonsParticipatedInByUserId(sq.userId),
);
addStatic("LeaderboardRepository.allXPLeaderboard", () =>
LeaderboardRepository.allXPLeaderboard(),
addStatic("LeaderboardRepository.findAllXPLeaderboard", () =>
LeaderboardRepository.findAllXPLeaderboard(),
);
addStatic("LeaderboardRepository.modeXPLeaderboard", () =>
LeaderboardRepository.modeXPLeaderboard("SZ"),
addStatic("LeaderboardRepository.findModeXPLeaderboard", () =>
LeaderboardRepository.findModeXPLeaderboard("SZ"),
);
add(
"LeaderboardRepository.weaponXPLeaderboard",
"LeaderboardRepository.findWeaponXPLeaderboard",
fx.heavyWeaponSplId,
(weaponSplId) => LeaderboardRepository.weaponXPLeaderboard(weaponSplId),
(weaponSplId) => LeaderboardRepository.findWeaponXPLeaderboard(weaponSplId),
);
add("LeaderboardRepository.userSPLeaderboard", fx.sq, (sq) =>
LeaderboardRepository.userSPLeaderboard(sq.season),
add("LeaderboardRepository.findUserSPLeaderboard", fx.sq, (sq) =>
LeaderboardRepository.findUserSPLeaderboard(sq.season),
);
add("LeaderboardRepository.seasonPopularUsersWeapon", fx.sq, (sq) =>
LeaderboardRepository.seasonPopularUsersWeapon(sq.season),
add("LeaderboardRepository.findSeasonPopularUsersWeapon", fx.sq, (sq) =>
LeaderboardRepository.findSeasonPopularUsersWeapon(sq.season),
);
// LFGRepository
addStatic("LFGRepository.posts.anon", () => LFGRepository.posts());
add("LFGRepository.posts.loggedIn", fx.heavyUser, (user) =>
LFGRepository.posts({ id: user.id, plusTier: 1 }),
addStatic("LFGRepository.findAllPosts.anon", () =>
LFGRepository.findAllPosts(),
);
add("LFGRepository.findAllPosts.loggedIn", fx.heavyUser, (user) =>
LFGRepository.findAllPosts({ id: user.id, plusTier: 1 }),
);
add("LFGRepository.findByAuthorUserId", fx.lfgAuthorId, (authorId) =>
LFGRepository.findByAuthorUserId(authorId),
@@ -331,13 +337,43 @@ export function buildCases(fx: Fixtures): {
);
// MatchProfileRepository
add("MatchProfileRepository.settingsByUserId", fx.heavyUser, (user) =>
MatchProfileRepository.settingsByUserId(user.id),
add("MatchProfileRepository.findSettingsByUserId", fx.heavyUser, (user) =>
MatchProfileRepository.findSettingsByUserId(user.id),
);
// MmrSkillRepository (app/features/mmr)
add("MmrSkillRepository.seasonProgressionByUserId", fx.sq, (sq) =>
MmrSkillRepository.seasonProgressionByUserId(sq),
// SkillRepository (app/features/mmr)
add("SkillRepository.findCurrentUserSkills", fx.skillBatch, (skillBatch) =>
SkillRepository.findCurrentUserSkills({
season: skillBatch.season,
userIds: skillBatch.userIds,
}),
);
add("SkillRepository.findCurrentTeamSkills", fx.skillBatch, (skillBatch) =>
SkillRepository.findCurrentTeamSkills({
season: skillBatch.season,
identifiers: skillBatch.identifiers,
}),
);
add(
"SkillRepository.findOrderedUserOrdinalsBySeason",
fx.skillBatch,
(skillBatch) =>
SkillRepository.findOrderedUserOrdinalsBySeason(skillBatch.season),
);
add("SkillRepository.existsBySeason", fx.skillBatch, (skillBatch) =>
SkillRepository.existsBySeason(skillBatch.season),
);
add("SkillRepository.findSeedingSkills", fx.skillBatch, (skillBatch) =>
SkillRepository.findSeedingSkills({
type: "RANKED",
userIds: skillBatch.userIds,
}),
);
add("SkillRepository.findSeasonProgressionByUserId", fx.sq, (sq) =>
SkillRepository.findSeasonProgressionByUserId(sq),
);
add("SkillRepository.findSeasonActiveDaysByUserId", fx.sq, (sq) =>
SkillRepository.findSeasonActiveDaysByUserId(sq),
);
// NotificationRepository
@@ -348,9 +384,9 @@ export function buildCases(fx: Fixtures): {
NotificationRepository.findAllByType(notification.type),
);
add(
"NotificationRepository.subscriptionsByUserIds",
"NotificationRepository.findAllSubscriptionsByUserIds",
fx.manyUserIds,
(userIds) => NotificationRepository.subscriptionsByUserIds(userIds),
(userIds) => NotificationRepository.findAllSubscriptionsByUserIds(userIds),
);
// PlusSuggestionRepository
@@ -361,14 +397,16 @@ export function buildCases(fx: Fixtures): {
);
// PlusVotingRepository
addStatic("PlusVotingRepository.allPlusTiersFromLatestVoting", () =>
PlusVotingRepository.allPlusTiersFromLatestVoting(),
addStatic("PlusVotingRepository.findAllPlusTiersFromLatestVoting", () =>
PlusVotingRepository.findAllPlusTiersFromLatestVoting(),
);
add("PlusVotingRepository.resultsByMonthYear", fx.plusVoting, (voting) =>
PlusVotingRepository.resultsByMonthYear(voting),
add("PlusVotingRepository.findResultsByMonthYear", fx.plusVoting, (voting) =>
PlusVotingRepository.findResultsByMonthYear(voting),
);
add("PlusVotingRepository.usersForVoting", fx.plusTierOneUser, (user) =>
PlusVotingRepository.usersForVoting(user),
add(
"PlusVotingRepository.findAllUsersForVoting",
fx.plusTierOneUser,
(user) => PlusVotingRepository.findAllUsersForVoting(user),
);
add("PlusVotingRepository.hasVoted", fx.plusVoting, (voting) =>
PlusVotingRepository.hasVoted({
@@ -427,23 +465,35 @@ export function buildCases(fx: Fixtures): {
// GroupMatchContinueVoteRepository
add(
"GroupMatchContinueVoteRepository.findForGroups",
"GroupMatchContinueVoteRepository.findAllByGroupIds",
fx.heavyGroupIds,
(groupIds) => GroupMatchContinueVoteRepository.findForGroups(groupIds),
(groupIds) => GroupMatchContinueVoteRepository.findAllByGroupIds(groupIds),
);
// PlayerStatRepository
add("PlayerStatRepository.seasonMapWinrateByUserId", fx.sq, (sq) =>
PlayerStatRepository.seasonMapWinrateByUserId(sq),
add("PlayerStatRepository.findSeasonMapWinrateByUserId", fx.sq, (sq) =>
PlayerStatRepository.findSeasonMapWinrateByUserId(sq),
);
add("PlayerStatRepository.seasonSetWinrateByUserId", fx.sq, (sq) =>
PlayerStatRepository.seasonSetWinrateByUserId(sq),
add("PlayerStatRepository.findSeasonSetWinrateByUserId", fx.sq, (sq) =>
PlayerStatRepository.findSeasonSetWinrateByUserId(sq),
);
add("PlayerStatRepository.seasonStagesByUserId", fx.sq, (sq) =>
PlayerStatRepository.seasonStagesByUserId(sq),
add("PlayerStatRepository.findSeasonStagesByUserId", fx.sq, (sq) =>
PlayerStatRepository.findSeasonStagesByUserId(sq),
);
add("PlayerStatRepository.seasonMatesEnemiesByUserId", fx.sq, (sq) =>
PlayerStatRepository.seasonMatesEnemiesByUserId({ ...sq, type: "MATE" }),
add("PlayerStatRepository.findSeasonMatesEnemiesByUserId", fx.sq, (sq) =>
PlayerStatRepository.findSeasonMatesEnemiesByUserId({
...sq,
type: "MATE",
}),
);
add("PlayerStatRepository.findSeasonSetScoresByUserId", fx.sq, (sq) =>
PlayerStatRepository.findSeasonSetScoresByUserId(sq),
);
add("PlayerStatRepository.findSeasonBestSetsByUserId", fx.sq, (sq) =>
PlayerStatRepository.findSeasonBestSetsByUserId({ ...sq, limit: 3 }),
);
add("PlayerStatRepository.findSeasonTournamentRunsByUserId", fx.sq, (sq) =>
PlayerStatRepository.findSeasonTournamentRunsByUserId(sq),
);
// ReportedWeaponRepository
@@ -457,14 +507,16 @@ export function buildCases(fx: Fixtures): {
fx.heavyTournamentMatchId,
(matchId) => ReportedWeaponRepository.findByTournamentMatchId(matchId),
);
add("ReportedWeaponRepository.seasonReportedWeaponsByUserId", fx.sq, (sq) =>
ReportedWeaponRepository.seasonReportedWeaponsByUserId(sq),
add(
"ReportedWeaponRepository.findSeasonReportedWeaponsByUserId",
fx.sq,
(sq) => ReportedWeaponRepository.findSeasonReportedWeaponsByUserId(sq),
);
add(
"ReportedWeaponRepository.weaponUsageStats",
"ReportedWeaponRepository.findAllWeaponUsageStats",
both(fx.sq, fx.heavyStageModeCombo),
([sq, combo]) =>
ReportedWeaponRepository.weaponUsageStats({
ReportedWeaponRepository.findAllWeaponUsageStats({
userId: sq.userId,
season: sq.season,
mode: combo.mode,
@@ -476,36 +528,51 @@ export function buildCases(fx: Fixtures): {
add("SQMatchRepository.findById", fx.heavyGroupMatchId, (matchId) =>
SQMatchRepository.findById(matchId),
);
add("SQMatchRepository.seasonResultPagesByUserId", fx.sq, (sq) =>
SQMatchRepository.seasonResultPagesByUserId(sq),
add("SQMatchRepository.countSeasonResultPagesByUserId", fx.sq, (sq) =>
SQMatchRepository.countSeasonResultPagesByUserId(sq),
);
add("SQMatchRepository.seasonResultsByUserId", fx.sq, (sq) =>
SQMatchRepository.seasonResultsByUserId({ ...sq, page: 1 }),
add("SQMatchRepository.findSeasonResultsByUserId", fx.sq, (sq) =>
SQMatchRepository.findSeasonResultsByUserId({ ...sq, page: 1 }),
);
add("SQMatchRepository.seasonCanceledMatchesByUserId", fx.sq, (sq) =>
SQMatchRepository.seasonCanceledMatchesByUserId(sq),
add("SQMatchRepository.findSeasonCanceledMatchesByUserId", fx.sq, (sq) =>
SQMatchRepository.findSeasonCanceledMatchesByUserId(sq),
);
add(
"SQMatchRepository.findCancelReportsByGroupMatchId",
fx.heavyGroupMatchId,
(matchId) => SQMatchRepository.findCancelReportsByGroupMatchId(matchId),
);
add(
"SQMatchRepository.findCancelNominationCountsByUserIds",
both(fx.manyUserIds, fx.sq),
([userIds, sq]) =>
SQMatchRepository.findCancelNominationCountsByUserIds({
userIds,
season: sq.season,
}),
);
// QStreamsRepository
addStatic("QStreamsRepository.activeMatchPlayers", () =>
QStreamsRepository.activeMatchPlayers(),
addStatic("QStreamsRepository.findAllActiveMatchPlayers", () =>
QStreamsRepository.findAllActiveMatchPlayers(),
);
// PrivateUserNoteRepository (relies on the benchmark's actor context)
addStatic("PrivateUserNoteRepository.ownNotes.all", () =>
PrivateUserNoteRepository.ownNotes(),
addStatic("PrivateUserNoteRepository.findAllOwn.all", () =>
PrivateUserNoteRepository.findAllOwn(),
);
add(
"PrivateUserNoteRepository.ownNotes.byTargets",
"PrivateUserNoteRepository.findAllOwn.byTargets",
fx.manyUserIds,
(userIds) => PrivateUserNoteRepository.ownNotes(userIds),
(userIds) => PrivateUserNoteRepository.findAllOwn(userIds),
);
// SQGroupRepository
add(
"SQGroupRepository.mapModePreferencesByGroupId",
"SQGroupRepository.findMapModePreferencesByGroupId",
fx.heavyGroupIds,
(groupIds) => SQGroupRepository.mapModePreferencesByGroupId(groupIds[0]),
(groupIds) =>
SQGroupRepository.findMapModePreferencesByGroupId(groupIds[0]),
);
addStatic("SQGroupRepository.findCurrentGroups", () =>
SQGroupRepository.findCurrentGroups(),
@@ -513,14 +580,14 @@ export function buildCases(fx: Fixtures): {
addStatic("SQGroupRepository.findActiveGroupMembers", () =>
SQGroupRepository.findActiveGroupMembers(),
);
add("SQGroupRepository.allLikesByGroupId", fx.heavyGroupIds, (groupIds) =>
SQGroupRepository.allLikesByGroupId(groupIds[0]),
add("SQGroupRepository.findAllLikesByGroupId", fx.heavyGroupIds, (groupIds) =>
SQGroupRepository.findAllLikesByGroupId(groupIds[0]),
);
add("SQGroupRepository.friendsAndTeammates", fx.sq, (sq) =>
SQGroupRepository.friendsAndTeammates(sq.userId),
add("SQGroupRepository.findFriendsAndTeammates", fx.sq, (sq) =>
SQGroupRepository.findFriendsAndTeammates(sq.userId),
);
add("SQGroupRepository.mapModePreferencesBySeasonNth", fx.sq, (sq) =>
SQGroupRepository.mapModePreferencesBySeasonNth(sq.season),
add("SQGroupRepository.findAllMapModePreferencesBySeasonNth", fx.sq, (sq) =>
SQGroupRepository.findAllMapModePreferencesBySeasonNth(sq.season),
);
addStatic("SQGroupRepository.findRecentlyFinishedMatches", () =>
SQGroupRepository.findRecentlyFinishedMatches(),
@@ -555,16 +622,19 @@ export function buildCases(fx: Fixtures): {
add("TeamRepository.findResultsById", fx.heavyTeam, (team) =>
TeamRepository.findResultsById(team.id),
);
add("TeamRepository.teamsByMemberUserId", fx.heavyTeam, (team) =>
TeamRepository.teamsByMemberUserId(team.memberUserId),
add("TeamRepository.findAllByMemberUserId", fx.heavyTeam, (team) =>
TeamRepository.findAllByMemberUserId(team.memberUserId),
);
// XRankPlacementRepository
add("XRankPlacementRepository.isPlayerLinkedByUserId", fx.xrank, (xrank) =>
XRankPlacementRepository.isPlayerLinkedByUserId(xrank.userId),
);
add("XRankPlacementRepository.peakVerifiedXpByUserId", fx.xrank, (xrank) =>
XRankPlacementRepository.peakVerifiedXpByUserId(xrank.userId),
add(
"XRankPlacementRepository.findPeakVerifiedXpByUserId",
fx.xrank,
(xrank) =>
XRankPlacementRepository.findPeakVerifiedXpByUserId(xrank.userId),
);
add("XRankPlacementRepository.findPlacementsOfMonth", fx.xrank, (xrank) =>
XRankPlacementRepository.findPlacementsOfMonth({
@@ -580,8 +650,8 @@ export function buildCases(fx: Fixtures): {
add("XRankPlacementRepository.findPlacementsByUserId", fx.xrank, (xrank) =>
XRankPlacementRepository.findPlacementsByUserId(xrank.userId),
);
addStatic("XRankPlacementRepository.monthYears", () =>
XRankPlacementRepository.monthYears(),
addStatic("XRankPlacementRepository.findAllMonthYears", () =>
XRankPlacementRepository.findAllMonthYears(),
);
add("XRankPlacementRepository.findPeaksByUserId", fx.xrank, (xrank) =>
XRankPlacementRepository.findPeaksByUserId(xrank.userId, "both"),
@@ -628,14 +698,14 @@ export function buildCases(fx: Fixtures): {
add("TournamentLFGRepository.findSubGroups", fx.lfgTournament, (lfg) =>
TournamentLFGRepository.findSubGroups(lfg.tournamentId),
);
add("TournamentLFGRepository.allLikesByTeamId", fx.lfgTournament, (lfg) =>
TournamentLFGRepository.allLikesByTeamId(lfg.teamId),
add("TournamentLFGRepository.findAllLikesByTeamId", fx.lfgTournament, (lfg) =>
TournamentLFGRepository.findAllLikesByTeamId(lfg.teamId),
);
add(
"TournamentLFGRepository.getSubsForTournament",
fx.subsTournamentId,
(tournamentId) =>
TournamentLFGRepository.getSubsForTournament(tournamentId),
"TournamentLFGRepository.findAllSubsByTournamentId",
fx.lfgTournament,
(lfg) =>
TournamentLFGRepository.findAllSubsByTournamentId(lfg.tournamentId),
);
// TournamentMatchRepository
@@ -655,16 +725,18 @@ export function buildCases(fx: Fixtures): {
(matchId) => TournamentMatchRepository.findResultsByMatchId(matchId),
);
add(
"TournamentMatchRepository.allResultsByTournamentId",
"TournamentMatchRepository.findAllResultsByTournamentId",
fx.heavyTournamentId,
(tournamentId) =>
TournamentMatchRepository.allResultsByTournamentId(tournamentId),
TournamentMatchRepository.findAllResultsByTournamentId(tournamentId),
);
add(
"TournamentMatchRepository.userParticipationByTournamentId",
"TournamentMatchRepository.findUserParticipationByTournamentId",
fx.heavyTournamentId,
(tournamentId) =>
TournamentMatchRepository.userParticipationByTournamentId(tournamentId),
TournamentMatchRepository.findUserParticipationByTournamentId(
tournamentId,
),
);
add(
"TournamentMatchRepository.findByTournamentTeamId",
@@ -728,10 +800,12 @@ export function buildCases(fx: Fixtures): {
}),
);
add(
"TournamentOrganizationRepository.allBannedUsersByOrganizationId",
"TournamentOrganizationRepository.findAllBannedUsersByOrganizationId",
fx.heavyOrg,
(org) =>
TournamentOrganizationRepository.allBannedUsersByOrganizationId(org.id),
TournamentOrganizationRepository.findAllBannedUsersByOrganizationId(
org.id,
),
);
add(
"TournamentOrganizationRepository.isUserBannedByOrganization",
@@ -768,8 +842,10 @@ export function buildCases(fx: Fixtures): {
add("SavedCalendarEventRepository.countByUserId", fx.heavyUser, (user) =>
SavedCalendarEventRepository.countByUserId(user.id),
);
add("SavedCalendarEventRepository.upcoming", fx.heavyUser, (user) =>
SavedCalendarEventRepository.upcoming(user.id),
add(
"SavedCalendarEventRepository.findAllUpcomingByUserId",
fx.heavyUser,
(user) => SavedCalendarEventRepository.findAllUpcomingByUserId(user.id),
);
// TournamentAuditLogRepository
@@ -851,13 +927,13 @@ export function buildCases(fx: Fixtures): {
(tournamentId) => TournamentRepository.findPreparedMapsById(tournamentId),
);
add(
"TournamentRepository.relatedUsersByTournamentIds",
"TournamentRepository.findRelatedUsersByTournamentIds",
fx.recentTournamentIds,
(tournamentIds) =>
TournamentRepository.relatedUsersByTournamentIds(tournamentIds),
TournamentRepository.findRelatedUsersByTournamentIds(tournamentIds),
);
addStatic("TournamentRepository.forShowcase", () =>
TournamentRepository.forShowcase(),
addStatic("TournamentRepository.findAllForShowcase", () =>
TournamentRepository.findAllForShowcase(),
);
add(
"TournamentRepository.findAllBetweenTwoTimestamps",
@@ -865,21 +941,21 @@ export function buildCases(fx: Fixtures): {
(window) => TournamentRepository.findAllBetweenTwoTimestamps(window),
);
add(
"TournamentRepository.topThreeResultsByTournamentId",
"TournamentRepository.findTopThreeResultsByTournamentId",
fx.heavyTournamentId,
(tournamentId) =>
TournamentRepository.topThreeResultsByTournamentId(tournamentId),
TournamentRepository.findTopThreeResultsByTournamentId(tournamentId),
);
add(
"TournamentRepository.friendCodesByTournamentId",
"TournamentRepository.findFriendCodesByTournamentId",
fx.heavyTournamentId,
(tournamentId) =>
TournamentRepository.friendCodesByTournamentId(tournamentId),
TournamentRepository.findFriendCodesByTournamentId(tournamentId),
);
add(
"TournamentRepository.pickBanEventsByMatchId",
"TournamentRepository.findPickBanEventsByMatchId",
fx.heavyTournamentMatchId,
(matchId) => TournamentRepository.pickBanEventsByMatchId(matchId),
(matchId) => TournamentRepository.findPickBanEventsByMatchId(matchId),
);
addStatic("TournamentRepository.searchByName", () =>
TournamentRepository.searchByName(SEARCH_QUERY),
@@ -901,24 +977,42 @@ export function buildCases(fx: Fixtures): {
TournamentTeamRepository.findRecentlyPlayedMapsByIds({ teamIds }),
);
// TrophyRepository
addStatic("TrophyRepository.all", () => TrophyRepository.all());
add("TrophyRepository.findById", fx.trophy, (trophy) =>
TrophyRepository.findById(trophy.heavyTrophyId),
);
add("TrophyRepository.findByOwnerUserId", fx.trophy, (trophy) =>
TrophyRepository.findByOwnerUserId(trophy.ownerUserId),
);
add("TrophyRepository.findTournamentsByTrophyId", fx.trophy, (trophy) =>
TrophyRepository.findTournamentsByTrophyId(trophy.heavyTrophyId),
);
add("TrophyRepository.findWinsByOwner", fx.trophy, (trophy) =>
TrophyRepository.findWinsByOwner(trophy.wins),
);
// UserCardRepository
add("UserCardRepository.userCards", fx.manyUserIds, (userIds) =>
UserCardRepository.userCards({
add("UserCardRepository.findAllByUserIds", fx.manyUserIds, (userIds) =>
UserCardRepository.findAllByUserIds({
userIds,
include: { friendCode: true },
includeHiddenStats: true,
}),
);
add("UserCardRepository.cardEditExtras", fx.heavyUser, (user) =>
UserCardRepository.cardEditExtras(user.id),
add("UserCardRepository.findCardEditExtrasByUserId", fx.heavyUser, (user) =>
UserCardRepository.findCardEditExtrasByUserId(user.id),
);
// UserRepository
add("UserRepository.identifierToUserId", fx.heavyUser, (user) =>
UserRepository.identifierToUserId(user.identifier),
add("UserRepository.findIdByIdentifier", fx.heavyUser, (user) =>
UserRepository.findIdByIdentifier(user.identifier),
);
add("UserRepository.identifierToBuildFields", fx.heavyUser, (user) =>
UserRepository.identifierToBuildFields(user.identifier),
add("UserRepository.findCountriesByUserIds", fx.skillBatch, (skillBatch) =>
UserRepository.findCountriesByUserIds(skillBatch.userIds),
);
add("UserRepository.findBuildFieldsByIdentifier", fx.heavyUser, (user) =>
UserRepository.findBuildFieldsByIdentifier(user.identifier),
);
add("UserRepository.findLayoutDataByIdentifier", fx.heavyUser, (user) =>
UserRepository.findLayoutDataByIdentifier(user.identifier, user.id),
@@ -926,20 +1020,20 @@ export function buildCases(fx: Fixtures): {
add("UserRepository.findProfileByIdentifier", fx.heavyUser, (user) =>
UserRepository.findProfileByIdentifier(user.identifier),
);
add("UserRepository.ownedBadgesByUserId", fx.badgeOwnerUserId, (userId) =>
UserRepository.ownedBadgesByUserId(userId),
add("UserRepository.findOwnedBadgesByUserId", fx.badgeOwnerUserId, (userId) =>
UserRepository.findOwnedBadgesByUserId(userId),
);
add("UserRepository.widgetsEnabledByIdentifier", fx.heavyUser, (user) =>
UserRepository.widgetsEnabledByIdentifier(user.identifier),
add("UserRepository.findEnabledWidgetsByIdentifier", fx.heavyUser, (user) =>
UserRepository.findEnabledWidgetsByIdentifier(user.identifier),
);
add("UserRepository.preferencesByUserId", fx.heavyUser, (user) =>
UserRepository.preferencesByUserId(user.id),
add("UserRepository.findPreferencesByUserId", fx.heavyUser, (user) =>
UserRepository.findPreferencesByUserId(user.id),
);
add("UserRepository.storedWidgetsByUserId", fx.heavyUser, (user) =>
UserRepository.storedWidgetsByUserId(user.id),
add("UserRepository.findStoredWidgetsByUserId", fx.heavyUser, (user) =>
UserRepository.findStoredWidgetsByUserId(user.id),
);
add("UserRepository.widgetsByUserId", fx.heavyUser, (user) =>
UserRepository.widgetsByUserId(user.identifier),
add("UserRepository.findWidgetsByUserId", fx.heavyUser, (user) =>
UserRepository.findWidgetsByUserId(user.identifier),
);
add("UserRepository.findByCustomUrl", fx.userCustomUrl, (customUrl) =>
UserRepository.findByCustomUrl(customUrl),
@@ -978,32 +1072,32 @@ export function buildCases(fx: Fixtures): {
add("UserRepository.searchExact", fx.userCustomUrl, (customUrl) =>
UserRepository.searchExact({ customUrl }),
);
add("UserRepository.currentFriendCodeByUserId", fx.heavyUser, (user) =>
UserRepository.currentFriendCodeByUserId(user.id),
add("UserRepository.findCurrentFriendCodeByUserId", fx.heavyUser, (user) =>
UserRepository.findCurrentFriendCodeByUserId(user.id),
);
add("UserRepository.friendCodesByUserId", fx.heavyUser, (user) =>
UserRepository.friendCodesByUserId(user.id),
add("UserRepository.findFriendCodesByUserId", fx.heavyUser, (user) =>
UserRepository.findFriendCodesByUserId(user.id),
);
addStatic("UserRepository.allCurrentFriendCodes", () =>
UserRepository.allCurrentFriendCodes(),
addStatic("UserRepository.findAllCurrentFriendCodes", () =>
UserRepository.findAllCurrentFriendCodes(),
);
add("UserRepository.inGameNameByUserId", fx.heavyUser, (user) =>
UserRepository.inGameNameByUserId(user.id),
add("UserRepository.findInGameNameByUserId", fx.heavyUser, (user) =>
UserRepository.findInGameNameByUserId(user.id),
);
add("UserRepository.patronSinceByUserId", fx.heavyUser, (user) =>
UserRepository.patronSinceByUserId(user.id),
add("UserRepository.findPatronStartedAtByUserId", fx.heavyUser, (user) =>
UserRepository.findPatronStartedAtByUserId(user.id),
);
add("UserRepository.joinOrderByUserId", fx.heavyUser, (user) =>
UserRepository.joinOrderByUserId(user.id),
add("UserRepository.findJoinOrderByUserId", fx.heavyUser, (user) =>
UserRepository.findJoinOrderByUserId(user.id),
);
add("UserRepository.commissionsByUserId", fx.heavyUser, (user) =>
UserRepository.commissionsByUserId(user.id),
add("UserRepository.findCommissionsByUserId", fx.heavyUser, (user) =>
UserRepository.findCommissionsByUserId(user.id),
);
add("UserRepository.anyUserPrefersNoScreen", fx.manyUserIds, (userIds) =>
UserRepository.anyUserPrefersNoScreen(userIds),
);
add("UserRepository.socialLinksByUserId", fx.heavyUser, (user) =>
UserRepository.socialLinksByUserId(user.id),
add("UserRepository.findSocialLinksByUserId", fx.heavyUser, (user) =>
UserRepository.findSocialLinksByUserId(user.id),
);
add(
"UserRepository.findIdsByTwitchUsernames",
@@ -1011,8 +1105,8 @@ export function buildCases(fx: Fixtures): {
(twitchUsernames) =>
UserRepository.findIdsByTwitchUsernames(twitchUsernames),
);
add("UserRepository.weaponPoolByUserId", fx.heavyUser, (user) =>
UserRepository.weaponPoolByUserId(user.id),
add("UserRepository.findWeaponPoolByUserId", fx.heavyUser, (user) =>
UserRepository.findWeaponPoolByUserId(user.id),
);
// VodRepository

View File

@@ -1,6 +1,7 @@
import { sub } from "date-fns";
import { db } from "~/db/sql";
import type { Tables } from "~/db/tables";
import type { SkillTeamIdentifier } from "~/features/mmr/mmr-utils";
import type {
MainWeaponId,
ModeShort,
@@ -18,6 +19,11 @@ export interface Fixtures {
buildId: number | null;
heavyWeaponSplId: MainWeaponId | null;
sq: { userId: number; season: number } | null;
skillBatch: {
season: number;
userIds: number[];
identifiers: SkillTeamIdentifier[];
} | null;
heavyGroupMatchId: number | null;
heavyGroupIds: [number, number] | null;
heavyStageModeCombo: { stageId: StageId; mode: ModeShort } | null;
@@ -56,6 +62,11 @@ export interface Fixtures {
badgeOwnerUserId: number | null;
badgeAuthorId: number | null;
badgeManagerUserId: number | null;
trophy: {
heavyTrophyId: number;
ownerUserId: number;
wins: { trophyId: number; userId: number };
} | null;
manyUserIds: number[] | null;
notification: { userId: number; type: Tables["Notification"]["type"] } | null;
heavyAssociation: {
@@ -65,7 +76,6 @@ export interface Fixtures {
} | null;
lfgAuthorId: number | null;
lfgTournament: { tournamentId: number; teamId: number } | null;
subsTournamentId: number | null;
auditTournamentId: number | null;
xrank: {
userId: number;
@@ -106,6 +116,7 @@ export async function resolveFixtures(): Promise<Fixtures> {
buildId: await resolveBuildId(),
heavyWeaponSplId: await resolveHeavyWeaponSplId(),
sq: await resolveSq(),
skillBatch: await resolveSkillBatch(),
heavyGroupMatchId,
heavyGroupIds: await resolveHeavyGroupIds(heavyGroupMatchId),
heavyStageModeCombo: await resolveHeavyStageModeCombo(),
@@ -137,12 +148,12 @@ export async function resolveFixtures(): Promise<Fixtures> {
badgeOwnerUserId: await resolveBadgeOwnerUserId(),
badgeAuthorId: await resolveBadgeAuthorId(),
badgeManagerUserId: await resolveBadgeManagerUserId(),
trophy: await resolveTrophy(),
manyUserIds: await resolveManyUserIds(heavyTournamentId),
notification: await resolveNotification(),
heavyAssociation: await resolveHeavyAssociation(),
lfgAuthorId: await resolveLfgAuthorId(),
lfgTournament: await resolveLfgTournament(),
subsTournamentId: (await resolveSubsTournamentId()) ?? heavyTournamentId,
auditTournamentId: (await resolveAuditTournamentId()) ?? heavyTournamentId,
xrank: await resolveXRank(),
heavyArtUserId: await resolveHeavyArtUserId(),
@@ -282,6 +293,50 @@ async function resolveSq() {
return { userId: userRow.userId, season: seasonRow.season };
}
/**
* Batch sizes a big tournament's finalization asks for: every player of the event and
* every roster that appeared on a map (several per set, hence far more than teams).
*/
const SKILL_BATCH_USERS = 300;
const SKILL_BATCH_IDENTIFIERS = 500;
async function resolveSkillBatch() {
const seasonRow = await db
.selectFrom("Skill")
.select(({ fn }) => ["season", fn.countAll<number>().as("count")])
.groupBy("season")
.orderBy("count", "desc")
.limit(1)
.executeTakeFirst();
if (!seasonRow) return null;
const userRows = await db
.selectFrom("Skill")
.select("userId")
.distinct()
.where("season", "=", seasonRow.season)
.where("userId", "is not", null)
.limit(SKILL_BATCH_USERS)
.execute();
const identifierRows = await db
.selectFrom("Skill")
.select("identifier")
.distinct()
.where("season", "=", seasonRow.season)
.where("identifier", "is not", null)
.limit(SKILL_BATCH_IDENTIFIERS)
.execute();
return {
season: seasonRow.season,
userIds: userRows.map((row) => row.userId as number),
identifiers: identifierRows.map(
(row) => row.identifier as SkillTeamIdentifier,
),
};
}
async function resolveHeavyGroupMatchId() {
const row = await db
.selectFrom("GroupMatchMap")
@@ -505,7 +560,7 @@ async function resolveCalendarAuthorId() {
async function resolveCalendarWindow() {
const row = await db
.selectFrom("CalendarEventDate")
.select(({ fn }) => fn.max("startTime").as("maxStartTime"))
.select(({ fn }) => fn.max("startsAt").as("maxStartTime"))
.executeTakeFirst();
if (typeof row?.maxStartTime !== "number") return null;
@@ -517,7 +572,7 @@ async function resolveCalendarWindow() {
async function resolveScrimWindow() {
const row = await db
.selectFrom("ScrimPost")
.select(({ fn }) => fn.max("at").as("maxAt"))
.select(({ fn }) => fn.max("startsAt").as("maxAt"))
.executeTakeFirst();
if (typeof row?.maxAt !== "number") return null;
@@ -596,15 +651,15 @@ async function resolveHeavyOrg() {
"CalendarEventDate.eventId",
"CalendarEvent.id",
)
.select(["CalendarEvent.name", "CalendarEventDate.startTime"])
.select(["CalendarEvent.name", "CalendarEventDate.startsAt"])
.where("CalendarEvent.organizationId", "=", orgRow.id)
.orderBy("CalendarEventDate.startTime", "desc")
.orderBy("CalendarEventDate.startsAt", "desc")
.limit(1)
.executeTakeFirst();
if (!latestEvent) return null;
const latestEventDate = databaseTimestampToDate(latestEvent.startTime);
const windowStart = latestEvent.startTime - 90 * 24 * 60 * 60;
const latestEventDate = databaseTimestampToDate(latestEvent.startsAt);
const windowStart = latestEvent.startsAt - 90 * 24 * 60 * 60;
return {
id: orgRow.id,
@@ -614,7 +669,7 @@ async function resolveHeavyOrg() {
eventMonth: latestEventDate.getUTCMonth(),
eventYear: latestEventDate.getUTCFullYear(),
windowStart,
windowEnd: latestEvent.startTime,
windowEnd: latestEvent.startsAt,
};
}
@@ -734,6 +789,45 @@ async function resolveBadgeManagerUserId() {
return row?.userId ?? null;
}
async function resolveTrophy() {
const heavyTrophyRow = await db
.selectFrom("TrophyOwner")
.select(({ fn }) => ["trophyId", fn.countAll<number>().as("count")])
.groupBy("trophyId")
.orderBy("count", "desc")
.limit(1)
.executeTakeFirst();
if (!heavyTrophyRow) return null;
const ownerRow = await db
.selectFrom("TrophyOwner")
.select(({ fn }) => ["userId", fn.countAll<number>().as("count")])
.groupBy("userId")
.orderBy("count", "desc")
.limit(1)
.executeTakeFirst();
if (!ownerRow) return null;
const winsRow = await db
.selectFrom("TrophyOwner")
.select(({ fn }) => [
"trophyId",
"userId",
fn.countAll<number>().as("count"),
])
.groupBy(["trophyId", "userId"])
.orderBy("count", "desc")
.limit(1)
.executeTakeFirst();
if (!winsRow) return null;
return {
heavyTrophyId: heavyTrophyRow.trophyId,
ownerUserId: ownerRow.userId,
wins: { trophyId: winsRow.trophyId, userId: winsRow.userId },
};
}
async function resolveManyUserIds(heavyTournamentId: number | null) {
if (heavyTournamentId !== null) {
const rows = await db
@@ -841,18 +935,6 @@ async function resolveLfgTournament() {
return { tournamentId: row.tournamentId, teamId: row.id };
}
async function resolveSubsTournamentId() {
const row = await db
.selectFrom("TournamentSub")
.select(({ fn }) => ["tournamentId", fn.countAll<number>().as("count")])
.groupBy("tournamentId")
.orderBy("count", "desc")
.limit(1)
.executeTakeFirst();
return row?.tournamentId ?? null;
}
async function resolveAuditTournamentId() {
const row = await db
.selectFrom("TournamentAuditLog")

View File

@@ -55,11 +55,11 @@ const tournaments = await db
"Tournament.settings",
"Tournament.isFinalized",
"CalendarEvent.name",
"CalendarEventDate.startTime",
"CalendarEventDate.startsAt",
])
.where("CalendarEventDate.startTime", ">=", seasonStartTimestamp)
.where("CalendarEventDate.startTime", "<=", seasonEndTimestamp)
.orderBy("CalendarEventDate.startTime")
.where("CalendarEventDate.startsAt", ">=", seasonStartTimestamp)
.where("CalendarEventDate.startsAt", "<=", seasonEndTimestamp)
.orderBy("CalendarEventDate.startsAt")
.execute();
const unfinalizedTournaments = tournaments.filter(

View File

@@ -1,65 +0,0 @@
/** biome-ignore-all lint/suspicious/noConsole: Biome v2 migration */
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import Database from "better-sqlite3";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const ROOT_DIR = path.join(__dirname, "..");
const MIGRATIONS_DIR = path.join(ROOT_DIR, "migrations");
const DB_FILES = [
path.join(ROOT_DIR, "db-test.sqlite3"),
...fs
.readdirSync(path.join(ROOT_DIR, "e2e", "seeds"))
.filter((f) => f.startsWith("db-seed-") && f.endsWith(".sqlite3"))
.map((f) => path.join(ROOT_DIR, "e2e", "seeds", f)),
];
const migrationFilesOnDisk = fs
.readdirSync(MIGRATIONS_DIR)
.filter((f) => f.endsWith(".js"))
.sort();
let hasErrors = false;
for (const dbPath of DB_FILES) {
const relativePath = path.relative(ROOT_DIR, dbPath);
if (!fs.existsSync(dbPath)) {
console.warn(`Warning: ${relativePath} does not exist, skipping`);
continue;
}
const db = new Database(dbPath, { readonly: true });
const rows = db
.prepare("SELECT name FROM migrations ORDER BY id ASC")
.all() as Array<{ name: string }>;
db.close();
const migrationsInDb = new Set(rows.map((r) => r.name));
const missingMigrations = migrationFilesOnDisk.filter(
(name) => !migrationsInDb.has(name),
);
if (missingMigrations.length > 0) {
hasErrors = true;
console.error(
`\n${relativePath} is missing ${missingMigrations.length} migration(s):`,
);
for (const name of missingMigrations) {
console.error(` - ${name}`);
}
}
}
if (hasErrors) {
console.error(
"\nRun `pnpm run test:e2e:generate-seeds` to regenerate test databases.",
);
process.exit(1);
} else {
console.log("All test databases have the latest migrations.");
}

View File

@@ -64,7 +64,7 @@ async function main() {
for (const [div, divsTeams] of grouped) {
logger.info(`Creating division ${div}...`);
const createdEvent = await CalendarRepository.create({
const createdEvent = await CalendarRepository.insert({
parentTournamentId: tournament.ctx.id,
authorId: tournament.ctx.author.id,
bracketProgression: tournament.ctx.settings.bracketProgression,
@@ -75,7 +75,7 @@ async function main() {
name: `${tournament.ctx.name} - ${div.startsWith("Division") ? div : `Division ${div}`}`,
organizationId: tournament.ctx.organization?.id ?? null,
rules: tournament.ctx.rules,
startTimes: [dateToDatabaseTimestamp(tournament.ctx.startTime)],
startTimes: [dateToDatabaseTimestamp(tournament.ctx.startsAt)],
tags: null,
tournamentToCopyId: tournament.ctx.id,
avatarImgId: calendarEvent.avatarImgId ?? undefined,

View File

@@ -0,0 +1,68 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const MIGRATION_FOLDER = fileURLToPath(
new URL("../migrations", import.meta.url),
);
const TEMPLATE = `import type { Kysely } from "kysely";
/** TODO: describe what this migration changes */
export async function up(db: Kysely<any>): Promise<void> {
// kysely does not wrap sqlite migrations in a transaction, so do it here
await db.transaction().execute(async (trx) => {
await trx.schema.alterTable("TODO").addColumn("TODO", "text").execute();
});
}
`;
function main() {
const description = toKebabCase(process.argv.slice(2).join(" "));
if (!description) {
throw new Error(
'Missing migration description e.g. pnpm run migrate:new "add user pronouns"',
);
}
const fileName = `${timestamp(new Date())}-${description}.ts`;
const filePath = path.join(MIGRATION_FOLDER, fileName);
if (fs.existsSync(filePath)) {
throw new Error(`${fileName} already exists`);
}
fs.writeFileSync(filePath, TEMPLATE);
// biome-ignore lint/suspicious/noConsole: CLI script output
console.log(`Created migrations/${fileName}`);
}
/** Sortable UTC stamp, so two branches can never claim the same slot. */
function timestamp(date: Date) {
const pad = (value: number) => String(value).padStart(2, "0");
return [
date.getUTCFullYear(),
pad(date.getUTCMonth() + 1),
pad(date.getUTCDate()),
pad(date.getUTCHours()),
pad(date.getUTCMinutes()),
pad(date.getUTCSeconds()),
].join("");
}
function toKebabCase(input: string) {
return input
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
}
try {
main();
} catch (error) {
// biome-ignore lint/suspicious/noConsole: CLI script output
console.error((error as Error).message);
process.exit(1);
}

View File

@@ -1,4 +1,4 @@
import { sql } from "~/db/sql";
import { db } from "~/db/sql";
import invariant from "~/utils/invariant";
import { logger } from "~/utils/logger";
@@ -6,10 +6,11 @@ const discordId = process.argv[2]?.trim();
invariant(discordId, "discord id is required (argument 1)");
sql
.prepare(
'delete from "Skill" where "userId" = (select id from "User" where discordId = @discordId)',
await db
.deleteFrom("Skill")
.where("userId", "in", (eb) =>
eb.selectFrom("User").select("User.id").where("discordId", "=", discordId),
)
.run({ discordId });
.execute();
logger.info(`Deleted skill of user with discord id: ${discordId}`);

View File

@@ -1,4 +1,4 @@
import { sql } from "~/db/sql";
import { db } from "~/db/sql";
import invariant from "~/utils/invariant";
import { logger } from "~/utils/logger";
@@ -6,16 +6,18 @@ const discordId = process.argv[2]?.trim();
invariant(discordId, "discord id is required (argument 1)");
const user = sql
.prepare('select id from "User" where discordId = @discordId')
.get({ discordId }) as { id: number } | undefined;
const user = await db
.selectFrom("User")
.select("id")
.where("discordId", "=", discordId)
.executeTakeFirst();
invariant(user, `user with discord id ${discordId} not found`);
const userId = user.id;
sql.prepare('delete from "Build" where ownerId = @userId').run({ userId });
sql.prepare('delete from "UserWeapon" where userId = @userId').run({ userId });
sql.prepare('delete from "User" where id = @userId').run({ userId });
await db.deleteFrom("Build").where("ownerId", "=", userId).execute();
await db.deleteFrom("UserWeapon").where("userId", "=", userId).execute();
await db.deleteFrom("User").where("id", "=", userId).execute();
logger.info(`Deleted user with discord id: ${discordId}`);

101
scripts/ensure-test-db.ts Normal file
View File

@@ -0,0 +1,101 @@
import { execSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { DatabaseSync } from "node:sqlite";
import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const ROOT_DIR = path.join(__dirname, "..");
const MIGRATIONS_DIR = path.join(ROOT_DIR, "migrations");
const TEST_DB_PATH = path.join(ROOT_DIR, "db-test.sqlite3");
export function setup() {
ensureMigratedDb(TEST_DB_PATH);
// Test workers only ever read this file, to copy its schema into their own
// in-memory database. Taking it out of WAL drops the -wal/-shm sidecars, so
// those concurrent opens need no write access and cannot contend.
const db = new DatabaseSync(TEST_DB_PATH);
db.exec("PRAGMA journal_mode = DELETE");
db.close();
}
/**
* Ensures the SQLite file at `dbPath` has every migration applied: creates it
* if missing, applies pending migrations, and rebuilds it from scratch if it
* contains a migration that no longer exists on disk.
*/
export function ensureMigratedDb(dbPath: string) {
const resolvedPath = path.resolve(ROOT_DIR, dbPath);
if (!fs.existsSync(resolvedPath)) {
migrateUp(resolvedPath);
return;
}
const applied = appliedMigrations(resolvedPath);
const onDisk = migrationFilesOnDisk();
const hasDrift =
applied === null || applied.some((name) => !onDisk.has(name));
if (hasDrift) {
deleteDbFiles(resolvedPath);
migrateUp(resolvedPath);
return;
}
const appliedSet = new Set(applied);
const hasPending = [...onDisk].some((name) => !appliedSet.has(name));
if (hasPending) {
migrateUp(resolvedPath);
}
}
function migrationFilesOnDisk() {
return new Set(
fs
.readdirSync(MIGRATIONS_DIR)
.filter((file) => file.endsWith(".ts"))
// kysely tracks migrations by file name without the extension
.map((file) => file.slice(0, -".ts".length)),
);
}
function appliedMigrations(dbPath: string) {
const db = new DatabaseSync(dbPath, { readOnly: true });
try {
const hasMigrationsTable = db
.prepare(
"select 1 from sqlite_master where type = 'table' and name = 'kysely_migration'",
)
.get();
if (!hasMigrationsTable) return null;
const rows = db
.prepare("select name from kysely_migration")
.all() as Array<{
name: string;
}>;
return rows.map((row) => row.name);
} catch {
return null;
} finally {
db.close();
}
}
function deleteDbFiles(dbPath: string) {
for (const suffix of ["", "-shm", "-wal"]) {
fs.rmSync(`${dbPath}${suffix}`, { force: true });
}
}
function migrateUp(dbPath: string) {
execSync("pnpm run migrate up", {
cwd: ROOT_DIR,
stdio: "inherit",
env: { ...process.env, DB_PATH: dbPath },
});
}

View File

@@ -1,73 +0,0 @@
import { execSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import Database from "better-sqlite3";
import { SEED_VARIATIONS } from "../app/features/api-private/constants";
const E2E_SEEDS_DIR = "e2e/seeds";
const BASE_TEST_DB = "db-test.sqlite3";
const E2E_WORKER_DB_PATTERN = /^db-test-e2e-\d+\.sqlite3$/;
async function generatePreSeededDatabases() {
// biome-ignore lint/suspicious/noConsole: CLI script output
console.log("Generating pre-seeded databases for e2e tests...\n");
for (const file of fs.readdirSync(".")) {
if (E2E_WORKER_DB_PATTERN.test(file)) {
fs.unlinkSync(file);
// biome-ignore lint/suspicious/noConsole: CLI script output
console.log(`Deleted stale worker db: ${file}`);
}
}
if (!fs.existsSync(E2E_SEEDS_DIR)) {
fs.mkdirSync(E2E_SEEDS_DIR, { recursive: true });
}
const baseDbPath = path.resolve(BASE_TEST_DB);
if (!fs.existsSync(baseDbPath)) {
// biome-ignore lint/suspicious/noConsole: CLI script output
console.error(
`Base test database not found: ${baseDbPath}. Run migrations first.`,
);
process.exit(1);
}
for (const variation of SEED_VARIATIONS) {
const outputPath = path.join(E2E_SEEDS_DIR, `db-seed-${variation}.sqlite3`);
// biome-ignore lint/suspicious/noConsole: CLI script output
console.log(`Generating ${variation}...`);
// remove stale WAL/SHM sidecars so a fresh copy isn't paired with a leftover
// WAL from a previously crashed run (which reads as "database disk image is malformed")
for (const sidecar of [`${outputPath}-wal`, `${outputPath}-shm`]) {
if (fs.existsSync(sidecar)) {
fs.unlinkSync(sidecar);
}
}
fs.copyFileSync(baseDbPath, outputPath);
execSync(
`pnpm exec vite-node scripts/seed-single-variation.ts -- ${variation} ${outputPath}`,
{ stdio: "inherit" },
);
const db = new Database(outputPath);
db.pragma("wal_checkpoint(TRUNCATE)");
db.close();
const stats = fs.statSync(outputPath);
// biome-ignore lint/suspicious/noConsole: CLI script output
console.log(
`${variation}: ${(stats.size / 1024 / 1024).toFixed(2)} MB\n`,
);
}
// biome-ignore lint/suspicious/noConsole: CLI script output
console.log(`Done! Pre-seeded databases saved to ${E2E_SEEDS_DIR}/`);
}
// biome-ignore lint/suspicious/noConsole: CLI script output
generatePreSeededDatabases().catch(console.error);

79
scripts/migrate.ts Normal file
View File

@@ -0,0 +1,79 @@
import fs from "node:fs/promises";
import path from "node:path";
import { DatabaseSync } from "node:sqlite";
import { fileURLToPath, pathToFileURL } from "node:url";
import { Kysely } from "kysely";
import { FileMigrationProvider, Migrator } from "kysely/migration";
import { NodeSqliteDialect } from "../app/db/node-sqlite-dialect.ts";
const MIGRATION_FOLDER = fileURLToPath(
new URL("../migrations", import.meta.url),
);
try {
process.loadEnvFile();
} catch {
// .env is optional; in production DB_PATH comes from the host environment
}
async function main() {
const command = process.argv[2] ?? "up";
if (command !== "up") {
throw new Error(
`Unknown command "${command}". Only "up" is supported; migrations are never rolled back.`,
);
}
const dbPath = process.env.DB_PATH;
if (!dbPath) {
throw new Error("DB_PATH is not set");
}
const database = new DatabaseSync(dbPath);
database.exec("PRAGMA journal_mode = WAL");
database.exec("PRAGMA foreign_keys = ON");
database.exec("PRAGMA busy_timeout = 5000");
const db = new Kysely<any>({
dialect: new NodeSqliteDialect({ database }),
});
const migrator = new Migrator({
db,
provider: new FileMigrationProvider({
fs,
path,
migrationFolder: MIGRATION_FOLDER,
import: (filePath) => import(pathToFileURL(filePath).href),
}),
});
const { error, results } = await migrator.migrateToLatest();
for (const result of results ?? []) {
if (result.status === "Success") {
log(`${result.migrationName}`);
} else if (result.status === "Error") {
log(`${result.migrationName}`);
}
}
await db.destroy();
if (error) throw error;
if (!results?.length) {
log(`No pending migrations for ${dbPath}`);
}
}
function log(message: string) {
// biome-ignore lint/suspicious/noConsole: CLI script output
console.log(message);
}
main().catch((error) => {
// biome-ignore lint/suspicious/noConsole: CLI script output
console.error(error);
process.exit(1);
});

View File

@@ -78,8 +78,8 @@ async function getParticipantsForOrgInMonth(
)
.select(({ fn }) => fn.count<number>("tmgrp.userId").distinct().as("count"))
.where("ce.organizationId", "=", organizationId)
.where("ced.startTime", ">=", startTimestamp)
.where("ced.startTime", "<", endTimestamp)
.where("ced.startsAt", ">=", startTimestamp)
.where("ced.startsAt", "<", endTimestamp)
.where("ttci.checkedInAt", "is not", null)
.where("ttci.isCheckOut", "=", 0)
.executeTakeFirst();
@@ -98,7 +98,7 @@ async function getOrgsWithRecentTournaments(
.select("ce.organizationId")
.distinct()
.where("ce.organizationId", "is not", null)
.where("ced.startTime", ">=", earliestTimestamp)
.where("ced.startsAt", ">=", earliestTimestamp)
.execute();
return orgs.map((org) => org.organizationId!);

View File

@@ -1,5 +1,3 @@
import { sql } from "~/db/sql";
import type { Tables } from "~/db/tables";
import * as BadgeRepository from "~/features/badges/BadgeRepository.server";
import * as BuildRepository from "~/features/builds/BuildRepository.server";
import * as XRankPlacementRepository from "~/features/top-search/XRankPlacementRepository.server";
@@ -17,9 +15,7 @@ invariant(
"jsonNumber must be an integer (argument 1)",
);
type Placements = Array<
Omit<Tables["XRankPlacement"], "playerId" | "id"> & { playerSplId: string }
>;
type Placements = XRankPlacementRepository.XRankPlacementInsertArgs[];
const modes = ["splatzones", "towercontrol", "rainmaker", "clamblitz"] as const;
const modeToShort = {
@@ -35,7 +31,9 @@ void main();
async function main() {
const placements: Placements = [];
wipeMonthYearPlacements(resolveMonthYear(jsonNumber));
await XRankPlacementRepository.deleteAllByMonthYear(
resolveMonthYear(jsonNumber),
);
for (const mode of modes) {
for (const region of regions) {
for (const includeWeapon of [false]) {
@@ -51,7 +49,7 @@ async function main() {
}
}
addPlacements(placements);
await XRankPlacementRepository.insertMany(placements);
await XRankPlacementRepository.refreshAllPeakXp();
await BadgeRepository.syncXPBadges();
await BuildRepository.recalculateAllSortValues();
@@ -139,67 +137,3 @@ function resolveMonthYear(number: number) {
year: start.getFullYear(),
};
}
const addPlayerStm = sql.prepare(/* sql */ `
insert into "SplatoonPlayer" ("splId")
values (@splId)
on conflict ("splId") do nothing
`);
const addPlacementStm = sql.prepare(/* sql */ `
insert into "XRankPlacement" (
"weaponSplId",
"name",
"nameDiscriminator",
"power",
"rank",
"title",
"badges",
"bannerSplId",
"playerId",
"month",
"year",
"region",
"mode"
)
values (
@weaponSplId,
@name,
@nameDiscriminator,
@power,
@rank,
@title,
@badges,
@bannerSplId,
(select "id" from "SplatoonPlayer" where "splId" = @playerSplId),
@month,
@year,
@region,
@mode
)
`);
function addPlacements(placements: Placements) {
sql.transaction(() => {
for (const placement of placements) {
addPlayerStm.run({ splId: placement.playerSplId });
addPlacementStm.run(placement);
}
})();
}
function wipeMonthYearPlacements({
month,
year,
}: {
month: number;
year: number;
}) {
const wipeMonthYearPlacementsStm = sql.prepare(/* sql */ `
delete from "XRankPlacement"
where "month" = @month
and "year" = @year
`);
wipeMonthYearPlacementsStm.run({ month, year });
}

View File

@@ -1,4 +1,4 @@
import { sql } from "~/db/sql";
import { db } from "~/db/sql";
import invariant from "~/utils/invariant";
import { logger } from "~/utils/logger";
@@ -12,8 +12,10 @@ invariant(
"displayName of badge must have at least one uppercase letter",
);
sql
.prepare("update badge set displayName = @newName where id = @id")
.run({ id, newName });
await db
.updateTable("Badge")
.set({ displayName: newName })
.where("id", "=", Number(id))
.execute();
logger.info(`Added updated name. New name: ${newName}`);

View File

@@ -1,5 +1,5 @@
import { ordinal } from "openskill";
import { db, sql } from "~/db/sql";
import { db } from "~/db/sql";
import type { Tables } from "~/db/tables";
import { TIERS, type TierName } from "~/features/mmr/mmr-constants";
import * as SkillRepository from "~/features/mmr/SkillRepository.server";
@@ -14,38 +14,26 @@ invariant(rawNth, "nth of new season needed (argument 1)");
const nth = Number(rawNth);
invariant(!Number.isNaN(nth), "nth of new season must be a number");
const skillsExistStm = sql.prepare(/* sql */ `
select
1
from "Skill"
where
"season" = @season
limit 1
`);
invariant(
skillsExistStm.get({ season: nth - 1 }),
await SkillRepository.existsBySeason(nth - 1),
`No skills for season ${nth - 1}`,
);
invariant(
!skillsExistStm.get({ season: nth }),
!(await SkillRepository.existsBySeason(nth)),
`Skills for season ${nth} already exist`,
);
const activeMatchExistsStm = sql.prepare(/* sql */ `
select
"GroupMatch"."id"
from "GroupMatch"
left join "Skill" on "Skill"."groupMatchId" = "GroupMatch"."id"
where
"Skill"."id" is null
`);
const idsOfActiveMatches = activeMatchExistsStm
.all()
.map((row) => (row as any).id) as number[];
const idsOfActiveMatches = (
await db
.selectFrom("GroupMatch")
.leftJoin("Skill", "Skill.groupMatchId", "GroupMatch.id")
.select("GroupMatch.id")
.where("Skill.id", "is", null)
.execute()
).map((row) => row.id);
invariant(
!activeMatchExistsStm.get(),
idsOfActiveMatches.length === 0,
`There are active matches: (ids: ${idsOfActiveMatches.join(", ")})`,
);
@@ -70,11 +58,13 @@ const TIER_TO_NEW_TIER: Record<TierName, TierName> = {
// - For +3 members, consider the last 2 seasons
// - For non-plus members, consider the last season only
const getAllSkills = async () => {
const skills = [
freshUserSkills(nth - 1).userSkills,
freshUserSkills(nth - 2).userSkills,
freshUserSkills(nth - 3).userSkills,
];
const skills = (
await Promise.all([
freshUserSkills(nth - 1),
freshUserSkills(nth - 2),
freshUserSkills(nth - 3),
])
).map((seasonSkills) => seasonSkills.userSkills);
const plusServerMembers = await db
.selectFrom("PlusTier")
@@ -136,28 +126,19 @@ const groupedSkills = skillsToConsider.reduce(
{} as Record<TierName, typeof skillsToConsider>,
);
const skillStm = sql.prepare(/* sql */ `
select
*
from "Skill"
where
"userId" = @userId
and "ordinal" = @ordinal
`);
const midPoints = Object.entries(groupedSkills).reduce(
(acc, [tier, skills]) => {
const midPoint = skills[Math.floor(skills.length / 2)];
const midPointSkill = skillStm.get({
userId: midPoint.userId,
ordinal: midPoint.ordinal,
}) as Tables["Skill"];
invariant(midPointSkill, "midPointSkill not found");
const midPoints = {} as Record<TierName, Tables["Skill"]>;
for (const [tier, skills] of Object.entries(groupedSkills)) {
const midPoint = skills[Math.floor(skills.length / 2)];
const midPointSkill = await db
.selectFrom("Skill")
.selectAll()
.where("userId", "=", midPoint.userId)
.where("ordinal", "=", midPoint.ordinal)
.executeTakeFirst();
invariant(midPointSkill, "midPointSkill not found");
acc[tier as TierName] = midPointSkill;
return acc;
},
{} as Record<TierName, Tables["Skill"]>,
);
midPoints[tier as TierName] = midPointSkill;
}
const newSkills = allSkills.map((s) => {
const newTier = TIER_TO_NEW_TIER[s.tier.name];

View File

@@ -1,3 +1,5 @@
import { Config } from "~/config";
export const SEED_ART_URLS = [
"https://images.unsplash.com/photo-1611627474565-2367887415d1?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLDF8fHx8fHwxNjg4NTU1NTA2&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
"https://images.unsplash.com/photo-1625120742520-3f085b6894ba?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLDF8fHx8fHwxNjg4NTU1NTI2&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
@@ -33,16 +35,43 @@ export function getArtFilename(index: number): string {
return `art-${index}.jpg`;
}
export const SEED_TEAM_IMAGES = [
{ filename: "alliance-rogue.png", teamId: 1 },
{ filename: "default.png", teamId: null },
/** Logos in `app/db/seed/img`, uploaded to the local image storage under their own
* name. Anything the seed sets as a logo has to be one of these to render in dev. */
export const SEED_LOGO_FILENAMES = [
"alliance-rogue.png",
"default.png",
"in-the-zone.png",
"luti.png",
"paddling-pool.png",
"picnic.png",
"swim-or-sink.png",
"the-depths.png",
];
export const SEED_TOURNAMENT_IMAGES = [
{ filename: "picnic.png", tournamentId: 1 },
{ filename: "in-the-zone.png", tournamentId: 2 },
{ filename: "paddling-pool.png", tournamentId: 3 },
{ filename: "swim-or-sink.png", tournamentId: 4 },
{ filename: "the-depths.png", tournamentId: 5 },
{ filename: "luti.png", tournamentId: 6 },
];
/** The logo of a tournament that has none of its own, uploaded under the name the
* app looks it up by. */
export const SEED_DEFAULT_TOURNAMENT_LOGO = {
sourceFilename: "default.png",
filename: Config.tournamentDefaultLogo,
};
/** How many numbered logos are uploaded. A seed run needing more than this many
* ends up with logos that 404 — raise it rather than reusing one, the url column
* being unique. */
export const SEED_NUMBERED_LOGO_COUNT = 200;
/** Filename of a logo the seed hands to something it has no particular logo for.
* Numbered because every image row needs a url of its own, cycling the logos so
* that a page full of them is not a page of one logo. */
export function numberedLogoFilename(index: number) {
return `seed-logo-${index}.png`;
}
/** The uploads a numbered logo filename resolves to, in the storage the app reads. */
export const SEED_NUMBERED_LOGOS = Array.from(
{ length: SEED_NUMBERED_LOGO_COUNT },
(_, index) => ({
filename: numberedLogoFilename(index),
sourceFilename: SEED_LOGO_FILENAMES[index % SEED_LOGO_FILENAMES.length],
}),
);

View File

@@ -7,8 +7,9 @@ import { logger } from "~/utils/logger";
import {
getArtFilename,
SEED_ART_URLS,
SEED_TEAM_IMAGES,
SEED_TOURNAMENT_IMAGES,
SEED_DEFAULT_TOURNAMENT_LOGO,
SEED_LOGO_FILENAMES,
SEED_NUMBERED_LOGOS,
} from "./seed-art-urls";
async function checkMinioConnection(): Promise<boolean> {
@@ -192,17 +193,22 @@ export async function seedImages(): Promise<void> {
`\n✅ Art image seeding complete: ${successCount} uploaded, ${skippedCount} already existed, ${failCount} failed`,
);
logger.info(
`\n📥 Processing ${SEED_TEAM_IMAGES.length} team images and ${SEED_TOURNAMENT_IMAGES.length} tournament images`,
);
const localImages = [
...SEED_LOGO_FILENAMES.map((filename) => ({
filename,
sourceFilename: filename,
})),
SEED_DEFAULT_TOURNAMENT_LOGO,
...SEED_NUMBERED_LOGOS,
];
const localImages = [...SEED_TEAM_IMAGES, ...SEED_TOURNAMENT_IMAGES];
logger.info(`\n📥 Processing ${localImages.length} logo images`);
let localSuccessCount = 0;
let localFailCount = 0;
let localSkippedCount = 0;
for (const { filename } of localImages) {
for (const { filename, sourceFilename } of localImages) {
const smallFilename = filename.replace(/\.(\w+)$/, "-small.$1");
try {
@@ -215,7 +221,7 @@ export async function seedImages(): Promise<void> {
` ↷ Files ${filename} and ${smallFilename} already exist in Minio`,
);
} else {
const imageBuffer = await readLocalImage(filename);
const imageBuffer = await readLocalImage(sourceFilename);
if (!regularExists) {
logger.info(` Uploading ${filename} to Minio...`);
@@ -238,6 +244,6 @@ export async function seedImages(): Promise<void> {
}
logger.info(
`\n✅ Local image seeding complete: ${localSuccessCount} uploaded, ${localSkippedCount} already existed, ${localFailCount} failed`,
`\n✅ Logo image seeding complete: ${localSuccessCount} uploaded, ${localSkippedCount} already existed, ${localFailCount} failed`,
);
}

View File

@@ -1,15 +0,0 @@
import type { SeedVariation } from "~/features/api-private/routes/seed";
const variation = process.argv[2] as SeedVariation;
const dbPath = process.argv[3];
if (!variation || !dbPath) {
// biome-ignore lint/suspicious/noConsole: CLI script output
console.error("Usage: seed-single-variation.ts <variation> <dbPath>");
process.exit(1);
}
process.env.DB_PATH = dbPath;
const { seed } = await import("../app/db/seed/index");
await seed(variation === "DEFAULT" ? null : variation);

8
scripts/seed.ts Normal file
View File

@@ -0,0 +1,8 @@
import { seed } from "~/db/seed";
const startedAt = Date.now();
await seed();
// biome-ignore lint/suspicious/noConsole: CLI script output
console.log(`Seeded in ${((Date.now() - startedAt) / 1000).toFixed(1)}s`);

View File

@@ -1,4 +1,4 @@
import { sql } from "~/db/sql";
import { db } from "~/db/sql";
import * as Seasons from "~/features/mmr/core/Seasons";
import invariant from "~/utils/invariant";
import { logger } from "~/utils/logger";
@@ -11,11 +11,11 @@ const currentSeasonNth = Seasons.currentOrPrevious()?.nth;
invariant(currentSeasonNth, "current season nth is required");
sql
.prepare(
'update "User" set plusSkippedForSeasonNth = @plusSkippedForSeasonNth where discordId = @discordId',
)
.run({ discordId, plusSkippedForSeasonNth: currentSeasonNth });
await db
.updateTable("User")
.set({ plusSkippedForSeasonNth: currentSeasonNth })
.where("discordId", "=", discordId)
.execute();
logger.info(
`Plus Server admission will be skipped for Discord ID: ${discordId} (season ${currentSeasonNth})`,

View File

@@ -1,4 +1,4 @@
import { sql } from "~/db/sql";
import { db } from "~/db/sql";
import invariant from "~/utils/invariant";
const discordId = process.argv[2]?.trim();
@@ -10,24 +10,16 @@ invariant(discordId !== discordId2, "discord ids must be different");
const tempDiscordId = "temp-discord-id";
const stm = sql.prepare(
/** sql */ `update "User" set "discordId" = @newDiscordId where "discordId" = @discordId;`,
);
// swap user discordIds
sql.transaction(() => {
stm.run({
discordId: discordId,
newDiscordId: tempDiscordId,
});
await db.transaction().execute(async (trx) => {
const swap = (from: string, to: string) =>
trx
.updateTable("User")
.set({ discordId: to })
.where("discordId", "=", from)
.execute();
stm.run({
discordId: discordId2,
newDiscordId: discordId,
});
stm.run({
discordId: tempDiscordId,
newDiscordId: discordId2,
});
})();
await swap(discordId, tempDiscordId);
await swap(discordId2, discordId);
await swap(tempDiscordId, discordId2);
});

View File

@@ -1,4 +1,5 @@
import { db } from "~/db/sql";
import * as BadgeRepository from "~/features/badges/BadgeRepository.server";
import { logger } from "~/utils/logger";
const HOMEMADE_BADGES_URL =
@@ -162,14 +163,7 @@ async function addBadge(badge: {
displayName: string;
authorId: number;
}) {
return db
.insertInto("Badge")
.values({
code: badge.code,
displayName: badge.displayName,
authorId: badge.authorId,
})
.execute();
return BadgeRepository.insert({ ...badge, hue: null });
}
main();

View File

@@ -1,5 +1,8 @@
{
"extends": "../tsconfig.json",
"include": ["./**/*.ts", "../types/**/*.d.ts"],
"exclude": []
"exclude": [],
"compilerOptions": {
"allowImportingTsExtensions": true
}
}

View File

@@ -1,4 +1,4 @@
import { sql } from "~/db/sql";
import { db } from "~/db/sql";
import invariant from "~/utils/invariant";
import { logger } from "~/utils/logger";
@@ -6,10 +6,10 @@ const discordId = process.argv[2]?.trim();
invariant(discordId, "discord id is required (argument 1)");
sql
.prepare(
'update "User" set plusSkippedForSeasonNth = null where discordId = @discordId',
)
.run({ discordId });
await db
.updateTable("User")
.set({ plusSkippedForSeasonNth: null })
.where("discordId", "=", discordId)
.execute();
logger.info(`Plus Server admission unskipped for Discord ID: ${discordId}`);