Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import tseslint from "typescript-eslint";

export default tseslint.config(
{
ignores: ["node_modules", "dist", "./eslint.config.mjs"],
ignores: ["node_modules", "dist", "__mocks__", "./eslint.config.mjs"],
},
eslint.configs.recommended,
tseslint.configs.recommended,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE `Season` ADD COLUMN `type` ENUM('RANKED', 'PLAY_OFF', 'TOURNEY', 'CASUAL') NOT NULL DEFAULT 'RANKED';
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
-- Collect every game type used by each legacy season across both variants.
CREATE TEMPORARY TABLE `_SeasonGameType` (
`seasonId` VARCHAR(191) NOT NULL,
`type` ENUM('RANKED', 'PLAY_OFF', 'TOURNEY', 'CASUAL') NOT NULL,
PRIMARY KEY (`seasonId`, `type`)
);

INSERT IGNORE INTO `_SeasonGameType` (`seasonId`, `type`)
SELECT `seasonId`, `type` FROM `JapaneseGame`;

INSERT IGNORE INTO `_SeasonGameType` (`seasonId`, `type`)
SELECT `seasonId`, `type` FROM `HongKongGame`;

-- Prefer to keep ranked games attached to the original season. For seasons
-- without ranked games, keep one of their existing types on the original row.
CREATE TEMPORARY TABLE `_SeasonPrimaryType` AS
SELECT
`seasonId`,
CASE
WHEN SUM(`type` = 'RANKED') > 0 THEN 'RANKED'
ELSE MIN(`type`)
END AS `type`
FROM `_SeasonGameType`
GROUP BY `seasonId`;

UPDATE `Season` AS `season`
INNER JOIN `_SeasonPrimaryType` AS `primaryType`
ON `primaryType`.`seasonId` = `season`.`id`
SET `season`.`type` = `primaryType`.`type`;

-- Map every additional type to a deterministic cloned season.
CREATE TEMPORARY TABLE `_SeasonTypeMap` AS
SELECT
`gameType`.`seasonId` AS `oldSeasonId`,
`gameType`.`type`,
CASE
WHEN `gameType`.`type` = `primaryType`.`type` THEN `gameType`.`seasonId`
ELSE CONCAT('split_', MD5(CONCAT(`gameType`.`seasonId`, ':', `gameType`.`type`)))
END AS `newSeasonId`
FROM `_SeasonGameType` AS `gameType`
INNER JOIN `_SeasonPrimaryType` AS `primaryType`
ON `primaryType`.`seasonId` = `gameType`.`seasonId`;

INSERT INTO `Season` (`id`, `name`, `type`, `startDate`, `endDate`)
SELECT
`mapping`.`newSeasonId`,
`season`.`name`,
`mapping`.`type`,
`season`.`startDate`,
`season`.`endDate`
FROM `_SeasonTypeMap` AS `mapping`
INNER JOIN `Season` AS `season`
ON `season`.`id` = `mapping`.`oldSeasonId`
WHERE `mapping`.`newSeasonId` <> `mapping`.`oldSeasonId`;

UPDATE `JapaneseGame` AS `game`
INNER JOIN `_SeasonTypeMap` AS `mapping`
ON `mapping`.`oldSeasonId` = `game`.`seasonId`
AND `mapping`.`type` = `game`.`type`
SET `game`.`seasonId` = `mapping`.`newSeasonId`;

UPDATE `HongKongGame` AS `game`
INNER JOIN `_SeasonTypeMap` AS `mapping`
ON `mapping`.`oldSeasonId` = `game`.`seasonId`
AND `mapping`.`type` = `game`.`type`
SET `game`.`seasonId` = `mapping`.`newSeasonId`;

DROP TEMPORARY TABLE `_SeasonTypeMap`;
DROP TEMPORARY TABLE `_SeasonPrimaryType`;
DROP TEMPORARY TABLE `_SeasonGameType`;

-- Game type is now owned exclusively by the season container.
ALTER TABLE `JapaneseGame` DROP COLUMN `type`;
ALTER TABLE `HongKongGame` DROP COLUMN `type`;
225 changes: 112 additions & 113 deletions backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
@@ -1,45 +1,45 @@
generator client {
provider = "prisma-client-js"
provider = "prisma-client-js"
}

datasource db {
provider = "mysql"
url = env("DATABASE_URL")
provider = "mysql"
url = env("DATABASE_URL")
}

enum GameType {
RANKED
PLAY_OFF
TOURNEY
CASUAL
RANKED
PLAY_OFF
TOURNEY
CASUAL
}

enum JapaneseTransactionType {
DEAL_IN
SELF_DRAW
DEAL_IN_PAO
SELF_DRAW_PAO
NAGASHI_MANGAN
INROUND_RYUUKYOKU
DEAL_IN
SELF_DRAW
DEAL_IN_PAO
SELF_DRAW_PAO
NAGASHI_MANGAN
INROUND_RYUUKYOKU
}

enum HongKongTransactionType {
DEAL_IN
SELF_DRAW
DEAL_IN_PAO
SELF_DRAW_PAO
DEAL_IN
SELF_DRAW
DEAL_IN_PAO
SELF_DRAW_PAO
}

enum GameStatus {
IN_PROGRESS
FINISHED
IN_PROGRESS
FINISHED
}

enum Wind {
EAST
SOUTH
WEST
NORTH
EAST
SOUTH
WEST
NORTH
}

model Player {
Expand All @@ -62,118 +62,117 @@ model Player {
}

model JapanesePlayerGame {
id String @id @default(cuid())
wind Wind
eloChange Float?
player Player @relation(fields: [playerId], references: [id])
playerId String
chomboCount Int @default(0)
game JapaneseGame? @relation(fields: [gameId], references: [id], onDelete: Cascade)
gameId Int?
id String @id @default(cuid())
wind Wind
eloChange Float?
player Player @relation(fields: [playerId], references: [id])
playerId String
chomboCount Int @default(0)
game JapaneseGame? @relation(fields: [gameId], references: [id], onDelete: Cascade)
gameId Int?
}

model HongKongPlayerGame {
id String @id @default(cuid())
wind Wind
eloChange Float?
player Player @relation(fields: [playerId], references: [id])
playerId String
chomboCount Int @default(0)
game HongKongGame? @relation(fields: [gameId], references: [id])
gameId Int?
id String @id @default(cuid())
wind Wind
eloChange Float?
player Player @relation(fields: [playerId], references: [id])
playerId String
chomboCount Int @default(0)
game HongKongGame? @relation(fields: [gameId], references: [id])
gameId Int?
}

model Season {
id String @id @default(cuid())
name String
startDate DateTime @default(now())
endDate DateTime
japaneseGames JapaneseGame[]
hongKongGames HongKongGame[]
id String @id @default(cuid())
name String
type GameType
startDate DateTime @default(now())
endDate DateTime
japaneseGames JapaneseGame[]
hongKongGames HongKongGame[]
}

model JapaneseGame {
id Int @id @default(autoincrement())
season Season @relation(fields: [seasonId], references: [id])
seasonId String
status GameStatus
type GameType
createdAt DateTime @default(now())
endedAt DateTime?
recordedBy Player @relation(fields: [recordedById], references: [id])
recordedById String
rounds JapaneseRound[]
players JapanesePlayerGame[]
id Int @id @default(autoincrement())
season Season @relation(fields: [seasonId], references: [id])
seasonId String
status GameStatus
createdAt DateTime @default(now())
endedAt DateTime?
recordedBy Player @relation(fields: [recordedById], references: [id])
recordedById String
rounds JapaneseRound[]
players JapanesePlayerGame[]
}

model JapaneseRound {
id String @id @default(cuid())
roundCount Int
roundWind Wind
roundNumber Int
bonus Int
startRiichiStickCount Int
endRiichiStickCount Int
game JapaneseGame @relation(fields: [gameId], references: [id], onDelete: Cascade)
gameId Int
player0Riichi Boolean @default(false)
player1Riichi Boolean @default(false)
player2Riichi Boolean @default(false)
player3Riichi Boolean @default(false)
player0Tenpai Boolean @default(false)
player1Tenpai Boolean @default(false)
player2Tenpai Boolean @default(false)
player3Tenpai Boolean @default(false)
transactions JapaneseTransaction[]
id String @id @default(cuid())
roundCount Int
roundWind Wind
roundNumber Int
bonus Int
startRiichiStickCount Int
endRiichiStickCount Int
game JapaneseGame @relation(fields: [gameId], references: [id], onDelete: Cascade)
gameId Int
player0Riichi Boolean @default(false)
player1Riichi Boolean @default(false)
player2Riichi Boolean @default(false)
player3Riichi Boolean @default(false)
player0Tenpai Boolean @default(false)
player1Tenpai Boolean @default(false)
player2Tenpai Boolean @default(false)
player3Tenpai Boolean @default(false)
transactions JapaneseTransaction[]
}

model JapaneseTransaction {
id String @id @default(cuid())
transactionType JapaneseTransactionType
player0ScoreChange Int @default(0)
player1ScoreChange Int @default(0)
player2ScoreChange Int @default(0)
player3ScoreChange Int @default(0)
han Int?
fu Int?
dora Int?
paoPlayerIndex Int?
round JapaneseRound @relation(fields: [roundId], references: [id], onDelete: Cascade)
roundId String
id String @id @default(cuid())
transactionType JapaneseTransactionType
player0ScoreChange Int @default(0)
player1ScoreChange Int @default(0)
player2ScoreChange Int @default(0)
player3ScoreChange Int @default(0)
han Int?
fu Int?
dora Int?
paoPlayerIndex Int?
round JapaneseRound @relation(fields: [roundId], references: [id], onDelete: Cascade)
roundId String
}

model HongKongGame {
id Int @id @default(autoincrement())
season Season @relation(fields: [seasonId], references: [id])
seasonId String
status GameStatus
type GameType
createdAt DateTime @default(now())
endedAt DateTime?
recordedBy Player @relation(fields: [recordedById], references: [id])
recordedById String
rounds HongKongRound[]
players HongKongPlayerGame[]
id Int @id @default(autoincrement())
season Season @relation(fields: [seasonId], references: [id])
seasonId String
status GameStatus
createdAt DateTime @default(now())
endedAt DateTime?
recordedBy Player @relation(fields: [recordedById], references: [id])
recordedById String
rounds HongKongRound[]
players HongKongPlayerGame[]
}

model HongKongRound {
id String @id @default(cuid())
roundCount Int
roundWind Wind
roundNumber Int
transactions HongKongTransaction[]
game HongKongGame @relation(fields: [gameId], references: [id], onDelete: Cascade)
gameId Int
id String @id @default(cuid())
roundCount Int
roundWind Wind
roundNumber Int
transactions HongKongTransaction[]
game HongKongGame @relation(fields: [gameId], references: [id], onDelete: Cascade)
gameId Int
}

model HongKongTransaction {
id String @id @default(cuid())
transactionType HongKongTransactionType
player0ScoreChange Int @default(0)
player1ScoreChange Int @default(0)
player2ScoreChange Int @default(0)
player3ScoreChange Int @default(0)
hand Int?
round HongKongRound @relation(fields: [roundId], references: [id], onDelete: Cascade)
roundId String
id String @id @default(cuid())
transactionType HongKongTransactionType
player0ScoreChange Int @default(0)
player1ScoreChange Int @default(0)
player2ScoreChange Int @default(0)
player3ScoreChange Int @default(0)
hand Int?
round HongKongRound @relation(fields: [roundId], references: [id], onDelete: Cascade)
roundId String
}
13 changes: 2 additions & 11 deletions backend/src/controllers/admin.controller.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import { Request, Response } from "express";
import { deletePlayer, findAllPlayers, updatePlayer } from "../services/player.service";
import createError from "http-errors";
import {
createSeason,
deleteSeason,
findCurrentSeason,
updateSeason,
} from "../services/season.service";
import { createSeason, deleteSeason, updateSeason } from "../services/season.service";
import { makeDummyAdmins } from "../services/admin.service";
import { playerSchema } from "../validation/player.validation";
import { createSeasonSchema, updateSeasonSchema } from "../validation/season.validation";
Expand Down Expand Up @@ -45,18 +40,14 @@ const deletePlayerHandler = async (req: Request, res: Response): Promise<void> =
};

const createSeasonHandler = async (req: Request, res: Response): Promise<void> => {
if (await findCurrentSeason()) {
throw createError.Conflict("Season already in progress");
}

const season = createSeasonSchema.parse(req.body?.season);
const startDate = new Date(season.startDate);
const endDate = new Date(season.endDate);
if (endDate < new Date()) {
throw createError.BadRequest("End date must be in the future");
}

const createdSeason = await createSeason(season.name, startDate, endDate);
const createdSeason = await createSeason(season.name, season.type, startDate, endDate);
res.json({ ...createdSeason });
};

Expand Down
Loading