mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-11-15 01:12:36 -06:00
Merge remote-tracking branch 'origin/main' into css-rework-sidenav
This commit is contained in:
142
scripts/add-leaderboard-teams-to-tournament.ts
Normal file
142
scripts/add-leaderboard-teams-to-tournament.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
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";
|
||||
import invariant from "~/utils/invariant";
|
||||
import { logger } from "~/utils/logger";
|
||||
|
||||
const tournamentIdArg = process.argv[2]?.trim();
|
||||
const placementsArg = process.argv[3]?.trim();
|
||||
|
||||
invariant(tournamentIdArg, "tournamentId is required (argument 1)");
|
||||
invariant(placementsArg, "placements are required (argument 2), e.g. 1,2,3");
|
||||
|
||||
const tournamentId = Number(tournamentIdArg);
|
||||
invariant(
|
||||
Number.isInteger(tournamentId) && tournamentId > 0,
|
||||
"tournamentId must be a positive integer",
|
||||
);
|
||||
|
||||
const placements = placementsArg.split(",").map((p) => Number(p.trim()));
|
||||
for (const p of placements) {
|
||||
invariant(
|
||||
Number.isInteger(p) && p > 0,
|
||||
`each placement must be a positive integer, got: ${p}`,
|
||||
);
|
||||
}
|
||||
|
||||
async function loadTournament() {
|
||||
try {
|
||||
return await tournamentFromDB({
|
||||
user: undefined,
|
||||
tournamentId,
|
||||
});
|
||||
} catch {
|
||||
throw new Error(`Tournament with id ${tournamentId} not found`);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const tournament = await loadTournament();
|
||||
|
||||
invariant(!tournament.hasStarted, "Tournament has already started");
|
||||
|
||||
const season = Seasons.currentOrPrevious();
|
||||
invariant(season, "No current or previous season found");
|
||||
|
||||
const leaderboard = await LeaderboardRepository.teamLeaderboardBySeason({
|
||||
season: season.nth,
|
||||
onlyOneEntryPerUser: true,
|
||||
});
|
||||
|
||||
const entries = placements.map((placement) => {
|
||||
const entry = leaderboard.find((e) => e.placementRank === placement);
|
||||
invariant(entry, `No leaderboard entry found for placement #${placement}`);
|
||||
return entry;
|
||||
});
|
||||
|
||||
const existingTeamNames = new Set(tournament.ctx.teams.map((t) => t.name));
|
||||
|
||||
let teamNameCounter = 1;
|
||||
const resolvedNames = entries.map((entry) => {
|
||||
return entry.team?.name ?? `Team ${teamNameCounter++}`;
|
||||
});
|
||||
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
const entry = entries[i];
|
||||
const teamName = resolvedNames[i];
|
||||
|
||||
for (const member of entry.members) {
|
||||
const existingTeam = tournament.teamMemberOfByUser({ id: member.id });
|
||||
invariant(
|
||||
!existingTeam,
|
||||
`User ${member.username} (id: ${member.id}) is already on team "${existingTeam?.name}"`,
|
||||
);
|
||||
|
||||
const user = await UserRepository.findLeanById(member.id);
|
||||
invariant(user, `User with id ${member.id} not found`);
|
||||
invariant(user.friendCode, `User ${member.username} has no friend code`);
|
||||
|
||||
if (tournament.ctx.settings.requireInGameNames) {
|
||||
const inGameName = await UserRepository.inGameNameByUserId(member.id);
|
||||
invariant(
|
||||
inGameName,
|
||||
`User ${member.username} has no in-game name (required by tournament)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
invariant(
|
||||
!existingTeamNames.has(teamName),
|
||||
`Team name "${teamName}" is already taken in the tournament`,
|
||||
);
|
||||
existingTeamNames.add(teamName);
|
||||
}
|
||||
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
const entry = entries[i];
|
||||
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,
|
||||
prefersNotToHost: 0,
|
||||
teamId: entry.team?.id ?? null,
|
||||
},
|
||||
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({
|
||||
newTeamId: tournamentTeam.id,
|
||||
userId: member.id,
|
||||
inGameName: memberInGameName ?? null,
|
||||
tournamentId,
|
||||
});
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`Created team "${teamName}" (placement #${entry.placementRank}) with members: ${entry.members.map((m) => m.username).join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`Done. Added ${entries.length} teams to tournament ${tournamentId}`,
|
||||
);
|
||||
}
|
||||
|
||||
void main();
|
||||
65
scripts/check-test-db-migrations.ts
Normal file
65
scripts/check-test-db-migrations.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
/** 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 `npm run test:e2e:generate-seeds` to regenerate test databases.",
|
||||
);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log("All test databases have the latest migrations.");
|
||||
}
|
||||
29
scripts/fix-in-game-names.ts
Normal file
29
scripts/fix-in-game-names.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import "dotenv/config";
|
||||
import { sql } from "~/db/sql";
|
||||
import { IN_GAME_NAME_REGEXP } from "~/features/user-page/user-page-constants";
|
||||
import { logger } from "~/utils/logger";
|
||||
|
||||
const users = sql
|
||||
.prepare('SELECT id, "inGameName" FROM "User" WHERE "inGameName" IS NOT NULL')
|
||||
.all() as { id: number; inGameName: string }[];
|
||||
|
||||
const invalidUsers = users.filter(
|
||||
(user) => !IN_GAME_NAME_REGEXP.test(user.inGameName),
|
||||
);
|
||||
|
||||
logger.info("Invalid in-game names:");
|
||||
for (const user of invalidUsers) {
|
||||
logger.info(`- ${user.inGameName} (id: ${user.id})`);
|
||||
}
|
||||
|
||||
const updateStmt = sql.prepare(
|
||||
'UPDATE "User" SET "inGameName" = NULL WHERE id = @id',
|
||||
);
|
||||
|
||||
for (const user of invalidUsers) {
|
||||
updateStmt.run({ id: user.id });
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`Fixed ${invalidUsers.length} invalid in-game names (out of ${users.length} total)`,
|
||||
);
|
||||
Reference in New Issue
Block a user