mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-10-01 07:37:55 -05:00
Move app to apps/web-react in pnpm workspace layout
This commit is contained in:
1791
apps/web-react/migrations/20260803000000-initial.sql
Normal file
1791
apps/web-react/migrations/20260803000000-initial.sql
Normal file
File diff suppressed because it is too large
Load Diff
77
apps/web-react/migrations/20260803000000-initial.ts
Normal file
77
apps/web-react/migrations/20260803000000-initial.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import fs from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { type Kysely, sql } from "kysely";
|
||||
|
||||
const SCHEMA_PATH = fileURLToPath(
|
||||
new URL("./20260803000000-initial.sql", import.meta.url),
|
||||
);
|
||||
|
||||
const STATEMENT_SEPARATOR = "--> statement-breakpoint";
|
||||
|
||||
const LAST_COLLAPSED_MIGRATION = "164-drop-tournament-sub.js";
|
||||
|
||||
/**
|
||||
* Creates the schema as it stood when the 165 hand written migrations that came
|
||||
* before it were collapsed into this one. Only ever bootstraps an empty database:
|
||||
* production and any copy of it already has these tables, so there this migration
|
||||
* only drops the bookkeeping table of the migration runner it replaces and records
|
||||
* itself as run.
|
||||
*/
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await assertNotBehindCollapsePoint(db);
|
||||
|
||||
// left behind by ley, which tracked the migrations collapsed into this one
|
||||
await sql`DROP TABLE IF EXISTS "migrations"`.execute(db);
|
||||
|
||||
if (await hasSchema(db)) return;
|
||||
|
||||
await db.transaction().execute(async (trx) => {
|
||||
for (const statement of schemaStatements()) {
|
||||
await sql.raw(statement).execute(trx);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A database still carrying ley's bookkeeping table is only safe to adopt as is if
|
||||
* ley ran every migration collapsed into this one. Otherwise it would silently be
|
||||
* left short of tables and columns this migration assumes are already there, which
|
||||
* surfaces much later as confusing "no such table" errors at query time.
|
||||
*/
|
||||
async function assertNotBehindCollapsePoint(db: Kysely<any>) {
|
||||
const { rows: bookkeeping } = await sql<{ name: string }>`
|
||||
SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'migrations'
|
||||
`.execute(db);
|
||||
|
||||
if (bookkeeping.length === 0) return;
|
||||
|
||||
const { rows: applied } = await sql<{ name: string }>`
|
||||
SELECT name FROM "migrations" WHERE name = ${LAST_COLLAPSED_MIGRATION}
|
||||
`.execute(db);
|
||||
|
||||
if (applied.length > 0) return;
|
||||
|
||||
throw new Error(
|
||||
`Database has not run "${LAST_COLLAPSED_MIGRATION}", the last of the migrations collapsed into 20260803000000-initial, so it can't be migrated any further. Replace it with an up to date copy of production.`,
|
||||
);
|
||||
}
|
||||
|
||||
async function hasSchema(db: Kysely<any>) {
|
||||
const { rows } = await sql<{ name: string }>`
|
||||
SELECT name FROM sqlite_master
|
||||
WHERE type = 'table'
|
||||
AND name NOT LIKE 'sqlite_%'
|
||||
AND name NOT IN ('kysely_migration', 'kysely_migration_lock')
|
||||
LIMIT 1
|
||||
`.execute(db);
|
||||
|
||||
return rows.length > 0;
|
||||
}
|
||||
|
||||
function schemaStatements() {
|
||||
return fs
|
||||
.readFileSync(SCHEMA_PATH, "utf8")
|
||||
.split(STATEMENT_SEPARATOR)
|
||||
.map((statement) => statement.trim())
|
||||
.filter((statement) => statement.length > 0);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { type Kysely, sql } from "kysely";
|
||||
|
||||
/** Lets players report why a SendouQ match was cancelled and who caused it */
|
||||
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
|
||||
.createTable("GroupMatchCancelReport")
|
||||
.addColumn("id", "integer", (col) => col.primaryKey().autoIncrement())
|
||||
.addColumn("groupMatchId", "integer", (col) =>
|
||||
col.notNull().references("GroupMatch.id").onDelete("cascade"),
|
||||
)
|
||||
.addColumn("groupId", "integer", (col) =>
|
||||
col.notNull().references("Group.id").onDelete("cascade"),
|
||||
)
|
||||
.addColumn("authorUserId", "integer", (col) =>
|
||||
col.notNull().references("User.id").onDelete("cascade"),
|
||||
)
|
||||
.addColumn("reason", "text", (col) => col.notNull())
|
||||
.addColumn("createdAt", "integer", (col) =>
|
||||
col.notNull().defaultTo(sql`(strftime('%s', 'now'))`),
|
||||
)
|
||||
.addUniqueConstraint(
|
||||
"group_match_cancel_report_group_match_id_group_id",
|
||||
["groupMatchId", "groupId"],
|
||||
)
|
||||
// every table in this schema is strict
|
||||
.modifyEnd(sql`strict`)
|
||||
.execute();
|
||||
|
||||
await trx.schema
|
||||
.createTable("GroupMatchCancelReportPlayer")
|
||||
.addColumn("cancelReportId", "integer", (col) =>
|
||||
col
|
||||
.notNull()
|
||||
.references("GroupMatchCancelReport.id")
|
||||
.onDelete("cascade"),
|
||||
)
|
||||
.addColumn("userId", "integer", (col) =>
|
||||
col.notNull().references("User.id").onDelete("cascade"),
|
||||
)
|
||||
.addUniqueConstraint(
|
||||
"group_match_cancel_report_player_cancel_report_id_user_id",
|
||||
["cancelReportId", "userId"],
|
||||
)
|
||||
.modifyEnd(sql`strict`)
|
||||
.execute();
|
||||
|
||||
await trx.schema
|
||||
.createIndex("group_match_cancel_report_player_user_id_idx")
|
||||
.on("GroupMatchCancelReportPlayer")
|
||||
.column("userId")
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import { type Kysely, sql } from "kysely";
|
||||
|
||||
const duplicateMapping = sql`
|
||||
select
|
||||
dup."id" as "dupId",
|
||||
(
|
||||
select min(kept."id")
|
||||
from "Trophy" kept
|
||||
where kept."code" is null
|
||||
and kept."name" = dup."name"
|
||||
and kept."creatorId" is dup."creatorId"
|
||||
) as "keepId"
|
||||
from "Trophy" dup
|
||||
where dup."code" is null
|
||||
and exists (
|
||||
select 1
|
||||
from "Trophy" kept
|
||||
where kept."code" is null
|
||||
and kept."name" = dup."name"
|
||||
and kept."creatorId" is dup."creatorId"
|
||||
and kept."id" < dup."id"
|
||||
)
|
||||
`;
|
||||
|
||||
/**
|
||||
* Persists trophy submission acceptance instead of deriving it from the approval
|
||||
* count, so that raising TROPHY_APPROVALS_REQUIRED cannot put already accepted
|
||||
* submissions back into the review queue. Also deletes duplicate trophies that
|
||||
* re-approving an already accepted submission created, keeping the oldest one.
|
||||
*/
|
||||
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("PendingTrophy")
|
||||
.addColumn("acceptedAt", "integer")
|
||||
.execute();
|
||||
|
||||
// Repoint references at duplicates to the kept trophy before deleting, so
|
||||
// the on-delete cascades cannot destroy owner rows or pending submissions.
|
||||
// A duplicate is a user-submitted trophy sharing name and creator with an
|
||||
// older one, which the review flow cannot legitimately produce.
|
||||
await sql`
|
||||
update "TrophyOwner"
|
||||
set "trophyId" = m."keepId"
|
||||
from (${duplicateMapping}) m
|
||||
where "TrophyOwner"."trophyId" = m."dupId"
|
||||
and not exists (
|
||||
select 1
|
||||
from "TrophyOwner" existing
|
||||
where existing."tournamentId" = "TrophyOwner"."tournamentId"
|
||||
and existing."userId" = "TrophyOwner"."userId"
|
||||
and existing."trophyId" = m."keepId"
|
||||
)
|
||||
`.execute(trx);
|
||||
|
||||
await sql`
|
||||
update "CalendarEvent"
|
||||
set "trophyId" = m."keepId"
|
||||
from (${duplicateMapping}) m
|
||||
where "CalendarEvent"."trophyId" = m."dupId"
|
||||
`.execute(trx);
|
||||
|
||||
await sql`
|
||||
update "PendingTrophy"
|
||||
set "targetTrophyId" = m."keepId"
|
||||
from (${duplicateMapping}) m
|
||||
where "PendingTrophy"."targetTrophyId" = m."dupId"
|
||||
`.execute(trx);
|
||||
|
||||
await sql`
|
||||
delete from "Trophy"
|
||||
where "id" in (select "dupId" from (${duplicateMapping}))
|
||||
`.execute(trx);
|
||||
|
||||
// Backfill submissions accepted under the old threshold of 2 approvals.
|
||||
// A row only counts as accepted if its trophy was actually created.
|
||||
await sql`
|
||||
update "PendingTrophy"
|
||||
set "acceptedAt" = (
|
||||
select max("createdAt")
|
||||
from "PendingTrophyApproval"
|
||||
where "PendingTrophyApproval"."pendingTrophyId" = "PendingTrophy"."id"
|
||||
)
|
||||
where "declinedAt" is null
|
||||
and (
|
||||
select count(*)
|
||||
from "PendingTrophyApproval"
|
||||
where "PendingTrophyApproval"."pendingTrophyId" = "PendingTrophy"."id"
|
||||
) >= 2
|
||||
and (
|
||||
(
|
||||
"targetTrophyId" is null
|
||||
and exists (
|
||||
select 1
|
||||
from "Trophy"
|
||||
where "Trophy"."name" = "PendingTrophy"."name"
|
||||
and "Trophy"."creatorId" = "PendingTrophy"."submitterUserId"
|
||||
)
|
||||
)
|
||||
or (
|
||||
"targetTrophyId" is not null
|
||||
and exists (
|
||||
select 1
|
||||
from "Trophy"
|
||||
where "Trophy"."id" = "PendingTrophy"."targetTrophyId"
|
||||
and "Trophy"."name" = "PendingTrophy"."name"
|
||||
and "Trophy"."model" = "PendingTrophy"."model"
|
||||
)
|
||||
)
|
||||
)
|
||||
`.execute(trx);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { type Kysely, sql } from "kysely";
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await db.transaction().execute(async (trx) => {
|
||||
await trx.schema
|
||||
.alterTable("TournamentTeamMember")
|
||||
.addColumn("isOrganizerAdded", "integer", (col) =>
|
||||
col.notNull().defaultTo(sql`0`),
|
||||
)
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { Kysely } from "kysely";
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await db.transaction().execute(async (trx) => {
|
||||
await trx.schema
|
||||
.alterTable("User")
|
||||
.addColumn("tournamentName", "text")
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { Kysely } from "kysely";
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await db.transaction().execute(async (trx) => {
|
||||
await trx.schema.alterTable("GroupMember").dropColumn("role").execute();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { type Kysely, sql } from "kysely";
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await db.transaction().execute(async (trx) => {
|
||||
await trx.schema
|
||||
.createTable("GroupSuggestion")
|
||||
.addColumn("suggesterGroupId", "integer", (col) =>
|
||||
col.notNull().references("Group.id").onDelete("cascade"),
|
||||
)
|
||||
.addColumn("targetGroupId", "integer", (col) =>
|
||||
col.notNull().references("Group.id").onDelete("cascade"),
|
||||
)
|
||||
.addColumn("createdByUserId", "integer", (col) =>
|
||||
col.notNull().references("User.id").onDelete("cascade"),
|
||||
)
|
||||
.addColumn("createdAt", "integer", (col) =>
|
||||
col.notNull().defaultTo(sql`(strftime('%s', 'now'))`),
|
||||
)
|
||||
.addUniqueConstraint(
|
||||
"group_suggestion_suggester_group_id_target_group_id",
|
||||
["suggesterGroupId", "targetGroupId"],
|
||||
)
|
||||
// every table in this schema is strict
|
||||
.modifyEnd(sql`strict`)
|
||||
.execute();
|
||||
|
||||
await trx.schema
|
||||
.createIndex("group_suggestion_target_group_id")
|
||||
.on("GroupSuggestion")
|
||||
.column("targetGroupId")
|
||||
.execute();
|
||||
|
||||
await trx.schema
|
||||
.alterTable("GroupLike")
|
||||
.addColumn("createdByUserId", "integer", (col) =>
|
||||
col.references("User.id").onDelete("set null"),
|
||||
)
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
58
apps/web-react/migrations/20260807143400-sq-ready-check.ts
Normal file
58
apps/web-react/migrations/20260807143400-sq-ready-check.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { type Kysely, sql } from "kysely";
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await db.transaction().execute(async (trx) => {
|
||||
await trx.schema
|
||||
.createTable("GroupReadyCheck")
|
||||
.addColumn("id", "integer", (col) => col.primaryKey())
|
||||
.addColumn("alphaGroupId", "integer", (col) =>
|
||||
col.notNull().references("Group.id").onDelete("cascade"),
|
||||
)
|
||||
.addColumn("bravoGroupId", "integer", (col) =>
|
||||
col.notNull().references("Group.id").onDelete("cascade"),
|
||||
)
|
||||
.addColumn("createdAt", "integer", (col) =>
|
||||
col.notNull().defaultTo(sql`(strftime('%s', 'now'))`),
|
||||
)
|
||||
// every table in this schema is strict
|
||||
.modifyEnd(sql`strict`)
|
||||
.execute();
|
||||
|
||||
await trx.schema
|
||||
.createIndex("group_ready_check_alpha_group_id")
|
||||
.on("GroupReadyCheck")
|
||||
.column("alphaGroupId")
|
||||
.unique()
|
||||
.execute();
|
||||
|
||||
await trx.schema
|
||||
.createIndex("group_ready_check_bravo_group_id")
|
||||
.on("GroupReadyCheck")
|
||||
.column("bravoGroupId")
|
||||
.unique()
|
||||
.execute();
|
||||
|
||||
await trx.schema
|
||||
.createTable("GroupReadyCheckConfirmation")
|
||||
.addColumn("readyCheckId", "integer", (col) =>
|
||||
col.notNull().references("GroupReadyCheck.id").onDelete("cascade"),
|
||||
)
|
||||
.addColumn("userId", "integer", (col) =>
|
||||
col.notNull().references("User.id").onDelete("cascade"),
|
||||
)
|
||||
.addColumn("createdAt", "integer", (col) =>
|
||||
col.notNull().defaultTo(sql`(strftime('%s', 'now'))`),
|
||||
)
|
||||
.addUniqueConstraint(
|
||||
"group_ready_check_confirmation_ready_check_id_user_id",
|
||||
["readyCheckId", "userId"],
|
||||
)
|
||||
.modifyEnd(sql`strict`)
|
||||
.execute();
|
||||
|
||||
await trx.schema
|
||||
.alterTable("GroupMember")
|
||||
.addColumn("missedReadyCheckAt", "integer")
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
95
apps/web-react/migrations/20260807143401-ingest.ts
Normal file
95
apps/web-react/migrations/20260807143401-ingest.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { type Kysely, sql } from "kysely";
|
||||
|
||||
/** Tables for scanner-ingested matches and their links to reported game results */
|
||||
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
|
||||
.createTable("IngestedMatch")
|
||||
.addColumn("id", "integer", (col) => col.primaryKey())
|
||||
.addColumn("povUserId", "integer", (col) =>
|
||||
col.references("User.id").onDelete("set null"),
|
||||
)
|
||||
.addColumn("submitterUserId", "integer", (col) =>
|
||||
col.references("User.id").onDelete("set null"),
|
||||
)
|
||||
.addColumn("playedAt", "integer")
|
||||
.addColumn("data", "text", (col) => col.notNull())
|
||||
.addColumn("matchHash", "text", (col) => col.unique().notNull())
|
||||
.addColumn("tournamentIdHint", "integer", (col) =>
|
||||
col.references("Tournament.id").onDelete("set null"),
|
||||
)
|
||||
.addColumn("groupMatchIdHint", "integer", (col) =>
|
||||
col.references("GroupMatch.id").onDelete("set null"),
|
||||
)
|
||||
.addColumn("createdAt", "integer", (col) =>
|
||||
col.notNull().defaultTo(sql`(strftime('%s', 'now'))`),
|
||||
)
|
||||
// every table in this schema is strict
|
||||
.modifyEnd(sql`strict`)
|
||||
.execute();
|
||||
|
||||
await trx.schema
|
||||
.createIndex("ingested_match_pov_user_id_played_at")
|
||||
.on("IngestedMatch")
|
||||
.columns(["povUserId", "playedAt"])
|
||||
.execute();
|
||||
|
||||
await trx.schema
|
||||
.createIndex("ingested_match_tournament_id_hint")
|
||||
.on("IngestedMatch")
|
||||
.column("tournamentIdHint")
|
||||
.execute();
|
||||
|
||||
await trx.schema
|
||||
.createIndex("ingested_match_group_match_id_hint")
|
||||
.on("IngestedMatch")
|
||||
.column("groupMatchIdHint")
|
||||
.execute();
|
||||
|
||||
await trx.schema
|
||||
.createTable("IngestedMatchLink")
|
||||
.addColumn("id", "integer", (col) => col.primaryKey())
|
||||
.addColumn("ingestedMatchId", "integer", (col) =>
|
||||
col
|
||||
.notNull()
|
||||
.unique()
|
||||
.references("IngestedMatch.id")
|
||||
.onDelete("cascade"),
|
||||
)
|
||||
.addColumn("tournamentMatchGameResultId", "integer", (col) =>
|
||||
col.references("TournamentMatchGameResult.id").onDelete("cascade"),
|
||||
)
|
||||
.addColumn("groupMatchMapId", "integer", (col) =>
|
||||
col.references("GroupMatchMap.id").onDelete("cascade"),
|
||||
)
|
||||
.addColumn("createdAt", "integer", (col) =>
|
||||
col.notNull().defaultTo(sql`(strftime('%s', 'now'))`),
|
||||
)
|
||||
.addCheckConstraint(
|
||||
"ingested_match_link_one_target",
|
||||
sql`("tournamentMatchGameResultId" is not null) + ("groupMatchMapId" is not null) = 1`,
|
||||
)
|
||||
.modifyEnd(sql`strict`)
|
||||
.execute();
|
||||
|
||||
await trx.schema
|
||||
.createIndex("ingested_match_link_tournament_match_game_result_id")
|
||||
.on("IngestedMatchLink")
|
||||
.column("tournamentMatchGameResultId")
|
||||
.execute();
|
||||
|
||||
await trx.schema
|
||||
.createIndex("ingested_match_link_group_match_map_id")
|
||||
.on("IngestedMatchLink")
|
||||
.column("groupMatchMapId")
|
||||
.execute();
|
||||
|
||||
// ingest context resolution prunes reported games by a createdAt window
|
||||
await trx.schema
|
||||
.createIndex("tournament_match_game_result_created_at")
|
||||
.on("TournamentMatchGameResult")
|
||||
.column("createdAt")
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { type Kysely, sql } from "kysely";
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await db.transaction().execute(async (trx) => {
|
||||
await trx.schema
|
||||
.alterTable("User")
|
||||
.addColumn("xpDivision", "text")
|
||||
.execute();
|
||||
|
||||
// a self-reported peak XP was the only way to pick a division before, so it carries over
|
||||
await sql`
|
||||
update "User"
|
||||
set "xpDivision" = iif("unverifiedPeakXP" ->> '$.takoroka' is not null, 'JPN', 'WEST')
|
||||
where "unverifiedPeakXP" is not null
|
||||
`.execute(trx);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { type Kysely, sql } from "kysely";
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await db.transaction().execute(async (trx) => {
|
||||
// same browser subscribed multiple times; keep only the latest row per endpoint
|
||||
await sql`
|
||||
delete from "NotificationUserSubscription"
|
||||
where "id" not in (
|
||||
select max("id")
|
||||
from "NotificationUserSubscription"
|
||||
group by json_extract("subscription", '$.endpoint')
|
||||
)
|
||||
`.execute(trx);
|
||||
|
||||
await sql`
|
||||
create unique index "notification_user_subscription_endpoint"
|
||||
on "NotificationUserSubscription" (json_extract("subscription", '$.endpoint'))
|
||||
`.execute(trx);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { type Kysely, sql } from "kysely";
|
||||
|
||||
/** Recently used pick-up rosters that can be reused when making a new scrim post */
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await db.transaction().execute(async (trx) => {
|
||||
await trx.schema
|
||||
.createTable("ScrimPickupRoster")
|
||||
.addColumn("id", "integer", (col) => col.primaryKey())
|
||||
.addColumn("userId", "integer", (col) =>
|
||||
col.notNull().references("User.id").onDelete("cascade"),
|
||||
)
|
||||
.addColumn("usedAt", "integer", (col) =>
|
||||
col.notNull().defaultTo(sql`(strftime('%s', 'now'))`),
|
||||
)
|
||||
// every table in this schema is strict
|
||||
.modifyEnd(sql`strict`)
|
||||
.execute();
|
||||
|
||||
await trx.schema
|
||||
.createIndex("scrim_pickup_roster_user_id_used_at")
|
||||
.on("ScrimPickupRoster")
|
||||
.columns(["userId", "usedAt"])
|
||||
.execute();
|
||||
|
||||
await trx.schema
|
||||
.createTable("ScrimPickupRosterUser")
|
||||
.addColumn("scrimPickupRosterId", "integer", (col) =>
|
||||
col.notNull().references("ScrimPickupRoster.id").onDelete("cascade"),
|
||||
)
|
||||
.addColumn("userId", "integer", (col) =>
|
||||
col.notNull().references("User.id").onDelete("cascade"),
|
||||
)
|
||||
.addPrimaryKeyConstraint("scrim_pickup_roster_user_pk", [
|
||||
"scrimPickupRosterId",
|
||||
"userId",
|
||||
])
|
||||
.modifyEnd(sql`strict`)
|
||||
.execute();
|
||||
|
||||
await trx.schema
|
||||
.createIndex("scrim_pickup_roster_user_user_id")
|
||||
.on("ScrimPickupRosterUser")
|
||||
.column("userId")
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { type Kysely, sql } from "kysely";
|
||||
|
||||
/** Teams that don't count for team leaderboard placements, e.g. because they want to qualify with another roster */
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await db.transaction().execute(async (trx) => {
|
||||
await trx.schema
|
||||
.createTable("LeaderboardTeamSkip")
|
||||
.addColumn("id", "integer", (col) => col.primaryKey())
|
||||
.addColumn("season", "integer", (col) => col.notNull())
|
||||
.addColumn("identifier", "text", (col) => col.notNull())
|
||||
.addColumn("skippedByUserId", "integer", (col) =>
|
||||
col.notNull().references("User.id").onDelete("cascade"),
|
||||
)
|
||||
.addColumn("createdAt", "integer", (col) =>
|
||||
col.notNull().defaultTo(sql`(strftime('%s', 'now'))`),
|
||||
)
|
||||
// every table in this schema is strict
|
||||
.modifyEnd(sql`strict`)
|
||||
.execute();
|
||||
|
||||
await trx.schema
|
||||
.createIndex("leaderboard_team_skip_season_identifier")
|
||||
.on("LeaderboardTeamSkip")
|
||||
.columns(["season", "identifier"])
|
||||
.unique()
|
||||
.execute();
|
||||
|
||||
// the team that the hardcoded list this table replaces held. Attributed to the
|
||||
// admin, who made the call back then. Inserts nothing on databases without them
|
||||
await sql`
|
||||
insert into "LeaderboardTeamSkip" ("season", "identifier", "skippedByUserId")
|
||||
select 5, '9403-13562-15916-38062', "User"."id" from "User" where "User"."id" = 274
|
||||
`.execute(trx);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user