250 lines
9.8 KiB
TypeScript
250 lines
9.8 KiB
TypeScript
// src/lib/server/db/schema.ts
|
|
//
|
|
// CHANGE: Add `role` column to user table in auth.schema.ts (see that file).
|
|
// This file adds the permission helper utilities.
|
|
|
|
import {
|
|
pgTable,
|
|
integer,
|
|
text,
|
|
uuid,
|
|
boolean,
|
|
timestamp,
|
|
jsonb,
|
|
index,
|
|
uniqueIndex,
|
|
real
|
|
} from 'drizzle-orm/pg-core';
|
|
import { relations } from 'drizzle-orm';
|
|
|
|
export * from './auth.schema';
|
|
|
|
// ── Teams ───────────────────────────────────────────────────────────────
|
|
export const team = pgTable('team', {
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
name: text('name').notNull(),
|
|
isPersonal: boolean('is_personal').default(false).notNull(),
|
|
ownerId: text('owner_id'),
|
|
avatarUrl: text('avatar_url'),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull()
|
|
});
|
|
|
|
// ── Team Members ────────────────────────────────────────────────────────
|
|
export const teamMember = pgTable('team_member', {
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
teamId: uuid('team_id')
|
|
.notNull()
|
|
.references(() => team.id, { onDelete: 'cascade' }),
|
|
userId: text('user_id').notNull(),
|
|
role: text('role').default('member'),
|
|
joinedAt: timestamp('joined_at', { withTimezone: true }).defaultNow()
|
|
});
|
|
|
|
// ── Mappools ────────────────────────────────────────────────────────────
|
|
export const mappool = pgTable('mappool', {
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
name: text('name').notNull(),
|
|
createdBy: text('created_by'),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull()
|
|
});
|
|
|
|
// ── Mappool Slots ───────────────────────────────────────────────────────
|
|
export const mappoolSlot = pgTable('mappool_slot', {
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
mappoolId: uuid('mappool_id')
|
|
.notNull()
|
|
.references(() => mappool.id, { onDelete: 'cascade' }),
|
|
category: text('category').notNull(),
|
|
orderInCategory: integer('order_in_category').notNull(),
|
|
beatmapId: text('beatmap_id').notNull(),
|
|
starRating: real('star_rating'),
|
|
mods: text('mods').array().default([]).notNull()
|
|
});
|
|
|
|
// ── Matches ─────────────────────────────────────────────────────────────
|
|
export const match = pgTable(
|
|
'match',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
name: text('name'),
|
|
state: text('state').default('CREATED').notNull(),
|
|
config: jsonb('config').notNull(),
|
|
mappoolId: uuid('mappool_id').references(() => mappool.id),
|
|
osuLobbyId: integer('osu_lobby_id'),
|
|
scheduledAt: timestamp('scheduled_at', { withTimezone: true }),
|
|
startedAt: timestamp('started_at', { withTimezone: true }),
|
|
finishedAt: timestamp('finished_at', { withTimezone: true }),
|
|
winnerId: uuid('winner_id').references(() => team.id),
|
|
createdBy: text('created_by'),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull()
|
|
},
|
|
(t) => [index('idx_match_state').on(t.state)]
|
|
);
|
|
|
|
// ── Match Participants (a team in a match) ──────────────────────────────
|
|
export const matchParticipant = pgTable(
|
|
'match_participant',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
matchId: uuid('match_id')
|
|
.notNull()
|
|
.references(() => match.id, { onDelete: 'cascade' }),
|
|
teamId: uuid('team_id')
|
|
.notNull()
|
|
.references(() => team.id),
|
|
slot: integer('slot').notNull(),
|
|
score: integer('score').default(0).notNull(),
|
|
rollValue: integer('roll_value'),
|
|
pickOrder: integer('pick_order')
|
|
},
|
|
(t) => [
|
|
index('idx_mp_match_team').on(t.matchId, t.teamId),
|
|
uniqueIndex('idx_mp_match_slot').on(t.matchId, t.slot),
|
|
index('idx_mp_match').on(t.matchId)
|
|
]
|
|
);
|
|
|
|
// ── Match Participant Players (a player fielded for this match) ─────────
|
|
export const matchParticipantPlayer = pgTable(
|
|
'match_participant_player',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
participantId: uuid('participant_id')
|
|
.notNull()
|
|
.references(() => matchParticipant.id, { onDelete: 'cascade' }),
|
|
userId: text('user_id').notNull()
|
|
},
|
|
(t) => [
|
|
uniqueIndex('idx_mpp_participant_user').on(t.participantId, t.userId),
|
|
index('idx_mpp_participant').on(t.participantId)
|
|
]
|
|
);
|
|
|
|
// ── Match Games ─────────────────────────────────────────────────────────
|
|
export const matchGame = pgTable(
|
|
'match_game',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
matchId: uuid('match_id')
|
|
.notNull()
|
|
.references(() => match.id, { onDelete: 'cascade' }),
|
|
gameNumber: integer('game_number').notNull(),
|
|
mappoolSlotId: uuid('mappool_slot_id')
|
|
.notNull()
|
|
.references(() => mappoolSlot.id),
|
|
pickedByParticipantId: uuid('picked_by_participant_id').references(() => matchParticipant.id),
|
|
winnerParticipantId: uuid('winner_participant_id').references(() => matchParticipant.id),
|
|
state: text('state').default('PENDING').notNull(),
|
|
startedAt: timestamp('started_at', { withTimezone: true }),
|
|
finishedAt: timestamp('finished_at', { withTimezone: true })
|
|
},
|
|
(t) => [index('idx_match_game_match').on(t.matchId)]
|
|
);
|
|
|
|
// ── Match Game Scores (per-player per-map) ──────────────────────────────
|
|
export const matchGameScore = pgTable(
|
|
'match_game_score',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
matchGameId: uuid('match_game_id')
|
|
.notNull()
|
|
.references(() => matchGame.id, { onDelete: 'cascade' }),
|
|
playerId: uuid('player_id')
|
|
.notNull()
|
|
.references(() => matchParticipantPlayer.id),
|
|
score: integer('score').default(0).notNull(),
|
|
accuracy: real('accuracy').default(0).notNull(),
|
|
maxCombo: integer('max_combo').default(0).notNull(),
|
|
count300: integer('count_300').default(0).notNull(),
|
|
count100: integer('count_100').default(0).notNull(),
|
|
count50: integer('count_50').default(0).notNull(),
|
|
countMiss: integer('count_miss').default(0).notNull(),
|
|
mods: text('mods').array().default([]).notNull(),
|
|
passed: boolean('passed').default(false).notNull()
|
|
},
|
|
(t) => [index('idx_mgs_game').on(t.matchGameId)]
|
|
);
|
|
|
|
// ── Match Queue ─────────────────────────────────────────────────────────
|
|
export const matchQueue = pgTable(
|
|
'match_queue',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
userId: text('user_id').notNull(),
|
|
teamId: uuid('team_id')
|
|
.notNull()
|
|
.references(() => team.id),
|
|
elo: integer('elo').default(1000).notNull(),
|
|
joinedAt: timestamp('joined_at', { withTimezone: true }).defaultNow().notNull()
|
|
},
|
|
(t) => [
|
|
uniqueIndex('idx_queue_user').on(t.userId),
|
|
index('idx_queue_elo').on(t.elo)
|
|
]
|
|
);
|
|
|
|
// ── Player ELO ──────────────────────────────────────────────────────────
|
|
export const playerRating = pgTable('player_rating', {
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
userId: text('user_id').notNull().unique(),
|
|
elo: integer('elo').default(1000).notNull(),
|
|
wins: integer('wins').default(0).notNull(),
|
|
losses: integer('losses').default(0).notNull(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull()
|
|
});
|
|
|
|
// ── Relations ───────────────────────────────────────────────────────────
|
|
|
|
export const teamRelations = relations(team, ({ many }) => ({
|
|
members: many(teamMember)
|
|
}));
|
|
|
|
export const teamMemberRelations = relations(teamMember, ({ one }) => ({
|
|
team: one(team, { fields: [teamMember.teamId], references: [team.id] })
|
|
}));
|
|
|
|
export const mappoolRelations = relations(mappool, ({ many }) => ({
|
|
slots: many(mappoolSlot)
|
|
}));
|
|
|
|
export const mappoolSlotRelations = relations(mappoolSlot, ({ one }) => ({
|
|
mappool: one(mappool, { fields: [mappoolSlot.mappoolId], references: [mappool.id] })
|
|
}));
|
|
|
|
export const matchRelations = relations(match, ({ one, many }) => ({
|
|
mappool: one(mappool, { fields: [match.mappoolId], references: [mappool.id] }),
|
|
participants: many(matchParticipant),
|
|
games: many(matchGame)
|
|
}));
|
|
|
|
export const matchParticipantRelations = relations(matchParticipant, ({ one, many }) => ({
|
|
match: one(match, { fields: [matchParticipant.matchId], references: [match.id] }),
|
|
team: one(team, { fields: [matchParticipant.teamId], references: [team.id] }),
|
|
players: many(matchParticipantPlayer)
|
|
}));
|
|
|
|
export const matchParticipantPlayerRelations = relations(matchParticipantPlayer, ({ one }) => ({
|
|
participant: one(matchParticipant, {
|
|
fields: [matchParticipantPlayer.participantId],
|
|
references: [matchParticipant.id]
|
|
})
|
|
}));
|
|
|
|
export const matchGameRelations = relations(matchGame, ({ one, many }) => ({
|
|
match: one(match, { fields: [matchGame.matchId], references: [match.id] }),
|
|
slot: one(mappoolSlot, { fields: [matchGame.mappoolSlotId], references: [mappoolSlot.id] }),
|
|
scores: many(matchGameScore)
|
|
}));
|
|
|
|
export const matchGameScoreRelations = relations(matchGameScore, ({ one }) => ({
|
|
game: one(matchGame, { fields: [matchGameScore.matchGameId], references: [matchGame.id] }),
|
|
player: one(matchParticipantPlayer, {
|
|
fields: [matchGameScore.playerId],
|
|
references: [matchParticipantPlayer.id]
|
|
})
|
|
}));
|
|
|
|
export const matchQueueRelations = relations(matchQueue, ({ one }) => ({
|
|
team: one(team, { fields: [matchQueue.teamId], references: [team.id] })
|
|
}));
|