From 6b3a4fa2f5629075c27efecb80ad494f8f9abb45 Mon Sep 17 00:00:00 2001 From: zk Date: Sat, 28 Mar 2026 12:13:27 +0000 Subject: [PATCH] game_stats -> game_profiles --- Justfile-db | 20 +-- Justfile-test | 22 +++ db/migrations/20260301154256_genesis.sql | 2 +- typescript/client/src/app/pages/LoginPage.tsx | 135 +++++++++++------- typescript/db/src/generated/index.ts | 2 +- .../db/src/generated/public/GameProfile.ts | 26 ++++ .../db/src/generated/public/PublicSchema.ts | 6 +- .../{game-stats.ts => game-profiles.ts} | 12 +- .../router/api/v1/users/_userID/router.ts | 12 +- typescript/server/src/utils/user.test.ts | 12 +- typescript/server/src/utils/user.ts | 2 +- 11 files changed, 157 insertions(+), 94 deletions(-) create mode 100644 typescript/db/src/generated/public/GameProfile.ts rename typescript/server/src/lib/db-formats/{game-stats.ts => game-profiles.ts} (71%) diff --git a/Justfile-db b/Justfile-db index 4d4c0a9dd..b595e7010 100644 --- a/Justfile-db +++ b/Justfile-db @@ -14,6 +14,8 @@ db-reset POSTGRES_DB=DEFAULT_DB: tachidb database create tachidb migrate run + just db-load-seeds "{{POSTGRES_DB}}" + # Show the status of all applied (and failed) migrations. db-migrate-status POSTGRES_DB=DEFAULT_DB: psql "{{POSTGRES_URL}}/{{POSTGRES_DB}}" -c \ @@ -21,24 +23,6 @@ db-migrate-status POSTGRES_DB=DEFAULT_DB: FROM \"_migration\" \ ORDER BY version" -# Create (or recreate) the bot test template database with all migrations applied. -# Workers clone from this template rather than re-running migrations each time. -bot-db-test-template-reset: - #!/bin/bash - export POSTGRES_URL="{{POSTGRES_URL}}/tachi_bot_test_template" - tachidb database drop || true - tachidb database create - tachidb migrate run - -# Create (or recreate) the server test template database with all migrations applied. -# Workers clone from this template rather than re-running migrations each time. -server-db-test-template-reset: - #!/bin/bash - export POSTGRES_URL="{{POSTGRES_URL}}/tachi_server_test_template" - tachidb database drop || true - tachidb database create - tachidb migrate run - # Load the seeds into the database. db-load-seeds POSTGRES_DB=DEFAULT_DB: cd typescript/server && POSTGRES_URL="{{POSTGRES_URL}}/{{POSTGRES_DB}}" \ diff --git a/Justfile-test b/Justfile-test index 1aebcde3c..9068c44e3 100644 --- a/Justfile-test +++ b/Justfile-test @@ -70,3 +70,25 @@ test-typescript FILTER="*": # Run all parity suites, or a single one if a name is given. test-parity suite="": TACHI_SERVER=https://127.0.0.1:8080 bun vitest run {{ if suite != "" { "tests/" + suite + ".test.ts" } else { "" } }} + +# ==> Orchestration of parallel db tests + +# Create (or recreate) the bot test template database with all migrations applied. +# Workers clone from this template rather than re-running migrations each time. +bot-db-test-template-reset: + #!/bin/bash + export POSTGRES_URL="{{POSTGRES_URL}}/tachi_bot_test_template" + tachidb database drop || true + tachidb database create + tachidb migrate run + +# Create (or recreate) the server test template database with all migrations applied. +# Workers clone from this template rather than re-running migrations each time. +server-db-test-template-reset: + #!/bin/bash + export POSTGRES_URL="{{POSTGRES_URL}}/tachi_server_test_template" + tachidb database drop || true + tachidb database create + tachidb migrate run + +# <== End of parallel db test justscripts \ No newline at end of file diff --git a/db/migrations/20260301154256_genesis.sql b/db/migrations/20260301154256_genesis.sql index fa5e90658..24a3b7a54 100644 --- a/db/migrations/20260301154256_genesis.sql +++ b/db/migrations/20260301154256_genesis.sql @@ -549,7 +549,7 @@ CREATE TABLE "game_settings_showcase" ( data JSONB NOT NULL ); -CREATE TABLE "game_stats" ( +CREATE TABLE "game_profile" ( user_id BIGINT REFERENCES account(id) NOT NULL, game GAME NOT NULL, diff --git a/typescript/client/src/app/pages/LoginPage.tsx b/typescript/client/src/app/pages/LoginPage.tsx index 1b1a7c149..5f140837d 100644 --- a/typescript/client/src/app/pages/LoginPage.tsx +++ b/typescript/client/src/app/pages/LoginPage.tsx @@ -6,7 +6,7 @@ import { HumaniseError } from "#util/humanise-error"; import { HistorySafeGoBack } from "#util/misc"; import { useFormik } from "formik"; import React, { useContext, useRef, useState } from "react"; -import { Button, Form } from "react-bootstrap"; +import { Alert, Button, Form } from "react-bootstrap"; import ReCAPTCHA from "react-google-recaptcha"; import toast from "react-hot-toast"; import { Link, useHistory } from "react-router-dom"; @@ -21,68 +21,99 @@ export default function LoginPage() { const recaptchaRef = useRef(null); + const performLogin = async (values: { + username: string; + "!password": string; + captcha: string; + }) => { + setErr(""); + + const rj = await APIFetchV1( + "/auth/login", + { + method: "POST", + body: JSON.stringify({ + username: values.username.trim(), + "!password": values["!password"], + captcha: values.captcha, + }), + headers: { + "Content-Type": "application/json", + }, + }, + false, + false, + ); + + if (recaptchaRef.current) { + recaptchaRef.current.reset(); + } + + if (!rj.success) { + setErr(HumaniseError(rj.description)); + return; + } + + const userRJ = await APIFetchV1("/users/me"); + + if (userRJ.statusCode === 403) { + setErr("You are banned."); + return; + } + + if (!userRJ.success) { + console.error("Error retrieving own user?"); + setErr("An internal server error has occurred."); + return; + } + + toast.success("Logged in!"); + + setTimeout(() => { + setUser(userRJ.body); + localStorage.setItem("isLoggedIn", "true"); + + HistorySafeGoBack(history); + }, 500); + }; + const formik = useFormik({ initialValues: { username: "", "!password": "", captcha: "", }, - onSubmit: async (values) => { - setErr(""); - - const rj = await APIFetchV1( - "/auth/login", - { - method: "POST", - body: JSON.stringify({ - username: values.username.trim(), - "!password": values["!password"], - captcha: values.captcha, - }), - headers: { - "Content-Type": "application/json", - }, - }, - false, - false, - ); - - if (recaptchaRef.current) { - recaptchaRef.current.reset(); - } - - if (!rj.success) { - setErr(HumaniseError(rj.description)); - return; - } - - const userRJ = await APIFetchV1("/users/me"); - - if (userRJ.statusCode === 403) { - setErr("You are banned."); - return; - } - - if (!userRJ.success) { - console.error("Error retrieving own user?"); - setErr("An internal server error has occurred."); - return; - } - - toast.success("Logged in!"); - - setTimeout(() => { - setUser(userRJ.body); - localStorage.setItem("isLoggedIn", "true"); - - HistorySafeGoBack(history); - }, 500); - }, + onSubmit: performLogin, }); return ( } heading="Log In">
+ {import.meta.env.VITE_IS_LOCAL_DEV && ( + +
+ + You are in local development mode. You can login as the admin + account. + + +
+
+ )} Username ; + + game: ColumnType; + + ratings: ColumnType; + + classes: ColumnType; +} + +export type GameProfile = Selectable; + +export type NewGameProfile = Insertable; + +export type GameProfileUpdate = Updateable; diff --git a/typescript/db/src/generated/public/PublicSchema.ts b/typescript/db/src/generated/public/PublicSchema.ts index a5279e1d4..2bc0cff59 100644 --- a/typescript/db/src/generated/public/PublicSchema.ts +++ b/typescript/db/src/generated/public/PublicSchema.ts @@ -18,13 +18,13 @@ import type { default as ImportLockTable } from './ImportLock'; import type { default as ScoreBlacklistTable } from './ScoreBlacklist'; import type { default as PrivDiscordUserMapTable } from './PrivDiscordUserMap'; import type { default as TableFolderTable } from './TableFolder'; -import type { default as GameStatsTable } from './GameStats'; import type { default as PrivOauth2AuthTokenTable } from './PrivOauth2AuthToken'; import type { default as ImportSessionTable } from './ImportSession'; import type { default as PrivSvcFerCardTable } from './PrivSvcFerCard'; import type { default as PbComposedFromTable } from './PbComposedFrom'; import type { default as BmsCourseLookupTable } from './BmsCourseLookup'; import type { default as SongSearchTermTable } from './SongSearchTerm'; +import type { default as GameProfileTable } from './GameProfile'; import type { default as ImportGameTable } from './ImportGame'; import type { default as ImportGoalTable } from './ImportGoal'; import type { default as OrphanChartUserTable } from './OrphanChartUser'; @@ -104,8 +104,6 @@ export default interface PublicSchema { table_folder: TableFolderTable; - game_stats: GameStatsTable; - priv_oauth2_auth_token: PrivOauth2AuthTokenTable; import_session: ImportSessionTable; @@ -118,6 +116,8 @@ export default interface PublicSchema { song_search_term: SongSearchTermTable; + game_profile: GameProfileTable; + import_game: ImportGameTable; import_goal: ImportGoalTable; diff --git a/typescript/server/src/lib/db-formats/game-stats.ts b/typescript/server/src/lib/db-formats/game-profiles.ts similarity index 71% rename from typescript/server/src/lib/db-formats/game-stats.ts rename to typescript/server/src/lib/db-formats/game-profiles.ts index de1fe4595..518d2c7f5 100644 --- a/typescript/server/src/lib/db-formats/game-stats.ts +++ b/typescript/server/src/lib/db-formats/game-profiles.ts @@ -8,15 +8,15 @@ import { } from "tachi-common"; import { type Database } from "tachi-db"; -export const SELECT_GAME_STATS = [ - "game_stats.user_id", - "game_stats.game", - "game_stats.ratings", - "game_stats.classes", +export const SELECT_GAME_PROFILE = [ + "game_profile.user_id", + "game_profile.game", + "game_profile.ratings", + "game_profile.classes", ] as const; export function ToGameStatsDocument( - row: Selection, + row: Selection, ): UserGameStats { const { game, playtype } = V3ToGamePT(row.game); diff --git a/typescript/server/src/server/router/api/v1/users/_userID/router.ts b/typescript/server/src/server/router/api/v1/users/_userID/router.ts index 74ac9f262..d340ffac4 100644 --- a/typescript/server/src/server/router/api/v1/users/_userID/router.ts +++ b/typescript/server/src/server/router/api/v1/users/_userID/router.ts @@ -6,7 +6,7 @@ import { ACTION_ChangeUsername } from "#actions/change-username.js"; import { ACTION_UpdateUser } from "#actions/update-user.js"; import { GetRecentActivity } from "#lib/activity/activity"; import { ONE_MONTH } from "#lib/constants/time"; -import { SELECT_GAME_STATS, ToGameStatsDocument } from "#lib/db-formats/game-stats.js"; +import { SELECT_GAME_PROFILE, ToGameStatsDocument } from "#lib/db-formats/game-profiles.js"; import { log } from "#lib/log/log"; import { GetRivalIDs } from "#lib/rivals/rivals"; import prValidate from "#server/middleware/prudence-validate"; @@ -153,8 +153,8 @@ router.get("/game-stats", async (req, res) => { { __rankingData?: Record; } & UserGameStats - > = await DB.selectFrom("game_stats") - .select(SELECT_GAME_STATS) + > = await DB.selectFrom("game_profile") + .select(SELECT_GAME_PROFILE) .where("user_id", "=", user.id) .execute() .then((res) => res.map(ToGameStatsDocument)); @@ -567,12 +567,12 @@ router.get( const user = GetUser(req); - const gameStatRows = await DB.selectFrom("game_stats") - .select(SELECT_GAME_STATS) + const gameProfileRows = await DB.selectFrom("game_profile") + .select(SELECT_GAME_PROFILE) .where("user_id", "=", user.id) .execute(); - const gpts = gameStatRows.map(ToGameStatsDocument); + const gpts = gameProfileRows.map(ToGameStatsDocument); const data: Partial> = {}; diff --git a/typescript/server/src/utils/user.test.ts b/typescript/server/src/utils/user.test.ts index 29780ac50..c377eea9f 100644 --- a/typescript/server/src/utils/user.test.ts +++ b/typescript/server/src/utils/user.test.ts @@ -11,7 +11,7 @@ async function seedGameStats( ktLampRating: number | null, game: "iidx-sp" = "iidx-sp", ) { - await DB.insertInto("game_stats") + await DB.insertInto("game_profile") .values({ user_id: userId, game, @@ -96,7 +96,7 @@ describe("GetUsersRankingAndOutOf", () => { const user1 = await seedUser({ username: "bpi_high" }); const user2 = await seedUser({ username: "bpi_low" }); - await DB.insertInto("game_stats") + await DB.insertInto("game_profile") .values({ user_id: user1.id, game: "iidx-sp", @@ -104,7 +104,7 @@ describe("GetUsersRankingAndOutOf", () => { classes: JSON.stringify({}), }) .execute(); - await DB.insertInto("game_stats") + await DB.insertInto("game_profile") .values({ user_id: user2.id, game: "iidx-sp", @@ -139,7 +139,7 @@ describe("GetUsersRankingAndOutOf", () => { const sdvxUser = await seedUser({ username: "sdvx_player" }); await seedGameStats(iidxUser.id, 10, "iidx-sp"); - await DB.insertInto("game_stats") + await DB.insertInto("game_profile") .values({ user_id: sdvxUser.id, game: "sdvx", @@ -162,7 +162,7 @@ describe("GetAllRankings", () => { const user1 = await seedUser({ username: "top_player" }); const user2 = await seedUser({ username: "bot_player" }); - await DB.insertInto("game_stats") + await DB.insertInto("game_profile") .values({ user_id: user1.id, game: "iidx-sp", @@ -170,7 +170,7 @@ describe("GetAllRankings", () => { classes: JSON.stringify({}), }) .execute(); - await DB.insertInto("game_stats") + await DB.insertInto("game_profile") .values({ user_id: user2.id, game: "iidx-sp", diff --git a/typescript/server/src/utils/user.ts b/typescript/server/src/utils/user.ts index d1e6e817f..ce54d5d61 100644 --- a/typescript/server/src/utils/user.ts +++ b/typescript/server/src/utils/user.ts @@ -231,7 +231,7 @@ export async function GetUsersRankingAndOutOf( const v3Game = GamePTToV3(stats.game, stats.playtype); const userRating = stats.ratings[ratingAlg] ?? null; - const result = await DB.selectFrom("game_stats") + const result = await DB.selectFrom("game_profile") .select([ (eb) => eb.fn.countAll().as("out_of"), sql`COUNT(*) FILTER (WHERE (ratings->>${ratingAlg})::numeric > ${userRating})`.as(