sendou.ink/app/db/tables.ts
Kalle 8dc92140fc
Some checks failed
E2E Tests / e2e (push) Has been cancelled
Tests and checks on push / run-checks-and-tests (push) Has been cancelled
Updates translation progress / update-translation-progress-issue (push) Has been cancelled
Optimize builds loaders (#3076)
2026-05-17 16:21:13 +03:00

1453 lines
38 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type {
ColumnType,
GeneratedAlways,
Insertable,
JSONColumnType,
Selectable,
Updateable,
} from "kysely";
import type { AssociationVisibility } from "~/features/associations/associations-types";
import type { tags } from "~/features/calendar/calendar-constants";
import type { CalendarFilters } from "~/features/calendar/calendar-types";
import type { TieredSkill } from "~/features/mmr/tiered.server";
import type { Notification as NotificationValue } from "~/features/notifications/notifications-types";
import type { ScrimFilters } from "~/features/scrims/scrims-types";
import type { TEAM_MEMBER_ROLES } from "~/features/team/team-constants";
import type { TournamentTierNumber } from "~/features/tournament/core/tiering";
import type * as PickBan from "~/features/tournament-bracket/core/PickBan";
import type * as Progression from "~/features/tournament-bracket/core/Progression";
import type { StoredWidget } from "~/features/user-page/core/widgets/types";
import type { ParticipantResult } from "~/modules/brackets-model";
import type {
Ability,
BuildAbilitiesTuple,
MainWeaponId,
ModeShort,
StageId,
} from "~/modules/in-game-lists/types";
import type { JSONColumnTypeNullable } from "~/utils/kysely.server";
type Generated<T> =
T extends ColumnType<infer S, infer I, infer U>
? ColumnType<S, I | undefined, U>
: ColumnType<T, T | undefined, T>;
export type MemberRole = (typeof TEAM_MEMBER_ROLES)[number];
/** In SQLite booleans are presented as 0 (false) and 1 (true) */
export type DBBoolean = number;
export const CUSTOM_THEME_VARS = [
"--_base-h",
"--_base-c-0",
"--_base-c-1",
"--_base-c-2",
"--_base-c-3",
"--_base-c-4",
"--_base-c-5",
"--_base-c-6",
"--_base-c-7",
"--_acc-h",
"--_acc-c-0",
"--_acc-c-1",
"--_acc-c-2",
"--_acc-c-3",
"--_acc-c-4",
"--_acc-c-5",
"--_second-h",
"--_second-c-0",
"--_second-c-1",
"--_second-c-2",
"--_second-c-3",
"--_second-c-4",
"--_second-c-5",
"--_chat-h",
"--_radius-box",
"--_radius-field",
"--_radius-selector",
"--_border-width",
"--_size-field",
"--_size-selector",
"--_size-spacing",
] as const;
export type CustomThemeVar = (typeof CUSTOM_THEME_VARS)[number];
export type CustomTheme = Omit<Record<CustomThemeVar, number>, "--_chat-h"> & {
"--_chat-h": number | null;
};
export interface Team {
avatarImgId: number | null;
bannerImgId: number | null;
bio: string | null;
createdAt: Generated<number>;
customUrl: string;
customTheme: JSONColumnTypeNullable<CustomTheme>;
deletedAt: number | null;
id: GeneratedAlways<number>;
inviteCode: string;
name: string;
bsky: string | null;
mapModePreferences: JSONColumnTypeNullable<UserMapModePreferences>;
/** Team's tag, typically used in-game in front of users' names to indicate they are a member of the team. */
tag: string | null;
}
export interface TeamMember {
createdAt: Generated<number>;
isOwner: Generated<number>;
isManager: Generated<number>;
leftAt: number | null;
role: MemberRole | null;
teamId: number;
userId: number;
isMainTeam: DBBoolean;
}
export interface Art {
authorId: number;
createdAt: Generated<number>;
description: string | null;
id: GeneratedAlways<number>;
imgId: number;
isShowcase: Generated<DBBoolean>;
}
export interface ArtTag {
authorId: number;
createdAt: Generated<number>;
id: GeneratedAlways<number>;
name: string;
}
export interface ArtUserMetadata {
artId: number;
userId: number;
}
export interface TaggedArt {
artId: number;
tagId: number;
}
export interface Badge {
id: GeneratedAlways<number>;
code: string;
displayName: string;
hue: number | null;
/** Who made the badge? If null, a legacy badge. */
authorId: number | null;
}
export interface BadgeManager {
badgeId: number;
userId: number;
}
export type BadgeOwner = {
badgeId: number;
userId: number;
count: number;
};
export interface Build {
clothesGearSplId: number | null;
description: string | null;
headGearSplId: number | null;
id: GeneratedAlways<number>;
modes: JSONColumnTypeNullable<ModeShort[]>;
ownerId: number;
private: DBBoolean | null;
shoesGearSplId: number | null;
title: string;
updatedAt: Generated<number>;
/** 3x4 ability tuple (head/clothes/shoes × main + 3 subs). */
abilities: JSONColumnTypeNullable<BuildAbilitiesTuple>;
/** Serialized ability+AP combo (e.g. `SSU_30,ISS_10`) used to group identical builds for the popular builds view. */
abilitiesSignature: string | null;
}
export type GearType = "HEAD" | "CLOTHES" | "SHOES";
export interface BuildWeapon {
buildId: number;
weaponSplId: MainWeaponId;
/** Alt skins collapse to their base weapon (e.g. Hero Shot Replica `45` → Splattershot `40`). Indexed for the builds-by-weapon, popular, and stats queries so they can filter `= ?` against a covering index instead of `IN (alt skins…)`. */
canonicalWeaponSplId: MainWeaponId;
/** Mirror of `Build.updatedAt`. Denormalized so the `(canonicalWeaponSplId, sortValue, updatedAt, buildId)` covering index serves the builds-by-weapon list. */
updatedAt: Generated<number>;
/** Per-weapon sort priority: `plusTier * 2 + (this weapon is top500 ? 0 : 1)` for public builds, NULL for private. */
sortValue: number | null;
}
/** Per-build ability point sums across all gear slots. Used to compute global `abilityPointAverages`. */
export interface BuildAbilitySum {
buildId: number;
ability: Ability;
abilityPoints: number;
}
/** Per-weapon, per-build ability point sums. Used to compute per-weapon `abilityPointAverages`. One row per canonical weapon × build × ability with non-zero AP. */
export interface BuildWeaponAbility {
canonicalWeaponSplId: MainWeaponId;
buildId: number;
ability: Ability;
abilityPoints: number;
}
export type CalendarEventTag = keyof typeof tags;
export interface CalendarEvent {
authorId: number;
bracketUrl: string;
description: string | null;
discordInviteCode: string | null;
id: GeneratedAlways<number>;
discordUrl: GeneratedAlways<string | null>;
name: string;
participantCount: number | null;
tags: string | null;
hidden: Generated<DBBoolean>;
tournamentId: number | null;
organizationId: number | null;
avatarImgId: number | null;
}
export interface CalendarEventBadge {
badgeId: number;
eventId: number;
}
export interface CalendarEventDate {
eventId: number;
id: GeneratedAlways<number>;
startTime: number;
}
export interface CalendarEventResultPlayer {
name: string | null;
teamId: number;
userId: number | null;
}
export interface CalendarEventResultTeam {
eventId: number;
id: GeneratedAlways<number>;
name: string;
placement: number;
}
export interface Group {
chatCode: string | null;
createdAt: Generated<number>;
id: GeneratedAlways<number>;
inviteCode: string;
latestActionAt: Generated<number>;
/** If truthy, group was at least partly made in the matchmaking UI (/q/looking) */
matchmade: Generated<DBBoolean>;
status: "PREPARING" | "ACTIVE" | "INACTIVE";
teamId: number | null;
}
export interface GroupLike {
createdAt: Generated<number>;
likerGroupId: number;
targetGroupId: number;
isRechallenge: DBBoolean | null;
}
type CalculatingSkill = {
calculated: false;
matchesCount: number;
matchesCountNeeded: number;
/** Freshly calculated skill */
newSp?: number;
};
export type UserSkillDifference =
| {
calculated: true;
spDiff: number;
oldSp?: number;
newSp?: number;
}
| CalculatingSkill;
export type GroupSkillDifference =
| {
calculated: true;
oldSp: number;
newSp: number;
}
| CalculatingSkill;
export type ParsedMemento = {
users: Record<
number,
{
plusTier?: PlusTier["tier"];
skill?: TieredSkill | "CALCULATING";
skillDifference?: UserSkillDifference;
}
>;
groups: Record<
number,
{
tier?: TieredSkill["tier"];
skillDifference?: GroupSkillDifference;
}
>;
modePreferences?: Partial<
Record<ModeShort, Array<{ userId: number; preference?: Preference }>>
>;
/** mapPreferences of season 2 */
mapPreferences?: Array<{ userId: number; preference?: Preference }[]>;
pools: Array<{
userId: number;
pool: UserMapModePreferences["pool"];
teamName?: string;
}>;
};
export interface GroupMatch {
alphaGroupId: number;
bravoGroupId: number;
chatCode: string | null;
confirmedAt: number | null;
confirmedByUserId: number | null;
createdAt: Generated<number>;
id: GeneratedAlways<number>;
memento: JSONColumnTypeNullable<ParsedMemento>;
cancelRequestedByUserId: number | null;
cancelAcceptedByUserId: number | null;
}
export interface GroupMatchContinueVote {
id: GeneratedAlways<number>;
groupId: number;
userId: number;
isContinuing: DBBoolean;
votedAt: Generated<number>;
}
export interface GroupMatchMap {
id: GeneratedAlways<number>;
index: number;
matchId: number;
mode: ModeShort;
reportedAt: number | null;
reportedByUserId: number | null;
source: string;
stageId: StageId;
winnerGroupId: number | null;
}
export interface GroupMember {
createdAt: Generated<number>;
groupId: number;
note: string | null;
role: "OWNER" | "MANAGER" | "REGULAR";
userId: number;
}
export interface PrivateUserNote {
authorId: number;
targetId: number;
text: string | null;
sentiment: "POSITIVE" | "NEUTRAL" | "NEGATIVE";
updatedAt: Generated<number>;
}
/** Log-in links generated via the Lohi Discord bot commands. */
export interface LogInLink {
code: string;
expiresAt: number;
userId: number;
}
export type LFGType =
| "PLAYER_FOR_TEAM"
| "PLAYER_FOR_COACH"
| "TEAM_FOR_PLAYER"
| "TEAM_FOR_COACH"
| "TEAM_FOR_SCRIM"
| "COACH_FOR_TEAM";
export const LFG_TYPES = [
"PLAYER_FOR_TEAM",
"PLAYER_FOR_COACH",
"TEAM_FOR_PLAYER",
"TEAM_FOR_COACH",
"TEAM_FOR_SCRIM",
"COACH_FOR_TEAM",
] as const;
export interface LFGPost {
id: GeneratedAlways<number>;
type: LFGType;
text: string;
/** e.g. Europe/Helsinki */
timezone: string;
authorId: number;
teamId: number | null;
plusTierVisibility: number | null;
languages: string | null;
updatedAt: Generated<number>;
createdAt: GeneratedAlways<number>;
}
export interface MapPoolMap {
calendarEventId: number | null;
mode: ModeShort;
stageId: StageId;
tieBreakerCalendarEventId: number | null;
tournamentTeamId: number | null;
}
export interface MapResult {
losses: number;
mode: ModeShort;
season: number;
stageId: StageId;
userId: number;
wins: number;
}
export interface PlayerResult {
mapLosses: number;
mapWins: number;
otherUserId: number;
ownerUserId: number;
season: number;
setLosses: number;
setWins: number;
type: string;
}
export interface PlusSuggestion {
authorId: number;
createdAt: GeneratedAlways<number>;
id: GeneratedAlways<number>;
month: number;
suggestedId: number;
text: string;
tier: number;
updatedAt: number | null;
year: number;
}
export interface PlusTier {
tier: number;
userId: number;
}
export interface PlusVote {
authorId: number;
month: number;
score: number;
tier: number;
validAfter: number;
votedId: number;
year: number;
}
export interface PlusVotingResult {
votedId: number;
tier: number;
score: number;
month: number;
year: number;
wasSuggested: DBBoolean;
}
export interface ReportedWeapon {
groupMatchId: number | null;
tournamentMatchId: number | null;
mapIndex: number;
userId: number;
weaponSplId: MainWeaponId;
createdAt: Generated<number>;
}
export interface Skill {
groupMatchId: number | null;
id: GeneratedAlways<number>;
identifier: string | null;
matchesCount: number;
mu: number;
ordinal: number;
sigma: number;
season: number;
tournamentId: number | null;
userId: number | null;
createdAt: number | null;
}
export interface SkillTeamUser {
skillId: number;
userId: number;
}
/** Used for tournament auto-seeding. Calculates off tournament matches same as SP but does not have seasonal resets. */
export interface SeedingSkill {
mu: number;
ordinal: number;
sigma: number;
userId: number;
type: "RANKED" | "UNRANKED";
}
export interface SplatoonPlayer {
id: GeneratedAlways<number>;
splId: string;
userId: number | null;
/** Players best XP across both divisions. Denormalized for performance. */
peakXp: number | null;
}
export interface TaggedArt {
artId: number;
tagId: number;
}
// AUTO = style where teams pick their map pool ahead of time and the map lists are automatically made for each round
// could also have the traditional style where TO picks the maps later
type TournamentMapPickingStyle =
| "TO"
| "AUTO_ALL"
| "AUTO_SZ"
| "AUTO_TC"
| "AUTO_RM"
| "AUTO_CB";
export interface TournamentSettings {
bracketProgression: Progression.ParsedBracket[];
/** @deprecated use bracketProgression instead */
teamsPerGroup?: number;
/** @deprecated use bracketProgression instead */
thirdPlaceMatch?: boolean;
isRanked?: boolean;
enableNoScreenToggle?: boolean;
/** Enable the subs tab, default true */
enableSubs?: boolean;
requireInGameNames?: boolean;
isInvitational?: boolean;
/** Can teams add subs on their own while tournament is in progress? */
autonomousSubs?: boolean;
/** Timestamp (SQLite format) when reg closes, if missing then means closes at start time */
regClosesAt?: number;
/** @deprecated use bracketProgression instead */
swiss?: {
groupCount: number;
roundCount: number;
};
minMembersPerTeam?: number;
/** Maximum number of team members that can be registered (only applies to 4v4 tournaments) */
maxMembersPerTeam?: number;
isTest?: boolean;
isDraft?: boolean;
requireSendouQParticipation?: boolean;
}
export interface CastedMatchesInfo {
/** Array for matches that are locked because they are pending to be casted */
lockedMatches: Array<{ twitchAccount: string; matchId: number }>;
/** What matches are streamed currently & where */
castedMatches: { twitchAccount: string; matchId: number }[];
castedMatchHistory?: Array<{
twitchAccount: string;
matchId: number;
timestamp: number;
}>;
}
export interface Tournament {
settings: JSONColumnType<TournamentSettings>;
id: GeneratedAlways<number>;
mapPickingStyle: TournamentMapPickingStyle;
/** Maps prepared ahead of time for rounds. Follows settings.bracketProgression order. Null in the spot if not defined yet for that bracket. */
preparedMaps: JSONColumnTypeNullable<(PreparedMaps | null)[]>;
castTwitchAccounts: JSONColumnTypeNullable<string[]>;
castedMatchesInfo: JSONColumnTypeNullable<CastedMatchesInfo>;
rules: string | null;
/** Related "parent tournament", the tournament that contains the original sign-ups (for leagues) */
parentTournamentId: number | null;
/** Is the tournament finalized meaning all the matches are played and TO has locked it making it read-only */
isFinalized: Generated<DBBoolean>;
/** Snapshot of teams and rosters when seeds were last saved. Used to detect NEW teams/players. */
seedingSnapshot: JSONColumnTypeNullable<SeedingSnapshot>;
/** Tournament tier based on top teams' skill. 1=X, 2=S+, 3=S, 4=A+, 5=A, 6=B+, 7=B, 8=C+, 9=C */
tier: TournamentTierNumber | null;
vodsLastSyncAt: Generated<number | null>;
/** How many times vods have been synced (automatic process that happens when tournament has concluded). */
vodsSyncCount: Generated<number>;
}
export interface SeedingSnapshot {
savedAt: number;
teams: Array<{
teamId: number;
members: Array<{ userId: number; username: string }>;
}>;
}
export interface PreparedMaps {
authorId: number;
createdAt: number;
maps: Array<TournamentRoundMaps & { roundId: number; groupId: number }>;
eliminationTeamCount?: number;
}
export interface SavedCalendarEvent {
id: GeneratedAlways<number>;
userId: number;
calendarEventId: number;
createdAt: Generated<number>;
}
export interface TournamentBadgeOwner {
badgeId: number;
userId: number;
/** Which tournament the badge is from, if null was added manually by a badge manager as opposed to once a tournament was finalized. */
tournamentId: number | null;
/** How many times this badge was awarded to this user from this source. Tournament rows are always 1; manual grants aggregate repeat awards here. */
count: Generated<number>;
}
/** A group is a logical structure used to group multiple rounds together.
- In round-robin stages, a group is a pool.
- In swiss, a group is also a pool (can have one or multiple groups)
- In elimination stages, a group is a bracket.
- A single elimination stage can have one or two groups:
- The unique bracket.
- If enabled, the Consolation Final.
- A double elimination stage can have two or three groups:
- Upper and lower brackets.
- If enabled, the Grand Final.
*/
export interface TournamentGroup {
id: GeneratedAlways<number>;
number: number;
stageId: number;
}
export const TournamentMatchStatus = {
/** The two matches leading to this one are not completed yet. */
Locked: 0,
/** One participant is ready and waiting for the other one. */
Waiting: 1,
/** Both participants are ready to start. */
Ready: 2,
/** The match is running. */
Running: 3,
/** The match is completed. */
Completed: 4,
};
export interface TournamentMatch {
chatCode: string | null;
groupId: number;
id: GeneratedAlways<number>;
number: number;
opponentOne: JSONColumnType<ParticipantResult>;
opponentTwo: JSONColumnType<ParticipantResult>;
roundId: number;
stageId: number;
status: (typeof TournamentMatchStatus)[keyof typeof TournamentMatchStatus];
// set when match becomes ongoing (both teams ready and no earlier matches for either team)
// for swiss: set at creation time
startedAt: number | null;
}
/** Represents one decision, pick or ban, during tournaments pick/ban (counterpick, ban 2) phase. */
export interface TournamentMatchPickBanEvent {
type: "PICK" | "BAN" | "ROLL" | "MODE_PICK" | "MODE_BAN";
stageId: StageId | null;
mode: ModeShort | null;
matchId: number;
authorId: number | null;
number: number;
createdAt: GeneratedAlways<number>;
}
export interface TournamentMatchGameResult {
createdAt: Generated<number>;
id: GeneratedAlways<number>;
matchId: number;
mode: ModeShort;
number: number;
reporterId: number;
source: string;
stageId: StageId;
winnerTeamId: number;
opponentOnePoints: number | null;
opponentTwoPoints: number | null;
}
export interface TournamentMatchGameResultParticipant {
matchGameResultId: number;
userId: number;
tournamentTeamId: number;
}
export type WinLossParticipationArray = Array<"W" | "L" | null>;
export interface TournamentResult {
isHighlight: Generated<DBBoolean>;
participantCount: number;
placement: number;
tournamentId: number;
tournamentTeamId: number;
/**
* The result of sets in the tournament.
* E.g. ["W", "L", null] would mean the user won the first set, lost the second and did not play the third.
* */
setResults: JSONColumnType<WinLossParticipationArray>;
/** The SP change in total after the finalization of a ranked tournament. */
spDiff: number | null;
userId: number;
/** Division label for tournaments with multiple starting brackets (e.g., "D1", "D2") */
div: string | null;
}
export interface TournamentRoundMaps {
list?: Array<{ mode: ModeShort; stageId: StageId }> | null;
count: number;
type: "BEST_OF" | "PLAY_ALL";
pickBan?: PickBan.Type | null;
customFlow?: CustomPickBanFlow | null;
}
export const WHO_SIDES = [
"ALPHA",
"BRAVO",
"HIGHER_SEED",
"LOWER_SEED",
"WINNER",
"LOSER",
] as const;
export type WhoSide = (typeof WHO_SIDES)[number];
export const ACTION_TYPES = [
"ROLL",
"PICK",
"BAN",
"MODE_PICK",
"MODE_BAN",
] as const;
export type ActionType = (typeof ACTION_TYPES)[number];
export interface CustomPickBanStep {
action: ActionType;
side?: WhoSide;
}
export interface CustomPickBanFlow {
preSet: CustomPickBanStep[];
postGame: CustomPickBanStep[];
}
/**
* A round is a logical structure used to group multiple matches together.
- In round-robin stages, a round can be viewed as a list of matches that can be played at the same time.
- In swiss, a round is a list of matches that are played at the same time.
- In elimination stages, a round is a round of a bracket, e.g. 8th finals, semi-finals, etc.
*/
export interface TournamentRound {
groupId: number;
id: GeneratedAlways<number>;
number: number;
stageId: number;
maps: JSONColumnType<TournamentRoundMaps>;
}
// when updating this also update `defaultBracketSettings` in tournament-utils.ts
export interface TournamentStageSettings {
// SE
thirdPlaceMatch?: boolean;
// RR
teamsPerGroup?: number;
/** (RR only) When true, teams are split into A and B divisions and matches only pair A-vs-B. Only valid on starting brackets. */
hasAbDivisions?: boolean;
// SWISS
groupCount?: number;
// SWISS
roundCount?: number;
/** (Swiss only) Number of wins required for a team to advance early. When set, teams advance at this win count and are eliminated at (roundCount - advanceThreshold + 1) losses. */
advanceThreshold?: number;
}
export const TOURNAMENT_STAGE_TYPES = [
"single_elimination",
"double_elimination",
"round_robin",
"swiss",
] as const;
/** A stage is an intermediate phase in a tournament. In essence a bracket. */
export interface TournamentStage {
id: GeneratedAlways<number>;
name: string;
number: number;
settings: string;
tournamentId: number;
type: (typeof TOURNAMENT_STAGE_TYPES)[number];
// not Generated<> because SQLite doesn't allow altering tables to add columns with default values :(
createdAt: number | null;
}
/** Tournament sub post, shown in a list of subs available for teams to pick from. */
export interface TournamentSub {
bestWeapons: string;
/** 0 = no, 1 = yes, 2 = listen only */
canVc: number;
createdAt: Generated<number>;
message: string | null;
okWeapons: string | null;
tournamentId: number;
userId: number;
visibility: "+1" | "+2" | "+3" | "ALL";
}
export interface TournamentLFGLike {
likerTeamId: number;
targetTeamId: number;
createdAt: Generated<number>;
}
export interface TournamentStaff {
tournamentId: number;
userId: number;
role: "ORGANIZER" | "STREAMER";
}
export interface TournamentTeam {
createdAt: Generated<number>;
id: GeneratedAlways<number>;
inviteCode: string;
name: string;
prefersNotToHost: Generated<DBBoolean>;
droppedOut: Generated<DBBoolean>;
seed: number | null;
/** For formats that have many starting brackets, where should the team start? */
startingBracketIdx: number | null;
activeRosterUserIds: JSONColumnTypeNullable<number[]>;
tournamentId: number;
teamId: number | null;
avatarImgId: number | null;
isLooking: Generated<DBBoolean>;
isPlaceholder: Generated<DBBoolean>;
lfgNote: string | null;
chatCode: Generated<string | null>;
/** A/B division assignment for bipartite round robin brackets. `0` = A, `1` = B, `null` = unassigned. */
abDivision: number | null;
}
export interface TournamentTeamCheckIn {
checkedInAt: number;
/** Which bracket checked in for. If missing is check in for the whole event. */
bracketIdx: number | null;
tournamentTeamId: number;
/** Indicates that this bracket defaults to checked in and this team has been explicitly checked out from it */
isCheckOut: Generated<number>;
}
export interface TournamentTeamMember {
createdAt: Generated<number>;
inGameName: string | null;
tournamentTeamId: number;
userId: number;
role: Generated<"OWNER" | "MANAGER" | "REGULAR">;
isStayAsSub: Generated<DBBoolean>;
// denormalized from TournamentTeam.isLooking
isLooking: Generated<DBBoolean>;
}
export interface TournamentOrganization {
id: GeneratedAlways<number>;
name: string;
slug: string;
description: string | null;
socials: JSONColumnTypeNullable<string[]>;
avatarImgId: number | null;
isEstablished: Generated<DBBoolean>;
}
export const TOURNAMENT_ORGANIZATION_ROLES = [
"ADMIN",
"MEMBER",
"ORGANIZER",
"STREAMER",
] as const;
type TournamentOrganizationRole =
(typeof TOURNAMENT_ORGANIZATION_ROLES)[number];
export interface TournamentOrganizationMember {
organizationId: number;
userId: number;
role: TournamentOrganizationRole;
roleDisplayName: string | null;
}
export interface TournamentOrganizationBadge {
organizationId: number;
badgeId: number;
}
export interface TournamentOrganizationSeries {
id: GeneratedAlways<number>;
organizationId: number;
name: string;
description: string | null;
substringMatches: JSONColumnType<string[]>;
showLeaderboard: Generated<number>;
tierHistory: JSONColumnTypeNullable<TournamentTierNumber[]>;
}
export interface TournamentBracketProgressionOverride {
sourceBracketIdx: number;
destinationBracketIdx: number;
tournamentTeamId: number;
tournamentId: number;
}
export interface TournamentOrganizationBannedUser {
organizationId: number;
userId: number;
privateNote: string | null;
updatedAt: Generated<number>;
expiresAt: number | null;
}
/** Indicates a user trusts another. Allows direct adding to groups/teams without invite links. */
export interface TrustRelationship {
trustGiverUserId: number;
trustReceiverUserId: number;
lastUsedAt: number;
}
/** Mutual friendship between two users. Invariant: userOneId < userTwoId. */
export interface Friendship {
id: GeneratedAlways<number>;
userOneId: number;
userTwoId: number;
createdAt: Generated<number>;
}
/** Pending friend request from one user to another. */
export interface FriendRequest {
id: GeneratedAlways<number>;
senderId: number;
receiverId: number;
createdAt: Generated<number>;
}
export interface UnvalidatedUserSubmittedImage {
id: GeneratedAlways<number>;
submitterUserId: number;
url: string;
/** When was the image validated? If `null` should be hidden from other users. */
validatedAt: number | null;
}
export interface UnvalidatedVideo {
eventId: number | null;
id: GeneratedAlways<number>;
submitterUserId: number;
title: string;
type: string;
validatedAt: number | null;
youtubeDate: number;
youtubeId: string;
}
// missing means "neutral"
export type Preference = "AVOID" | "PREFER";
export interface UserMapModePreferences {
modes: Array<{
mode: ModeShort;
/** Users opinion on the mode, `undefined` means neutral */
preference?: Preference;
}>;
pool: Array<{
mode: ModeShort;
stages: StageId[];
}>;
}
export interface QWeaponPool {
weaponSplId: MainWeaponId;
isFavorite: number;
}
export const BUILD_SORT_IDENTIFIERS = [
"UPDATED_AT",
"TOP_500",
"WEAPON_POOL",
"WEAPON_IN_GAME_ORDER",
"ALPHABETICAL_TITLE",
"MODE",
"HEADGEAR_ID",
"CLOTHES_ID",
"SHOES_ID",
"PUBLIC_BUILD",
"PRIVATE_BUILD",
] as const;
export type BuildSort = (typeof BUILD_SORT_IDENTIFIERS)[number];
export interface UserPreferences {
disableBuildAbilitySorting?: boolean;
disallowScrimPickupsFromUntrusted?: boolean;
defaultCalendarFilters?: CalendarFilters;
defaultScrimsFilters?: ScrimFilters;
/**
* What time format the user prefers?
*
* "auto" = use browser default (default value)
* "24h" = 24 hour format (e.g. 14:00)
* "12h" = 12 hour format (e.g. 2:00 PM)
* */
clockFormat?: "24h" | "12h" | "auto";
/** Is the new widget based user page enabled? (Supporter early preview) */
newProfileEnabled?: boolean;
/** Is spoiler-free mode enabled? Hides recent tournament results and scores until the user chooses to reveal them. */
spoilerFreeMode?: boolean;
weaponReportDefaultOpen?: boolean;
}
export const SUBJECT_PRONOUNS = ["he", "she", "they", "it", "any"] as const;
export const OBJECT_PRONOUNS = [
"him",
"her",
"them",
"its",
"all",
...SUBJECT_PRONOUNS,
] as const;
export type Pronouns = {
subject: (typeof SUBJECT_PRONOUNS)[number];
object: (typeof OBJECT_PRONOUNS)[number];
};
export interface User {
/** 1 = permabanned, timestamp = ban active till then */
banned: Generated<number | null>;
bannedReason: string | null;
bio: string | null;
commissionsOpen: Generated<number | null>;
commissionsOpenedAt: number | null;
commissionText: string | null;
country: string | null;
customTheme: JSONColumnTypeNullable<CustomTheme>;
customUrl: string | null;
discordAvatar: string | null;
discordId: string;
discordName: string;
customName: string | null;
/** coalesce(customName, discordName) */
username: ColumnType<string, never, never>;
discordUniqueName: string | null;
/** User's favorite badges they want to show on the front page of the badge display. Index = 0 big badge. */
favoriteBadgeIds: ColumnType<number[] | null, string | null, string | null>;
id: GeneratedAlways<number>;
inGameName: string | null;
isArtist: Generated<DBBoolean | null>;
isVideoAdder: Generated<DBBoolean | null>;
isTournamentOrganizer: Generated<DBBoolean | null>;
isApiAccesser: Generated<DBBoolean | null>;
languages: string | null;
motionSens: number | null;
pronouns: JSONColumnTypeNullable<Pronouns>;
patronSince: number | null;
patronTier: number | null;
patronTill: number | null;
showDiscordUniqueName: Generated<DBBoolean>;
stickSens: number | null;
twitch: string | null;
bsky: string | null;
battlefy: string | null;
vc: Generated<"YES" | "NO" | "LISTEN_ONLY">;
youtubeId: string | null;
mapModePreferences: JSONColumnTypeNullable<UserMapModePreferences>;
qWeaponPool: JSONColumnTypeNullable<QWeaponPool[]>;
plusSkippedForSeasonNth: number | null;
noScreen: Generated<DBBoolean>;
/** User doesn't have access to SplatNet 3 to join rooms made by others */
noSplatnet: Generated<DBBoolean>;
buildSorting: JSONColumnTypeNullable<BuildSort[]>;
preferences: JSONColumnTypeNullable<UserPreferences>;
/** User creation date. Can be null because we did not always save this. */
createdAt: number | null;
joinOrder: number | null;
/** Last message used when creating a tournament sub post */
lastSubMessage: string | null;
}
/** Represents User joined with PlusTier table */
export type UserWithPlusTier = Tables["User"] & {
plusTier: PlusTier["tier"] | null;
};
export interface UserResultHighlight {
teamId: number;
userId: number;
}
export interface UserSubmittedImage {
id: GeneratedAlways<number>;
submitterUserId: number | null;
url: string;
validatedAt: number | null;
}
export interface UserWeapon {
createdAt: Generated<number>;
isFavorite: Generated<DBBoolean>;
order: number;
userId: number;
weaponSplId: MainWeaponId;
}
export interface UserFriendCode {
friendCode: string;
userId: number;
submitterUserId: number;
createdAt: GeneratedAlways<number>;
}
export interface UserWidget {
userId: number;
index: number;
widget: JSONColumnType<StoredWidget>;
}
export type ApiTokenType = "read" | "write";
export interface ApiToken {
id: GeneratedAlways<number>;
userId: number;
token: string;
type: Generated<ApiTokenType>;
createdAt: GeneratedAlways<number>;
}
export interface LiveStream {
id: GeneratedAlways<number>;
userId: number | null;
viewerCount: number;
thumbnailUrl: string;
twitch: string | null;
}
export interface TournamentStreamer {
id: GeneratedAlways<number>;
userId: number | null;
tournamentId: number;
twitchAccount: string;
}
export interface TournamentMatchVod {
id: GeneratedAlways<number>;
matchId: number;
userId: number | null;
platform: string;
account: string;
platformVideoId: string;
timestampSeconds: number;
viewCount: number;
}
export interface BanLog {
id: GeneratedAlways<number>;
userId: number;
banned: number | null;
bannedReason: string | null;
bannedByUserId: number;
createdAt: GeneratedAlways<number>;
}
export interface ModNote {
id: GeneratedAlways<number>;
userId: number;
authorId: number;
text: string;
createdAt: GeneratedAlways<number>;
isDeleted: Generated<DBBoolean>;
}
export interface Video {
eventId: number | null;
id: GeneratedAlways<number>;
submitterUserId: number;
title: string;
type: "SCRIM" | "TOURNAMENT" | "MATCHMAKING" | "CAST" | "SENDOUQ";
validatedAt: number | null;
youtubeDate: number;
youtubeId: string;
}
export interface VideoMatch {
id: GeneratedAlways<number>;
mode: ModeShort;
stageId: StageId;
startsAt: number;
videoId: number;
}
export interface VideoMatchPlayer {
player: number;
playerName: string | null;
playerUserId: number | null;
videoMatchId: number;
weaponSplId: number;
}
export interface XRankPlacement {
badges: string;
bannerSplId: number;
id: GeneratedAlways<number>;
mode: ModeShort;
month: number;
name: string;
nameDiscriminator: string;
playerId: number;
power: number;
rank: number;
region: "WEST" | "JPN";
title: string;
weaponSplId: MainWeaponId;
year: number;
}
export interface ScrimPost {
id: GeneratedAlways<number>;
/** When is the scrim scheduled to happen */
at: number;
/** Optional end of time range indicating team accepts scrims starting between at and rangeEnd */
rangeEnd: number | null;
/** Highest LUTI div accepted */
maxDiv: number | null;
/** Lowest LUTI div accepted */
minDiv: number | null;
/** Who sees the post */
visibility: JSONColumnTypeNullable<AssociationVisibility>;
/** Any additional info */
text: string | null;
/** The key to access the scrim chat, used after scrim is scheduled with another team */
chatCode: string;
/** Refers to the team looking for the team (can also be a pick-up) */
teamId: number | null;
/** Indicates if anyone in the post can manage it */
managedByAnyone: DBBoolean;
/** When the scrim was canceled */
canceledAt: number | null;
/** User id who canceled the scrim */
canceledByUserId: number | null;
/** Reason for canceling the scrim */
cancelReason: string | null;
/** When the post was made was it scheduled for a future time slot (as opposed to looking now) */
isScheduledForFuture: Generated<DBBoolean>;
/** Maps/modes the scrim is available for. If null means no preference unless "mapsTournamentId" is set */
maps: "SZ" | "ALL" | "RANKED" | null;
/** If set, specifies the maps of a tournament to play */
mapsTournamentId: number | null;
createdAt: GeneratedAlways<number>;
updatedAt: Generated<number>;
}
export interface ScrimPostUser {
scrimPostId: number;
userId: number;
/** User is the author of the post */
isOwner: number;
}
export interface ScrimPostRequest {
id: GeneratedAlways<number>;
scrimPostId: number;
teamId: number | null;
message: string | null;
/** Specific time selected by requester (required when post has rangeEnd) */
at: number | null;
isAccepted: Generated<DBBoolean>;
createdAt: GeneratedAlways<number>;
}
export interface ScrimPostRequestUser {
scrimPostRequestId: number;
/** User that made the request */
userId: number;
isOwner: DBBoolean;
}
export interface Association {
id: GeneratedAlways<number>;
name: string;
inviteCode: string;
createdAt: GeneratedAlways<number>;
}
export interface AssociationMember {
userId: number;
associationId: number;
role: "MEMBER" | "ADMIN";
}
export interface Notification {
id: GeneratedAlways<number>;
type: NotificationValue["type"];
meta: JSONColumnTypeNullable<Record<string, number | string>>;
pictureUrl: string | null;
createdAt: GeneratedAlways<number>;
}
export interface NotificationUser {
notificationId: number;
userId: number;
seen: Generated<DBBoolean>;
}
export interface NotificationSubscription {
endpoint: string;
keys: {
auth: string;
p256dh: string;
};
}
/** A subscription of user's browser indicating where push notifications can be sent to. */
export interface NotificationUserSubscription {
id: GeneratedAlways<number>;
userId: number;
subscription: JSONColumnType<NotificationSubscription>;
}
export interface RoomLink {
userId: number;
url: string;
createdAt: Generated<number>;
refreshedAt: Generated<number>;
}
export const SPLATOON_ROTATION_TYPES = ["SERIES", "OPEN", "X"] as const;
export type SplatoonRotationType = (typeof SPLATOON_ROTATION_TYPES)[number];
export interface SplatoonRotation {
id: GeneratedAlways<number>;
type: SplatoonRotationType;
mode: string;
stageId1: number;
stageId2: number;
startTime: number;
endTime: number;
}
export type Tables = { [P in keyof DB]: Selectable<DB[P]> };
export type TablesInsertable = { [P in keyof DB]: Insertable<DB[P]> };
export type TablesUpdatable = { [P in keyof DB]: Updateable<DB[P]> };
export interface DB {
AllTeam: Team;
AllTeamMember: TeamMember;
ApiToken: ApiToken;
Art: Art;
LiveStream: LiveStream;
ArtTag: ArtTag;
ArtUserMetadata: ArtUserMetadata;
TaggedArt: TaggedArt;
Badge: Badge;
BadgeManager: BadgeManager;
BadgeOwner: BadgeOwner;
TournamentBadgeOwner: TournamentBadgeOwner;
BanLog: BanLog;
ModNote: ModNote;
Build: Build;
BuildAbilitySum: BuildAbilitySum;
BuildWeapon: BuildWeapon;
BuildWeaponAbility: BuildWeaponAbility;
CalendarEvent: CalendarEvent;
CalendarEventBadge: CalendarEventBadge;
CalendarEventDate: CalendarEventDate;
CalendarEventResultPlayer: CalendarEventResultPlayer;
CalendarEventResultTeam: CalendarEventResultTeam;
Group: Group;
GroupLike: GroupLike;
GroupMatch: GroupMatch;
GroupMatchContinueVote: GroupMatchContinueVote;
GroupMatchMap: GroupMatchMap;
GroupMember: GroupMember;
PrivateUserNote: PrivateUserNote;
LogInLink: LogInLink;
LFGPost: LFGPost;
MapPoolMap: MapPoolMap;
MapResult: MapResult;
PlayerResult: PlayerResult;
PlusSuggestion: PlusSuggestion;
PlusTier: PlusTier;
PlusVote: PlusVote;
PlusVotingResult: PlusVotingResult;
RoomLink: RoomLink;
ReportedWeapon: ReportedWeapon;
Skill: Skill;
SkillTeamUser: SkillTeamUser;
SeedingSkill: SeedingSkill;
SplatoonPlayer: SplatoonPlayer;
Team: Team;
TeamMember: TeamMember;
TeamMemberWithSecondary: TeamMember;
Tournament: Tournament;
TournamentStaff: TournamentStaff;
TournamentGroup: TournamentGroup;
TournamentLFGLike: TournamentLFGLike;
TournamentMatch: TournamentMatch;
TournamentMatchPickBanEvent: TournamentMatchPickBanEvent;
TournamentMatchGameResult: TournamentMatchGameResult;
TournamentMatchGameResultParticipant: TournamentMatchGameResultParticipant;
TournamentResult: TournamentResult;
TournamentRound: TournamentRound;
TournamentStage: TournamentStage;
TournamentSub: TournamentSub;
TournamentTeam: TournamentTeam;
TournamentTeamCheckIn: TournamentTeamCheckIn;
TournamentTeamMember: TournamentTeamMember;
TournamentOrganization: TournamentOrganization;
TournamentOrganizationMember: TournamentOrganizationMember;
TournamentOrganizationBadge: TournamentOrganizationBadge;
TournamentOrganizationSeries: TournamentOrganizationSeries;
TournamentBracketProgressionOverride: TournamentBracketProgressionOverride;
TournamentOrganizationBannedUser: TournamentOrganizationBannedUser;
TournamentStreamer: TournamentStreamer;
TournamentMatchVod: TournamentMatchVod;
TrustRelationship: TrustRelationship;
Friendship: Friendship;
FriendRequest: FriendRequest;
UnvalidatedUserSubmittedImage: UnvalidatedUserSubmittedImage;
UnvalidatedVideo: UnvalidatedVideo;
User: User;
UserResultHighlight: UserResultHighlight;
UserSubmittedImage: UserSubmittedImage;
UserWeapon: UserWeapon;
UserFriendCode: UserFriendCode;
UserWidget: UserWidget;
Video: Video;
VideoMatch: VideoMatch;
VideoMatchPlayer: VideoMatchPlayer;
XRankPlacement: XRankPlacement;
ScrimPost: ScrimPost;
ScrimPostUser: ScrimPostUser;
ScrimPostRequest: ScrimPostRequest;
ScrimPostRequestUser: ScrimPostRequestUser;
Association: Association;
AssociationMember: AssociationMember;
Notification: Notification;
NotificationUser: NotificationUser;
NotificationUserSubscription: NotificationUserSubscription;
SavedCalendarEvent: SavedCalendarEvent;
SplatoonRotation: SplatoonRotation;
}