diff --git a/.eslintrc.js b/.eslintrc.js index 0646ccf0d..b9bb9293f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -15,7 +15,10 @@ module.exports = { "plugin:react-hooks/recommended", ], rules: { - "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }], + "@typescript-eslint/no-unused-vars": [ + "warn", + { argsIgnorePattern: "^_", ignoreRestSiblings: true }, + ], "no-constant-condition": ["error", { checkLoops: false }], "no-console": ["warn", { allow: ["warn", "error"] }], "no-warning-comments": ["warn", { terms: ["xxx"] }], diff --git a/app/components/Badge.tsx b/app/components/Badge.tsx index f55e00274..f6c7b69b5 100644 --- a/app/components/Badge.tsx +++ b/app/components/Badge.tsx @@ -1,4 +1,3 @@ -import type { Badge as BadgeDBType } from "~/db/types"; import { badgeUrl } from "~/utils/urls"; import { Image } from "./Image"; @@ -8,7 +7,7 @@ export function Badge({ isAnimated, size, }: { - badge: Pick; + badge: { displayName: string; hue?: number | null; code: string }; onClick?: () => void; isAnimated: boolean; size: number; diff --git a/app/components/BuildCard.tsx b/app/components/BuildCard.tsx index d18e39c59..4bd87c652 100644 --- a/app/components/BuildCard.tsx +++ b/app/components/BuildCard.tsx @@ -8,7 +8,7 @@ import type { UserWithPlusTier, } from "~/db/types"; import { useIsMounted } from "~/hooks/useIsMounted"; -import { useUser } from "~/modules/auth"; +import { useUser } from "~/features/auth/core"; import type { Ability as AbilityType, ModeShort, diff --git a/app/components/Catcher.tsx b/app/components/Catcher.tsx index 6937f2578..470f3a33e 100644 --- a/app/components/Catcher.tsx +++ b/app/components/Catcher.tsx @@ -1,6 +1,6 @@ import { isRouteErrorResponse, useRouteError } from "@remix-run/react"; import { Button } from "~/components/Button"; -import { useUser } from "~/modules/auth"; +import { useUser } from "~/features/auth/core"; import { ERROR_GIRL_IMAGE_PATH, LOG_IN_URL, diff --git a/app/components/Chart.tsx b/app/components/Chart.tsx index 167be83f4..145ad663f 100644 --- a/app/components/Chart.tsx +++ b/app/components/Chart.tsx @@ -4,7 +4,7 @@ import { type AxisOptions, Chart as ReactChart } from "react-charts"; import type { TooltipRendererProps } from "react-charts/types/components/TooltipRenderer"; import { useTranslation } from "~/hooks/useTranslation"; import { useIsMounted } from "~/hooks/useIsMounted"; -import { Theme, useTheme } from "~/modules/theme"; +import { Theme, useTheme } from "~/features/theme/core/provider"; export default function Chart({ options, diff --git a/app/components/Combobox.tsx b/app/components/Combobox.tsx index 760e58ab3..d97036cd3 100644 --- a/app/components/Combobox.tsx +++ b/app/components/Combobox.tsx @@ -19,7 +19,7 @@ import { nonDamagingSpecialWeaponIds, specialWeaponIds, } from "~/modules/in-game-lists/weapon-ids"; -import { type SerializedMapPoolEvent } from "~/routes/calendar/map-pool-events"; +import { type SerializedMapPoolEvent } from "~/features/calendar/routes/map-pool-events"; import type { Unpacked } from "~/utils/types"; import { gearImageUrl, diff --git a/app/components/Main.tsx b/app/components/Main.tsx index bbe02424b..67ecd1d30 100644 --- a/app/components/Main.tsx +++ b/app/components/Main.tsx @@ -2,7 +2,7 @@ import { useCatch, useLocation } from "@remix-run/react"; import clsx from "clsx"; import type * as React from "react"; import { useMatches } from "react-router"; -import { useUser } from "~/modules/auth"; +import { useUser } from "~/features/auth/core"; import type { RootLoaderData } from "~/root"; import { SideNav } from "app/components/layout/SideNav"; diff --git a/app/components/MapPoolSelector.tsx b/app/components/MapPoolSelector.tsx index af684c36c..ae9ee7ab3 100644 --- a/app/components/MapPoolSelector.tsx +++ b/app/components/MapPoolSelector.tsx @@ -7,7 +7,7 @@ import { type StageId, } from "~/modules/in-game-lists"; import { modes, stageIds } from "~/modules/in-game-lists"; -import { MapPool } from "~/modules/map-pool-serializer"; +import { MapPool } from "~/features/map-list-generator/core/map-pool"; import { modeImageUrl, stageImageUrl } from "~/utils/urls"; import { Button } from "~/components/Button"; import { split, startsWith } from "~/utils/strings"; @@ -15,7 +15,7 @@ import { CrossIcon } from "./icons/Cross"; import { ArrowLongLeftIcon } from "./icons/ArrowLongLeft"; import * as React from "react"; import type { CalendarEvent } from "~/db/types"; -import type { SerializedMapPoolEvent } from "~/routes/calendar/map-pool-events"; +import type { SerializedMapPoolEvent } from "~/features/calendar/routes/map-pool-events"; import { assertType } from "~/utils/types"; import { MapPoolEventsCombobox } from "./Combobox"; diff --git a/app/components/UserSearch.tsx b/app/components/UserSearch.tsx index 8c51796df..95085095c 100644 --- a/app/components/UserSearch.tsx +++ b/app/components/UserSearch.tsx @@ -3,7 +3,7 @@ import { useFetcher } from "@remix-run/react"; import clsx from "clsx"; import * as React from "react"; import { useDebounce } from "react-use"; -import type { UserSearchLoaderData } from "~/routes/u"; +import type { UserSearchLoaderData } from "~/features/user-search/routes/u"; import { Avatar } from "./Avatar"; import { useTranslation } from "~/hooks/useTranslation"; diff --git a/app/components/layout/SelectedThemeIcon.tsx b/app/components/layout/SelectedThemeIcon.tsx index 878bfa639..5f3143e04 100644 --- a/app/components/layout/SelectedThemeIcon.tsx +++ b/app/components/layout/SelectedThemeIcon.tsx @@ -1,8 +1,8 @@ import { useTranslation } from "~/hooks/useTranslation"; -import { Theme, useTheme } from "~/modules/theme"; import { MoonIcon } from "../icons/Moon"; import { SunIcon } from "../icons/Sun"; import { SunAndMoonIcon } from "../icons/SunAndMoon"; +import { Theme, useTheme } from "~/features/theme/core/provider"; const ThemeIcons = { [Theme.LIGHT]: SunIcon, diff --git a/app/components/layout/ThemeChanger.tsx b/app/components/layout/ThemeChanger.tsx index 11740b173..b406e618c 100644 --- a/app/components/layout/ThemeChanger.tsx +++ b/app/components/layout/ThemeChanger.tsx @@ -1,11 +1,11 @@ import { useTranslation } from "~/hooks/useTranslation"; -import { Theme, useTheme } from "~/modules/theme"; import { Button } from "../Button"; import { MoonIcon } from "../icons/Moon"; import { SunIcon } from "../icons/Sun"; import { SunAndMoonIcon } from "../icons/SunAndMoon"; import { Popover } from "../Popover"; import { SelectedThemeIcon } from "./SelectedThemeIcon"; +import { Theme, useTheme } from "~/features/theme/core/provider"; const ThemeIcons = { [Theme.LIGHT]: SunIcon, diff --git a/app/components/layout/UserItem.tsx b/app/components/layout/UserItem.tsx index 973874360..5545fce9b 100644 --- a/app/components/layout/UserItem.tsx +++ b/app/components/layout/UserItem.tsx @@ -1,6 +1,6 @@ import { Link } from "@remix-run/react"; import { useTranslation } from "~/hooks/useTranslation"; -import { useUser } from "~/modules/auth"; +import { useUser } from "~/features/auth/core"; import { userPage } from "~/utils/urls"; import { Avatar } from "../Avatar"; import { LogInIcon } from "../icons/LogIn"; diff --git a/app/constants.ts b/app/constants.ts index 1eeb9eecc..5cb92b991 100644 --- a/app/constants.ts +++ b/app/constants.ts @@ -1,4 +1,4 @@ -import allTags from "~/routes/calendar/tags.json"; +import allTags from "~/features/calendar/tags.json"; import type { CalendarEventTag } from "./db/types"; import type { BuildAbilitiesTupleWithUnknown } from "./modules/in-game-lists"; diff --git a/app/db/index.ts b/app/db/index.ts deleted file mode 100644 index 69ed79a04..000000000 --- a/app/db/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -import * as users from "./models/users/queries.server"; -import * as plusSuggestions from "./models/plusSuggestions/queries.server"; -import * as plusVotes from "./models/plusVotes/queries.server"; -import * as badges from "./models/badges/queries.server"; -import * as calendarEvents from "./models/calendar/queries.server"; -import * as builds from "./models/builds/queries.server"; - -export const db = { - users, - plusSuggestions, - plusVotes, - badges, - calendarEvents, - builds, -}; diff --git a/app/db/models/badges/createManager.sql b/app/db/models/badges/createManager.sql deleted file mode 100644 index 861ba504c..000000000 --- a/app/db/models/badges/createManager.sql +++ /dev/null @@ -1,4 +0,0 @@ -insert into - "BadgeManager" ("badgeId", "userId") -values - (@badgeId, @userId) \ No newline at end of file diff --git a/app/db/models/badges/createOwner.sql b/app/db/models/badges/createOwner.sql deleted file mode 100644 index 3904fb130..000000000 --- a/app/db/models/badges/createOwner.sql +++ /dev/null @@ -1,4 +0,0 @@ -insert into - "TournamentBadgeOwner" ("badgeId", "userId") -values - (@badgeId, @userId) \ No newline at end of file diff --git a/app/db/models/badges/deleteManyManagers.sql b/app/db/models/badges/deleteManyManagers.sql deleted file mode 100644 index 28d1a77a3..000000000 --- a/app/db/models/badges/deleteManyManagers.sql +++ /dev/null @@ -1,4 +0,0 @@ -delete from - "BadgeManager" -where - "badgeId" = @badgeId \ No newline at end of file diff --git a/app/db/models/badges/deleteManyOwners.sql b/app/db/models/badges/deleteManyOwners.sql deleted file mode 100644 index 16091ca65..000000000 --- a/app/db/models/badges/deleteManyOwners.sql +++ /dev/null @@ -1,4 +0,0 @@ -delete from - "TournamentBadgeOwner" -where - "badgeId" = @badgeId \ No newline at end of file diff --git a/app/db/models/badges/findAll.sql b/app/db/models/badges/findAll.sql deleted file mode 100644 index b6ad1bc11..000000000 --- a/app/db/models/badges/findAll.sql +++ /dev/null @@ -1,7 +0,0 @@ -select - "Badge"."id", - "Badge"."code", - "Badge"."displayName", - "Badge"."hue" -from - "Badge" \ No newline at end of file diff --git a/app/db/models/badges/findByOwnerId.sql b/app/db/models/badges/findByOwnerId.sql deleted file mode 100644 index 6f16c0e99..000000000 --- a/app/db/models/badges/findByOwnerId.sql +++ /dev/null @@ -1,24 +0,0 @@ -select - "Badge"."code", - "Badge"."displayName", - "Badge"."id", - "Badge"."hue", - count("BadgeOwner"."badgeId") as count -from - "BadgeOwner" - join "Badge" on "Badge"."id" = "BadgeOwner"."badgeId" -where - "BadgeOwner"."userId" = @userId -group by - "BadgeOwner"."badgeId", - "BadgeOwner"."userId" -order by - "Badge"."id" = ( - select - "favoriteBadgeId" - from - "User" - where - "id" = @userId - ) desc, - "Badge"."id" asc diff --git a/app/db/models/badges/managedByUserId.sql b/app/db/models/badges/managedByUserId.sql deleted file mode 100644 index 1f73530ec..000000000 --- a/app/db/models/badges/managedByUserId.sql +++ /dev/null @@ -1,10 +0,0 @@ -select - "Badge"."id", - "Badge"."code", - "Badge"."displayName", - "Badge"."hue" -from - "BadgeManager" - join "Badge" on "Badge"."id" = "BadgeManager"."badgeId" -where - "userId" = @userId \ No newline at end of file diff --git a/app/db/models/badges/managersByBadgeId.sql b/app/db/models/badges/managersByBadgeId.sql deleted file mode 100644 index 800ea45d9..000000000 --- a/app/db/models/badges/managersByBadgeId.sql +++ /dev/null @@ -1,10 +0,0 @@ -select - "User"."id", - "User"."discordId", - "User"."discordName", - "User"."discordDiscriminator" -from - "BadgeManager" - join "User" on "User"."id" = "BadgeManager"."userId" -where - "BadgeManager"."badgeId" = @id \ No newline at end of file diff --git a/app/db/models/badges/ownersByBadgeId.sql b/app/db/models/badges/ownersByBadgeId.sql deleted file mode 100644 index 61335c0a6..000000000 --- a/app/db/models/badges/ownersByBadgeId.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - count("BadgeOwner"."badgeId") as count, - "User"."id", - "User"."discordId", - "User"."discordName", - "User"."discordDiscriminator" -from - "BadgeOwner" - join "User" on "User"."id" = "BadgeOwner"."userId" -where - "BadgeOwner"."badgeId" = @id -group by - "User"."id" -order by - count desc, - "User"."discordName" collate nocase asc \ No newline at end of file diff --git a/app/db/models/badges/queries.server.ts b/app/db/models/badges/queries.server.ts deleted file mode 100644 index fc28179b1..000000000 --- a/app/db/models/badges/queries.server.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { sql } from "../../sql"; -import type { Badge, User } from "../../types"; - -import deleteManyManagersSql from "./deleteManyManagers.sql"; -import createManagerSql from "./createManager.sql"; -import deleteManyOwnersSql from "./deleteManyOwners.sql"; -import createOwnerSql from "./createOwner.sql"; -import findByOwnerIdSql from "./findByOwnerId.sql"; -import findAllSql from "./findAll.sql"; -import ownersByBadgeIdSql from "./ownersByBadgeId.sql"; -import managersByBadgeIdSql from "./managersByBadgeId.sql"; -import managedByUserIdSql from "./managedByUserId.sql"; - -const deleteManyManagersStm = sql.prepare(deleteManyManagersSql); -const createManagerStm = sql.prepare(createManagerSql); -export const upsertManyManagers = sql.transaction( - ({ - badgeId, - managerIds, - }: { - badgeId: Badge["id"]; - managerIds: Array; - }) => { - deleteManyManagersStm.run({ - badgeId, - }); - - for (const userId of managerIds) { - createManagerStm.run({ - userId, - badgeId, - }); - } - }, -); - -const deleteManyOwnersStm = sql.prepare(deleteManyOwnersSql); -const createOwnerStm = sql.prepare(createOwnerSql); -export const upsertManyOwners = sql.transaction( - ({ - badgeId, - ownerIds, - }: { - badgeId: Badge["id"]; - ownerIds: Array; - }) => { - deleteManyOwnersStm.run({ - badgeId, - }); - - for (const userId of ownerIds) { - createOwnerStm.run({ - userId, - badgeId, - }); - } - }, -); - -const findByOwnerIdStm = sql.prepare(findByOwnerIdSql); - -export type FindByOwnerId = Array< - Pick & { - count: number; - } ->; - -export function findByOwnerId(userId: User["id"]) { - return findByOwnerIdStm.all({ userId }) as FindByOwnerId; -} - -const findAllStm = sql.prepare(findAllSql); - -export type FindAll = Array>; - -export function all() { - return findAllStm.all() as FindAll; -} - -export type OwnersByBadgeId = Array< - Pick & { - count: number; - } ->; - -const ownersByBadgeIdStm = sql.prepare(ownersByBadgeIdSql); -export function ownersByBadgeId(id: Badge["id"]) { - return ownersByBadgeIdStm.all({ id }) as OwnersByBadgeId; -} - -export type ManagersByBadgeId = Array< - Pick ->; - -const managersByBadgeIdStm = sql.prepare(managersByBadgeIdSql); -export function managersByBadgeId(id: Badge["id"]) { - return managersByBadgeIdStm.all({ id }) as ManagersByBadgeId; -} - -const managedByUserIdStm = sql.prepare(managedByUserIdSql); - -export function managedByUserId(userId: User["id"]) { - return managedByUserIdStm.all({ userId }) as Array< - Pick - >; -} diff --git a/app/db/models/builds/countByUserId.sql b/app/db/models/builds/countByUserId.sql deleted file mode 100644 index 953af853b..000000000 --- a/app/db/models/builds/countByUserId.sql +++ /dev/null @@ -1,10 +0,0 @@ -select - count(*) as "count" -from - "Build" -where - "Build"."ownerId" = @userId - and ( - "Build"."private" = 0 - or "Build"."ownerId" = @loggedInUserId - ) diff --git a/app/db/models/builds/createBuild.sql b/app/db/models/builds/createBuild.sql deleted file mode 100644 index b71db65f3..000000000 --- a/app/db/models/builds/createBuild.sql +++ /dev/null @@ -1,22 +0,0 @@ -insert into - "Build" ( - "ownerId", - "title", - "description", - "modes", - "headGearSplId", - "clothesGearSplId", - "shoesGearSplId", - "private" - ) -values - ( - @ownerId, - @title, - @description, - @modes, - @headGearSplId, - @clothesGearSplId, - @shoesGearSplId, - @private - ) returning * diff --git a/app/db/models/builds/createBuildAbility.sql b/app/db/models/builds/createBuildAbility.sql deleted file mode 100644 index de45f02b7..000000000 --- a/app/db/models/builds/createBuildAbility.sql +++ /dev/null @@ -1,4 +0,0 @@ -insert into - "BuildAbility" ("buildId", "gearType", "ability", "slotIndex") -values - (@buildId, @gearType, @ability, @slotIndex) \ No newline at end of file diff --git a/app/db/models/builds/createBuildWeapon.sql b/app/db/models/builds/createBuildWeapon.sql deleted file mode 100644 index 4e728bcd8..000000000 --- a/app/db/models/builds/createBuildWeapon.sql +++ /dev/null @@ -1,4 +0,0 @@ -insert into - "BuildWeapon" ("buildId", "weaponSplId") -values - (@buildId, @weaponSplId) \ No newline at end of file diff --git a/app/db/models/builds/deleteById.sql b/app/db/models/builds/deleteById.sql deleted file mode 100644 index 48ca9b425..000000000 --- a/app/db/models/builds/deleteById.sql +++ /dev/null @@ -1,4 +0,0 @@ -delete from - "Build" -where - "id" = @id; \ No newline at end of file diff --git a/app/db/models/builds/queries.server.ts b/app/db/models/builds/queries.server.ts deleted file mode 100644 index 16337f50d..000000000 --- a/app/db/models/builds/queries.server.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { sql } from "~/db/sql"; -import type { - Build, - BuildWeapon, - GearType, - UserWithPlusTier, -} from "~/db/types"; -import { modesShort, type ModeShort } from "~/modules/in-game-lists"; -import type { BuildAbilitiesTuple } from "~/modules/in-game-lists/types"; - -import countByUserIdSql from "./countByUserId.sql"; -import createBuildSql from "./createBuild.sql"; -import createBuildAbilitySql from "./createBuildAbility.sql"; -import createBuildWeaponSql from "./createBuildWeapon.sql"; -import deleteByIdSql from "./deleteById.sql"; - -const createBuildStm = sql.prepare(createBuildSql); -const createBuildWeaponStm = sql.prepare(createBuildWeaponSql); -const createBuildAbilityStm = sql.prepare(createBuildAbilitySql); -const countByUserIdStm = sql.prepare(countByUserIdSql); -const deleteByIdStm = sql.prepare(deleteByIdSql); - -interface CreateArgs { - ownerId: Build["ownerId"]; - title: Build["title"]; - description: Build["description"]; - modes: Array | null; - headGearSplId: Build["headGearSplId"]; - clothesGearSplId: Build["clothesGearSplId"]; - shoesGearSplId: Build["shoesGearSplId"]; - weaponSplIds: Array; - abilities: BuildAbilitiesTuple; - private: Build["private"]; -} -export const create = sql.transaction((build: CreateArgs) => { - const createdBuild = createBuildStm.get({ - ownerId: build.ownerId, - title: build.title, - description: build.description, - modes: - build.modes && build.modes.length > 0 - ? JSON.stringify( - build.modes - .slice() - .sort((a, b) => modesShort.indexOf(a) - modesShort.indexOf(b)), - ) - : null, - headGearSplId: build.headGearSplId, - clothesGearSplId: build.clothesGearSplId, - shoesGearSplId: build.shoesGearSplId, - private: build.private, - }) as Build; - - for (const weaponSplId of build.weaponSplIds) { - createBuildWeaponStm.run({ - buildId: createdBuild.id, - weaponSplId, - }); - } - - for (const [rowI, row] of build.abilities.entries()) { - const gearType: GearType = - rowI === 0 ? "HEAD" : rowI === 1 ? "CLOTHES" : "SHOES"; - - for (const [abilityI, ability] of row.entries()) { - createBuildAbilityStm.run({ - buildId: createdBuild.id, - gearType, - ability, - slotIndex: abilityI, - }); - } - } -}); - -export const updateByReplacing = sql.transaction( - (build: CreateArgs & { id: Build["id"] }) => { - deleteByIdStm.run({ id: build.id }); - create(build); - }, -); - -export function countByUserId({ - userId, - loggedInUserId, -}: { - userId: Build["ownerId"]; - loggedInUserId?: UserWithPlusTier["id"]; -}) { - return ((countByUserIdStm.get({ userId, loggedInUserId }) as any)?.count ?? - 0) as number; -} - -export function deleteById(id: Build["id"]) { - deleteByIdStm.run({ id }); -} diff --git a/app/db/models/calendar/create.sql b/app/db/models/calendar/create.sql deleted file mode 100644 index 8d061948e..000000000 --- a/app/db/models/calendar/create.sql +++ /dev/null @@ -1,20 +0,0 @@ -insert into - "CalendarEvent" ( - "name", - "authorId", - "tags", - "description", - "discordInviteCode", - "bracketUrl", - "tournamentId" - ) -values - ( - @name, - @authorId, - @tags, - @description, - @discordInviteCode, - @bracketUrl, - @tournamentId - ) returning * diff --git a/app/db/models/calendar/createBadge.sql b/app/db/models/calendar/createBadge.sql deleted file mode 100644 index 69255d51a..000000000 --- a/app/db/models/calendar/createBadge.sql +++ /dev/null @@ -1,4 +0,0 @@ -insert into - "CalendarEventBadge" ("eventId", "badgeId") -values - (@eventId, @badgeId) \ No newline at end of file diff --git a/app/db/models/calendar/createDate.sql b/app/db/models/calendar/createDate.sql deleted file mode 100644 index c3fde4018..000000000 --- a/app/db/models/calendar/createDate.sql +++ /dev/null @@ -1,4 +0,0 @@ -insert into - "CalendarEventDate" ("eventId", "startTime") -values - (@eventId, @startTime) \ No newline at end of file diff --git a/app/db/models/calendar/createMapPoolMap.sql b/app/db/models/calendar/createMapPoolMap.sql deleted file mode 100644 index 9f26e4f3a..000000000 --- a/app/db/models/calendar/createMapPoolMap.sql +++ /dev/null @@ -1,4 +0,0 @@ -insert into - "MapPoolMap" ("calendarEventId", "stageId", "mode") -values - (@calendarEventId, @stageId, @mode) diff --git a/app/db/models/calendar/createTieBreakerMapPoolMap.sql b/app/db/models/calendar/createTieBreakerMapPoolMap.sql deleted file mode 100644 index 27d990fe0..000000000 --- a/app/db/models/calendar/createTieBreakerMapPoolMap.sql +++ /dev/null @@ -1,4 +0,0 @@ -insert into - "MapPoolMap" ("tieBreakerCalendarEventId", "stageId", "mode") -values - (@calendarEventId, @stageId, @mode) diff --git a/app/db/models/calendar/createTournament.sql b/app/db/models/calendar/createTournament.sql deleted file mode 100644 index 701fc3c1e..000000000 --- a/app/db/models/calendar/createTournament.sql +++ /dev/null @@ -1,4 +0,0 @@ -insert into - "Tournament" ("mapPickingStyle", "format") -values - (@mapPickingStyle, @format) returning * diff --git a/app/db/models/calendar/deleteBadgesByEventId.sql b/app/db/models/calendar/deleteBadgesByEventId.sql deleted file mode 100644 index b3900fd2a..000000000 --- a/app/db/models/calendar/deleteBadgesByEventId.sql +++ /dev/null @@ -1,4 +0,0 @@ -delete from - "CalendarEventBadge" -where - "eventId" = @eventId \ No newline at end of file diff --git a/app/db/models/calendar/deleteById.sql b/app/db/models/calendar/deleteById.sql deleted file mode 100644 index c277eb9e1..000000000 --- a/app/db/models/calendar/deleteById.sql +++ /dev/null @@ -1,4 +0,0 @@ -delete from - "CalendarEvent" -where - "id" = @id diff --git a/app/db/models/calendar/deleteDatesByEventId.sql b/app/db/models/calendar/deleteDatesByEventId.sql deleted file mode 100644 index 41da0cfd1..000000000 --- a/app/db/models/calendar/deleteDatesByEventId.sql +++ /dev/null @@ -1,4 +0,0 @@ -delete from - "CalendarEventDate" -where - "eventId" = @eventId \ No newline at end of file diff --git a/app/db/models/calendar/deleteMapPoolMaps.sql b/app/db/models/calendar/deleteMapPoolMaps.sql deleted file mode 100644 index b6a853ce2..000000000 --- a/app/db/models/calendar/deleteMapPoolMaps.sql +++ /dev/null @@ -1,5 +0,0 @@ -delete from - "MapPoolMap" -where - "calendarEventId" = @calendarEventId - or "tieBreakerCalendarEventId" = @calendarEventId diff --git a/app/db/models/calendar/deleteResultTeamsByEventId.sql b/app/db/models/calendar/deleteResultTeamsByEventId.sql deleted file mode 100644 index 1d5c5ee83..000000000 --- a/app/db/models/calendar/deleteResultTeamsByEventId.sql +++ /dev/null @@ -1,4 +0,0 @@ -delete from - "CalendarEventResultTeam" -where - "eventId" = @eventId \ No newline at end of file diff --git a/app/db/models/calendar/deleteTournamentById.sql b/app/db/models/calendar/deleteTournamentById.sql deleted file mode 100644 index 9e626e78b..000000000 --- a/app/db/models/calendar/deleteTournamentById.sql +++ /dev/null @@ -1,4 +0,0 @@ -delete from - "Tournament" -where - id = @id diff --git a/app/db/models/calendar/eventsToReport.sql b/app/db/models/calendar/eventsToReport.sql deleted file mode 100644 index 9e51cc0c1..000000000 --- a/app/db/models/calendar/eventsToReport.sql +++ /dev/null @@ -1,19 +0,0 @@ -select - "CalendarEvent"."id", - "CalendarEvent"."name", - ( - select - max("startTime") - from - "CalendarEventDate" - where - "eventId" = "CalendarEvent"."id" - ) as "startTime" -from - "CalendarEvent" -where - "CalendarEvent"."authorId" = @authorId - and "startTime" > @lowerLimitTime - and "startTime" < @upperLimitTime - and "CalendarEvent"."participantCount" is null - and "CalendarEvent"."tournamentId" is null diff --git a/app/db/models/calendar/findAllBetweenTwoTimestamps.sql b/app/db/models/calendar/findAllBetweenTwoTimestamps.sql deleted file mode 100644 index 3eae540af..000000000 --- a/app/db/models/calendar/findAllBetweenTwoTimestamps.sql +++ /dev/null @@ -1,42 +0,0 @@ -select - "CalendarEvent"."name", - "CalendarEvent"."discordUrl", - "CalendarEvent"."bracketUrl", - "CalendarEvent"."tags", - "CalendarEvent"."tournamentId", - "CalendarEventDate"."id" as "eventDateId", - "CalendarEventDate"."eventId", - "CalendarEventDate"."startTime", - "User"."discordName", - "User"."discordDiscriminator", - "CalendarEventRanks"."nthAppearance", - exists ( - select - 1 - from - "CalendarEventBadge" - where - "CalendarEventBadge"."eventId" = "CalendarEventDate"."eventId" - ) as "hasBadge" -from - "CalendarEvent" - join "CalendarEventDate" on "CalendarEvent"."id" = "CalendarEventDate"."eventId" - join "User" on "CalendarEvent"."authorId" = "User"."id" - join ( - select - "id", - "eventId", - "startTime", - rank() over( - partition by "eventId" - order by - "startTime" asc - ) "nthAppearance" - from - "CalendarEventDate" - ) "CalendarEventRanks" on "CalendarEventDate"."id" = "CalendarEventRanks"."id" -where - "CalendarEventDate"."startTime" between @startTime - and @endTime -order by - "CalendarEventDate"."startTime" asc diff --git a/app/db/models/calendar/findAllEventsWithMapPools.sql b/app/db/models/calendar/findAllEventsWithMapPools.sql deleted file mode 100644 index f80ef787b..000000000 --- a/app/db/models/calendar/findAllEventsWithMapPools.sql +++ /dev/null @@ -1,18 +0,0 @@ -select - "CalendarEvent"."id", - "CalendarEvent"."name", - json_group_array( - json_object( - 'stageId', - "MapPoolMap"."stageId", - 'mode', - "MapPoolMap"."mode" - ) - ) as "mapPool" -from - "CalendarEvent" - join "MapPoolMap" on "CalendarEvent"."id" = "MapPoolMap"."calendarEventId" -group by - "CalendarEvent"."id" -order by - "CalendarEvent"."id" desc \ No newline at end of file diff --git a/app/db/models/calendar/findBadgesByEventId.sql b/app/db/models/calendar/findBadgesByEventId.sql deleted file mode 100644 index 5993f5cf2..000000000 --- a/app/db/models/calendar/findBadgesByEventId.sql +++ /dev/null @@ -1,10 +0,0 @@ -select - "Badge"."id", - "Badge"."code", - "Badge"."hue", - "Badge"."displayName" -from - "CalendarEventBadge" - join "Badge" on "CalendarEventBadge"."badgeId" = "Badge"."id" -where - "CalendarEventBadge"."eventId" = @eventId \ No newline at end of file diff --git a/app/db/models/calendar/findById.sql b/app/db/models/calendar/findById.sql deleted file mode 100644 index 4ee704d73..000000000 --- a/app/db/models/calendar/findById.sql +++ /dev/null @@ -1,34 +0,0 @@ -select - "CalendarEvent"."name", - "CalendarEvent"."description", - "CalendarEvent"."discordInviteCode", - "CalendarEvent"."discordUrl", - "CalendarEvent"."bracketUrl", - "CalendarEvent"."tags", - "CalendarEvent"."tournamentId", - "CalendarEvent"."participantCount", - "Tournament"."mapPickingStyle", - "User"."id" as "authorId", - exists ( - select - 1 - from - "CalendarEventBadge" - where - "CalendarEventBadge"."eventId" = "CalendarEventDate"."eventId" - ) as "hasBadge", - "CalendarEventDate"."startTime", - "CalendarEventDate"."eventId", - "User"."discordName", - "User"."discordDiscriminator", - "User"."discordId", - "User"."discordAvatar" -from - "CalendarEvent" - join "CalendarEventDate" on "CalendarEvent"."id" = "CalendarEventDate"."eventId" - join "User" on "CalendarEvent"."authorId" = "User"."id" - left join "Tournament" on "CalendarEvent"."tournamentId" = "Tournament"."id" -where - "CalendarEvent"."id" = @id -order by - "CalendarEventDate"."startTime" asc diff --git a/app/db/models/calendar/findMapPoolByEventId.sql b/app/db/models/calendar/findMapPoolByEventId.sql deleted file mode 100644 index 137d77cd2..000000000 --- a/app/db/models/calendar/findMapPoolByEventId.sql +++ /dev/null @@ -1,7 +0,0 @@ -select - "stageId", - "mode" -from - "MapPoolMap" -where - "calendarEventId" = @calendarEventId diff --git a/app/db/models/calendar/findMatesByResultTeamId.sql b/app/db/models/calendar/findMatesByResultTeamId.sql deleted file mode 100644 index c6232dc79..000000000 --- a/app/db/models/calendar/findMatesByResultTeamId.sql +++ /dev/null @@ -1,16 +0,0 @@ -select - "CalendarEventResultPlayer"."name", - "User"."id", - "User"."discordName" as "discordName", - "User"."discordDiscriminator" as "discordDiscriminator", - "User"."discordId" as "discordId", - "User"."discordAvatar" as "discordAvatar" -from - "CalendarEventResultPlayer" - left join "User" on "User"."id" = "CalendarEventResultPlayer"."userId" -where - "teamId" = @teamId - and ( - "CalendarEventResultPlayer"."userId" is null - or "CalendarEventResultPlayer"."userId" != @userId - ) \ No newline at end of file diff --git a/app/db/models/calendar/findMatesByTournamentTeamId.sql b/app/db/models/calendar/findMatesByTournamentTeamId.sql deleted file mode 100644 index dac04fcb9..000000000 --- a/app/db/models/calendar/findMatesByTournamentTeamId.sql +++ /dev/null @@ -1,12 +0,0 @@ -select - "User"."id", - "User"."discordName", - "User"."discordDiscriminator", - "User"."discordId", - "User"."discordAvatar" -from - "TournamentTeamMember" - left join "User" on "User"."id" = "TournamentTeamMember"."userId" -where - "TournamentTeamMember"."tournamentTeamId" = @teamId - and "TournamentTeamMember"."userId" != @userId diff --git a/app/db/models/calendar/findRecentMapPoolsByAuthorId.sql b/app/db/models/calendar/findRecentMapPoolsByAuthorId.sql deleted file mode 100644 index 6ef3c3ba8..000000000 --- a/app/db/models/calendar/findRecentMapPoolsByAuthorId.sql +++ /dev/null @@ -1,21 +0,0 @@ -select - "CalendarEvent"."id", - "CalendarEvent"."name", - json_group_array( - json_object( - 'stageId', - "MapPoolMap"."stageId", - 'mode', - "MapPoolMap"."mode" - ) - ) as "mapPool" -from - "CalendarEvent" - join "MapPoolMap" on "CalendarEvent"."id" = "MapPoolMap"."calendarEventId" -where - "CalendarEvent"."authorId" = @authorId -group by - "CalendarEvent"."id" -order by - "CalendarEvent"."id" desc -limit 5 \ No newline at end of file diff --git a/app/db/models/calendar/findResultsByUserId.sql b/app/db/models/calendar/findResultsByUserId.sql deleted file mode 100644 index d4b478a92..000000000 --- a/app/db/models/calendar/findResultsByUserId.sql +++ /dev/null @@ -1,58 +0,0 @@ -select - "CalendarEvent"."id" as "eventId", - null as "tournamentId", - "CalendarEventResultTeam"."placement", - "CalendarEvent"."participantCount", - "CalendarEvent"."name" as "eventName", - "CalendarEventResultTeam"."id" as "teamId", - "CalendarEventResultTeam"."name" as "teamName", - ( - select - max("startTime") - from - "CalendarEventDate" - where - "eventId" = "CalendarEvent"."id" - ) as "startTime", - exists ( - select - 1 - from - "UserResultHighlight" - where - "userId" = @userId - and "teamId" = "CalendarEventResultTeam"."id" - ) as "isHighlight" -from - "CalendarEventResultPlayer" - join "CalendarEventResultTeam" on "CalendarEventResultTeam"."id" = "CalendarEventResultPlayer"."teamId" - join "CalendarEvent" on "CalendarEvent"."id" = "CalendarEventResultTeam"."eventId" -where - "CalendarEventResultPlayer"."userId" = @userId -union -all -select - null as "eventId", - "TournamentResult"."tournamentId", - "TournamentResult"."placement", - "TournamentResult"."participantCount", - "CalendarEvent"."name" as "eventName", - "TournamentTeam"."id" as "teamId", - "TournamentTeam"."name" as "teamName", - ( - select - max("startTime") - from - "CalendarEventDate" - where - "eventId" = "CalendarEvent"."id" - ) as "startTime", - "TournamentResult"."isHighlight" -from - "TournamentResult" - left join "TournamentTeam" on "TournamentTeam"."id" = "TournamentResult"."tournamentTeamId" - left join "CalendarEvent" on "CalendarEvent"."tournamentId" = "TournamentResult"."tournamentId" -where - "TournamentResult"."userId" = @userId -order by - "startTime" desc diff --git a/app/db/models/calendar/findTieBreakerMapPoolByEventId.sql b/app/db/models/calendar/findTieBreakerMapPoolByEventId.sql deleted file mode 100644 index 285160949..000000000 --- a/app/db/models/calendar/findTieBreakerMapPoolByEventId.sql +++ /dev/null @@ -1,7 +0,0 @@ -select - "stageId", - "mode" -from - "MapPoolMap" -where - "tieBreakerCalendarEventId" = @calendarEventId diff --git a/app/db/models/calendar/findTournamentTeamMemberCountsByEventId.sql b/app/db/models/calendar/findTournamentTeamMemberCountsByEventId.sql deleted file mode 100644 index 8484305ff..000000000 --- a/app/db/models/calendar/findTournamentTeamMemberCountsByEventId.sql +++ /dev/null @@ -1,9 +0,0 @@ -select - count(*) as "memberCount" -from - "TournamentTeam" - left join "TournamentTeamMember" on "TournamentTeamMember"."tournamentTeamId" = "TournamentTeam"."id" -where - "TournamentTeam"."tournamentId" = @tournamentId -group by - "TournamentTeam"."id"; diff --git a/app/db/models/calendar/findWinnersByEventId.sql b/app/db/models/calendar/findWinnersByEventId.sql deleted file mode 100644 index 1643590f1..000000000 --- a/app/db/models/calendar/findWinnersByEventId.sql +++ /dev/null @@ -1,18 +0,0 @@ -select - "CalendarEventResultTeam"."id", - "CalendarEventResultTeam"."name" as "teamName", - "CalendarEventResultTeam"."placement", - "CalendarEventResultPlayer"."userId" as "playerId", - "CalendarEventResultPlayer"."name" as "playerName", - "User"."discordName" as "playerDiscordName", - "User"."discordDiscriminator" as "playerDiscordDiscriminator", - "User"."discordId" as "playerDiscordId", - "User"."discordAvatar" as "playerDiscordAvatar" -from - "CalendarEventResultTeam" - left join "CalendarEventResultPlayer" on "CalendarEventResultPlayer"."teamId" = "CalendarEventResultTeam"."id" - left join "User" on "User"."id" = "CalendarEventResultPlayer"."userId" -where - "CalendarEventResultTeam"."eventId" = @eventId -order by - "CalendarEventResultTeam"."placement" asc \ No newline at end of file diff --git a/app/db/models/calendar/insertResultPlayer.sql b/app/db/models/calendar/insertResultPlayer.sql deleted file mode 100644 index e24b033fc..000000000 --- a/app/db/models/calendar/insertResultPlayer.sql +++ /dev/null @@ -1,4 +0,0 @@ -insert into - "CalendarEventResultPlayer" ("teamId", "userId", "name") -values - (@teamId, @userId, @name) \ No newline at end of file diff --git a/app/db/models/calendar/insertResultTeam.sql b/app/db/models/calendar/insertResultTeam.sql deleted file mode 100644 index 117a0b81e..000000000 --- a/app/db/models/calendar/insertResultTeam.sql +++ /dev/null @@ -1,4 +0,0 @@ -insert into - "CalendarEventResultTeam" ("eventId", "name", "placement") -values - (@eventId, @name, @placement) returning * \ No newline at end of file diff --git a/app/db/models/calendar/queries.server.ts b/app/db/models/calendar/queries.server.ts deleted file mode 100644 index 680331f2d..000000000 --- a/app/db/models/calendar/queries.server.ts +++ /dev/null @@ -1,564 +0,0 @@ -import { dateToDatabaseTimestamp } from "~/utils/dates"; -import { sql } from "../../sql"; -import type { - CalendarEvent, - CalendarEventDate, - User, - Badge, - CalendarEventTag, - CalendarEventBadge, - CalendarEventResultTeam, - CalendarEventResultPlayer, - MapPoolMap, - Tournament, -} from "../../types"; -import { MapPool } from "~/modules/map-pool-serializer"; - -import createSql from "./create.sql"; -import updateSql from "./update.sql"; -import createDateSql from "./createDate.sql"; -import deleteDatesByEventIdSql from "./deleteDatesByEventId.sql"; -import createBadgeSql from "./createBadge.sql"; -import deleteBadgesByEventIdSql from "./deleteBadgesByEventId.sql"; -import updateParticipantsCountSql from "./updateParticipantsCount.sql"; -import deleteResultTeamsByEventIdSql from "./deleteResultTeamsByEventId.sql"; -import insertResultTeamSql from "./insertResultTeam.sql"; -import insertResultPlayerSql from "./insertResultPlayer.sql"; -import findWinnersByEventIdSql from "./findWinnersByEventId.sql"; -import findResultsByUserIdSql from "./findResultsByUserId.sql"; -import findMatesByTournamentTeamIdSql from "./findMatesByTournamentTeamId.sql"; -import findMatesByResultTeamIdSql from "./findMatesByResultTeamId.sql"; -import findAllBetweenTwoTimestampsSql from "./findAllBetweenTwoTimestamps.sql"; -import findByIdSql from "./findById.sql"; -import startTimesOfRangeSql from "./startTimesOfRange.sql"; -import findBadgesByEventIdSql from "./findBadgesByEventId.sql"; -import eventsToReportSql from "./eventsToReport.sql"; -import createMapPoolMapSql from "./createMapPoolMap.sql"; -import deleteMapPoolMapsSql from "./deleteMapPoolMaps.sql"; -import createTieBreakerMapPoolMapSql from "./createTieBreakerMapPoolMap.sql"; -import findMapPoolByEventIdSql from "./findMapPoolByEventId.sql"; -import findRecentMapPoolsByAuthorIdSql from "./findRecentMapPoolsByAuthorId.sql"; -import findAllEventsWithMapPoolsSql from "./findAllEventsWithMapPools.sql"; -import findTieBreakerMapPoolByEventIdSql from "./findTieBreakerMapPoolByEventId.sql"; -import deleteByIdSql from "./deleteById.sql"; -import createTournamentSql from "./createTournament.sql"; -import deleteTournamentByIdSql from "./deleteTournamentById.sql"; -import findTournamentTeamMemberCountsByEventIdSql from "./findTournamentTeamMemberCountsByEventId.sql"; -import { sumArray } from "~/utils/number"; - -const createStm = sql.prepare(createSql); -const updateStm = sql.prepare(updateSql); -const createDateStm = sql.prepare(createDateSql); -const deleteDatesByEventIdStm = sql.prepare(deleteDatesByEventIdSql); -const createBadgeStm = sql.prepare(createBadgeSql); -const deleteBadgesByEventIdStm = sql.prepare(deleteBadgesByEventIdSql); -const createMapPoolMapStm = sql.prepare(createMapPoolMapSql); -const deleteMapPoolMapsStm = sql.prepare(deleteMapPoolMapsSql); -const createTieBreakerMapPoolMapStm = sql.prepare( - createTieBreakerMapPoolMapSql, -); -const findMapPoolByEventIdStm = sql.prepare(findMapPoolByEventIdSql); -const findTieBreakerMapPoolByEventIdtm = sql.prepare( - findTieBreakerMapPoolByEventIdSql, -); -const deleteByIdStm = sql.prepare(deleteByIdSql); -const deleteTournamentByIdStm = sql.prepare(deleteTournamentByIdSql); -const createTournamentStm = sql.prepare(createTournamentSql); - -const createTournament = ( - args: Omit, -) => { - return createTournamentStm.get(args) as Tournament; -}; - -export type CreateArgs = Pick< - CalendarEvent, - | "name" - | "authorId" - | "tags" - | "description" - | "discordInviteCode" - | "bracketUrl" -> & { - startTimes: Array; - badges: Array; - mapPoolMaps?: Array>; - createTournament: boolean; - mapPickingStyle: Tournament["mapPickingStyle"]; -}; -export const create = sql.transaction( - ({ - startTimes, - badges, - mapPoolMaps = [], - ...calendarEventArgs - }: CreateArgs) => { - let tournamentId; - if (calendarEventArgs.createTournament) { - tournamentId = createTournament({ - // TODO: format picking - format: "DE", - mapPickingStyle: calendarEventArgs.mapPickingStyle, - }).id; - } - const createdEvent = createStm.get({ - ...calendarEventArgs, - tournamentId, - }) as CalendarEvent; - - for (const startTime of startTimes) { - createDateStm.run({ - eventId: createdEvent.id, - startTime, - }); - } - - for (const badgeId of badges) { - createBadgeStm.run({ - eventId: createdEvent.id, - badgeId, - }); - } - - upsertMapPool({ - eventId: createdEvent.id, - mapPoolMaps, - isFullTournament: calendarEventArgs.createTournament, - }); - - return createdEvent.id; - }, -); - -export type Update = Omit< - CreateArgs, - "authorId" | "createTournament" | "mapPickingStyle" -> & { - eventId: CalendarEvent["id"]; -}; -export const update = sql.transaction( - ({ - startTimes, - badges, - mapPoolMaps = [], - eventId, - ...calendarEventArgs - }: Update) => { - const event = updateStm.get({ - ...calendarEventArgs, - eventId, - }) as CalendarEvent; - - deleteDatesByEventIdStm.run({ eventId }); - for (const startTime of startTimes) { - createDateStm.run({ - eventId, - startTime, - }); - } - - deleteBadgesByEventIdStm.run({ eventId }); - for (const badgeId of badges) { - createBadgeStm.run({ - eventId, - badgeId, - }); - } - - // can't edit tournament specific info after creation - if (!event.tournamentId) { - upsertMapPool({ - eventId, - mapPoolMaps, - isFullTournament: false, - }); - } - }, -); - -function upsertMapPool({ - eventId, - mapPoolMaps, - isFullTournament, -}: { - eventId: Update["eventId"]; - mapPoolMaps: NonNullable; - isFullTournament: boolean; -}) { - deleteMapPoolMapsStm.run({ calendarEventId: eventId }); - if (isFullTournament) { - for (const mapPoolArgs of mapPoolMaps) { - createTieBreakerMapPoolMapStm.run({ - calendarEventId: eventId, - ...mapPoolArgs, - }); - } - } else { - for (const mapPoolArgs of mapPoolMaps) { - createMapPoolMapStm.run({ - calendarEventId: eventId, - ...mapPoolArgs, - }); - } - } -} - -const updateParticipantsCountStm = sql.prepare(updateParticipantsCountSql); -const deleteResultTeamsByEventIdStm = sql.prepare( - deleteResultTeamsByEventIdSql, -); -const insertResultTeamStm = sql.prepare(insertResultTeamSql); -const insertResultPlayerStm = sql.prepare(insertResultPlayerSql); - -export const upsertReportedScores = sql.transaction( - ({ - eventId, - participantCount, - results, - }: { - eventId: CalendarEvent["id"]; - participantCount: CalendarEvent["participantCount"]; - results: Array<{ - teamName: CalendarEventResultTeam["name"]; - placement: CalendarEventResultTeam["placement"]; - players: Array<{ - userId: CalendarEventResultPlayer["userId"]; - name: CalendarEventResultPlayer["name"]; - }>; - }>; - }) => { - updateParticipantsCountStm.run({ eventId, participantCount }); - deleteResultTeamsByEventIdStm.run({ eventId }); - - for (const { players, ...teamArgs } of results) { - const teamInDb = insertResultTeamStm.get({ - eventId, - name: teamArgs.teamName, - placement: teamArgs.placement, - }) as CalendarEventResultTeam; - - for (const playerArgs of players) { - insertResultPlayerStm.run({ - teamId: teamInDb.id, - userId: playerArgs.userId, - name: playerArgs.name, - }); - } - } - }, -); - -const findWinnersByEventIdStm = sql.prepare(findWinnersByEventIdSql); - -export function findResultsByEventId(eventId: CalendarEvent["id"]) { - const rows = findWinnersByEventIdStm.all({ eventId }) as Array<{ - id: CalendarEventResultTeam["id"]; - teamName: CalendarEventResultTeam["name"]; - placement: CalendarEventResultTeam["placement"]; - playerId: CalendarEventResultPlayer["userId"]; - playerName: CalendarEventResultPlayer["name"] | null; - playerDiscordName: User["discordName"] | null; - playerDiscordDiscriminator: User["discordDiscriminator"] | null; - playerDiscordId: User["discordId"] | null; - playerDiscordAvatar: User["discordAvatar"]; - }>; - - const result: Array<{ - teamName: CalendarEventResultTeam["name"]; - placement: CalendarEventResultTeam["placement"]; - players: Array< - | string - | Pick< - User, - | "id" - | "discordId" - | "discordName" - | "discordDiscriminator" - | "discordAvatar" - > - >; - }> = []; - - for (const row of rows) { - const team = result.find((team) => team.teamName === row.teamName); - const player = row.playerName ?? { - // player name and user id are mutually exclusive - // also if user id exists we know a joined user also has to exist - id: row.playerId!, - discordId: row.playerDiscordId!, - discordName: row.playerDiscordName!, - discordDiscriminator: row.playerDiscordDiscriminator!, - discordAvatar: row.playerDiscordAvatar, - }; - - if (team) { - team.players.push(player); - } else { - result.push({ - teamName: row.teamName, - placement: row.placement, - players: [player], - }); - } - } - - return result; -} - -const findResultsByUserIdStm = sql.prepare(findResultsByUserIdSql); -const findMatesByResultTeamIdStm = sql.prepare(findMatesByResultTeamIdSql); -const findMatesByTournamentTeamIdStm = sql.prepare( - findMatesByTournamentTeamIdSql, -); -export function findResultsByUserId(userId: User["id"]) { - return ( - findResultsByUserIdStm.all({ userId }) as Array<{ - eventId: CalendarEvent["id"] | null; - tournamentId: CalendarEvent["tournamentId"] | null; - teamId: CalendarEventResultTeam["id"]; - eventName: CalendarEvent["name"]; - teamName: CalendarEventResultTeam["name"]; - placement: CalendarEventResultTeam["placement"]; - participantCount: CalendarEvent["participantCount"]; - startTime: CalendarEventDate["startTime"]; - isHighlight: number; - }> - ).map((row) => ({ - ...row, - isHighlight: Boolean(row.isHighlight), - mates: ( - (row.tournamentId - ? findMatesByTournamentTeamIdStm - : findMatesByResultTeamIdStm - ).all({ - teamId: row.teamId, - userId, - }) as Array<{ - name: CalendarEventResultPlayer["name"]; - id: User["id"]; - discordName: User["discordName"]; - discordDiscriminator: User["discordDiscriminator"]; - discordId: User["discordId"]; - discordAvatar: User["discordAvatar"]; - }> - ).map(({ name, ...mate }) => name ?? mate), - })); -} - -const findAllBetweenTwoTimestampsStm = sql.prepare( - findAllBetweenTwoTimestampsSql, -); - -function addTagArray< - T extends { - hasBadge: number; - tags?: CalendarEvent["tags"]; - tournamentId: CalendarEvent["tournamentId"]; - }, ->(arg: T) { - const { hasBadge, ...row } = arg; - const tags = (row.tags ? row.tags.split(",") : []) as Array; - - if (hasBadge) tags.unshift("BADGE"); - if (row.tournamentId) tags.unshift("FULL_TOURNAMENT"); - - return { ...row, tags }; -} - -const findTournamentTeamMemberCountsByEventIdStm = sql.prepare( - findTournamentTeamMemberCountsByEventIdSql, -); - -function addParticipantsCounts< - T extends { tournamentId: CalendarEvent["tournamentId"] }, ->(arg: T) { - if (!arg.tournamentId) return arg; - - const counts = findTournamentTeamMemberCountsByEventIdStm.all({ - tournamentId: arg.tournamentId, - }) as Array<{ - memberCount: number; - }>; - - return { - ...arg, - participantCounts: { - teams: counts.length, - players: sumArray(counts.map((c) => c.memberCount)), - }, - }; -} - -export function findAllBetweenTwoTimestamps({ - startTime, - endTime, -}: { - startTime: Date; - endTime: Date; -}) { - const rows = findAllBetweenTwoTimestampsStm.all({ - startTime: dateToDatabaseTimestamp(startTime), - endTime: dateToDatabaseTimestamp(endTime), - }) as Array< - Pick< - CalendarEvent, - "name" | "discordUrl" | "bracketUrl" | "tags" | "tournamentId" - > & - Pick & { - eventDateId: CalendarEventDate["id"]; - } & Pick & { - nthAppearance: number; - } & { hasBadge: number } & { - participantCounts?: { teams: number; players: number }; - } - >; - - return rows.map(addTagArray).map(addBadges).map(addParticipantsCounts); -} - -const findByIdStm = sql.prepare(findByIdSql); -export function findById(id: CalendarEvent["id"]) { - const [firstRow, ...rest] = findByIdStm.all({ id }) as Array< - Pick< - CalendarEvent, - | "name" - | "description" - | "discordUrl" - | "discordInviteCode" - | "bracketUrl" - | "tags" - | "authorId" - | "participantCount" - | "tournamentId" - > & - Pick & - Pick & - Pick< - User, - "discordName" | "discordDiscriminator" | "discordId" | "discordAvatar" - > & { hasBadge: number } - >; - - if (!firstRow) return null; - - return addTagArray({ - ...firstRow, - startTimes: [firstRow, ...rest].map((row) => row.startTime), - startTime: undefined, - }); -} - -function addBadges< - T extends { eventId: CalendarEvent["id"]; tags?: CalendarEvent["tags"] }, ->(arg: T) { - return { - ...arg, - badgePrizes: arg.tags?.includes("BADGE") - ? findBadgesByEventId(arg.eventId) - : [], - }; -} - -const startTimesOfRangeStm = sql.prepare(startTimesOfRangeSql); -export function startTimesOfRange({ - startTime, - endTime, -}: { - startTime: Date; - endTime: Date; -}) { - return ( - startTimesOfRangeStm.all({ - startTime: dateToDatabaseTimestamp(startTime), - endTime: dateToDatabaseTimestamp(endTime), - }) as Array> - ).map(({ startTime }) => startTime); -} - -const findBadgesByEventIdStm = sql.prepare(findBadgesByEventIdSql); -export function findBadgesByEventId(eventId: CalendarEvent["id"]) { - return findBadgesByEventIdStm.all({ eventId }) as Array< - Pick - >; -} - -export function findMapPoolByEventId(calendarEventId: CalendarEvent["id"]) { - const rows = findMapPoolByEventIdStm.all({ calendarEventId }) as Array< - Pick - >; - - if (rows.length === 0) return; - - return MapPool.parse(rows); -} - -const eventsToReportStm = sql.prepare(eventsToReportSql); -export function eventsToReport(authorId?: CalendarEvent["authorId"]) { - if (!authorId) return []; - - const oneMonthAgo = new Date(); - oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1); - - return ( - eventsToReportStm.all({ - authorId, - upperLimitTime: dateToDatabaseTimestamp(new Date()), - lowerLimitTime: dateToDatabaseTimestamp(oneMonthAgo), - }) as Array> - ).map((row) => ({ id: row.id, name: row.name })); -} - -const findRecentMapPoolsByAuthorIdStm = sql.prepare( - findRecentMapPoolsByAuthorIdSql, -); -export function findRecentMapPoolsByAuthorId( - authorId: CalendarEvent["authorId"], -) { - return ( - findRecentMapPoolsByAuthorIdStm.all({ authorId }) as Array< - Pick & { - mapPool: string; - } - > - ).map((row) => ({ - id: row.id, - name: row.name, - serializedMapPool: MapPool.serialize(JSON.parse(row.mapPool)), - })); -} - -const findAllEventsWithMapPoolsStm = sql.prepare(findAllEventsWithMapPoolsSql); -export function findAllEventsWithMapPools() { - return ( - findAllEventsWithMapPoolsStm.all() as Array< - Pick & { - mapPool: string; - } - > - ).map((row) => ({ - id: row.id, - name: row.name, - serializedMapPool: MapPool.serialize(JSON.parse(row.mapPool)), - })); -} - -export function findTieBreakerMapPoolByEventId( - calendarEventId: string | number, -) { - return findTieBreakerMapPoolByEventIdtm.all({ calendarEventId }) as Array< - Pick - >; -} - -export const deleteById = sql.transaction( - ({ - eventId, - tournamentId, - }: { - eventId: number; - tournamentId: number | null; - }) => { - deleteByIdStm.run({ id: eventId }); - if (tournamentId) deleteTournamentByIdStm.run({ id: tournamentId }); - }, -); diff --git a/app/db/models/calendar/startTimesOfRange.sql b/app/db/models/calendar/startTimesOfRange.sql deleted file mode 100644 index b21061211..000000000 --- a/app/db/models/calendar/startTimesOfRange.sql +++ /dev/null @@ -1,7 +0,0 @@ -select - "CalendarEventDate"."startTime" -from - "CalendarEventDate" -where - "CalendarEventDate"."startTime" between @startTime - and @endTime \ No newline at end of file diff --git a/app/db/models/calendar/update.sql b/app/db/models/calendar/update.sql deleted file mode 100644 index fec802434..000000000 --- a/app/db/models/calendar/update.sql +++ /dev/null @@ -1,10 +0,0 @@ -update - "CalendarEvent" -set - "name" = @name, - "tags" = @tags, - "description" = @description, - "discordInviteCode" = @discordInviteCode, - "bracketUrl" = @bracketUrl -where - "id" = @eventId returning *; diff --git a/app/db/models/calendar/updateParticipantsCount.sql b/app/db/models/calendar/updateParticipantsCount.sql deleted file mode 100644 index a27abdd16..000000000 --- a/app/db/models/calendar/updateParticipantsCount.sql +++ /dev/null @@ -1,6 +0,0 @@ -update - "CalendarEvent" -set - "participantCount" = @participantCount -where - "id" = @eventId \ No newline at end of file diff --git a/app/db/models/plusSuggestions/create.sql b/app/db/models/plusSuggestions/create.sql deleted file mode 100644 index 1e63f66db..000000000 --- a/app/db/models/plusSuggestions/create.sql +++ /dev/null @@ -1,18 +0,0 @@ -insert into - "PlusSuggestion" ( - "text", - "authorId", - "suggestedId", - "month", - "year", - "tier" - ) -values - ( - @text, - @authorId, - @suggestedId, - @month, - @year, - @tier - ) \ No newline at end of file diff --git a/app/db/models/plusSuggestions/delete.sql b/app/db/models/plusSuggestions/delete.sql deleted file mode 100644 index d7494039b..000000000 --- a/app/db/models/plusSuggestions/delete.sql +++ /dev/null @@ -1,4 +0,0 @@ -delete from - "PlusSuggestion" -where - "id" = @id \ No newline at end of file diff --git a/app/db/models/plusSuggestions/deleteAll.sql b/app/db/models/plusSuggestions/deleteAll.sql deleted file mode 100644 index 0f35d8d60..000000000 --- a/app/db/models/plusSuggestions/deleteAll.sql +++ /dev/null @@ -1,7 +0,0 @@ -delete from - "PlusSuggestion" -where - "suggestedId" = @suggestedId - and "tier" = @tier - and "month" = @month - and "year" = @year \ No newline at end of file diff --git a/app/db/models/plusSuggestions/deleteSuggestionWithComments.sql b/app/db/models/plusSuggestions/deleteSuggestionWithComments.sql deleted file mode 100644 index cef2867c6..000000000 --- a/app/db/models/plusSuggestions/deleteSuggestionWithComments.sql +++ /dev/null @@ -1,7 +0,0 @@ -delete from - "PlusSuggestion" -where - "month" = @month - and "year" = @year - and "suggestedId" = @suggestedId - and "tier" = @tier \ No newline at end of file diff --git a/app/db/models/plusSuggestions/findVisibleForUser.sql b/app/db/models/plusSuggestions/findVisibleForUser.sql deleted file mode 100644 index 889d74127..000000000 --- a/app/db/models/plusSuggestions/findVisibleForUser.sql +++ /dev/null @@ -1,25 +0,0 @@ -select - suggestion."id", - suggestion."createdAt", - suggestion."text", - suggestion."tier", - author."id" as "authorId", - author."discordId" as "authorDiscordId", - author."discordName" as "authorDiscordName", - author."discordDiscriminator" as "authorDiscordDiscriminator", - suggested."id" as "suggestedId", - suggested."discordId" as "suggestedDiscordId", - suggested."discordName" as "suggestedDiscordName", - suggested."discordDiscriminator" as "suggestedDiscordDiscriminator", - suggested."discordAvatar" as "suggestedDiscordAvatar", - suggested."bio" as "suggestedBio" -from - "PlusSuggestion" as suggestion - join "User" as author on suggestion."authorId" = author."id" - join "User" as suggested on suggestion."suggestedId" = suggested."id" -where - "month" = @month - and "year" = @year - and "tier" >= @plusTier -order by - suggestion."id" asc \ No newline at end of file diff --git a/app/db/models/plusSuggestions/queries.server.ts b/app/db/models/plusSuggestions/queries.server.ts deleted file mode 100644 index fc774cf44..000000000 --- a/app/db/models/plusSuggestions/queries.server.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { formatDistance } from "date-fns"; -import type { MonthYear } from "~/modules/plus-server"; -import { nextNonCompletedVoting } from "~/modules/plus-server"; -import { atOrError } from "~/utils/arrays"; -import { databaseTimestampToDate } from "~/utils/dates"; -import { sql } from "../../sql"; -import type { PlusSuggestion, User, UserWithPlusTier } from "../../types"; - -import createSql from "./create.sql"; -import findVisibleForUserSql from "./findVisibleForUser.sql"; -import tiersSuggestedForSql from "./tiersSuggestedFor.sql"; -import deleteSql from "./delete.sql"; -import deleteAllSql from "./deleteAll.sql"; -import deleteSuggestionWithCommentsSql from "./deleteSuggestionWithComments.sql"; - -const createStm = sql.prepare(createSql); -export function create( - args: Pick< - PlusSuggestion, - "text" | "authorId" | "suggestedId" | "month" | "year" | "tier" - >, -) { - createStm.run(args); -} - -const findVisibleForUserStm = sql.prepare(findVisibleForUserSql); - -export interface FindVisibleForUserSuggestedUserInfo { - suggestedUser: Pick< - User, - | "id" - | "discordId" - | "discordName" - | "discordDiscriminator" - | "discordAvatar" - | "bio" - >; - suggestions: (Pick & { - createdAtRelative: string; - author: Pick< - User, - "id" | "discordId" | "discordName" | "discordDiscriminator" - >; - })[]; -} -export interface FindVisibleForUser { - [tier: string]: FindVisibleForUserSuggestedUserInfo[]; -} - -export function findVisibleForUser( - args: MonthYear & - Pick & { includeBio?: boolean }, -): FindVisibleForUser | undefined { - if (typeof args.plusTier !== "number") return; - return sortNewestPlayersToBeSuggestedFirst( - mapFindVisibleForUserRowsToResult( - findVisibleForUserStm.all(args), - args.includeBio, - ), - ); -} - -export function findAll(args: MonthYear & { includeBio?: boolean }) { - // plusTier 0 is a bit hacky way to get all suggestions - // while reusing the query - return findVisibleForUser({ ...args, plusTier: 0 })!; -} - -function mapFindVisibleForUserRowsToResult( - rows: any[], - includeBio?: boolean, -): FindVisibleForUser { - return rows.reduce((result: FindVisibleForUser, row) => { - const usersOfTier = result[row.tier] ?? []; - result[row.tier] = usersOfTier; - - const suggestionInfo = { - id: row.id, - createdAt: row.createdAt, - createdAtRelative: formatDistance( - databaseTimestampToDate(row.createdAt), - new Date(), - { addSuffix: true }, - ), - text: row.text, - author: { - id: row.authorId, - discordId: row.authorDiscordId, - discordName: row.authorDiscordName, - discordDiscriminator: row.authorDiscordDiscriminator, - }, - }; - - const existingSuggestion = usersOfTier.find( - (suggestion) => suggestion.suggestedUser.id === row.suggestedId, - ); - - if (existingSuggestion) { - existingSuggestion.suggestions.push(suggestionInfo); - } else { - usersOfTier.push({ - suggestedUser: { - id: row.suggestedId, - discordId: row.suggestedDiscordId, - discordName: row.suggestedDiscordName, - discordDiscriminator: row.suggestedDiscordDiscriminator, - discordAvatar: row.suggestedDiscordAvatar, - bio: includeBio ? row.suggestedBio : null, - }, - suggestions: [suggestionInfo], - }); - } - - return result; - }, {}); -} - -function sortNewestPlayersToBeSuggestedFirst( - suggestions: FindVisibleForUser, -): FindVisibleForUser { - return Object.fromEntries( - Object.entries(suggestions).map(([tier, suggestions]) => [ - tier, - suggestions.sort( - (a, b) => - atOrError(b.suggestions, 0).createdAt - - atOrError(a.suggestions, 0).createdAt, - ), - ]), - ); -} - -const tiersSuggestedForStm = sql.prepare(tiersSuggestedForSql); -export function tiersSuggestedFor(args: MonthYear & { userId: User["id"] }) { - return JSON.parse( - tiersSuggestedForStm.pluck().get(args) as any, - ) as User["id"][]; -} - -const deleteStm = sql.prepare(deleteSql); - -export function del(id: PlusSuggestion["id"]) { - deleteStm.run({ id }); -} - -const deleteAllStm = sql.prepare(deleteAllSql); -export function deleteAll(args: Pick) { - deleteAllStm.run({ ...args, ...nextNonCompletedVoting(new Date()) }); -} - -const deleteSuggestionWithCommentsStm = sql.prepare( - deleteSuggestionWithCommentsSql, -); -export function deleteSuggestionWithComments( - args: Pick, -) { - deleteSuggestionWithCommentsStm.run(args); -} diff --git a/app/db/models/plusSuggestions/tiersSuggestedFor.sql b/app/db/models/plusSuggestions/tiersSuggestedFor.sql deleted file mode 100644 index daf7b220c..000000000 --- a/app/db/models/plusSuggestions/tiersSuggestedFor.sql +++ /dev/null @@ -1,15 +0,0 @@ -select - json_group_array("tier") -from - ( - select - distinct "tier" - from - "PlusSuggestion" - where - "month" = @month - and "year" = @year - and "suggestedId" = @userId - order by - "tier" asc - ) \ No newline at end of file diff --git a/app/db/models/plusVotes/create.sql b/app/db/models/plusVotes/create.sql deleted file mode 100644 index 793791e0f..000000000 --- a/app/db/models/plusVotes/create.sql +++ /dev/null @@ -1,20 +0,0 @@ -insert into - "PlusVote" ( - "month", - "year", - "tier", - "authorId", - "votedId", - "score", - "validAfter" - ) -values - ( - @month, - @year, - @tier, - @authorId, - @votedId, - @score, - @validAfter - ) \ No newline at end of file diff --git a/app/db/models/plusVotes/deleteMany.sql b/app/db/models/plusVotes/deleteMany.sql deleted file mode 100644 index 094d2d258..000000000 --- a/app/db/models/plusVotes/deleteMany.sql +++ /dev/null @@ -1,6 +0,0 @@ -delete from - "PlusVote" -where - "authorId" = @authorId - and "month" = @month - and "year" = @year \ No newline at end of file diff --git a/app/db/models/plusVotes/hasVoted.sql b/app/db/models/plusVotes/hasVoted.sql deleted file mode 100644 index 8e8affb64..000000000 --- a/app/db/models/plusVotes/hasVoted.sql +++ /dev/null @@ -1,8 +0,0 @@ -select - 1 -from - "PlusVote" -where - "authorId" = @userId - and "month" = @month - and "year" = @year \ No newline at end of file diff --git a/app/db/models/plusVotes/plusServerMembers.sql b/app/db/models/plusVotes/plusServerMembers.sql deleted file mode 100644 index b5399c489..000000000 --- a/app/db/models/plusVotes/plusServerMembers.sql +++ /dev/null @@ -1,12 +0,0 @@ -select - "User"."id", - "User"."discordId", - "User"."discordName", - "User"."discordDiscriminator", - "User"."discordAvatar", - "User"."bio" -from - "User" - join "PlusTier" on "User"."id" = "PlusTier"."userId" -where - "PlusTier"."tier" = @plusTier \ No newline at end of file diff --git a/app/db/models/plusVotes/queries.server.ts b/app/db/models/plusVotes/queries.server.ts deleted file mode 100644 index 52ff32e8d..000000000 --- a/app/db/models/plusVotes/queries.server.ts +++ /dev/null @@ -1,215 +0,0 @@ -import shuffle from "just-shuffle"; -import invariant from "tiny-invariant"; -import type { MonthYear } from "~/modules/plus-server"; -import { - nextNonCompletedVoting, - rangeToMonthYear, -} from "~/modules/plus-server"; -import { atOrError } from "~/utils/arrays"; -import { dateToDatabaseTimestamp } from "~/utils/dates"; -import type { Unpacked } from "~/utils/types"; -import { db } from "../.."; -import { sql } from "../../sql"; -import type { - PlusVote, - PlusVotingResult, - User, - UserWithPlusTier, -} from "../../types"; -import type { FindVisibleForUserSuggestedUserInfo } from "../plusSuggestions/queries.server"; - -import createSql from "./create.sql"; -import deleteManySql from "./deleteMany.sql"; -import resultsByMonthYearSql from "./resultsByMonthYear.sql"; -import plusServerMembersSql from "./plusServerMembers.sql"; -import hasVotedSql from "./hasVoted.sql"; - -const createStm = sql.prepare(createSql); -const deleteManyStm = sql.prepare(deleteManySql); - -export type UpsertManyPlusVotesArgs = (Pick< - PlusVote, - "month" | "year" | "tier" | "authorId" | "votedId" | "score" -> & { validAfter: Date })[]; -export const upsertMany = sql.transaction((votes: UpsertManyPlusVotesArgs) => { - const firstVote = atOrError(votes, 0); - deleteManyStm.run({ - authorId: firstVote.authorId, - month: firstVote.month, - year: firstVote.year, - }); - - for (const vote of votes) { - const { validAfter, ...rest } = vote; - createStm.run({ - ...rest, - validAfter: dateToDatabaseTimestamp(validAfter), - }); - } -}); - -type PlusVotingResultsByMonthYearDatabaseResult = (PlusVotingResultUser & - Pick)[]; - -const resultsByMonthYearStm = sql.prepare(resultsByMonthYearSql); - -type PlusVotingResultUser = Pick< - User, - "id" | "discordAvatar" | "discordDiscriminator" | "discordName" | "discordId" -> & - Pick; -export interface PlusVotingResultByMonthYear { - results: { - tier: number; - passed: PlusVotingResultUser[]; - failed: PlusVotingResultUser[]; - }[]; - scores: (Pick< - PlusVotingResult, - "score" | "tier" | "wasSuggested" | "passedVoting" - > & { userId: User["id"] })[]; -} - -export function resultsByMontYear( - args: MonthYear, -): PlusVotingResultByMonthYear { - const results = resultsByMonthYearStm.all( - args, - ) as PlusVotingResultsByMonthYearDatabaseResult; - - return { - results: groupPlusVotingResults(results), - scores: scoresFromPlusVotingResults(results), - }; -} - -function groupPlusVotingResults( - rows: PlusVotingResultsByMonthYearDatabaseResult, -): PlusVotingResultByMonthYear["results"] { - const grouped: Record< - number, - { passed: PlusVotingResultUser[]; failed: PlusVotingResultUser[] } - > = {}; - - for (const row of rows) { - const playersOfTier = grouped[row.tier] ?? { - passed: [], - failed: [], - }; - grouped[row.tier] = playersOfTier; - - playersOfTier[row.passedVoting ? "passed" : "failed"].push({ - id: row.id, - discordAvatar: row.discordAvatar, - discordDiscriminator: row.discordDiscriminator, - discordName: row.discordName, - discordId: row.discordId, - wasSuggested: row.wasSuggested, - }); - } - - return Object.entries(grouped) - .map(([tier, { passed, failed }]) => ({ - tier: Number(tier), - passed, - failed, - })) - .sort((a, b) => a.tier - b.tier); -} - -function scoresFromPlusVotingResults( - rows: PlusVotingResultsByMonthYearDatabaseResult, -) { - return rows - .map((row) => ({ - userId: row.id, - tier: row.tier, - score: row.score, - wasSuggested: row.wasSuggested, - passedVoting: row.passedVoting, - })) - .sort((a, b) => a.tier - b.tier); -} - -const plusServerMembersStm = sql.prepare(plusServerMembersSql); - -export type UsersForVoting = { - user: Pick< - User, - | "id" - | "discordId" - | "discordName" - | "discordDiscriminator" - | "discordAvatar" - | "bio" - >; - suggestions?: FindVisibleForUserSuggestedUserInfo["suggestions"]; -}[]; - -export function usersForVoting( - loggedInUser?: Pick, -) { - if (!loggedInUser || !loggedInUser.plusTier) return; - - const { month, year } = rangeToMonthYear(nextNonCompletedVoting(new Date())); - const members = plusServerMembersStm.all({ - plusTier: loggedInUser.plusTier, - }) as Unpacked["user"][]; - - const allSuggestedTiers = db.plusSuggestions.findVisibleForUser({ - plusTier: loggedInUser.plusTier, - month, - year, - includeBio: true, - }); - invariant(allSuggestedTiers); - const suggestedUsers = allSuggestedTiers[loggedInUser.plusTier] ?? []; - - const result: UsersForVoting = []; - - for (const member of members) { - result.push({ - user: { - id: member.id, - discordId: member.discordId, - discordName: member.discordName, - discordDiscriminator: member.discordDiscriminator, - discordAvatar: member.discordAvatar, - bio: member.bio, - }, - }); - } - - for (const { suggestedUser, suggestions } of suggestedUsers) { - result.push({ - user: { - id: suggestedUser.id, - discordId: suggestedUser.discordId, - discordName: suggestedUser.discordName, - discordDiscriminator: suggestedUser.discordDiscriminator, - discordAvatar: suggestedUser.discordAvatar, - bio: suggestedUser.bio, - }, - suggestions, - }); - } - - return shuffle(result.filter(({ user }) => user.id !== loggedInUser.id)); -} - -const hasVotedStm = sql.prepare(hasVotedSql); - -export function hasVoted({ - month, - year, - user, -}: MonthYear & { user?: Pick }) { - if (!user) return false; - return Boolean( - hasVotedStm.get({ - userId: user.id, - month, - year, - }), - ); -} diff --git a/app/db/models/plusVotes/resultsByMonthYear.sql b/app/db/models/plusVotes/resultsByMonthYear.sql deleted file mode 100644 index ba5ff1c56..000000000 --- a/app/db/models/plusVotes/resultsByMonthYear.sql +++ /dev/null @@ -1,18 +0,0 @@ -select - "PlusVotingResult"."wasSuggested", - "PlusVotingResult"."passedVoting", - "PlusVotingResult"."tier", - "PlusVotingResult"."score", - "User"."id", - "User"."discordAvatar", - "User"."discordDiscriminator", - "User"."discordName", - "User"."discordId" -from - "PlusVotingResult" - join "User" on "PlusVotingResult"."votedId" = "User".id -where - "PlusVotingResult"."month" = @month - and "PlusVotingResult"."year" = @year -order by - "User"."discordName" collate nocase asc diff --git a/app/db/models/users/addPatronData.sql b/app/db/models/users/addPatronData.sql deleted file mode 100644 index aa8aeff0c..000000000 --- a/app/db/models/users/addPatronData.sql +++ /dev/null @@ -1,8 +0,0 @@ -update - "User" -set - "patronTier" = @patronTier, - "patronSince" = @patronSince, - "patronTill" = null -where - "discordId" = @discordId diff --git a/app/db/models/users/addResultHighlight.sql b/app/db/models/users/addResultHighlight.sql deleted file mode 100644 index 341911c4d..000000000 --- a/app/db/models/users/addResultHighlight.sql +++ /dev/null @@ -1,10 +0,0 @@ -insert into - "UserResultHighlight" ( - "userId", - "teamId" - ) -values - ( - @userId, - @teamId - ) \ No newline at end of file diff --git a/app/db/models/users/addTournamentResultHighlight.sql b/app/db/models/users/addTournamentResultHighlight.sql deleted file mode 100644 index 7671f05f6..000000000 --- a/app/db/models/users/addTournamentResultHighlight.sql +++ /dev/null @@ -1,7 +0,0 @@ -update - "TournamentResult" -set - "isHighlight" = 1 -where - "userId" = @userId - and "tournamentTeamId" = @tournamentTeamId diff --git a/app/db/models/users/addUserWeapon.sql b/app/db/models/users/addUserWeapon.sql deleted file mode 100644 index da85cb512..000000000 --- a/app/db/models/users/addUserWeapon.sql +++ /dev/null @@ -1,4 +0,0 @@ -insert into - "UserWeapon" ("userId", "weaponSplId", "order", "isFavorite") -values - (@userId, @weaponSplId, @order, @isFavorite); diff --git a/app/db/models/users/deleteAllPatronData.sql b/app/db/models/users/deleteAllPatronData.sql deleted file mode 100644 index 0d0e776a2..000000000 --- a/app/db/models/users/deleteAllPatronData.sql +++ /dev/null @@ -1,8 +0,0 @@ -update - "User" -set - "patronTier" = null, - "patronSince" = null, - "patronTill" = null -where - "patronTill" < @now; diff --git a/app/db/models/users/deleteAllResultHighlights.sql b/app/db/models/users/deleteAllResultHighlights.sql deleted file mode 100644 index adc215286..000000000 --- a/app/db/models/users/deleteAllResultHighlights.sql +++ /dev/null @@ -1,4 +0,0 @@ -delete from - "UserResultHighlight" -where - "userId" = @userId \ No newline at end of file diff --git a/app/db/models/users/deleteAllTournamentResultHighlights.sql b/app/db/models/users/deleteAllTournamentResultHighlights.sql deleted file mode 100644 index b50da0fcf..000000000 --- a/app/db/models/users/deleteAllTournamentResultHighlights.sql +++ /dev/null @@ -1,6 +0,0 @@ -update - "TournamentResult" -set - "isHighlight" = 0 -where - "userId" = @userId diff --git a/app/db/models/users/deleteById.sql b/app/db/models/users/deleteById.sql deleted file mode 100644 index 43938ad7f..000000000 --- a/app/db/models/users/deleteById.sql +++ /dev/null @@ -1,4 +0,0 @@ -delete from - "User" -where - id = @id returning * \ No newline at end of file diff --git a/app/db/models/users/deleteUserWeapons.sql b/app/db/models/users/deleteUserWeapons.sql deleted file mode 100644 index 2b4a95322..000000000 --- a/app/db/models/users/deleteUserWeapons.sql +++ /dev/null @@ -1,4 +0,0 @@ -delete from - "UserWeapon" -where - "userId" = @userId; diff --git a/app/db/models/users/fillPlusTiers.sql b/app/db/models/users/fillPlusTiers.sql deleted file mode 100644 index 06d31a0b7..000000000 --- a/app/db/models/users/fillPlusTiers.sql +++ /dev/null @@ -1,9 +0,0 @@ -insert into - "PlusTier" ("userId", "tier") -select - "userId", - "tier" -from - "FreshPlusTier" -where - "tier" is not null diff --git a/app/db/models/users/findAll.sql b/app/db/models/users/findAll.sql deleted file mode 100644 index 6d190fc9c..000000000 --- a/app/db/models/users/findAll.sql +++ /dev/null @@ -1,9 +0,0 @@ -select - "User"."id", - "User"."discordId", - "User"."discordName", - "User"."discordDiscriminator", - "PlusTier"."tier" as "plusTier" -from - "User" - left join "PlusTier" on "PlusTier"."userId" = "User"."id" \ No newline at end of file diff --git a/app/db/models/users/findAllPatrons.sql b/app/db/models/users/findAllPatrons.sql deleted file mode 100644 index cc4bcd769..000000000 --- a/app/db/models/users/findAllPatrons.sql +++ /dev/null @@ -1,13 +0,0 @@ -select - "id", - "discordId", - "discordName", - "discordDiscriminator", - "patronTier" -from - "User" -where - "patronTier" is not null -order by - "patronTier" desc, - "patronSince" asc \ No newline at end of file diff --git a/app/db/models/users/findAllPlusMembers.sql b/app/db/models/users/findAllPlusMembers.sql deleted file mode 100644 index d081fd339..000000000 --- a/app/db/models/users/findAllPlusMembers.sql +++ /dev/null @@ -1,8 +0,0 @@ -select - "User"."discordId", - "PlusTier"."tier" as "plusTier" -from - "User" - left join "PlusTier" on "PlusTier"."userId" = "User"."id" -where - "PlusTier"."tier" is not null \ No newline at end of file diff --git a/app/db/models/users/findByIdentifier.sql b/app/db/models/users/findByIdentifier.sql deleted file mode 100644 index e352a295d..000000000 --- a/app/db/models/users/findByIdentifier.sql +++ /dev/null @@ -1,28 +0,0 @@ -select - "User".*, - "Team"."name" as "teamName", - "Team"."customUrl" as "teamCustomUrl", - "Team"."id" as "teamId", - "UserSubmittedImage"."url" as "teamAvatarUrl", - "PlusTier"."tier" as "plusTier", - json_group_array( - json_object( - 'weaponSplId', - "UserWeapon"."weaponSplId", - 'isFavorite', - "UserWeapon"."isFavorite" - ) - ) as "weapons" -from - "User" - left join "PlusTier" on "PlusTier"."userId" = "User"."id" - left join "UserWeapon" on "UserWeapon"."userId" = "User"."id" - left join "TeamMember" on "TeamMember"."userId" = "User"."id" - left join "Team" on "Team"."id" = "TeamMember"."teamId" - left join "UserSubmittedImage" on "Team"."avatarImgId" = "UserSubmittedImage"."id" -where - "User"."discordId" = @identifier - or "User"."id" = @identifier - or "User"."customUrl" = @identifier -order by - "UserWeapon"."order" asc diff --git a/app/db/models/users/forcePatron.sql b/app/db/models/users/forcePatron.sql deleted file mode 100644 index b7002b1b3..000000000 --- a/app/db/models/users/forcePatron.sql +++ /dev/null @@ -1,8 +0,0 @@ -update - User -set - "patronTier" = @patronTier, - "patronSince" = @patronSince, - "patronTill" = @patronTill -where - "id" = @id; diff --git a/app/db/models/users/linkPlayer.sql b/app/db/models/users/linkPlayer.sql deleted file mode 100644 index d05877163..000000000 --- a/app/db/models/users/linkPlayer.sql +++ /dev/null @@ -1,6 +0,0 @@ -update - "SplatoonPlayer" -set - "userId" = @userId -where - "id" = @playerId diff --git a/app/db/models/users/makeVideoAdder.sql b/app/db/models/users/makeVideoAdder.sql deleted file mode 100644 index ff9119813..000000000 --- a/app/db/models/users/makeVideoAdder.sql +++ /dev/null @@ -1,6 +0,0 @@ -update - "User" -set - "isVideoAdder" = 1 -where - "id" = @id diff --git a/app/db/models/users/queries.server.ts b/app/db/models/users/queries.server.ts deleted file mode 100644 index dc60807e5..000000000 --- a/app/db/models/users/queries.server.ts +++ /dev/null @@ -1,349 +0,0 @@ -import { sql } from "../../sql"; -import type { - CalendarEventResultTeam, - SplatoonPlayer, - TournamentTeam, - User, - UserWeapon, - UserWithPlusTier, -} from "../../types"; -import { parseDBJsonArray } from "~/utils/sql"; -import { dateToDatabaseTimestamp } from "~/utils/dates"; - -import addPatronDataSql from "./addPatronData.sql"; -import addResultHighlightSql from "./addResultHighlight.sql"; -import addTournamentResultHighlightSql from "./addTournamentResultHighlight.sql"; -import deleteAllPatronDataSql from "./deleteAllPatronData.sql"; -import deleteAllResultHighlightsSql from "./deleteAllResultHighlights.sql"; -import deleteAllTournamentResultHighlightsSql from "./deleteAllTournamentResultHighlights.sql"; -import deleteByIdSql from "./deleteById.sql"; -import findAllSql from "./findAll.sql"; -import findAllPatronsSql from "./findAllPatrons.sql"; -import findAllPlusMembersSql from "./findAllPlusMembers.sql"; -import findByIdentifierSql from "./findByIdentifier.sql"; -import searchSql from "./search.sql"; -import searchExactSql from "./searchExact.sql"; -import updateByDiscordIdSql from "./updateByDiscordId.sql"; -import updateDiscordIdSql from "./updateDiscordId.sql"; -import updateProfileSql from "./updateProfile.sql"; -import upsertSql from "./upsert.sql"; -import upsertLiteSql from "./upsertLite.sql"; -import addUserWeaponSql from "./addUserWeapon.sql"; -import deleteUserWeaponsSql from "./deleteUserWeapons.sql"; -import wipePlusTiersSql from "./wipePlusTiers.sql"; -import fillPlusTiersSql from "./fillPlusTiers.sql"; -import forcePatronSql from "./forcePatron.sql"; -import makeVideoAdderSql from "./makeVideoAdder.sql"; -import linkPlayerSql from "./linkPlayer.sql"; -import unlinkPlayerSql from "./unlinkPlayer.sql"; -import { syncXPBadges } from "~/features/badges"; - -const upsertStm = sql.prepare(upsertSql); -export function upsert( - input: Pick< - User, - | "discordId" - | "discordName" - | "discordDiscriminator" - | "discordAvatar" - | "twitch" - | "twitter" - | "youtubeId" - | "discordUniqueName" - >, -) { - return upsertStm.get(input) as User; -} - -const upsertLiteStm = sql.prepare(upsertLiteSql); -export function upsertLite( - input: Pick< - User, - | "discordId" - | "discordName" - | "discordDiscriminator" - | "discordAvatar" - | "discordUniqueName" - >, -) { - return upsertLiteStm.get(input) as User; -} - -const updateProfileStm = sql.prepare(updateProfileSql); -const addUserWeaponStm = sql.prepare(addUserWeaponSql); -const deleteUserWeaponsStm = sql.prepare(deleteUserWeaponsSql); -export const updateProfile = sql.transaction( - ({ - weapons, - ...rest - }: Pick< - User, - | "country" - | "id" - | "bio" - | "customUrl" - | "motionSens" - | "stickSens" - | "inGameName" - | "css" - | "favoriteBadgeId" - | "showDiscordUniqueName" - | "commissionText" - | "commissionsOpen" - > & { weapons: Pick[] }) => { - deleteUserWeaponsStm.run({ userId: rest.id }); - for (const [i, weapon] of weapons.entries()) { - addUserWeaponStm.run({ - userId: rest.id, - weaponSplId: weapon.weaponSplId, - isFavorite: weapon.isFavorite, - order: i + 1, - }); - } - - return updateProfileStm.get(rest) as User; - }, -); - -const updateByDiscordIdStm = sql.prepare(updateByDiscordIdSql); -export const updateMany = sql.transaction( - ( - argsArr: Array< - Pick< - User, - "discordAvatar" | "discordName" | "discordUniqueName" | "discordId" - > - >, - ) => { - for (const updateArgs of argsArr) { - updateByDiscordIdStm.run(updateArgs); - } - }, -); - -const deleteAllPatronDataStm = sql.prepare(deleteAllPatronDataSql); -const addPatronDataStm = sql.prepare(addPatronDataSql); -export type UpdatePatronDataArgs = Array< - Pick ->; -export const updatePatronData = sql.transaction( - (argsArr: UpdatePatronDataArgs) => { - deleteAllPatronDataStm.run({ now: dateToDatabaseTimestamp(new Date()) }); - - for (const args of argsArr) { - addPatronDataStm.run(args); - } - }, -); - -const deleteByIdStm = sql.prepare(deleteByIdSql); -const updateDiscordIdStm = sql.prepare(updateDiscordIdSql); -export const migrate = sql.transaction( - (args: { newUserId: User["id"]; oldUserId: User["id"] }) => { - const deletedUser = deleteByIdStm.get({ id: args.newUserId }) as User; - - updateDiscordIdStm.run({ - id: args.oldUserId, - discordId: deletedUser.discordId, - }); - }, -); - -const findByIdentifierStm = sql.prepare(findByIdentifierSql); -export function findByIdentifier(identifier: string | number) { - const { - weapons, - teamName, - teamCustomUrl, - teamAvatarUrl, - teamId, - css, - ...row - } = findByIdentifierStm.get({ identifier }) as any; - - if (!row.id) return; - - return { - ...row, - css: css ? JSON.parse(css) : undefined, - weapons: parseDBJsonArray(weapons), - team: teamName - ? { - name: teamName, - customUrl: teamCustomUrl, - avatarUrl: teamAvatarUrl, - id: teamId, - } - : undefined, - } as - | (Omit & { - css: Record; - weapons: Pick[]; - team?: { - name: string; - customUrl: string; - avatarUrl?: string; - id: number; - }; - }) - | undefined; -} - -const findAllStm = sql.prepare(findAllSql); - -export function findAll() { - return findAllStm.all() as Pick< - UserWithPlusTier, - "id" | "discordId" | "discordName" | "discordDiscriminator" | "plusTier" - >[]; -} - -const findAllPlusMembersStm = sql.prepare(findAllPlusMembersSql); -export function findAllPlusMembers() { - return findAllPlusMembersStm.all() as Array<{ - discordId: User["discordId"]; - plusTier: NonNullable; - }>; -} - -const findAllPatronsStm = sql.prepare(findAllPatronsSql); -export type FindAllPatrons = Array< - Pick< - User, - "id" | "discordId" | "discordName" | "discordDiscriminator" | "patronTier" - > ->; -export function findAllPatrons() { - return findAllPatronsStm.all() as FindAllPatrons; -} - -const forcePatronStm = sql.prepare(forcePatronSql); -export function forcePatron( - user: Pick, -) { - forcePatronStm.run(user); -} - -const deleteAllResultHighlightsStm = sql.prepare(deleteAllResultHighlightsSql); -const deleteAllTournamentResultHighlightsStm = sql.prepare( - deleteAllTournamentResultHighlightsSql, -); -const addResultHighlightStm = sql.prepare(addResultHighlightSql); -const addTournamentResultHighlightStm = sql.prepare( - addTournamentResultHighlightSql, -); -export type UpdateResultHighlightsArgs = { - userId: User["id"]; - resultTeamIds: Array; - resultTournamentTeamIds: Array; -}; -export const updateResultHighlights = sql.transaction( - ({ - userId, - resultTeamIds, - resultTournamentTeamIds, - }: UpdateResultHighlightsArgs) => { - deleteAllResultHighlightsStm.run({ userId }); - deleteAllTournamentResultHighlightsStm.run({ userId }); - for (const teamId of resultTeamIds) { - addResultHighlightStm.run({ userId, teamId }); - } - for (const tournamentTeamId of resultTournamentTeamIds) { - addTournamentResultHighlightStm.run({ userId, tournamentTeamId }); - } - }, -); - -const searchStm = sql.prepare(searchSql); -export function search({ input, limit }: { input: string; limit: number }) { - const searchString = `%${input}%`; - - return ( - searchStm.all({ - discordName: searchString, - inGameName: searchString, - discordUniqueName: searchString, - twitter: searchString, - limit, - }) as Array< - Pick< - UserWithPlusTier, - | "id" - | "discordId" - | "discordAvatar" - | "discordName" - | "discordDiscriminator" - | "customUrl" - | "inGameName" - | "discordUniqueName" - | "showDiscordUniqueName" - | "plusTier" - > - > - ).map((user) => ({ - ...user, - discordUniqueName: user.showDiscordUniqueName - ? user.discordUniqueName - : undefined, - })); -} - -const searchExactStm = sql.prepare(searchExactSql); -export function searchExact(args: { - discordId?: User["discordId"]; - customUrl?: User["customUrl"]; - id?: User["id"]; -}) { - return ( - searchExactStm.all({ - discordId: args.discordId ?? null, - customUrl: args.customUrl ?? null, - id: args.id ?? null, - }) as Array< - Pick< - UserWithPlusTier, - | "id" - | "discordId" - | "discordAvatar" - | "discordName" - | "discordDiscriminator" - | "customUrl" - | "inGameName" - | "discordUniqueName" - | "showDiscordUniqueName" - | "plusTier" - > - > - ).map((user) => ({ - ...user, - discordUniqueName: user.showDiscordUniqueName - ? user.discordUniqueName - : undefined, - })); -} - -const wipePlusTiersStm = sql.prepare(wipePlusTiersSql); -const fillPlusTiersStm = sql.prepare(fillPlusTiersSql); -export const refreshPlusTiers = sql.transaction(() => { - wipePlusTiersStm.run(); - fillPlusTiersStm.run(); -}); - -const makeVideoAdderStm = sql.prepare(makeVideoAdderSql); -export function makeVideoAdder(id: User["id"]) { - return makeVideoAdderStm.run({ id }); -} - -const linkPlayerStm = sql.prepare(linkPlayerSql); -const unlinkPlayerStm = sql.prepare(unlinkPlayerSql); -export function linkPlayer({ - userId, - playerId, -}: { - userId: User["id"]; - playerId: SplatoonPlayer["id"]; -}) { - unlinkPlayerStm.run({ userId }); - linkPlayerStm.run({ userId, playerId }); - syncXPBadges(); -} diff --git a/app/db/models/users/search.sql b/app/db/models/users/search.sql deleted file mode 100644 index 0d321e2ae..000000000 --- a/app/db/models/users/search.sql +++ /dev/null @@ -1,26 +0,0 @@ -select - "id", - "discordName", - "discordId", - "discordAvatar", - "discordDiscriminator", - "discordUniqueName", - "showDiscordUniqueName", - "customUrl", - "inGameName", - "PlusTier"."tier" as "plusTier" -from - "User" - left join "PlusTier" on "PlusTier"."userId" = "User"."id" -where - "discordName" like @discordName - or "inGameName" like @inGameName - or "discordUniqueName" like @discordUniqueName - or "twitter" like @twitter -order by - case - when "PlusTier"."tier" is null then 4 - else "PlusTier"."tier" - end asc -limit - @limit diff --git a/app/db/models/users/searchExact.sql b/app/db/models/users/searchExact.sql deleted file mode 100644 index ac65eaea4..000000000 --- a/app/db/models/users/searchExact.sql +++ /dev/null @@ -1,18 +0,0 @@ -select - "id", - "discordName", - "discordId", - "discordAvatar", - "discordDiscriminator", - "discordUniqueName", - "showDiscordUniqueName", - "customUrl", - "inGameName", - "PlusTier"."tier" as "plusTier" -from - "User" - left join "PlusTier" on "PlusTier"."userId" = "User"."id" -where - "discordId" = @discordId - or "customUrl" = @customUrl - or "id" = @id diff --git a/app/db/models/users/unlinkPlayer.sql b/app/db/models/users/unlinkPlayer.sql deleted file mode 100644 index d5bf4cb10..000000000 --- a/app/db/models/users/unlinkPlayer.sql +++ /dev/null @@ -1,6 +0,0 @@ -update - "SplatoonPlayer" -set - "userId" = null -where - "userId" = @userId diff --git a/app/db/models/users/updateByDiscordId.sql b/app/db/models/users/updateByDiscordId.sql deleted file mode 100644 index 09c9e1fa5..000000000 --- a/app/db/models/users/updateByDiscordId.sql +++ /dev/null @@ -1,8 +0,0 @@ -update - "User" -set - "discordAvatar" = @discordAvatar, - "discordName" = coalesce(@discordName, "discordName"), - "discordUniqueName" = coalesce(@discordUniqueName, "discordUniqueName") -where - "discordId" = @discordId diff --git a/app/db/models/users/updateDiscordId.sql b/app/db/models/users/updateDiscordId.sql deleted file mode 100644 index d8b6d8e3b..000000000 --- a/app/db/models/users/updateDiscordId.sql +++ /dev/null @@ -1,6 +0,0 @@ -update - "User" -set - "discordId" = @discordId -where - "id" = @id \ No newline at end of file diff --git a/app/db/models/users/updateProfile.sql b/app/db/models/users/updateProfile.sql deleted file mode 100644 index c7198b5d2..000000000 --- a/app/db/models/users/updateProfile.sql +++ /dev/null @@ -1,16 +0,0 @@ -update - "User" -set - "country" = @country, - "bio" = @bio, - "customUrl" = @customUrl, - "stickSens" = @stickSens, - "motionSens" = @motionSens, - "inGameName" = @inGameName, - "css" = @css, - "favoriteBadgeId" = @favoriteBadgeId, - "showDiscordUniqueName" = @showDiscordUniqueName, - "commissionsOpen" = @commissionsOpen, - "commissionText" = @commissionText -where - "id" = @id returning * diff --git a/app/db/models/users/upsert.sql b/app/db/models/users/upsert.sql deleted file mode 100644 index 99b34cfe8..000000000 --- a/app/db/models/users/upsert.sql +++ /dev/null @@ -1,31 +0,0 @@ -insert into - "User" ( - "discordId", - "discordName", - "discordDiscriminator", - "discordAvatar", - "discordUniqueName", - "twitch", - "twitter", - "youtubeId" - ) -values - ( - @discordId, - @discordName, - @discordDiscriminator, - @discordAvatar, - @discordUniqueName, - @twitch, - @twitter, - @youtubeId - ) on conflict("discordId") do -update -set - "discordName" = excluded."discordName", - "discordDiscriminator" = excluded."discordDiscriminator", - "discordAvatar" = excluded."discordAvatar", - "discordUniqueName" = excluded."discordUniqueName", - "twitch" = excluded."twitch", - "twitter" = excluded."twitter", - "youtubeId" = excluded."youtubeId" returning * diff --git a/app/db/models/users/upsertLite.sql b/app/db/models/users/upsertLite.sql deleted file mode 100644 index 1cd1c45b3..000000000 --- a/app/db/models/users/upsertLite.sql +++ /dev/null @@ -1,22 +0,0 @@ -insert into - "User" ( - "discordId", - "discordName", - "discordDiscriminator", - "discordAvatar", - "discordUniqueName" - ) -values - ( - @discordId, - @discordName, - @discordDiscriminator, - @discordAvatar, - @discordUniqueName - ) on conflict("discordId") do -update -set - "discordName" = excluded."discordName", - "discordDiscriminator" = excluded."discordDiscriminator", - "discordAvatar" = excluded."discordAvatar", - "discordUniqueName" = excluded."discordUniqueName" returning * diff --git a/app/db/models/users/wipePlusTiers.sql b/app/db/models/users/wipePlusTiers.sql deleted file mode 100644 index c390c0c88..000000000 --- a/app/db/models/users/wipePlusTiers.sql +++ /dev/null @@ -1,2 +0,0 @@ -delete from - "PlusTier" diff --git a/app/db/seed/index.ts b/app/db/seed/index.ts index f432e7a07..62362caad 100644 --- a/app/db/seed/index.ts +++ b/app/db/seed/index.ts @@ -1,10 +1,17 @@ import { faker } from "@faker-js/faker"; import capitalize from "just-capitalize"; import shuffle from "just-shuffle"; +import { nanoid } from "nanoid"; import invariant from "tiny-invariant"; import { ADMIN_DISCORD_ID, ADMIN_ID, INVITE_CODE_LENGTH } from "~/constants"; -import { db } from "~/db"; import { sql } from "~/db/sql"; +import allTags from "~/features/calendar/tags.json"; +import { createVod } from "~/features/vods/queries/createVod.server"; +import type { + AbilityType, + MainWeaponId, + StageId, +} from "~/modules/in-game-lists"; import { abilities, clothesGearIds, @@ -14,57 +21,53 @@ import { shoesGearIds, stageIds, } from "~/modules/in-game-lists"; -import type { - MainWeaponId, - StageId, - AbilityType, -} from "~/modules/in-game-lists"; import { rankedModesShort } from "~/modules/in-game-lists/modes"; -import { MapPool } from "~/modules/map-pool-serializer"; +import { MapPool } from "~/features/map-list-generator/core/map-pool"; import { lastCompletedVoting, nextNonCompletedVoting, rangeToMonthYear, -} from "~/modules/plus-server"; -import allTags from "~/routes/calendar/tags.json"; +} from "~/features/plus-voting/core"; import { dateToDatabaseTimestamp } from "~/utils/dates"; -import type { UpsertManyPlusVotesArgs } from "../models/plusVotes/queries.server"; -import { nanoid } from "nanoid"; import { mySlugify } from "~/utils/urls"; -import { createVod } from "~/features/vods/queries/createVod.server"; -import placements from "./placements.json"; -import { - NZAP_TEST_DISCORD_ID, - ADMIN_TEST_AVATAR, - NZAP_TEST_AVATAR, - NZAP_TEST_ID, - AMOUNT_OF_CALENDAR_EVENTS, -} from "./constants"; -import { TOURNAMENT } from "~/features/tournament/tournament-constants"; -import type { SeedVariation } from "~/routes/seed"; -import { nullFilledArray, pickRandomItem } from "~/utils/arrays"; -import type { Art, UserSubmittedImage } from "../types"; -import { createGroup } from "~/features/sendouq/queries/createGroup.server"; -import { MAP_LIST_PREFERENCE_OPTIONS } from "~/features/sendouq/q-constants"; -import { addMember } from "~/features/sendouq/queries/addMember.server"; -import { createMatch } from "~/features/sendouq/queries/createMatch.server"; -import type { TournamentMapListMap } from "~/modules/tournament-map-list-generator"; -import { addSkills } from "~/features/sendouq/queries/addSkills.server"; -import { reportScore } from "~/features/sendouq/queries/reportScore.server"; +import * as BuildRepository from "~/features/builds/BuildRepository.server"; +import * as CalendarRepository from "~/features/calendar/CalendarRepository.server"; +import * as PlusSuggestionRepository from "~/features/plus-suggestions/PlusSuggestionRepository.server"; +import * as PlusVotingRepository from "~/features/plus-voting/PlusVotingRepository.server"; import { calculateMatchSkills } from "~/features/sendouq/core/skills.server"; -import { winnersArrayToWinner } from "~/features/sendouq/q-utils"; -import { addReportedWeapons } from "~/features/sendouq/queries/addReportedWeapons.server"; -import { findMatchById } from "~/features/sendouq/queries/findMatchById.server"; -import { setGroupAsInactive } from "~/features/sendouq/queries/setGroupAsInactive.server"; -import { addMapResults } from "~/features/sendouq/queries/addMapResults.server"; import { summarizeMaps, summarizePlayerResults, } from "~/features/sendouq/core/summarizer.server"; -import { groupForMatch } from "~/features/sendouq/queries/groupForMatch.server"; +import { MAP_LIST_PREFERENCE_OPTIONS } from "~/features/sendouq/q-constants"; +import { winnersArrayToWinner } from "~/features/sendouq/q-utils"; +import { addMapResults } from "~/features/sendouq/queries/addMapResults.server"; +import { addMember } from "~/features/sendouq/queries/addMember.server"; import { addPlayerResults } from "~/features/sendouq/queries/addPlayerResults.server"; +import { addReportedWeapons } from "~/features/sendouq/queries/addReportedWeapons.server"; +import { addSkills } from "~/features/sendouq/queries/addSkills.server"; +import { createGroup } from "~/features/sendouq/queries/createGroup.server"; +import { createMatch } from "~/features/sendouq/queries/createMatch.server"; +import { findMatchById } from "~/features/sendouq/queries/findMatchById.server"; +import { groupForMatch } from "~/features/sendouq/queries/groupForMatch.server"; +import { reportScore } from "~/features/sendouq/queries/reportScore.server"; +import { setGroupAsInactive } from "~/features/sendouq/queries/setGroupAsInactive.server"; import { updateVCStatus } from "~/features/sendouq/queries/updateVCStatus.server"; +import { TOURNAMENT } from "~/features/tournament/tournament-constants"; +import * as UserRepository from "~/features/user-page/UserRepository.server"; +import type { TournamentMapListMap } from "~/modules/tournament-map-list-generator"; +import type { SeedVariation } from "~/features/api/routes/seed"; +import { nullFilledArray, pickRandomItem } from "~/utils/arrays"; +import type { Art, UserSubmittedImage } from "../types"; +import { + ADMIN_TEST_AVATAR, + AMOUNT_OF_CALENDAR_EVENTS, + NZAP_TEST_AVATAR, + NZAP_TEST_DISCORD_ID, + NZAP_TEST_ID, +} from "./constants"; +import placements from "./placements.json"; const calendarEventWithToToolsSz = () => calendarEventWithToTools(true); const calendarEventWithToToolsTeamsSz = () => @@ -113,12 +116,14 @@ const basicSeeds = (variation?: SeedVariation | null) => [ groups, ]; -export function seed(variation?: SeedVariation | null) { +export async function seed(variation?: SeedVariation | null) { wipeDB(); for (const seedFunc of basicSeeds(variation)) { if (!seedFunc) continue; - seedFunc(); + + // eslint-disable-next-line @typescript-eslint/await-thenable + await seedFunc(); } } @@ -165,7 +170,7 @@ function wipeDB() { } function adminUser() { - db.users.upsert({ + return UserRepository.upsert({ discordDiscriminator: "0", discordId: ADMIN_DISCORD_ID, discordName: "Sendou", @@ -203,7 +208,7 @@ function adminUserWeaponPool() { } function nzapUser() { - db.users.upsert({ + return UserRepository.upsert({ discordDiscriminator: "6227", discordId: NZAP_TEST_DISCORD_ID, discordName: "N-ZAP", @@ -215,9 +220,13 @@ function nzapUser() { }); } -function users() { +async function users() { const usedNames = new Set(); - new Array(500).fill(null).map(fakeUser(usedNames)).forEach(db.users.upsert); + for (let i = 0; i < 500; i++) { + const args = fakeUser(usedNames)(); + + await UserRepository.upsert(args); + } } function userProfiles() { @@ -354,8 +363,8 @@ const idToPlusTier = (id: number) => { throw new Error("Invalid id - no plus tier"); }; -function lastMonthsVoting() { - const votes: UpsertManyPlusVotesArgs = []; +async function lastMonthsVoting() { + const votes = []; const { month, year } = lastCompletedVoting(new Date()); @@ -370,7 +379,7 @@ function lastMonthsVoting() { year, score: 1, tier: idToPlusTier(id), - validAfter: fiveMinutesAgo, + validAfter: dateToDatabaseTimestamp(fiveMinutesAgo), votedId: id, }); } @@ -382,22 +391,22 @@ function lastMonthsVoting() { year, score: -1, tier: idToPlusTier(id), - validAfter: fiveMinutesAgo, + validAfter: dateToDatabaseTimestamp(fiveMinutesAgo), votedId: id, }); } - db.plusVotes.upsertMany(votes); + await PlusVotingRepository.upsertMany(votes); } -function lastMonthSuggestions() { +async function lastMonthSuggestions() { const usersSuggested = [ 3, 10, 14, 90, 120, 140, 200, 201, 203, 204, 205, 216, 217, 218, 219, 220, ]; const { month, year } = lastCompletedVoting(new Date()); for (const id of usersSuggested) { - db.plusSuggestions.create({ + await PlusSuggestionRepository.create({ authorId: 1, month, year, @@ -408,10 +417,10 @@ function lastMonthSuggestions() { } } -function thisMonthsSuggestions() { - const usersInPlus = db.users - .findAll() - .filter((u) => u.plusTier && u.id !== 1); // exclude admin +async function thisMonthsSuggestions() { + const usersInPlus = (await UserRepository.findAllPlusMembers()).filter( + (u) => u.id !== ADMIN_ID, + ); const { month, year } = rangeToMonthYear(nextNonCompletedVoting(new Date())); for (let userId = 150; userId < 190; userId++) { @@ -422,7 +431,7 @@ function thisMonthsSuggestions() { invariant(suggester); invariant(suggester.plusTier); - db.plusSuggestions.create({ + await PlusSuggestionRepository.create({ authorId: suggester.id, month, year, @@ -670,7 +679,7 @@ function calendarEventBadges() { } } -function calendarEventResults() { +async function calendarEventResults() { let userIds = userIdsInRandomOrder(); const eventIdsOfPast = new Set( ( @@ -689,7 +698,7 @@ function calendarEventResults() { // event id = 1 needs to be without results for e2e tests if (Math.random() < 0.3 || eventId === 1) continue; - db.calendarEvents.upsertReportedScores({ + await CalendarRepository.upsertReportedScores({ eventId, participantCount: faker.number.int({ min: 10, max: 250 }), results: new Array(faker.helpers.arrayElement([1, 1, 2, 3, 3, 3, 8, 8])) @@ -1049,7 +1058,7 @@ const randomAbility = (legalTypes: AbilityType[]) => { }; const adminWeaponPool = mainWeaponIds.filter(() => Math.random() > 0.8); -function adminBuilds() { +async function adminBuilds() { for (let i = 0; i < 50; i++) { const randomOrderHeadGear = shuffle(headGearIds.slice()); const randomOrderClothesGear = shuffle(clothesGearIds.slice()); @@ -1059,7 +1068,7 @@ function adminBuilds() { adminWeaponPool.filter((id) => id !== 40).slice(), ); - db.builds.create({ + await BuildRepository.create({ title: `${capitalize(faker.word.adjective())} ${capitalize( faker.word.noun(), )}`, @@ -1102,7 +1111,7 @@ function adminBuilds() { } } -function manySplattershotBuilds() { +async function manySplattershotBuilds() { // ensure 500 has at least one splattershot build for x placement test const users = [ ...userIdsInRandomOrder().filter( @@ -1121,7 +1130,7 @@ function manySplattershotBuilds() { (id) => id !== SPLATTERSHOT_ID, ); - db.builds.create({ + await BuildRepository.create({ private: 0, title: `${capitalize(faker.word.adjective())} ${capitalize( faker.word.noun(), diff --git a/app/db/sql.ts b/app/db/sql.ts index 1584cf062..19aee80e6 100644 --- a/app/db/sql.ts +++ b/app/db/sql.ts @@ -1,5 +1,7 @@ import Database from "better-sqlite3"; import invariant from "tiny-invariant"; +import { Kysely, ParseJSONResultsPlugin, SqliteDialect } from "kysely"; +import type { DB } from "./tables"; invariant(process.env["DB_PATH"], "DB_PATH env variable must be set"); export const sql = new Database(process.env["DB_PATH"]); @@ -7,3 +9,11 @@ export const sql = new Database(process.env["DB_PATH"]); sql.pragma("journal_mode = WAL"); sql.pragma("foreign_keys = ON"); sql.pragma("busy_timeout = 5000"); + +export const db = new Kysely({ + dialect: new SqliteDialect({ + database: sql, + }), + log: process.env.NODE_ENV === "development" ? ["query"] : undefined, + plugins: [new ParseJSONResultsPlugin()], +}); diff --git a/app/db/tables.ts b/app/db/tables.ts new file mode 100644 index 000000000..da3285722 --- /dev/null +++ b/app/db/tables.ts @@ -0,0 +1,631 @@ +import type { + ColumnType, + GeneratedAlways, + Insertable, + Selectable, + SqlBool, +} from "kysely"; +import type { + Ability, + MainWeaponId, + ModeShort, + StageId, +} from "~/modules/in-game-lists"; + +export type Generated = T extends ColumnType + ? ColumnType + : ColumnType; + +export interface AllTeam { + avatarImgId: number | null; + bannerImgId: number | null; + bio: string | null; + createdAt: Generated; + css: ColumnType | null, string | null, string | null>; + customUrl: string; + deletedAt: number | null; + id: GeneratedAlways; + inviteCode: string; + name: string; + twitter: string | null; +} + +export interface AllTeamMember { + createdAt: Generated; + isOwner: Generated; + leftAt: number | null; + role: string | null; + teamId: number; + userId: number; +} + +export interface Team { + avatarImgId: number | null; + bannerImgId: number | null; + bio: string | null; + createdAt: number | null; + css: ColumnType | null, string | null, string | null>; + customUrl: string; + deletedAt: number | null; + id: GeneratedAlways; + inviteCode: string; + name: string; + twitter: string | null; +} + +export interface TeamMember { + createdAt: number | null; + isOwner: number | null; + leftAt: number | null; + role: string | null; + teamId: number; + userId: number; +} + +export interface Art { + authorId: number; + createdAt: Generated; + description: string | null; + id: GeneratedAlways; + imgId: number; + isShowcase: Generated; +} + +export interface ArtTag { + authorId: number; + createdAt: Generated; + id: GeneratedAlways; + name: string; +} + +export interface ArtUserMetadata { + artId: number; + userId: number; +} + +export interface Badge { + code: string; + displayName: string; + hue: number | null; + id: GeneratedAlways; +} +// export type SelectableBadge = Selectable; + +export interface BadgeManager { + badgeId: number; + userId: number; +} + +export type BadgeOwner = { + badgeId: number | null; + userId: number | null; +}; + +export interface Build { + clothesGearSplId: number; + description: string | null; + headGearSplId: number; + id: GeneratedAlways; + modes: ColumnType; + ownerId: number; + private: number | null; + shoesGearSplId: number; + title: string; + updatedAt: Generated; +} + +export interface BuildAbility { + ability: Ability; + buildId: number; + gearType: string; + slotIndex: number; +} + +export interface BuildWeapon { + buildId: number; + weaponSplId: MainWeaponId; +} + +export interface CalendarEvent { + authorId: number; + bracketUrl: string; + description: string | null; + discordInviteCode: string | null; + id: GeneratedAlways; + discordUrl: GeneratedAlways; + name: string; + participantCount: number | null; + tags: string | null; + tournamentId: number | null; +} + +export interface CalendarEventBadge { + badgeId: number; + eventId: number; +} + +export interface CalendarEventDate { + eventId: number; + id: GeneratedAlways; + startTime: number; +} + +export interface CalendarEventResultPlayer { + name: string | null; + teamId: number; + userId: number | null; +} + +export interface CalendarEventResultTeam { + eventId: number; + id: GeneratedAlways; + name: string; + placement: number; +} + +export interface FreshPlusTier { + tier: string | null; + userId: number | null; +} + +export interface Group { + chatCode: string | null; + createdAt: Generated; + id: GeneratedAlways; + inviteCode: string; + latestActionAt: Generated; + mapListPreference: string; + status: string; + teamId: number | null; +} + +export interface GroupLike { + createdAt: Generated; + likerGroupId: number; + targetGroupId: number; +} + +export interface GroupMatch { + alphaGroupId: number; + bravoGroupId: number; + chatCode: string | null; + createdAt: Generated; + id: GeneratedAlways; + memento: string | null; + reportedAt: number | null; + reportedByUserId: number | null; +} + +export interface GroupMatchMap { + id: GeneratedAlways; + index: number; + matchId: number; + mode: ModeShort; + source: string; + stageId: StageId; + winnerGroupId: number | null; +} + +export interface GroupMember { + createdAt: Generated; + groupId: number; + note: string | null; + role: string; + userId: number; +} + +export interface LogInLink { + code: string; + expiresAt: number; + userId: number; +} + +export interface MapPoolMap { + calendarEventId: number | null; + groupId: 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 Migrations { + created_at: string; + id: GeneratedAlways; + name: string; +} + +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; + id: GeneratedAlways; + month: number; + suggestedId: number; + text: string; + tier: number; + year: number; +} + +export interface PlusTier { + tier: number; + userId: number | null; +} + +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: number; + passedVoting: number; +} + +export interface ReportedWeapon { + groupMatchMapId: number | null; + userId: number; + weaponSplId: number; +} + +export interface Skill { + groupMatchId: number | null; + id: GeneratedAlways; + identifier: string | null; + matchesCount: number; + mu: number; + ordinal: number; + season: number; + sigma: number; + tournamentId: number | null; + userId: number | null; +} + +export interface SkillTeamUser { + skillId: number; + userId: number; +} + +export interface SplatoonPlayer { + id: GeneratedAlways; + splId: string; + userId: 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 = + | "AUTO_ALL" + | "AUTO_SZ" + | "AUTO_TC" + | "AUTO_RM" + | "AUTO_CB"; + +type TournamentFormat = "SE" | "DE"; + +export interface Tournament { + format: TournamentFormat; + id: GeneratedAlways; + mapPickingStyle: TournamentMapPickingStyle; + showMapListGenerator: Generated; +} + +export interface TournamentBadgeOwner { + badgeId: number; + userId: number; +} + +export interface TournamentGroup { + id: GeneratedAlways; + number: number; + stageId: StageId; +} + +export interface TournamentMatch { + bestOf: Generated; + chatCode: string | null; + childCount: number; + groupId: number; + id: GeneratedAlways; + number: number; + opponentOne: string; + opponentTwo: string; + roundId: number; + stageId: StageId; + status: number; +} + +export interface TournamentMatchGameResult { + createdAt: Generated; + id: GeneratedAlways; + matchId: number; + mode: ModeShort; + number: number; + reporterId: number; + source: string; + stageId: StageId; + winnerTeamId: number; +} + +export interface TournamentMatchGameResultParticipant { + matchGameResultId: number; + userId: number; +} + +export interface TournamentResult { + isHighlight: Generated; + participantCount: number; + placement: number; + tournamentId: number; + tournamentTeamId: number; + userId: number; +} + +export interface TournamentRound { + groupId: number; + id: GeneratedAlways; + number: number; + stageId: StageId; +} + +export interface TournamentStage { + id: GeneratedAlways; + name: string; + number: number; + settings: string; + tournamentId: number; + type: string; +} + +export interface TournamentSub { + bestWeapons: string; + canVc: number; + createdAt: Generated; + message: string | null; + okWeapons: string | null; + tournamentId: number; + userId: number; + visibility: string; +} + +export interface TournamentTeam { + createdAt: Generated; + id: GeneratedAlways; + inviteCode: string; + name: string; + prefersNotToHost: Generated; + seed: number | null; + tournamentId: number; +} + +export interface TournamentTeamCheckIn { + checkedInAt: number; + tournamentTeamId: number; +} + +export interface TournamentTeamMember { + createdAt: Generated; + isOwner: Generated; + tournamentTeamId: number; + userId: number; +} + +export interface TrustRelationship { + trustGiverUserId: number; + trustReceiverUserId: number; +} + +export interface UnvalidatedUserSubmittedImage { + id: GeneratedAlways; + submitterUserId: number; + url: string; + validatedAt: number | null; +} + +export interface UnvalidatedVideo { + eventId: number | null; + id: GeneratedAlways; + submitterUserId: number; + title: string; + type: string; + validatedAt: number | null; + youtubeDate: number; + youtubeId: string; +} + +export interface User { + banned: Generated; + bio: string | null; + commissionsOpen: Generated; + commissionText: string | null; + country: string | null; + css: ColumnType | null, string | null, string | null>; + customUrl: string | null; + discordAvatar: string | null; + discordDiscriminator: string; + discordId: string; + discordName: string; + discordUniqueName: string | null; + favoriteBadgeId: number | null; + id: GeneratedAlways; + inGameName: string | null; + isArtist: Generated; + isVideoAdder: Generated; + languages: string | null; + motionSens: number | null; + patronSince: number | null; + patronTier: number | null; + patronTill: number | null; + showDiscordUniqueName: Generated; + stickSens: number | null; + twitch: string | null; + twitter: string | null; + vc: Generated; + youtubeId: string | null; +} + +export interface UserResultHighlight { + teamId: number; + userId: number; +} + +export interface UserSubmittedImage { + id: GeneratedAlways; + submitterUserId: number | null; + url: string | null; + validatedAt: number | null; +} + +export interface UserWeapon { + createdAt: Generated; + isFavorite: Generated; + order: number; + userId: number; + weaponSplId: MainWeaponId; +} + +export interface Video { + eventId: number | null; + id: GeneratedAlways; + submitterUserId: number | null; + title: string | null; + type: string | null; + validatedAt: number | null; + youtubeDate: number | null; + youtubeId: string | null; +} + +export interface VideoMatch { + id: GeneratedAlways; + 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; + mode: ModeShort; + month: number; + name: string; + nameDiscriminator: string; + playerId: number; + power: number; + rank: number; + region: string; + title: string; + weaponSplId: MainWeaponId; + year: number; +} + +export type Tables = { [P in keyof DB]: Selectable }; +export type TablesInsertable = { [P in keyof DB]: Insertable }; + +export interface DB { + AllTeam: AllTeam; + AllTeamMember: AllTeamMember; + Art: Art; + ArtTag: ArtTag; + ArtUserMetadata: ArtUserMetadata; + Badge: Badge; + BadgeManager: BadgeManager; + BadgeOwner: BadgeOwner; + Build: Build; + BuildAbility: BuildAbility; + BuildWeapon: BuildWeapon; + CalendarEvent: CalendarEvent; + CalendarEventBadge: CalendarEventBadge; + CalendarEventDate: CalendarEventDate; + CalendarEventResultPlayer: CalendarEventResultPlayer; + CalendarEventResultTeam: CalendarEventResultTeam; + FreshPlusTier: FreshPlusTier; + Group: Group; + GroupLike: GroupLike; + GroupMatch: GroupMatch; + GroupMatchMap: GroupMatchMap; + GroupMember: GroupMember; + LogInLink: LogInLink; + MapPoolMap: MapPoolMap; + MapResult: MapResult; + migrations: Migrations; + PlayerResult: PlayerResult; + PlusSuggestion: PlusSuggestion; + PlusTier: PlusTier; + PlusVote: PlusVote; + PlusVotingResult: PlusVotingResult; + ReportedWeapon: ReportedWeapon; + Skill: Skill; + SkillTeamUser: SkillTeamUser; + SplatoonPlayer: SplatoonPlayer; + TaggedArt: TaggedArt; + Team: Team; + TeamMember: TeamMember; + Tournament: Tournament; + TournamentBadgeOwner: TournamentBadgeOwner; + TournamentGroup: TournamentGroup; + TournamentMatch: TournamentMatch; + TournamentMatchGameResult: TournamentMatchGameResult; + TournamentMatchGameResultParticipant: TournamentMatchGameResultParticipant; + TournamentResult: TournamentResult; + TournamentRound: TournamentRound; + TournamentStage: TournamentStage; + TournamentSub: TournamentSub; + TournamentTeam: TournamentTeam; + TournamentTeamCheckIn: TournamentTeamCheckIn; + TournamentTeamMember: TournamentTeamMember; + TrustRelationship: TrustRelationship; + UnvalidatedUserSubmittedImage: UnvalidatedUserSubmittedImage; + UnvalidatedVideo: UnvalidatedVideo; + User: User; + UserResultHighlight: UserResultHighlight; + UserSubmittedImage: UserSubmittedImage; + UserWeapon: UserWeapon; + Video: Video; + VideoMatch: VideoMatch; + VideoMatchPlayer: VideoMatchPlayer; + XRankPlacement: XRankPlacement; +} diff --git a/app/db/types.ts b/app/db/types.ts index 8461d1575..b7d06adb7 100644 --- a/app/db/types.ts +++ b/app/db/types.ts @@ -6,7 +6,7 @@ import type { RankedModeShort, StageId, } from "~/modules/in-game-lists"; -import type allTags from "../routes/calendar/tags.json"; +import type allTags from "~/features/calendar/tags.json"; import type { TieredSkill } from "~/features/mmr/tiered.server"; export interface User { @@ -171,7 +171,7 @@ export interface Build { ownerId: number; title: string; /** Private builds are only visible on the user builds page to the owner only */ - private: number; + private: number | null; description: string | null; modes: string | null; headGearSplId: number; diff --git a/app/features/admin/AdminRepository.server.ts b/app/features/admin/AdminRepository.server.ts new file mode 100644 index 000000000..02a2060a4 --- /dev/null +++ b/app/features/admin/AdminRepository.server.ts @@ -0,0 +1,113 @@ +import { db, sql } from "~/db/sql"; +import { syncXPBadges } from "../badges"; +import type { Tables } from "~/db/tables"; +import { dateToDatabaseTimestamp } from "~/utils/dates"; + +const removeOldLikesStm = sql.prepare(/*sql*/ ` + delete from + "GroupLike" + where + "GroupLike"."createdAt" < cast(strftime('%s', datetime('now', 'start of day', '-7 days')) as int) +`); + +const removeOldGroupStm = sql.prepare(/*sql*/ ` + delete from + "Group" + where "Group"."id" in ( + select "Group"."id" + from "Group" + left join "GroupMatch" on "Group"."id" = "GroupMatch"."alphaGroupId" or "Group"."id" = "GroupMatch"."bravoGroupId" + where "Group"."status" = 'INACTIVE' + and "GroupMatch"."id" is null + ) +`); + +const cleanUpStm = sql.prepare(/*sql*/ ` + vacuum +`); + +export const cleanUp = () => { + removeOldLikesStm.run(); + removeOldGroupStm.run(); + cleanUpStm.run(); +}; + +export function migrate(args: { newUserId: number; oldUserId: number }) { + return db.transaction().execute(async (trx) => { + const deletedUser = await trx + .deleteFrom("User") + .where("User.id", "=", args.newUserId) + .returning("discordId") + .executeTakeFirstOrThrow(); + + await trx + .updateTable("User") + .set({ discordId: deletedUser.discordId }) + .where("User.id", "=", args.oldUserId) + .execute(); + }); +} + +export function refreshPlusTiers() { + return db.transaction().execute(async (trx) => { + await trx.deleteFrom("PlusTier").execute(); + + await trx + .insertInto("PlusTier") + .columns(["userId", "tier"]) + .expression((eb) => + eb + .selectFrom("FreshPlusTier") + .select(["FreshPlusTier.userId", "FreshPlusTier.tier"]) + .where("FreshPlusTier.tier", "is not", null), + ) + .execute(); + }); +} + +export function makeVideoAdderByUserId(userId: number) { + return db + .updateTable("User") + .set({ isVideoAdder: 1 }) + .where("User.id", "=", userId) + .execute(); +} + +export async function linkUserAndPlayer({ + userId, + playerId, +}: { + userId: number; + playerId: number; +}) { + await db + .updateTable("SplatoonPlayer") + .set({ userId: null }) + .where("SplatoonPlayer.userId", "=", userId) + .execute(); + + await db + .updateTable("SplatoonPlayer") + .set({ userId }) + .where("SplatoonPlayer.id", "=", playerId) + .execute(); + + syncXPBadges(); +} + +export function forcePatron(args: { + id: number; + patronTier: Tables["User"]["patronTier"]; + patronSince: Date; + patronTill: Date; +}) { + return db + .updateTable("User") + .set({ + patronTier: args.patronTier, + patronSince: dateToDatabaseTimestamp(args.patronSince), + patronTill: dateToDatabaseTimestamp(args.patronTill), + }) + .where("User.id", "=", args.id) + .execute(); +} diff --git a/app/features/admin/AdminService.server.ts b/app/features/admin/AdminService.server.ts deleted file mode 100644 index eb553107b..000000000 --- a/app/features/admin/AdminService.server.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { sql } from "~/db/sql"; - -interface AdminService { - cleanUp: () => void; -} - -const removeOldLikesStm = sql.prepare(/*sql*/ ` - delete from - "GroupLike" - where - "GroupLike"."createdAt" < cast(strftime('%s', datetime('now', 'start of day', '-7 days')) as int) -`); - -const removeOldGroupStm = sql.prepare(/*sql*/ ` - delete from - "Group" - where "Group"."id" in ( - select "Group"."id" - from "Group" - left join "GroupMatch" on "Group"."id" = "GroupMatch"."alphaGroupId" or "Group"."id" = "GroupMatch"."bravoGroupId" - where "Group"."status" = 'INACTIVE' - and "GroupMatch"."id" is null - ) -`); - -const cleanUpStm = sql.prepare(/*sql*/ ` - vacuum -`); - -export const cleanUp: AdminService["cleanUp"] = () => { - removeOldLikesStm.run(); - removeOldGroupStm.run(); - cleanUpStm.run(); -}; diff --git a/app/routes/admin.tsx b/app/features/admin/routes/admin.tsx similarity index 94% rename from app/routes/admin.tsx rename to app/features/admin/routes/admin.tsx index af94c0e9d..68e3f4c7f 100644 --- a/app/routes/admin.tsx +++ b/app/features/admin/routes/admin.tsx @@ -12,16 +12,14 @@ import { Catcher } from "~/components/Catcher"; import { Main } from "~/components/Main"; import { SubmitButton } from "~/components/SubmitButton"; import { UserSearch } from "~/components/UserSearch"; -import { db } from "~/db"; import { makeArtist } from "~/features/art"; -import { useUser } from "~/modules/auth"; +import { useUser } from "~/features/auth/core"; import { getUserId, isImpersonating, requireUserId, -} from "~/modules/auth/user.server"; +} from "~/features/auth/core/user.server"; import { isAdmin, isMod } from "~/permissions"; -import { dateToDatabaseTimestamp } from "~/utils/dates"; import { parseRequestFormData, validate, @@ -31,7 +29,7 @@ import { makeTitle } from "~/utils/strings"; import { assertUnreachable } from "~/utils/types"; import { impersonateUrl, SEED_URL, STOP_IMPERSONATING_URL } from "~/utils/urls"; import { _action, actualNumber } from "~/utils/zod"; -import * as AdminService from "~/features/admin/AdminService.server"; +import * as AdminRepository from "~/features/admin/AdminRepository.server"; export const meta: V2_MetaFunction = () => { return [{ title: makeTitle("Admin page") }]; @@ -81,7 +79,7 @@ export const action: ActionFunction = async ({ request }) => { case "MIGRATE": { validate(isAdmin(user), "Admin needed", 401); - db.users.migrate({ + await AdminRepository.migrate({ oldUserId: data["old-user"], newUserId: data["new-user"], }); @@ -90,24 +88,25 @@ export const action: ActionFunction = async ({ request }) => { case "REFRESH": { validate(isAdmin(user)); - db.users.refreshPlusTiers(); + await AdminRepository.refreshPlusTiers(); break; } case "FORCE_PATRON": { validate(isAdmin(user), "Admin needed", 401); - db.users.forcePatron({ + await AdminRepository.forcePatron({ id: data["user"], - patronSince: dateToDatabaseTimestamp(new Date()), + patronSince: new Date(), patronTier: data.patronTier, - patronTill: dateToDatabaseTimestamp(new Date(data.patronTill)), + patronTill: new Date(data.patronTill), }); break; } case "CLEAN_UP": { validate(isAdmin(user), "Admin needed", 401); - AdminService.cleanUp(); + // on purpose sync + AdminRepository.cleanUp(); break; } case "ARTIST": { @@ -119,13 +118,13 @@ export const action: ActionFunction = async ({ request }) => { case "VIDEO_ADDER": { validate(isMod(user), "Mod needed", 401); - db.users.makeVideoAdder(data["user"]); + await AdminRepository.makeVideoAdderByUserId(data["user"]); break; } case "LINK_PLAYER": { validate(isMod(user), "Mod needed", 401); - db.users.linkPlayer({ + await AdminRepository.linkUserAndPlayer({ userId: data["user"], playerId: data.playerId, }); diff --git a/app/routes/patrons.tsx b/app/features/api/routes/patrons.tsx similarity index 78% rename from app/routes/patrons.tsx rename to app/features/api/routes/patrons.tsx index 3a5184cdd..e0010ee09 100644 --- a/app/routes/patrons.tsx +++ b/app/features/api/routes/patrons.tsx @@ -1,9 +1,9 @@ import type { ActionFunction, LoaderArgs } from "@remix-run/node"; -import { db } from "~/db"; -import { getUserId } from "~/modules/auth/user.server"; +import { getUserId } from "~/features/auth/core/user.server"; import { updatePatreonData } from "~/modules/patreon"; import { canAccessLohiEndpoint, canPerformAdminActions } from "~/permissions"; import { validate } from "~/utils/remix"; +import * as UserRepository from "~/features/user-page/UserRepository.server"; export const action: ActionFunction = async ({ request }) => { const user = await getUserId(request); @@ -20,5 +20,5 @@ export const action: ActionFunction = async ({ request }) => { export const loader = ({ request }: LoaderArgs) => { validate(canAccessLohiEndpoint(request), "Invalid token", 403); - return db.users.findAllPatrons(); + return UserRepository.findAllPatrons(); }; diff --git a/app/routes/seed.tsx b/app/features/api/routes/seed.tsx similarity index 96% rename from app/routes/seed.tsx rename to app/features/api/routes/seed.tsx index 5351d327c..a89e5e115 100644 --- a/app/routes/seed.tsx +++ b/app/features/api/routes/seed.tsx @@ -21,7 +21,7 @@ export const action: ActionFunction = async ({ request }) => { schema: seedSchema, }); - seed(variation); + await seed(variation); return null; }; diff --git a/app/routes/users.tsx b/app/features/api/routes/users.tsx similarity index 73% rename from app/routes/users.tsx rename to app/features/api/routes/users.tsx index 0e4d8a0db..39d43040e 100644 --- a/app/routes/users.tsx +++ b/app/features/api/routes/users.tsx @@ -1,6 +1,6 @@ import type { ActionFunction } from "@remix-run/node"; -import { db } from "~/db"; import { canAccessLohiEndpoint } from "~/permissions"; +import * as UserRepository from "~/features/user-page/UserRepository.server"; export const action: ActionFunction = async ({ request }) => { if (!canAccessLohiEndpoint(request)) { @@ -8,7 +8,7 @@ export const action: ActionFunction = async ({ request }) => { } // input untyped but we trust Lohi to give us correctly shaped request here - db.users.updateMany(await request.json()); + UserRepository.updateMany(await request.json()); return null; }; diff --git a/app/features/art/routes/art.new.tsx b/app/features/art/routes/art.new.tsx index 4b3d874c6..0ec19b5e8 100644 --- a/app/features/art/routes/art.new.tsx +++ b/app/features/art/routes/art.new.tsx @@ -21,7 +21,7 @@ import { Toggle } from "~/components/Toggle"; import { CrossIcon } from "~/components/icons/Cross"; import { s3UploadHandler } from "~/features/img-upload"; import { useTranslation } from "~/hooks/useTranslation"; -import { requireUser } from "~/modules/auth"; +import { requireUser } from "~/features/auth/core"; import { dateToDatabaseTimestamp } from "~/utils/dates"; import { validate, diff --git a/app/modules/articles/constants.ts b/app/features/articles/articles-constants.ts similarity index 100% rename from app/modules/articles/constants.ts rename to app/features/articles/articles-constants.ts diff --git a/app/modules/articles/schemas.ts b/app/features/articles/articles-schemas.server.ts similarity index 100% rename from app/modules/articles/schemas.ts rename to app/features/articles/articles-schemas.server.ts diff --git a/app/modules/articles/bySlug.server.ts b/app/features/articles/core/bySlug.server.ts similarity index 88% rename from app/modules/articles/bySlug.server.ts rename to app/features/articles/core/bySlug.server.ts index db3f49425..167ef50e3 100644 --- a/app/modules/articles/bySlug.server.ts +++ b/app/features/articles/core/bySlug.server.ts @@ -1,9 +1,9 @@ import matter from "gray-matter"; import fs from "node:fs"; import { ZodError } from "zod"; -import { articleDataSchema } from "./schemas"; +import { articleDataSchema } from "../articles-schemas.server"; import path from "node:path"; -import { ARTICLES_FOLDER_PATH } from "./constants"; +import { ARTICLES_FOLDER_PATH } from "../articles-constants"; export function articleBySlug(slug: string) { try { diff --git a/app/modules/articles/list.server.ts b/app/features/articles/core/list.server.ts similarity index 88% rename from app/modules/articles/list.server.ts rename to app/features/articles/core/list.server.ts index 76bf12234..55da6ffe1 100644 --- a/app/modules/articles/list.server.ts +++ b/app/features/articles/core/list.server.ts @@ -2,8 +2,8 @@ import matter from "gray-matter"; import fs from "node:fs"; import path from "node:path"; import type { z } from "zod"; -import { ARTICLES_FOLDER_PATH } from "./constants"; -import { articleDataSchema } from "./schemas"; +import { ARTICLES_FOLDER_PATH } from "../articles-constants"; +import { articleDataSchema } from "../articles-schemas.server"; export async function mostRecentArticles(count: number) { const files = await fs.promises.readdir(ARTICLES_FOLDER_PATH); diff --git a/app/routes/a.$slug.tsx b/app/features/articles/routes/a.$slug.tsx similarity index 97% rename from app/routes/a.$slug.tsx rename to app/features/articles/routes/a.$slug.tsx index eaf6a021e..ea85f2a19 100644 --- a/app/routes/a.$slug.tsx +++ b/app/features/articles/routes/a.$slug.tsx @@ -8,7 +8,7 @@ import { } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; import * as React from "react"; -import { articleBySlug } from "~/modules/articles"; +import { articleBySlug } from "../core/bySlug.server"; import invariant from "tiny-invariant"; import { makeTitle } from "~/utils/strings"; import { diff --git a/app/routes/a.tsx b/app/features/articles/routes/a.tsx similarity index 96% rename from app/routes/a.tsx rename to app/features/articles/routes/a.tsx index 2e1bfd128..76b5adce2 100644 --- a/app/routes/a.tsx +++ b/app/features/articles/routes/a.tsx @@ -1,7 +1,7 @@ import { Main } from "~/components/Main"; import type { LinksFunction } from "@remix-run/node"; import { json } from "@remix-run/node"; -import { mostRecentArticles } from "~/modules/articles"; +import { mostRecentArticles } from "../core/list.server"; import styles from "~/styles/front.css"; import { useTranslation } from "~/hooks/useTranslation"; import type { SendouRouteHandle } from "~/utils/remix"; diff --git a/app/modules/auth/DiscordStrategy.server.ts b/app/features/auth/core/DiscordStrategy.server.ts similarity index 97% rename from app/modules/auth/DiscordStrategy.server.ts rename to app/features/auth/core/DiscordStrategy.server.ts index 16c54cecd..2f50c1bde 100644 --- a/app/modules/auth/DiscordStrategy.server.ts +++ b/app/features/auth/core/DiscordStrategy.server.ts @@ -1,10 +1,10 @@ import { DISCORD_AUTH_KEY } from "./authenticator.server"; -import { db } from "~/db"; import type { User } from "~/db/types"; import type { OAuth2Profile } from "remix-auth-oauth2"; import { OAuth2Strategy } from "remix-auth-oauth2"; import invariant from "tiny-invariant"; import { z } from "zod"; +import * as UserRepository from "~/features/user-page/UserRepository.server"; interface DiscordExtraParams extends Record { scope: string; @@ -63,7 +63,7 @@ export class DiscordStrategy extends OAuth2Strategy< const [user, connections] = discordUserDetailsSchema.parse(discordResponses); - const userFromDb = db.users.upsert({ + const userFromDb = await UserRepository.upsert({ discordAvatar: user.avatar ?? null, discordDiscriminator: user.discriminator, discordId: user.id, diff --git a/app/modules/auth/authenticator.server.ts b/app/features/auth/core/authenticator.server.ts similarity index 100% rename from app/modules/auth/authenticator.server.ts rename to app/features/auth/core/authenticator.server.ts diff --git a/app/modules/auth/errors.ts b/app/features/auth/core/errors.ts similarity index 100% rename from app/modules/auth/errors.ts rename to app/features/auth/core/errors.ts diff --git a/app/modules/auth/index.ts b/app/features/auth/core/index.ts similarity index 100% rename from app/modules/auth/index.ts rename to app/features/auth/core/index.ts diff --git a/app/modules/auth/routes.server.ts b/app/features/auth/core/routes.server.ts similarity index 91% rename from app/modules/auth/routes.server.ts rename to app/features/auth/core/routes.server.ts index f2c864650..72d09b35b 100644 --- a/app/modules/auth/routes.server.ts +++ b/app/features/auth/core/routes.server.ts @@ -12,11 +12,11 @@ import { authSessionStorage } from "./session.server"; import { getUserId } from "./user.server"; import { parseSearchParams, validate } from "~/utils/remix"; import { z } from "zod"; -import { createLogInLink } from "./queries/createLogInLink.server"; -import { userIdByLogInLinkCode } from "./queries/userIdByLogInLinkCode.server"; -import { deleteLogInLinkByCode } from "./queries/deleteLogInLinkByCode.server"; -import { db } from "~/db"; +import { createLogInLink } from "../queries/createLogInLink.server"; +import { userIdByLogInLinkCode } from "../queries/userIdByLogInLinkCode.server"; +import { deleteLogInLinkByCode } from "../queries/deleteLogInLinkByCode.server"; import isbot from "isbot"; +import * as UserRepository from "~/features/user-page/UserRepository.server"; export const callbackLoader: LoaderFunction = async ({ request }) => { const url = new URL(request.url); @@ -98,7 +98,7 @@ const createLogInLinkActionSchema = z.object({ updateOnly: z.enum(["true", "false"]), }); -export const createLogInLinkAction: ActionFunction = ({ request }) => { +export const createLogInLinkAction: ActionFunction = async ({ request }) => { const data = parseSearchParams({ request, schema: createLogInLinkActionSchema, @@ -108,7 +108,7 @@ export const createLogInLinkAction: ActionFunction = ({ request }) => { throw new Response(null, { status: 403 }); } - const user = db.users.upsertLite({ + const user = await UserRepository.upsert({ discordAvatar: data.discordAvatar ?? null, discordDiscriminator: "0", discordId: data.discordId, diff --git a/app/modules/auth/session.server.ts b/app/features/auth/core/session.server.ts similarity index 100% rename from app/modules/auth/session.server.ts rename to app/features/auth/core/session.server.ts diff --git a/app/modules/auth/user.server.ts b/app/features/auth/core/user.server.ts similarity index 90% rename from app/modules/auth/user.server.ts rename to app/features/auth/core/user.server.ts index a32f92a14..e765c90c7 100644 --- a/app/modules/auth/user.server.ts +++ b/app/features/auth/core/user.server.ts @@ -1,7 +1,7 @@ -import { db } from "~/db"; import type { User } from "~/db/types"; import { IMPERSONATED_SESSION_KEY, SESSION_KEY } from "./authenticator.server"; import { authSessionStorage } from "./session.server"; +import * as UserRepository from "~/features/user-page/UserRepository.server"; export async function getUserId( request: Request, @@ -23,7 +23,7 @@ export async function getUser(request: Request) { if (!userId) return; - return db.users.findByIdentifier(userId); + return UserRepository.findLeanById(userId); } export async function requireUserId(request: Request) { diff --git a/app/modules/auth/user.ts b/app/features/auth/core/user.ts similarity index 100% rename from app/modules/auth/user.ts rename to app/features/auth/core/user.ts diff --git a/app/modules/auth/queries/createLogInLink.server.ts b/app/features/auth/queries/createLogInLink.server.ts similarity index 100% rename from app/modules/auth/queries/createLogInLink.server.ts rename to app/features/auth/queries/createLogInLink.server.ts diff --git a/app/modules/auth/queries/deleteLogInLinkByCode.server.ts b/app/features/auth/queries/deleteLogInLinkByCode.server.ts similarity index 100% rename from app/modules/auth/queries/deleteLogInLinkByCode.server.ts rename to app/features/auth/queries/deleteLogInLinkByCode.server.ts diff --git a/app/modules/auth/queries/userIdByLogInLinkCode.server.ts b/app/features/auth/queries/userIdByLogInLinkCode.server.ts similarity index 100% rename from app/modules/auth/queries/userIdByLogInLinkCode.server.ts rename to app/features/auth/queries/userIdByLogInLinkCode.server.ts diff --git a/app/features/auth/routes/auth.callback.tsx b/app/features/auth/routes/auth.callback.tsx new file mode 100644 index 000000000..5c4a9315a --- /dev/null +++ b/app/features/auth/routes/auth.callback.tsx @@ -0,0 +1 @@ +export { callbackLoader as loader } from "~/features/auth/core"; diff --git a/app/features/auth/routes/auth.create-link.tsx b/app/features/auth/routes/auth.create-link.tsx new file mode 100644 index 000000000..e4cd1eb4a --- /dev/null +++ b/app/features/auth/routes/auth.create-link.tsx @@ -0,0 +1 @@ +export { createLogInLinkAction as action } from "~/features/auth/core"; diff --git a/app/features/auth/routes/auth.impersonate.stop.tsx b/app/features/auth/routes/auth.impersonate.stop.tsx new file mode 100644 index 000000000..abe828909 --- /dev/null +++ b/app/features/auth/routes/auth.impersonate.stop.tsx @@ -0,0 +1 @@ +export { stopImpersonatingAction as action } from "~/features/auth/core"; diff --git a/app/features/auth/routes/auth.impersonate.tsx b/app/features/auth/routes/auth.impersonate.tsx new file mode 100644 index 000000000..8124784b0 --- /dev/null +++ b/app/features/auth/routes/auth.impersonate.tsx @@ -0,0 +1 @@ +export { impersonateAction as action } from "~/features/auth/core"; diff --git a/app/features/auth/routes/auth.login.tsx b/app/features/auth/routes/auth.login.tsx new file mode 100644 index 000000000..63d2c68b8 --- /dev/null +++ b/app/features/auth/routes/auth.login.tsx @@ -0,0 +1 @@ +export { logInViaLinkLoader as loader } from "~/features/auth/core"; diff --git a/app/features/auth/routes/auth.logout.tsx b/app/features/auth/routes/auth.logout.tsx new file mode 100644 index 000000000..7c197b435 --- /dev/null +++ b/app/features/auth/routes/auth.logout.tsx @@ -0,0 +1 @@ +export { logOutAction as action } from "~/features/auth/core"; diff --git a/app/features/auth/routes/auth.tsx b/app/features/auth/routes/auth.tsx new file mode 100644 index 000000000..0e897e8d6 --- /dev/null +++ b/app/features/auth/routes/auth.tsx @@ -0,0 +1 @@ +export { logInAction as action } from "~/features/auth/core"; diff --git a/app/features/badges/BadgeRepository.server.ts b/app/features/badges/BadgeRepository.server.ts new file mode 100644 index 000000000..aaed3b39b --- /dev/null +++ b/app/features/badges/BadgeRepository.server.ts @@ -0,0 +1,132 @@ +import { db } from "~/db/sql"; +import { COMMON_USER_FIELDS } from "~/utils/kysely.server"; +import type { Unwrapped } from "~/utils/types"; + +export function all() { + return db + .selectFrom("Badge") + .select(["id", "displayName", "code", "hue"]) + .execute(); +} + +export async function findByOwnerId({ + userId, + favoriteBadgeId, +}: { + userId: number; + favoriteBadgeId: number | null; +}) { + const badges = await db + .selectFrom("BadgeOwner") + .innerJoin("Badge", "Badge.id", "BadgeOwner.badgeId") + .select(({ fn }) => [ + fn.count("BadgeOwner.badgeId").as("count"), + "Badge.id", + "Badge.displayName", + "Badge.code", + "Badge.hue", + ]) + .where("BadgeOwner.userId", "=", userId) + .groupBy(["BadgeOwner.badgeId", "BadgeOwner.userId"]) + .orderBy("Badge.id", "asc") + .execute(); + + if (!favoriteBadgeId) { + return badges; + } + + return badges.sort((a, b) => { + if (a.id === favoriteBadgeId) { + return -1; + } + if (b.id === favoriteBadgeId) { + return 1; + } + return 0; + }); +} + +export function findManagedByUserId(userId: number) { + return db + .selectFrom("BadgeManager") + .innerJoin("Badge", "Badge.id", "BadgeManager.badgeId") + .select(["Badge.id", "Badge.code", "Badge.displayName", "Badge.hue"]) + .where("BadgeManager.userId", "=", userId) + .execute(); +} + +export function findManagersByBadgeId(badgeId: number) { + return db + .selectFrom("BadgeManager") + .innerJoin("User", "BadgeManager.userId", "User.id") + .select(COMMON_USER_FIELDS) + .where("BadgeManager.badgeId", "=", badgeId) + .execute(); +} + +export type FindOwnersByBadgeIdItem = Unwrapped; +export function findOwnersByBadgeId(badgeId: number) { + return db + .selectFrom("BadgeOwner") + .innerJoin("User", "BadgeOwner.userId", "User.id") + .select(({ fn }) => [ + fn.count("BadgeOwner.badgeId").as("count"), + "User.id", + "User.discordId", + "User.discordName", + ]) + .where("BadgeOwner.badgeId", "=", badgeId) + .groupBy("User.id") + .orderBy("count", "desc") + .execute(); +} + +export function replaceManagers({ + badgeId, + managerIds, +}: { + badgeId: number; + managerIds: number[]; +}) { + return db.transaction().execute(async (trx) => { + await trx + .deleteFrom("BadgeManager") + .where("badgeId", "=", badgeId) + .execute(); + + await trx + .insertInto("BadgeManager") + .values( + managerIds.map((userId) => ({ + badgeId, + userId, + })), + ) + .execute(); + }); +} + +export function replaceOwners({ + badgeId, + ownerIds, +}: { + badgeId: number; + ownerIds: number[]; +}) { + return db.transaction().execute(async (trx) => { + await trx + .deleteFrom("TournamentBadgeOwner") + .where("badgeId", "=", badgeId) + .execute(); + + await trx + .insertInto("TournamentBadgeOwner") + .values( + ownerIds.map((userId) => ({ + badgeId, + userId, + })), + ) + .execute(); + }); +} diff --git a/app/features/badges/badges-schemas.server.ts b/app/features/badges/badges-schemas.server.ts new file mode 100644 index 000000000..590bbf826 --- /dev/null +++ b/app/features/badges/badges-schemas.server.ts @@ -0,0 +1,13 @@ +import { z } from "zod"; +import { _action, id, noDuplicates, safeJSONParse } from "~/utils/zod"; + +export const editBadgeActionSchema = z.union([ + z.object({ + _action: _action("MANAGERS"), + managerIds: z.preprocess(safeJSONParse, z.array(id).refine(noDuplicates)), + }), + z.object({ + _action: _action("OWNERS"), + ownerIds: z.preprocess(safeJSONParse, z.array(id)), + }), +]); diff --git a/app/routes/badges/$id/edit.tsx b/app/features/badges/routes/badges.$id.edit.tsx similarity index 81% rename from app/routes/badges/$id/edit.tsx rename to app/features/badges/routes/badges.$id.edit.tsx index b3a7ed3d6..ba4e5c0f7 100644 --- a/app/routes/badges/$id/edit.tsx +++ b/app/features/badges/routes/badges.$id.edit.tsx @@ -5,38 +5,21 @@ import * as React from "react"; import { z } from "zod"; import { Button, LinkButton } from "~/components/Button"; import { Dialog } from "~/components/Dialog"; -import { TrashIcon } from "~/components/icons/Trash"; import { Label } from "~/components/Label"; -import { db } from "~/db"; +import { UserSearch } from "~/components/UserSearch"; +import { TrashIcon } from "~/components/icons/Trash"; import type { User } from "~/db/types"; -import { useUser } from "~/modules/auth"; -import { requireUserId } from "~/modules/auth/user.server"; +import { useUser } from "~/features/auth/core"; +import { requireUserId } from "~/features/auth/core/user.server"; import { canEditBadgeManagers, canEditBadgeOwners } from "~/permissions"; import { atOrError } from "~/utils/arrays"; import { parseRequestFormData, validate } from "~/utils/remix"; -import { discordFullName } from "~/utils/strings"; import { assertUnreachable } from "~/utils/types"; import { badgePage } from "~/utils/urls"; -import { - _action, - actualNumber, - id, - noDuplicates, - safeJSONParse, -} from "~/utils/zod"; -import type { BadgeDetailsContext, BadgeDetailsLoaderData } from "../$id"; -import { UserSearch } from "~/components/UserSearch"; - -const editBadgeActionSchema = z.union([ - z.object({ - _action: _action("MANAGERS"), - managerIds: z.preprocess(safeJSONParse, z.array(id).refine(noDuplicates)), - }), - z.object({ - _action: _action("OWNERS"), - ownerIds: z.preprocess(safeJSONParse, z.array(id)), - }), -]); +import { actualNumber } from "~/utils/zod"; +import type { BadgeDetailsContext, BadgeDetailsLoaderData } from "./badges.$id"; +import { editBadgeActionSchema } from "../badges-schemas.server"; +import * as BadgeRepository from "../BadgeRepository.server"; export const action: ActionFunction = async ({ request, params }) => { const data = await parseRequestFormData({ @@ -50,18 +33,21 @@ export const action: ActionFunction = async ({ request, params }) => { case "MANAGERS": { validate(canEditBadgeManagers(user)); - db.badges.upsertManyManagers({ badgeId, managerIds: data.managerIds }); + await BadgeRepository.replaceManagers({ + badgeId, + managerIds: data.managerIds, + }); break; } case "OWNERS": { validate( canEditBadgeOwners({ user, - managers: db.badges.managersByBadgeId(badgeId), + managers: await BadgeRepository.findManagersByBadgeId(badgeId), }), ); - db.badges.upsertManyOwners({ badgeId, ownerIds: data.ownerIds }); + await BadgeRepository.replaceOwners({ badgeId, ownerIds: data.ownerIds }); break; } default: { @@ -105,12 +91,7 @@ export default function EditBadgePage() { } function Managers({ data }: { data: BadgeDetailsLoaderData }) { - const [managers, setManagers] = React.useState( - data.managers.map((m) => ({ - id: m.id, - discordFullName: discordFullName(m), - })), - ); + const [managers, setManagers] = React.useState(data.managers); const amountOfChanges = managers .filter((m) => !data.managers.some((om) => om.id === m.id)) @@ -137,10 +118,7 @@ function Managers({ data }: { data: BadgeDetailsLoaderData }) { className="mx-auto" inputName="new-manager" onChange={(user) => { - setManagers([ - ...managers, - { discordFullName: user.discordName, id: user.id }, - ]); + setManagers([...managers, user]); }} userIdsToOmit={userIdsToOmitFromCombobox} /> @@ -148,7 +126,7 @@ function Managers({ data }: { data: BadgeDetailsLoaderData }) {
    {managers.map((manager) => (
  • - {manager.discordFullName} + {manager.discordName} @@ -261,7 +274,7 @@ function Voting(data: Extract) { {currentUser ? (
    -

    {discordFullName(currentUser.user)}

    +

    {currentUser.user.discordName}

    - {currentUser.suggestions ? ( + {currentUser.suggestion ? ( ) : null} diff --git a/app/features/sendouq/components/GroupCard.tsx b/app/features/sendouq/components/GroupCard.tsx index cb5e333ad..2c97fbab0 100644 --- a/app/features/sendouq/components/GroupCard.tsx +++ b/app/features/sendouq/components/GroupCard.tsx @@ -12,7 +12,7 @@ import type { GroupMember as GroupMemberType, ParsedMemento } from "~/db/types"; import { ordinalToRoundedSp } from "~/features/mmr/mmr-utils"; import type { TieredSkill } from "~/features/mmr/tiered.server"; import { useTranslation } from "~/hooks/useTranslation"; -import { useUser } from "~/modules/auth"; +import { useUser } from "~/features/auth/core"; import { languagesUnified } from "~/modules/i18n/config"; import { SENDOUQ_LOOKING_PAGE, diff --git a/app/features/sendouq/core/match.server.ts b/app/features/sendouq/core/match.server.ts index 0dd3688e3..05be68a83 100644 --- a/app/features/sendouq/core/match.server.ts +++ b/app/features/sendouq/core/match.server.ts @@ -1,5 +1,5 @@ import type { Group, ParsedMemento } from "~/db/types"; -import { MapPool } from "~/modules/map-pool-serializer"; +import { MapPool } from "~/features/map-list-generator/core/map-pool"; import { createTournamentMapList } from "~/modules/tournament-map-list-generator"; import { SENDOUQ_BEST_OF } from "../q-constants"; import type { LookingGroup, LookingGroupWithInviteCode } from "../q-types"; diff --git a/app/features/sendouq/q-utils.ts b/app/features/sendouq/q-utils.ts index f857aae26..9d2c08d3b 100644 --- a/app/features/sendouq/q-utils.ts +++ b/app/features/sendouq/q-utils.ts @@ -1,6 +1,5 @@ import type { Group } from "~/db/types"; import { rankedModesShort } from "~/modules/in-game-lists/modes"; -import type { MapPool } from "~/modules/map-pool-serializer"; import { SENDOUQ_LOOKING_PAGE, SENDOUQ_PAGE, @@ -11,6 +10,7 @@ import { SENDOUQ } from "./q-constants"; import { stageIds } from "~/modules/in-game-lists"; import type { Params } from "@remix-run/react"; import invariant from "tiny-invariant"; +import type { MapPool } from "../map-list-generator/core/map-pool"; function groupRedirectLocation( group?: Pick & { matchId?: number }, diff --git a/app/features/sendouq/queries/createGroup.server.ts b/app/features/sendouq/queries/createGroup.server.ts index c73e54396..af532d91d 100644 --- a/app/features/sendouq/queries/createGroup.server.ts +++ b/app/features/sendouq/queries/createGroup.server.ts @@ -2,7 +2,7 @@ import { nanoid } from "nanoid"; import { INVITE_CODE_LENGTH } from "~/constants"; import { sql } from "~/db/sql"; import type { Group, GroupMember } from "~/db/types"; -import type { MapPool } from "~/modules/map-pool-serializer"; +import type { MapPool } from "~/features/map-list-generator/core/map-pool"; const createGroupStm = sql.prepare(/* sql */ ` insert into "Group" diff --git a/app/routes/play.tsx b/app/features/sendouq/routes/play.tsx similarity index 86% rename from app/routes/play.tsx rename to app/features/sendouq/routes/play.tsx index 7231a6ead..2a077ffe7 100644 --- a/app/routes/play.tsx +++ b/app/features/sendouq/routes/play.tsx @@ -1,6 +1,7 @@ import { type LoaderFunction, redirect } from "@remix-run/node"; import { SENDOUQ_PAGE } from "~/utils/urls"; +// SendouQ's old URL was /play export const loader: LoaderFunction = () => { throw redirect(SENDOUQ_PAGE); }; diff --git a/app/features/sendouq/routes/q.looking.tsx b/app/features/sendouq/routes/q.looking.tsx index 620fa75ad..564848851 100644 --- a/app/features/sendouq/routes/q.looking.tsx +++ b/app/features/sendouq/routes/q.looking.tsx @@ -13,8 +13,8 @@ import { Main } from "~/components/Main"; import { SubmitButton } from "~/components/SubmitButton"; import { useIsMounted } from "~/hooks/useIsMounted"; import { useTranslation } from "~/hooks/useTranslation"; -import { getUser, requireUser } from "~/modules/auth/user.server"; -import { MapPool } from "~/modules/map-pool-serializer"; +import { getUser, requireUser } from "~/features/auth/core/user.server"; +import { MapPool } from "~/features/map-list-generator/core/map-pool"; import { parseRequestFormData, validate, diff --git a/app/features/sendouq/routes/q.match.$id.tsx b/app/features/sendouq/routes/q.match.$id.tsx index 65790a454..4730bfec5 100644 --- a/app/features/sendouq/routes/q.match.$id.tsx +++ b/app/features/sendouq/routes/q.match.$id.tsx @@ -32,8 +32,8 @@ import { currentSeason } from "~/features/mmr"; import { resolveRoomPass } from "~/features/tournament-bracket/tournament-bracket-utils"; import { useIsMounted } from "~/hooks/useIsMounted"; import { useTranslation } from "~/hooks/useTranslation"; -import { useUser } from "~/modules/auth"; -import { getUserId, requireUser } from "~/modules/auth/user.server"; +import { useUser } from "~/features/auth/core"; +import { getUserId, requireUser } from "~/features/auth/core/user.server"; import type { MainWeaponId } from "~/modules/in-game-lists"; import { isMod } from "~/permissions"; import { cache } from "~/utils/cache.server"; diff --git a/app/features/sendouq/routes/q.preparing.tsx b/app/features/sendouq/routes/q.preparing.tsx index 3e0e64aa8..bd1c071f3 100644 --- a/app/features/sendouq/routes/q.preparing.tsx +++ b/app/features/sendouq/routes/q.preparing.tsx @@ -9,7 +9,7 @@ import { useFetcher, useLoaderData } from "@remix-run/react"; import invariant from "tiny-invariant"; import { Main } from "~/components/Main"; import { SubmitButton } from "~/components/SubmitButton"; -import { getUser, requireUser } from "~/modules/auth/user.server"; +import { getUser, requireUser } from "~/features/auth/core/user.server"; import type { SendouRouteHandle } from "~/utils/remix"; import { parseRequestFormData, validate } from "~/utils/remix"; import { makeTitle } from "~/utils/strings"; diff --git a/app/features/sendouq/routes/q.tsx b/app/features/sendouq/routes/q.tsx index 5caebce9f..419a7a8ff 100644 --- a/app/features/sendouq/routes/q.tsx +++ b/app/features/sendouq/routes/q.tsx @@ -39,9 +39,8 @@ import { } from "~/utils/urls"; import { stageIds } from "~/modules/in-game-lists"; import { rankedModesShort } from "~/modules/in-game-lists/modes"; -import { MapPool } from "~/modules/map-pool-serializer"; import { SubmitButton } from "~/components/SubmitButton"; -import { getUserId, requireUserId } from "~/modules/auth/user.server"; +import { getUserId, requireUserId } from "~/features/auth/core/user.server"; import { frontPageSchema } from "../q-schemas.server"; import { RequiredHiddenInput } from "~/components/RequiredHiddenInput"; import { createGroup } from "../queries/createGroup.server"; @@ -52,7 +51,7 @@ import { makeTitle } from "~/utils/strings"; import { currentSeason } from "~/features/mmr"; import type { RankingSeason } from "~/features/mmr/season"; import { nextSeason } from "~/features/mmr/season"; -import { useUser } from "~/modules/auth"; +import { useUser } from "~/features/auth/core"; import { Button } from "~/components/Button"; import { findGroupByInviteCode } from "../queries/findGroupByInviteCode.server"; import { Alert } from "~/components/Alert"; @@ -76,6 +75,7 @@ import { CrossIcon } from "~/components/icons/Cross"; import { updateVCStatus } from "../queries/updateVCStatus.server"; import { sql } from "~/db/sql"; import { deleteLikesByGroupId } from "../queries/deleteLikesByGroupId.server"; +import { MapPool } from "~/features/map-list-generator/core/map-pool"; export const handle: SendouRouteHandle = { i18n: ["q"], diff --git a/app/features/team/TeamRepository.server.ts b/app/features/team/TeamRepository.server.ts new file mode 100644 index 000000000..2e1fb583a --- /dev/null +++ b/app/features/team/TeamRepository.server.ts @@ -0,0 +1,10 @@ +import { db } from "~/db/sql"; + +export function findByUserId(userId: number) { + return db + .selectFrom("TeamMember") + .innerJoin("Team", "Team.id", "TeamMember.teamId") + .select(["Team.id", "Team.customUrl"]) + .where("TeamMember.userId", "=", userId) + .executeTakeFirst(); +} diff --git a/app/features/team/routes/t.$customUrl.edit.tsx b/app/features/team/routes/t.$customUrl.edit.tsx index 19eb491a4..7436092a6 100644 --- a/app/features/team/routes/t.$customUrl.edit.tsx +++ b/app/features/team/routes/t.$customUrl.edit.tsx @@ -17,7 +17,7 @@ import { Label } from "~/components/Label"; import { Main } from "~/components/Main"; import { SubmitButton } from "~/components/SubmitButton"; import { useTranslation } from "~/hooks/useTranslation"; -import { requireUserId } from "~/modules/auth/user.server"; +import { requireUserId } from "~/features/auth/core/user.server"; import { notFoundIfFalsy, parseRequestFormData, diff --git a/app/features/team/routes/t.$customUrl.join.tsx b/app/features/team/routes/t.$customUrl.join.tsx index aaca52935..6d4fe1f53 100644 --- a/app/features/team/routes/t.$customUrl.join.tsx +++ b/app/features/team/routes/t.$customUrl.join.tsx @@ -9,7 +9,7 @@ import { Main } from "~/components/Main"; import { SubmitButton } from "~/components/SubmitButton"; import { INVITE_CODE_LENGTH } from "~/constants"; import { useTranslation } from "~/hooks/useTranslation"; -import { requireUser } from "~/modules/auth"; +import { requireUser } from "~/features/auth/core"; import { notFoundIfFalsy, validate, diff --git a/app/features/team/routes/t.$customUrl.roster.tsx b/app/features/team/routes/t.$customUrl.roster.tsx index 1f31ff8aa..bda85413d 100644 --- a/app/features/team/routes/t.$customUrl.roster.tsx +++ b/app/features/team/routes/t.$customUrl.roster.tsx @@ -17,8 +17,8 @@ import { Main } from "~/components/Main"; import { SubmitButton } from "~/components/SubmitButton"; import { useBaseUrl } from "~/hooks/useBaseUrl"; import { useTranslation } from "~/hooks/useTranslation"; -import { useUser } from "~/modules/auth"; -import { requireUserId } from "~/modules/auth/user.server"; +import { useUser } from "~/features/auth/core"; +import { requireUserId } from "~/features/auth/core/user.server"; import type { SendouRouteHandle } from "~/utils/remix"; import { notFoundIfFalsy, parseRequestFormData, validate } from "~/utils/remix"; import { discordFullName, makeTitle } from "~/utils/strings"; diff --git a/app/features/team/routes/t.$customUrl.tsx b/app/features/team/routes/t.$customUrl.tsx index ec0df043e..aad4df5a0 100644 --- a/app/features/team/routes/t.$customUrl.tsx +++ b/app/features/team/routes/t.$customUrl.tsx @@ -19,8 +19,8 @@ import { WeaponImage } from "~/components/Image"; import { Main } from "~/components/Main"; import { Placement } from "~/components/Placement"; import { useTranslation } from "~/hooks/useTranslation"; -import { useUser } from "~/modules/auth"; -import { requireUserId } from "~/modules/auth/user.server"; +import { useUser } from "~/features/auth/core"; +import { requireUserId } from "~/features/auth/core/user.server"; import { notFoundIfFalsy, validate, diff --git a/app/features/team/routes/t.tsx b/app/features/team/routes/t.tsx index f95a8c7b6..50f7f7253 100644 --- a/app/features/team/routes/t.tsx +++ b/app/features/team/routes/t.tsx @@ -22,8 +22,8 @@ import { Input } from "~/components/Input"; import { Main } from "~/components/Main"; import { SubmitButton } from "~/components/SubmitButton"; import { useTranslation } from "~/hooks/useTranslation"; -import { useUser } from "~/modules/auth"; -import { getUserId, requireUserId } from "~/modules/auth/user.server"; +import { useUser } from "~/features/auth/core"; +import { getUserId, requireUserId } from "~/features/auth/core/user.server"; import { i18next } from "~/modules/i18n"; import { joinListToNaturalString } from "~/utils/arrays"; import type { SendouRouteHandle } from "~/utils/remix"; diff --git a/app/modules/theme/README.md b/app/features/theme/core/README.md similarity index 100% rename from app/modules/theme/README.md rename to app/features/theme/core/README.md diff --git a/app/modules/theme/provider.tsx b/app/features/theme/core/provider.tsx similarity index 100% rename from app/modules/theme/provider.tsx rename to app/features/theme/core/provider.tsx diff --git a/app/modules/theme/session.server.ts b/app/features/theme/core/session.server.ts similarity index 100% rename from app/modules/theme/session.server.ts rename to app/features/theme/core/session.server.ts diff --git a/app/modules/theme/action.server.ts b/app/features/theme/routes/theme.ts similarity index 71% rename from app/modules/theme/action.server.ts rename to app/features/theme/routes/theme.ts index 9e396e95d..7819135c2 100644 --- a/app/modules/theme/action.server.ts +++ b/app/features/theme/routes/theme.ts @@ -1,8 +1,11 @@ -import type { ActionFunction } from "@remix-run/node"; -import { json } from "@remix-run/node"; - -import { getThemeSession } from "./session.server"; -import { isTheme } from "./provider"; +import { + type LoaderFunction, + type ActionFunction, + redirect, + json, +} from "@remix-run/node"; +import { getThemeSession } from "../core/session.server"; +import { isTheme } from "../core/provider"; export const action: ActionFunction = async ({ request }) => { const themeSession = await getThemeSession(request); @@ -30,3 +33,5 @@ export const action: ActionFunction = async ({ request }) => { { headers: { "Set-Cookie": await themeSession.commit() } }, ); }; + +export const loader: LoaderFunction = () => redirect("/", { status: 404 }); diff --git a/app/features/tournament-bracket/components/ScoreReporter.tsx b/app/features/tournament-bracket/components/ScoreReporter.tsx index 91248d00b..f5705e396 100644 --- a/app/features/tournament-bracket/components/ScoreReporter.tsx +++ b/app/features/tournament-bracket/components/ScoreReporter.tsx @@ -25,7 +25,7 @@ import type { TournamentLoaderData, } from "~/features/tournament"; import { canAdminTournament } from "~/permissions"; -import { useUser } from "~/modules/auth"; +import { useUser } from "~/features/auth/core"; import { useIsMounted } from "~/hooks/useIsMounted"; import { databaseTimestampToDate } from "~/utils/dates"; import { NewTabs } from "~/components/NewTabs"; @@ -94,14 +94,14 @@ export function ScoreReporter({ {t("tournament:match.pool")}{" "} { HACKY_resolvePoolCode({ - event: parentRouteData.event, + event: parentRouteData.tournament, matchId: data.match.id, }).prefix } { HACKY_resolvePoolCode({ - event: parentRouteData.event, + event: parentRouteData.tournament, matchId: data.match.id, }).lastDigit } @@ -140,7 +140,7 @@ export function ScoreReporter({
    )} - {canAdminTournament({ user, event: parentRouteData.event }) && + {canAdminTournament({ user, tournament: parentRouteData.tournament }) && !parentRouteData.hasFinalized && presentational && !matchIsLockedError && ( diff --git a/app/features/tournament-bracket/components/TeamRosterInputs.tsx b/app/features/tournament-bracket/components/TeamRosterInputs.tsx index c63de7f76..bcb84c265 100644 --- a/app/features/tournament-bracket/components/TeamRosterInputs.tsx +++ b/app/features/tournament-bracket/components/TeamRosterInputs.tsx @@ -58,7 +58,7 @@ export function TeamRosterInputs({ {" "} { return [ @@ -101,11 +102,13 @@ export const links: LinksFunction = () => { export const action: ActionFunction = async ({ params, request }) => { const user = await requireUser(request); const tournamentId = tournamentIdFromParams(params); - const tournament = notFoundIfFalsy(findByIdentifier(tournamentId)); + const tournament = notFoundIfFalsy( + await TournamentRepository.findById(tournamentId), + ); const data = await parseRequestFormData({ request, schema: bracketSchema }); const manager = getTournamentManager("SQL"); - validate(canAdminTournament({ user, event: tournament })); + validate(canAdminTournament({ user, tournament })); switch (data._action) { case "START_TOURNAMENT": { @@ -288,7 +291,7 @@ export default function TournamentBracketsPage() { } navigate( tournamentMatchPage({ - eventId: parentRouteData.event.id, + eventId: parentRouteData.tournament.id, matchId: match.id, }), ); @@ -370,7 +373,7 @@ export default function TournamentBracketsPage() { if (process.env.NODE_ENV === "development") return true; return ( - databaseTimestampToDate(parentRouteData.event.startTime).getTime() < + databaseTimestampToDate(parentRouteData.tournament.startTime).getTime() < Date.now() ); }; @@ -382,7 +385,7 @@ export default function TournamentBracketsPage() { ) : null} {data.finalStandings && !parentRouteData.hasFinalized && - canAdminTournament({ user, event: parentRouteData.event }) ? ( + canAdminTournament({ user, tournament: parentRouteData.tournament }) ? (
    - {!canAdminTournament({ user, event: parentRouteData.event }) ? ( + {!canAdminTournament({ + user, + tournament: parentRouteData.tournament, + }) ? ( (); const lastEvent = useEventSource( - tournamentBracketsSubscribePage(parentRouteData.event.id), + tournamentBracketsSubscribePage(parentRouteData.tournament.id), { - event: bracketSubscriptionKey(parentRouteData.event.id), + event: bracketSubscriptionKey(parentRouteData.tournament.id), }, ); @@ -577,7 +583,7 @@ function TournamentProgressPrompt({ ownedTeamId }: { ownedTeamId: number }) { return ( {t("tournament:bracket.progress.thanksForPlaying", { - eventName: parentRouteData.event.name, + eventName: parentRouteData.tournament.name, })} ); @@ -594,7 +600,7 @@ function TournamentProgressPrompt({ ownedTeamId }: { ownedTeamId: number }) { { const loggedInUser = await getUserId(request); diff --git a/app/features/tournament-bracket/routes/to.$id.matches.$mid.tsx b/app/features/tournament-bracket/routes/to.$id.matches.$mid.tsx index cc3b268d2..3f9bd1be7 100644 --- a/app/features/tournament-bracket/routes/to.$id.matches.$mid.tsx +++ b/app/features/tournament-bracket/routes/to.$id.matches.$mid.tsx @@ -24,8 +24,8 @@ import { } from "~/features/tournament"; import { useSearchParamState } from "~/hooks/useSearchParamState"; import { useVisibilityChange } from "~/hooks/useVisibilityChange"; -import { requireUser, useUser } from "~/modules/auth"; -import { getUserId } from "~/modules/auth/user.server"; +import { requireUser, useUser } from "~/features/auth/core"; +import { getUserId } from "~/features/auth/core/user.server"; import { canAdminTournament, canReportTournamentScore } from "~/permissions"; import { notFoundIfFalsy, parseRequestFormData, validate } from "~/utils/remix"; import { assertUnreachable } from "~/utils/types"; @@ -35,7 +35,6 @@ import { tournamentTeamPage, userPage, } from "~/utils/urls"; -import { findByIdentifier } from "../../tournament/queries/findByIdentifier.server"; import { findTeamsByTournamentId } from "../../tournament/queries/findTeamsByTournamentId.server"; import { ScoreReporter } from "../components/ScoreReporter"; import { getTournamentManager } from "../core/brackets-manager"; @@ -53,6 +52,7 @@ import { matchSubscriptionKey, } from "../tournament-bracket-utils"; import bracketStyles from "../tournament-bracket.css"; +import * as TournamentRepository from "~/features/tournament/TournamentRepository.server"; export const links: LinksFunction = () => [ { @@ -71,7 +71,9 @@ export const action: ActionFunction = async ({ params, request }) => { }); const tournamentId = tournamentIdFromParams(params); - const event = notFoundIfFalsy(findByIdentifier(tournamentId)); + const tournament = notFoundIfFalsy( + await TournamentRepository.findById(tournamentId), + ); const validateCanReportScore = () => { const teams = findTeamsByTournamentId(tournamentId); @@ -83,7 +85,7 @@ export const action: ActionFunction = async ({ params, request }) => { validate( canReportTournamentScore({ - event, + tournament, match, ownedTeamId, user, @@ -219,7 +221,7 @@ export const action: ActionFunction = async ({ params, request }) => { invariant(typeof scoreTwo === "number", "Score two is missing"); invariant(scoreOne !== scoreTwo, "Scores are equal"); - validate(canAdminTournament({ event, user })); + validate(canAdminTournament({ tournament, user })); const results = findResultsByMatchId(matchId); const lastResult = results[results.length - 1]; @@ -267,7 +269,7 @@ export const action: ActionFunction = async ({ params, request }) => { eventId: nanoid(), userId: user.id, }); - emitter.emit(bracketSubscriptionKey(event.id), { + emitter.emit(bracketSubscriptionKey(tournament.id), { matchId: match.id, scores, isOver: @@ -307,7 +309,9 @@ export const loader = async ({ params, request }: LoaderArgs) => { match.players.some((p) => p.id === user?.id) || canAdminTournament({ user, - event: notFoundIfFalsy(findByIdentifier(tournamentId)), + tournament: notFoundIfFalsy( + await TournamentRepository.findById(tournamentId), + ), }); return { @@ -359,7 +363,7 @@ export default function TournamentMatchPage() { const isMemberOfATeam = data.match.players.some((p) => p.id === user?.id); const type = canReportTournamentScore({ - event: parentRouteData.event, + tournament: parentRouteData.tournament, match: data.match, ownedTeamId: parentRouteData.ownTeam?.id, user, @@ -384,7 +388,7 @@ export default function TournamentMatchPage() { {/* TODO: better title */}

    Match #{data.match.id}

    (); const lastEventId = useEventSource( tournamentMatchSubscribePage({ - eventId: parentRouteData.event.id, + eventId: parentRouteData.tournament.id, matchId: data.match.id, }), { @@ -541,7 +545,7 @@ function Rosters({ {teamOne ? ( (); if (parentRouteData.hasFinalized) { - return ; + return ( + + ); } return ( diff --git a/app/features/tournament/TournamentRepository.server.ts b/app/features/tournament/TournamentRepository.server.ts new file mode 100644 index 000000000..6bb42d151 --- /dev/null +++ b/app/features/tournament/TournamentRepository.server.ts @@ -0,0 +1,55 @@ +import { jsonArrayFrom, jsonObjectFrom } from "kysely/helpers/sqlite"; +import invariant from "tiny-invariant"; +import { db } from "~/db/sql"; +import { COMMON_USER_FIELDS } from "~/utils/kysely.server"; +import type { Unwrapped } from "~/utils/types"; + +export type FindById = NonNullable>; +export async function findById(id: number) { + const row = await db + .selectFrom("Tournament") + .innerJoin("CalendarEvent", "Tournament.id", "CalendarEvent.tournamentId") + // TODO: it does not support multiple dates + .innerJoin( + "CalendarEventDate", + "CalendarEvent.id", + "CalendarEventDate.eventId", + ) + .select(({ eb }) => [ + "Tournament.id", + "Tournament.mapPickingStyle", + "Tournament.format", + "Tournament.showMapListGenerator", + "CalendarEvent.id as eventId", + "CalendarEvent.name", + "CalendarEvent.description", + "CalendarEvent.bracketUrl", + "CalendarEventDate.startTime", + jsonObjectFrom( + eb + .selectFrom("User") + .whereRef("CalendarEvent.authorId", "=", "User.id") + .select(COMMON_USER_FIELDS), + ).as("author"), + jsonArrayFrom( + eb + .selectFrom("MapPoolMap") + .whereRef( + "MapPoolMap.tieBreakerCalendarEventId", + "=", + "CalendarEvent.id", + ) + .select(["MapPoolMap.stageId", "MapPoolMap.mode"]), + ).as("tieBreakerMapPool"), + ]) + .where("Tournament.id", "=", id) + .executeTakeFirst(); + + if (!row) return null; + + // TODO: can be made better when $narrowNotNull lands + const author = row.author; + invariant(author, "Tournament author is missing"); + + return { ...row, author }; +} diff --git a/app/features/tournament/queries/upsertCounterpickMaps.server.ts b/app/features/tournament/queries/upsertCounterpickMaps.server.ts index d5429063f..1896f6d12 100644 --- a/app/features/tournament/queries/upsertCounterpickMaps.server.ts +++ b/app/features/tournament/queries/upsertCounterpickMaps.server.ts @@ -1,6 +1,6 @@ import { sql } from "~/db/sql"; import type { TournamentTeam } from "~/db/types"; -import type { MapPool } from "~/modules/map-pool-serializer"; +import type { MapPool } from "~/features/map-list-generator/core/map-pool"; const deleteCounterpickMapsByTeamIdStm = sql.prepare(/* sql */ ` delete from diff --git a/app/features/tournament/routes/to.$id.admin.tsx b/app/features/tournament/routes/to.$id.admin.tsx index 2e62229e5..5c3a757bd 100644 --- a/app/features/tournament/routes/to.$id.admin.tsx +++ b/app/features/tournament/routes/to.$id.admin.tsx @@ -7,10 +7,9 @@ import { useTranslation } from "~/hooks/useTranslation"; import { canAdminTournament, isAdmin } from "~/permissions"; import { notFoundIfFalsy, parseRequestFormData, validate } from "~/utils/remix"; import { discordFullName } from "~/utils/strings"; -import { findByIdentifier } from "../queries/findByIdentifier.server"; import { findTeamsByTournamentId } from "../queries/findTeamsByTournamentId.server"; import { updateShowMapListGenerator } from "../queries/updateShowMapListGenerator.server"; -import { requireUserId } from "~/modules/auth/user.server"; +import { requireUserId } from "~/features/auth/core/user.server"; import { HACKY_resolveCheckInTime, tournamentIdFromParams, @@ -27,7 +26,7 @@ import hasTournamentStarted from "../queries/hasTournamentStarted.server"; import type { TournamentLoaderData } from "./to.$id"; import { joinTeam, leaveTeam } from "../queries/joinLeaveTeam.server"; import { deleteTeam } from "../queries/deleteTeam.server"; -import { useUser } from "~/modules/auth"; +import { useUser } from "~/features/auth/core"; import { calendarEditPage, calendarEventPage, @@ -37,6 +36,7 @@ import { Redirect } from "~/components/Redirect"; import { FormWithConfirm } from "~/components/FormWithConfirm"; import { findMapPoolByTeamId } from "~/features/tournament-bracket"; import { UserSearch } from "~/components/UserSearch"; +import * as TournamentRepository from "../TournamentRepository.server"; export const action: ActionFunction = async ({ request, params }) => { const user = await requireUserId(request); @@ -46,15 +46,17 @@ export const action: ActionFunction = async ({ request, params }) => { }); const tournamentId = tournamentIdFromParams(params); - const event = notFoundIfFalsy(findByIdentifier(tournamentId)); - const teams = findTeamsByTournamentId(event.id); + const tournament = notFoundIfFalsy( + await TournamentRepository.findById(tournamentId), + ); + const teams = findTeamsByTournamentId(tournament.id); - validate(canAdminTournament({ user, event }), "Unauthorized", 401); + validate(canAdminTournament({ user, tournament }), "Unauthorized", 401); switch (data._action) { case "UPDATE_SHOW_MAP_LIST_GENERATOR": { updateShowMapListGenerator({ - tournamentId: event.id, + tournamentId: tournament.id, showMapListGenerator: Number(data.show), }); break; @@ -79,7 +81,7 @@ export const action: ActionFunction = async ({ request, params }) => { const team = teams.find((t) => t.id === data.teamId); validate(team, "Invalid team id"); validateCanCheckIn({ - event, + event: tournament, team, mapPool: findMapPoolByTeamId(team.id), }); @@ -90,7 +92,7 @@ export const action: ActionFunction = async ({ request, params }) => { case "CHECK_OUT": { const team = teams.find((t) => t.id === data.teamId); validate(team, "Invalid team id"); - validate(!hasTournamentStarted(event.id), "Tournament has started"); + validate(!hasTournamentStarted(tournament.id), "Tournament has started"); checkOut(team.id); break; @@ -121,7 +123,7 @@ export const action: ActionFunction = async ({ request, params }) => { t.members.some((m) => m.userId === data.userId), ); - if (hasTournamentStarted(event.id)) { + if (hasTournamentStarted(tournament.id)) { validate( !previousTeam || !previousTeam.checkedInAt, "User is already on a checked in team", @@ -143,7 +145,7 @@ export const action: ActionFunction = async ({ request, params }) => { case "DELETE_TEAM": { const team = teams.find((t) => t.id === data.teamId); validate(team, "Invalid team id"); - validate(!hasTournamentStarted(event.id), "Tournament has started"); + validate(!hasTournamentStarted(tournament.id), "Tournament has started"); deleteTeam(team.id); break; @@ -163,8 +165,11 @@ export default function TournamentAdminPage() { const user = useUser(); - if (!canAdminTournament({ user, event: data.event }) || data.hasFinalized) { - return ; + if ( + !canAdminTournament({ user, tournament: data.tournament }) || + data.hasFinalized + ) { + return ; } return ( @@ -174,7 +179,7 @@ export default function TournamentAdminPage() {
    @@ -183,9 +188,9 @@ export default function TournamentAdminPage() { {!data.hasStarted ? (
    ); @@ -314,7 +316,7 @@ function MapList(props: Omit) { try { mapList = createTournamentMapList({ ...props, - tiebreakerMaps: new MapPool(data.tieBreakerMapPool), + tiebreakerMaps: new MapPool(data.tournament.tieBreakerMapPool), }); } catch (e) { console.error( @@ -334,7 +336,7 @@ function MapList(props: Omit) { maps: new MapPool([]), }, ], - tiebreakerMaps: new MapPool(data.tieBreakerMapPool), + tiebreakerMaps: new MapPool(data.tournament.tieBreakerMapPool), }); } diff --git a/app/features/tournament/routes/to.$id.register.tsx b/app/features/tournament/routes/to.$id.register.tsx index 35c0b7d41..09781efb5 100644 --- a/app/features/tournament/routes/to.$id.register.tsx +++ b/app/features/tournament/routes/to.$id.register.tsx @@ -28,8 +28,8 @@ import { UserIcon } from "~/components/icons/User"; import { useAutoRerender } from "~/hooks/useAutoRerender"; import { useIsMounted } from "~/hooks/useIsMounted"; import { useTranslation } from "~/hooks/useTranslation"; -import { useUser } from "~/modules/auth"; -import { getUser, requireUser } from "~/modules/auth/user.server"; +import { useUser } from "~/features/auth/core"; +import { getUser, requireUser } from "~/features/auth/core/user.server"; import type { ModeShort, RankedModeShort, @@ -37,7 +37,6 @@ import type { } from "~/modules/in-game-lists"; import { stageIds } from "~/modules/in-game-lists"; import { rankedModesShort } from "~/modules/in-game-lists/modes"; -import { MapPool } from "~/modules/map-pool-serializer"; import { databaseTimestampToDate } from "~/utils/dates"; import { notFoundIfFalsy, @@ -89,6 +88,8 @@ import { FormWithConfirm } from "~/components/FormWithConfirm"; import { deleteTeam } from "../queries/deleteTeam.server"; import { findMapPoolByTeamId } from "~/features/tournament-bracket"; import { Popover } from "~/components/Popover"; +import * as TeamRepository from "~/features/team/TeamRepository.server"; +import { MapPool } from "~/features/map-list-generator/core/map-pool"; export const handle: SendouRouteHandle = { breadcrumb: () => ({ @@ -191,7 +192,7 @@ export const action: ActionFunction = async ({ request, params }) => { validate( findTrustedPlayers({ userId: user.id, - teamId: user.team?.id, + teamId: (await TeamRepository.findByUserId(user.id))?.id, }).some((trustedPlayer) => trustedPlayer.id === data.userId), "No trust given from this user", ); @@ -241,7 +242,7 @@ export const loader = async ({ request, params }: LoaderArgs) => { mapPool: findMapPoolByTeamId(ownTeam.id), trustedPlayers: findTrustedPlayers({ userId: user.id, - teamId: user.team?.id, + teamId: (await TeamRepository.findByUserId(user.id))?.id, }), }; }; @@ -259,24 +260,26 @@ export default function TournamentRegisterPage() {
    -
    {parentRouteData.event.name}
    +
    + {parentRouteData.tournament.name} +
    {" "} - {discordFullName(parentRouteData.event.author)} + {parentRouteData.tournament.author.discordName}
    {" "} {isMounted ? databaseTimestampToDate( - parentRouteData.event.startTime, + parentRouteData.tournament.startTime, ).toLocaleString(i18n.language, { timeZoneName: "short", minute: "numeric", @@ -287,7 +290,7 @@ export default function TournamentRegisterPage() { : null}
    - {modesIncluded(parentRouteData.event).map((mode) => ( + {modesIncluded(parentRouteData.tournament).map((mode) => (
    @@ -296,7 +299,7 @@ export default function TournamentRegisterPage() {
    -
    {parentRouteData.event.description}
    +
    {parentRouteData.tournament.description}
    {isRegularMemberOfATeam ? ( {t("tournament:pre.inATeam")} ) : ( @@ -304,7 +307,7 @@ export default function TournamentRegisterPage() { )} {!parentRouteData.teamMemberOfName ? ( {t("tournament:pre.sub.prompt")} @@ -350,7 +353,7 @@ function RegistrationForms({ mapPool={data?.mapPool} members={ownTeamFromList?.members} /> - {ownTeam || !checkInHasEnded(parentRouteData.event) ? ( + {ownTeam || !checkInHasEnded(parentRouteData.tournament) ? ( - !isOneModeTournamentOf(parentRouteData.event) || - isOneModeTournamentOf(parentRouteData.event) === mode, + !isOneModeTournamentOf(parentRouteData.tournament) || + isOneModeTournamentOf(parentRouteData.tournament) === mode, ) .map((mode) => { - const tiebreakerStageId = parentRouteData.tieBreakerMapPool.find( - (stage) => stage.mode === mode, - )?.stageId; + const tiebreakerStageId = + parentRouteData.tournament.tieBreakerMapPool.find( + (stage) => stage.mode === mode, + )?.stageId; return (
    @@ -896,7 +902,7 @@ function CounterPickMapPoolPicker() { ) : null}
    {new Array( - isOneModeTournamentOf(parentRouteData.event) + isOneModeTournamentOf(parentRouteData.tournament) ? TOURNAMENT.COUNTERPICK_ONE_MODE_TOURNAMENT_MAPS_PER_MODE : TOURNAMENT.COUNTERPICK_MAPS_PER_MODE, ) @@ -934,7 +940,7 @@ function CounterPickMapPoolPicker() { })} {validateCounterPickMapPool( counterPickMapPool, - isOneModeTournamentOf(parentRouteData.event), + isOneModeTournamentOf(parentRouteData.tournament), ) === "VALID" ? ( )} diff --git a/app/features/tournament/routes/to.$id.seeds.tsx b/app/features/tournament/routes/to.$id.seeds.tsx index 9b5fcdba8..1a8e3a050 100644 --- a/app/features/tournament/routes/to.$id.seeds.tsx +++ b/app/features/tournament/routes/to.$id.seeds.tsx @@ -34,16 +34,16 @@ import type { TournamentLoaderData, TournamentLoaderTeam } from "./to.$id"; import { Image } from "~/components/Image"; import { navIconUrl, tournamentBracketsPage } from "~/utils/urls"; import { maxXPowers } from "../queries/maxXPowers.server"; -import { requireUser } from "~/modules/auth"; +import { requireUser } from "~/features/auth/core"; import { notFoundIfFalsy, parseRequestFormData, validate } from "~/utils/remix"; import { seedsActionSchema } from "../tournament-schemas.server"; import { updateTeamSeeds } from "../queries/updateTeamSeeds.server"; import { tournamentIdFromParams } from "../tournament-utils"; import hasTournamentStarted from "../queries/hasTournamentStarted.server"; import { canAdminTournament } from "~/permissions"; -import { findByIdentifier } from "../queries/findByIdentifier.server"; import { SubmitButton } from "~/components/SubmitButton"; import clone from "just-clone"; +import * as TournamentRepository from "../TournamentRepository.server"; export const action: ActionFunction = async ({ request, params }) => { const data = await parseRequestFormData({ @@ -52,11 +52,13 @@ export const action: ActionFunction = async ({ request, params }) => { }); const user = await requireUser(request); const tournamentId = tournamentIdFromParams(params); - const tournament = notFoundIfFalsy(findByIdentifier(tournamentId)); + const tournament = notFoundIfFalsy( + await TournamentRepository.findById(tournamentId), + ); const hasStarted = hasTournamentStarted(tournamentId); - validate(canAdminTournament({ user, event: tournament })); + validate(canAdminTournament({ user, tournament })); validate(!hasStarted, "Tournament has started"); updateTeamSeeds({ tournamentId, teamIds: data.seeds }); @@ -68,9 +70,11 @@ export const loader = async ({ params, request }: LoaderArgs) => { const user = await requireUser(request); const tournamentId = tournamentIdFromParams(params); const hasStarted = hasTournamentStarted(tournamentId); - const tournament = notFoundIfFalsy(findByIdentifier(tournamentId)); + const tournament = notFoundIfFalsy( + await TournamentRepository.findById(tournamentId), + ); - if (!canAdminTournament({ user, event: tournament }) || hasStarted) { + if (!canAdminTournament({ user, tournament }) || hasStarted) { throw redirect(tournamentBracketsPage(tournamentId)); } @@ -229,7 +233,7 @@ function SeedAlert({ teamOrder }: { teamOrder: number[] }) { return ( - + (); if (!parentRouteData.hasStarted || parentRouteData.hasFinalized) { - return ; + return ( + + ); } if (data.streams.length === 0) { diff --git a/app/features/tournament/routes/to.$id.teams.$tid.tsx b/app/features/tournament/routes/to.$id.teams.$tid.tsx index 9623a3da0..33efd6284 100644 --- a/app/features/tournament/routes/to.$id.teams.$tid.tsx +++ b/app/features/tournament/routes/to.$id.teams.$tid.tsx @@ -78,7 +78,7 @@ export default function TournamentTeamPage() { ); const team = parentRouteData.teams[teamIndex]; if (!team) { - return ; + return ; } return ( @@ -201,7 +201,7 @@ function SetInfo({ @@ -246,7 +246,7 @@ function SetInfo({ diff --git a/app/features/tournament/routes/to.$id.teams.tsx b/app/features/tournament/routes/to.$id.teams.tsx index e5874bba1..8445e09a6 100644 --- a/app/features/tournament/routes/to.$id.teams.tsx +++ b/app/features/tournament/routes/to.$id.teams.tsx @@ -15,7 +15,7 @@ export default function TournamentTeamsPage() { team={team} seed={i + 1} teamPageUrl={tournamentTeamPage({ - eventId: data.event.id, + eventId: data.tournament.id, tournamentTeamId: team.id, })} /> diff --git a/app/features/tournament/routes/to.$id.tsx b/app/features/tournament/routes/to.$id.tsx index e552c4f4a..46bb5ef9b 100644 --- a/app/features/tournament/routes/to.$id.tsx +++ b/app/features/tournament/routes/to.$id.tsx @@ -12,16 +12,14 @@ import { } from "@remix-run/react"; import { Main } from "~/components/Main"; import { SubNav, SubNavLink } from "~/components/SubNav"; -import { db } from "~/db"; import { useTranslation } from "~/hooks/useTranslation"; -import { useUser } from "~/modules/auth"; -import { getUser } from "~/modules/auth/user.server"; +import { useUser } from "~/features/auth/core"; +import { getUser } from "~/features/auth/core/user.server"; import { canAdminTournament } from "~/permissions"; import { notFoundIfFalsy, type SendouRouteHandle } from "~/utils/remix"; import { makeTitle } from "~/utils/strings"; import { assertUnreachable, type Unpacked } from "~/utils/types"; import { streamsByTournamentId } from "../core/streams.server"; -import { findByIdentifier } from "../queries/findByIdentifier.server"; import { findTeamsByTournamentId } from "../queries/findTeamsByTournamentId.server"; import hasTournamentStarted from "../queries/hasTournamentStarted.server"; import { teamHasCheckedIn, tournamentIdFromParams } from "../tournament-utils"; @@ -29,6 +27,7 @@ import styles from "../tournament.css"; import { findOwnTeam } from "../queries/findOwnTeam.server"; import { findSubsByTournamentId } from "~/features/tournament-subs"; import hasTournamentFinalized from "../queries/hasTournamentFinalized.server"; +import * as TournamentRepository from "../TournamentRepository.server"; export const shouldRevalidate: ShouldRevalidateFunction = (args) => { const wasMutation = args.formMethod === "POST"; @@ -52,7 +51,7 @@ export const meta: V2_MetaFunction = (args) => { if (!data) return []; - return [{ title: makeTitle(data.event.name) }]; + return [{ title: makeTitle(data.tournament.name) }]; }; export const links: LinksFunction = () => { @@ -69,7 +68,9 @@ export type TournamentLoaderData = SerializeFrom; export const loader = async ({ params, request }: LoaderArgs) => { const user = await getUser(request); const tournamentId = tournamentIdFromParams(params); - const event = notFoundIfFalsy(findByIdentifier(tournamentId)); + const tournament = notFoundIfFalsy( + await TournamentRepository.findById(tournamentId), + ); const hasStarted = hasTournamentStarted(tournamentId); let teams = findTeamsByTournamentId(tournamentId); @@ -107,10 +108,7 @@ export const loader = async ({ params, request }: LoaderArgs) => { }).length; return { - event, - tieBreakerMapPool: db.calendarEvents.findTieBreakerMapPoolByEventId( - event.eventId, - ), + tournament, ownTeam: user ? findOwnTeam({ tournamentId, @@ -148,7 +146,7 @@ export default function TournamentLayout() { {t("tournament:tabs.brackets")} - {data.event.showMapListGenerator ? ( + {data.tournament.showMapListGenerator ? ( {t("tournament:tabs.maps")} ) : null} @@ -164,11 +162,11 @@ export default function TournamentLayout() { {t("tournament:tabs.streams", { count: data.streamsCount })} ) : null} - {canAdminTournament({ user, event: data.event }) && + {canAdminTournament({ user, tournament: data.tournament }) && !data.hasStarted && ( {t("tournament:tabs.seeds")} )} - {canAdminTournament({ user, event: data.event }) && + {canAdminTournament({ user, tournament: data.tournament }) && !data.hasFinalized && ( {t("tournament:tabs.admin")} diff --git a/app/features/tournament/tournament-hooks.ts b/app/features/tournament/tournament-hooks.ts index de4bd32f0..f23f4ba42 100644 --- a/app/features/tournament/tournament-hooks.ts +++ b/app/features/tournament/tournament-hooks.ts @@ -16,8 +16,12 @@ export function useSelectCounterpickMapPoolState() { .filter((pair) => pair.mode === mode) .map((pair) => pair.stageId); - if (filteredStages.length !== mapPickCountPerMode(parentRouteData.event)) { - return new Array(mapPickCountPerMode(parentRouteData.event)).fill(null); + if ( + filteredStages.length !== mapPickCountPerMode(parentRouteData.tournament) + ) { + return new Array(mapPickCountPerMode(parentRouteData.tournament)).fill( + null, + ); } return filteredStages as [StageId, StageId]; @@ -40,7 +44,7 @@ export function useSelectCounterpickMapPoolState() { (e) => { setCounterpickMaps({ ...counterpickMaps, - [mode]: new Array(mapPickCountPerMode(parentRouteData.event)) + [mode]: new Array(mapPickCountPerMode(parentRouteData.tournament)) .fill(null) .map((_, i) => counterpickMaps[mode][i]) .map((stageId, j) => { diff --git a/app/features/tournament/tournament-utils.ts b/app/features/tournament/tournament-utils.ts index 028d11c68..b093db0a1 100644 --- a/app/features/tournament/tournament-utils.ts +++ b/app/features/tournament/tournament-utils.ts @@ -77,7 +77,9 @@ export function isOneModeTournamentOf( : null; } -export function HACKY_resolvePicture(event: TournamentLoaderData["event"]) { +export function HACKY_resolvePicture( + event: TournamentLoaderData["tournament"], +) { if (event.name.includes("In The Zone")) return "https://abload.de/img/screenshot2023-04-19a2bfv0.png"; @@ -87,25 +89,25 @@ export function HACKY_resolvePicture(event: TournamentLoaderData["event"]) { // hacky because db query not taking in account possibility of many start times // AND always assumed check-in starts 1h before export function HACKY_resolveCheckInTime( - event: Pick, + event: Pick, ) { return databaseTimestampToDate(event.startTime - 60 * 60); } -export function mapPickCountPerMode(event: TournamentLoaderData["event"]) { +export function mapPickCountPerMode(event: TournamentLoaderData["tournament"]) { return isOneModeTournamentOf(event) ? TOURNAMENT.COUNTERPICK_ONE_MODE_TOURNAMENT_MAPS_PER_MODE : TOURNAMENT.COUNTERPICK_MAPS_PER_MODE; } export function checkInHasStarted( - event: Pick, + event: Pick, ) { return HACKY_resolveCheckInTime(event).getTime() < Date.now(); } export function checkInHasEnded( - event: Pick, + event: Pick, ) { return databaseTimestampToDate(event.startTime).getTime() < Date.now(); } @@ -115,7 +117,7 @@ export function validateCanCheckIn({ team, mapPool, }: { - event: Pick; + event: Pick; team: FindTeamsByTournamentId[number]; mapPool: unknown[] | null; }) { diff --git a/app/features/user-page/UserRepository.server.ts b/app/features/user-page/UserRepository.server.ts new file mode 100644 index 000000000..a2c0df02f --- /dev/null +++ b/app/features/user-page/UserRepository.server.ts @@ -0,0 +1,481 @@ +import type { ExpressionBuilder, FunctionModule } from "kysely"; +import { sql } from "kysely"; +import { jsonArrayFrom, jsonObjectFrom } from "kysely/helpers/sqlite"; +import { db, sql as dbDirect } from "~/db/sql"; +import type { DB, TablesInsertable } from "~/db/tables"; +import type { User } from "~/db/types"; +import { dateToDatabaseTimestamp } from "~/utils/dates"; +import { COMMON_USER_FIELDS } from "~/utils/kysely.server"; +import { safeNumberParse } from "~/utils/number"; + +const identifierToUserIdQuery = (identifier: string) => + db + .selectFrom("User") + .select("User.id") + .where((eb) => { + // we don't want to parse discord id's as numbers (length = 18) + const parsedId = + identifier.length < 10 ? safeNumberParse(identifier) : null; + if (parsedId) { + return eb("User.id", "=", parsedId); + } + + return eb.or([ + eb("User.discordId", "=", identifier), + eb("User.customUrl", "=", identifier), + ]); + }); + +export function identifierToUserId(identifier: string) { + return identifierToUserIdQuery(identifier).executeTakeFirst(); +} + +export function findByIdentifier(identifier: string) { + return identifierToUserIdQuery(identifier) + .select(({ eb }) => [ + "User.discordAvatar", + "User.discordDiscriminator", + "User.discordId", + "User.discordName", + "User.showDiscordUniqueName", + "User.discordUniqueName", + "User.customUrl", + "User.inGameName", + "User.twitter", + "User.country", + "User.bio", + "User.motionSens", + "User.stickSens", + "User.css", + "User.twitch", + "User.twitter", + "User.youtubeId", + "User.favoriteBadgeId", + "User.banned", + "User.commissionText", + "User.commissionsOpen", + "User.patronTier", + jsonArrayFrom( + eb + .selectFrom("UserWeapon") + .select(["UserWeapon.weaponSplId", "UserWeapon.isFavorite"]) + .whereRef("UserWeapon.userId", "=", "User.id") + .orderBy("UserWeapon.order", "asc"), + ).as("weapons"), + jsonObjectFrom( + eb + .selectFrom("TeamMember") + .innerJoin("Team", "Team.id", "TeamMember.teamId") + .leftJoin( + "UserSubmittedImage", + "UserSubmittedImage.id", + "Team.avatarImgId", + ) + .select([ + "Team.name", + "Team.customUrl", + "Team.id", + "UserSubmittedImage.url as avatarUrl", + ]) + .whereRef("TeamMember.userId", "=", "User.id"), + ).as("team"), + ]) + .executeTakeFirst(); +} + +export function findLeanById(id: number) { + return db + .selectFrom("User") + .leftJoin("PlusTier", "PlusTier.userId", "User.id") + .where("User.id", "=", id) + .select([ + ...COMMON_USER_FIELDS, + "User.isArtist", + "User.isVideoAdder", + "User.patronTier", + "User.favoriteBadgeId", + "User.banned", + "User.languages", + "PlusTier.tier as plusTier", + ]) + .executeTakeFirst(); +} + +export function findAllPatrons() { + return db + .selectFrom("User") + .select([ + "User.id", + "User.discordId", + "User.discordName", + "User.discordDiscriminator", + "User.patronTier", + ]) + .where("User.patronTier", "is not", null) + .orderBy("User.patronTier", "desc") + .orderBy("User.patronSince", "asc") + .execute(); +} + +export function findAllPlusMembers() { + return db + .selectFrom("User") + .innerJoin("PlusTier", "PlusTier.userId", "User.id") + .select(["User.id", "User.discordId", "PlusTier.tier as plusTier"]) + .execute(); +} + +const withMaxEventStartTime = (eb: ExpressionBuilder) => { + return eb + .selectFrom("CalendarEventDate") + .select(({ fn }) => [fn.max("CalendarEventDate.startTime").as("startTime")]) + .whereRef("CalendarEventDate.eventId", "=", "CalendarEvent.id") + .as("startTime"); +}; +export function findResultsByUserId(userId: number) { + return db + .selectFrom("CalendarEventResultPlayer") + .innerJoin( + "CalendarEventResultTeam", + "CalendarEventResultTeam.id", + "CalendarEventResultPlayer.teamId", + ) + .innerJoin( + "CalendarEvent", + "CalendarEvent.id", + "CalendarEventResultTeam.eventId", + ) + .select(({ eb, exists, selectFrom }) => [ + "CalendarEvent.id as eventId", + sql`null`.as("tournamentId"), + "CalendarEventResultTeam.placement", + "CalendarEvent.participantCount", + "CalendarEvent.name as eventName", + "CalendarEventResultTeam.id as teamId", + "CalendarEventResultTeam.name as teamName", + // TODO: can we get rid of the "as"? + withMaxEventStartTime(eb as ExpressionBuilder), + exists( + selectFrom("UserResultHighlight") + .where("UserResultHighlight.userId", "=", userId) + .whereRef( + "UserResultHighlight.teamId", + "=", + "CalendarEventResultTeam.id", + ) + .select("UserResultHighlight.userId"), + ).as("isHighlight"), + jsonArrayFrom( + eb + .selectFrom("CalendarEventResultPlayer") + .leftJoin("User", "User.id", "CalendarEventResultPlayer.userId") + .select([...COMMON_USER_FIELDS, "CalendarEventResultPlayer.name"]) + .whereRef( + "CalendarEventResultPlayer.teamId", + "=", + "CalendarEventResultTeam.id", + ) + .where((eb) => + eb.or([ + eb("CalendarEventResultPlayer.userId", "is", null), + eb("CalendarEventResultPlayer.userId", "!=", userId), + ]), + ), + ).as("mates"), + ]) + .where("CalendarEventResultPlayer.userId", "=", userId) + .unionAll( + db + .selectFrom("TournamentResult") + .innerJoin( + "TournamentTeam", + "TournamentTeam.id", + "TournamentResult.tournamentTeamId", + ) + .innerJoin( + "CalendarEvent", + "CalendarEvent.tournamentId", + "TournamentResult.tournamentId", + ) + .select(({ eb }) => [ + sql`null`.as("eventId"), + "TournamentResult.tournamentId", + "TournamentResult.placement", + "TournamentResult.participantCount", + "CalendarEvent.name as eventName", + "TournamentTeam.id as teamId", + "TournamentTeam.name as teamName", + withMaxEventStartTime(eb), + "TournamentResult.isHighlight", + jsonArrayFrom( + eb + .selectFrom("TournamentTeamMember") + .innerJoin("User", "User.id", "TournamentTeamMember.userId") + .select([ + ...COMMON_USER_FIELDS, + sql`null`.as("name"), + ]) + .whereRef( + "TournamentTeamMember.tournamentTeamId", + "=", + "TournamentTeam.id", + ) + .where("TournamentTeamMember.userId", "!=", userId), + ).as("mates"), + ]) + .where("TournamentResult.userId", "=", userId), + ) + .orderBy("startTime", "desc") + .execute(); +} + +const searchSelectedFields = ({ fn }: { fn: FunctionModule }) => + [ + ...COMMON_USER_FIELDS, + "User.inGameName", + "PlusTier.tier as plusTier", + fn("iif", [ + "User.showDiscordUniqueName", + "User.discordUniqueName", + sql`null`, + ]).as("discordUniqueName"), + ] as const; +export function search({ query, limit }: { query: string; limit: number }) { + const criteria = `%${query}%`; + + return db + .selectFrom("User") + .leftJoin("PlusTier", "PlusTier.userId", "User.id") + .select(searchSelectedFields) + .where((eb) => + eb.or([ + eb("User.discordName", "like", criteria), + eb("User.inGameName", "like", criteria), + eb("User.discordUniqueName", "like", criteria), + eb("User.twitter", "like", criteria), + ]), + ) + .orderBy( + (eb) => + eb + .case() + .when("PlusTier.tier", "is", null) + .then(4) + .else(eb.ref("PlusTier.tier")) + .end(), + "asc", + ) + .limit(limit) + .execute(); +} + +export function searchExact(args: { + id?: number; + discordId?: string; + customUrl?: string; +}) { + let query = db + .selectFrom("User") + .leftJoin("PlusTier", "PlusTier.userId", "User.id") + .select(searchSelectedFields); + + if (args.id) { + query = query.where("User.id", "=", args.id); + } + + if (args.discordId) { + query = query.where("User.discordId", "=", args.discordId); + } + + if (args.customUrl) { + query = query.where("User.customUrl", "=", args.customUrl); + } + + return query.execute(); +} + +export function upsert( + args: Pick< + TablesInsertable["User"], + | "discordId" + | "discordName" + | "discordDiscriminator" + | "discordAvatar" + | "discordUniqueName" + | "twitch" + | "twitter" + | "youtubeId" + >, +) { + return db + .insertInto("User") + .values(args) + .onConflict((oc) => { + const { discordId, ...rest } = args; + + return oc.column("discordId").doUpdateSet(rest); + }) + .returning("id") + .executeTakeFirstOrThrow(); +} + +type UpdateProfileArgs = Pick< + TablesInsertable["User"], + | "country" + | "bio" + | "customUrl" + | "motionSens" + | "stickSens" + | "inGameName" + | "css" + | "favoriteBadgeId" + | "showDiscordUniqueName" + | "commissionText" + | "commissionsOpen" +> & { + userId: number; + weapons: Pick[]; +}; +export function updateProfile(args: UpdateProfileArgs) { + return db.transaction().execute(async (trx) => { + await trx + .deleteFrom("UserWeapon") + .where("userId", "=", args.userId) + .execute(); + + await trx + .insertInto("UserWeapon") + .values( + args.weapons.map((weapon, i) => ({ + userId: args.userId, + weaponSplId: weapon.weaponSplId, + isFavorite: weapon.isFavorite, + order: i + 1, + })), + ) + .execute(); + + return trx + .updateTable("User") + .set({ + country: args.country, + bio: args.bio, + customUrl: args.customUrl, + motionSens: args.motionSens, + stickSens: args.stickSens, + inGameName: args.inGameName, + css: args.css, + favoriteBadgeId: args.favoriteBadgeId, + showDiscordUniqueName: args.showDiscordUniqueName, + commissionText: args.commissionText, + commissionsOpen: args.commissionsOpen, + }) + .where("id", "=", args.userId) + .returning(["User.id", "User.customUrl", "User.discordId"]) + .executeTakeFirstOrThrow(); + }); +} + +type UpdateResultHighlightsArgs = { + userId: number; + resultTeamIds: Array; + resultTournamentTeamIds: Array; +}; +export function updateResultHighlights(args: UpdateResultHighlightsArgs) { + return db.transaction().execute(async (trx) => { + await trx + .deleteFrom("UserResultHighlight") + .where("userId", "=", args.userId) + .execute(); + + if (args.resultTeamIds.length > 0) { + await trx + .insertInto("UserResultHighlight") + .values( + args.resultTeamIds.map((teamId) => ({ + userId: args.userId, + teamId, + })), + ) + .execute(); + } + + await trx + .updateTable("TournamentResult") + .set({ + isHighlight: 0, + }) + .where("TournamentResult.userId", "=", args.userId) + .execute(); + + if (args.resultTournamentTeamIds.length > 0) { + await trx + .updateTable("TournamentResult") + .set({ + isHighlight: 1, + }) + .where("TournamentResult.userId", "=", args.userId) + .where( + "TournamentResult.tournamentTeamId", + "in", + args.resultTournamentTeamIds, + ) + .execute(); + } + }); +} + +export type UpdatePatronDataArgs = Array< + Pick +>; +export function updatePatronData(users: UpdatePatronDataArgs) { + return db.transaction().execute(async (trx) => { + await trx + .updateTable("User") + .set({ + patronTier: null, + patronSince: null, + patronTill: null, + }) + .where("patronTill", "<", dateToDatabaseTimestamp(new Date())) + .execute(); + + for (const user of users) { + await trx + .updateTable("User") + .set({ + patronTier: user.patronTier, + patronSince: user.patronSince, + patronTill: null, + }) + .where("User.discordId", "=", user.discordId) + .execute(); + } + }); +} + +// TODO: use Kysely +const updateByDiscordIdStm = dbDirect.prepare(/* sql */ ` + update + "User" + set + "discordAvatar" = @discordAvatar, + "discordName" = coalesce(@discordName, "discordName"), + "discordUniqueName" = coalesce(@discordUniqueName, "discordUniqueName") + where + "discordId" = @discordId +`); +export const updateMany = dbDirect.transaction( + ( + argsArr: Array< + Pick< + User, + "discordAvatar" | "discordName" | "discordUniqueName" | "discordId" + > + >, + ) => { + for (const updateArgs of argsArr) { + updateByDiscordIdStm.run(updateArgs); + } + }, +); diff --git a/app/routes/u.$identifier/results/components/UserResultsTable.tsx b/app/features/user-page/components/UserResultsTable.tsx similarity index 85% rename from app/routes/u.$identifier/results/components/UserResultsTable.tsx rename to app/features/user-page/components/UserResultsTable.tsx index 21401c1a6..6da3045fc 100644 --- a/app/routes/u.$identifier/results/components/UserResultsTable.tsx +++ b/app/features/user-page/components/UserResultsTable.tsx @@ -2,9 +2,8 @@ import { Link } from "@remix-run/react"; import { useTranslation } from "~/hooks/useTranslation"; import { Avatar } from "~/components/Avatar"; import { Placement } from "~/components/Placement"; -import { type UserPageLoaderData } from "~/routes/u.$identifier"; +import { type UserPageLoaderData } from "~/features/user-page/routes/u.$identifier"; import { databaseTimestampToDate } from "~/utils/dates"; -import { discordFullName } from "~/utils/strings"; import { calendarEventPage, tournamentBracketsPage, @@ -67,7 +66,7 @@ export function UserResultsTable({ : HIGHLIGHT_CHECKBOX_NAME } type="checkbox" - defaultChecked={result.isHighlight} + defaultChecked={Boolean(result.isHighlight)} /> )} @@ -102,7 +101,8 @@ export function UserResultsTable({ {result.participantCount} - {databaseTimestampToDate(result.startTime).toLocaleDateString( + {/* TODO: can be made better when $narrowNotNull lands */} + {databaseTimestampToDate(result.startTime!).toLocaleDateString( i18n.language, { day: "numeric", @@ -115,18 +115,19 @@ export function UserResultsTable({
      {result.mates.map((player) => (
    • - {typeof player === "string" ? ( - player + {player.name ? ( + player.name ) : ( + // as any but we know it's a user since it doesn't have name - - {discordFullName(player)} + + {player.discordName} )}
    • diff --git a/app/routes/u.$identifier/art.tsx b/app/features/user-page/routes/u.$identifier.art.tsx similarity index 94% rename from app/routes/u.$identifier/art.tsx rename to app/features/user-page/routes/u.$identifier.art.tsx index 064dec21b..05a05a893 100644 --- a/app/routes/u.$identifier/art.tsx +++ b/app/features/user-page/routes/u.$identifier.art.tsx @@ -1,13 +1,12 @@ import type { ActionFunction, LoaderArgs } from "@remix-run/node"; -import type { UserPageLoaderData } from "../u.$identifier"; -import { userParamsSchema } from "../u.$identifier"; +import type { UserPageLoaderData } from "./u.$identifier"; +import { userParamsSchema } from "./u.$identifier"; import { type SendouRouteHandle, notFoundIfFalsy, validate, parseRequestFormData, } from "~/utils/remix"; -import { db } from "~/db"; import { artsByUserId, ArtGrid, @@ -17,7 +16,7 @@ import { } from "~/features/art"; import { useLoaderData, useMatches } from "@remix-run/react"; import { useSearchParamState } from "~/hooks/useSearchParamState"; -import { useUser } from "~/modules/auth"; +import { useUser } from "~/features/auth/core"; import { deleteArt, deleteArtSchema } from "~/features/art"; import invariant from "tiny-invariant"; import { LinkButton } from "~/components/Button"; @@ -25,7 +24,8 @@ import { Popover } from "~/components/Popover"; import { countUnvalidatedArt } from "~/features/img-upload"; import { useTranslation } from "~/hooks/useTranslation"; import { newArtPage } from "~/utils/urls"; -import { getUserId, requireUserId } from "~/modules/auth/user.server"; +import { getUserId, requireUserId } from "~/features/auth/core/user.server"; +import * as UserRepository from "~/features/user-page/UserRepository.server"; export const handle: SendouRouteHandle = { i18n: ["art"], @@ -53,7 +53,9 @@ export const loader = async ({ params, request }: LoaderArgs) => { const loggedInUser = await getUserId(request); const { identifier } = userParamsSchema.parse(params); - const user = notFoundIfFalsy(db.users.findByIdentifier(identifier)); + const user = notFoundIfFalsy( + await UserRepository.identifierToUserId(identifier), + ); const arts = artsByUserId(user.id); diff --git a/app/routes/u.$identifier/builds/new.tsx b/app/features/user-page/routes/u.$identifier.builds.new.tsx similarity index 97% rename from app/routes/u.$identifier/builds/new.tsx rename to app/features/user-page/routes/u.$identifier.builds.new.tsx index 897e8dfd3..b09e4dad9 100644 --- a/app/routes/u.$identifier/builds/new.tsx +++ b/app/features/user-page/routes/u.$identifier.builds.new.tsx @@ -19,7 +19,6 @@ import { Label } from "~/components/Label"; import { RequiredHiddenInput } from "~/components/RequiredHiddenInput"; import { SubmitButton } from "~/components/SubmitButton"; import { BUILD } from "~/constants"; -import { db } from "~/db"; import type { GearType } from "~/db/types"; import { validatedBuildFromSearchParams, @@ -27,8 +26,8 @@ import { } from "~/features/build-analyzer"; import { buildsByUserId } from "~/features/builds"; import { useTranslation } from "~/hooks/useTranslation"; -import { requireUser } from "~/modules/auth"; -import { requireUserId } from "~/modules/auth/user.server"; +import { requireUser } from "~/features/auth/core"; +import { requireUserId } from "~/features/auth/core/user.server"; import { type Ability, clothesGearIds, @@ -65,6 +64,7 @@ import { toArray, weaponSplId, } from "~/utils/zod"; +import * as BuildRepository from "~/features/builds/BuildRepository.server"; const newBuildActionSchema = z.object({ buildToEditId: z.preprocess(actualNumber, id.nullish()), @@ -165,9 +165,9 @@ export const action: ActionFunction = async ({ request }) => { private: data.private, }; if (data.buildToEditId) { - db.builds.updateByReplacing({ id: data.buildToEditId, ...commonArgs }); + await BuildRepository.update({ id: data.buildToEditId, ...commonArgs }); } else { - db.builds.create(commonArgs); + await BuildRepository.create(commonArgs); } throw redirect(userBuildsPage(user)); diff --git a/app/routes/u.$identifier/builds/index.tsx b/app/features/user-page/routes/u.$identifier.builds.tsx similarity index 85% rename from app/routes/u.$identifier/builds/index.tsx rename to app/features/user-page/routes/u.$identifier.builds.tsx index a47c76bb5..1b91cb71c 100644 --- a/app/routes/u.$identifier/builds/index.tsx +++ b/app/features/user-page/routes/u.$identifier.builds.tsx @@ -4,25 +4,26 @@ import { z } from "zod"; import { BuildCard } from "~/components/BuildCard"; import { Button, LinkButton } from "~/components/Button"; import { BUILD } from "~/constants"; -import { db } from "~/db"; import { useTranslation } from "~/hooks/useTranslation"; -import { useUser } from "~/modules/auth"; -import { getUserId, requireUserId } from "~/modules/auth/user.server"; +import { useUser } from "~/features/auth/core"; +import { getUserId, requireUserId } from "~/features/auth/core/user.server"; import { atOrError } from "~/utils/arrays"; import { notFoundIfFalsy, parseRequestFormData, privatelyCachedJson, + validate, type SendouRouteHandle, } from "~/utils/remix"; import { userNewBuildPage } from "~/utils/urls"; import { actualNumber, id } from "~/utils/zod"; -import { userParamsSchema, type UserPageLoaderData } from "../../u.$identifier"; +import { userParamsSchema, type UserPageLoaderData } from "./u.$identifier"; import type { MainWeaponId } from "~/modules/in-game-lists"; import { mainWeaponIds } from "~/modules/in-game-lists"; import { WeaponImage } from "~/components/Image"; import { useSearchParamState } from "~/hooks/useSearchParamState"; -import { buildsByUserId } from "~/features/builds"; +import * as BuildRepository from "~/features/builds/BuildRepository.server"; +import * as UserRepository from "~/features/user-page/UserRepository.server"; const buildsActionSchema = z.object({ buildToDeleteId: z.preprocess(actualNumber, id), @@ -35,15 +36,14 @@ export const action: ActionFunction = async ({ request }) => { schema: buildsActionSchema, }); - if ( - !buildsByUserId({ userId: user.id, loggedInUserId: user?.id }).some( - (build) => build.id === data.buildToDeleteId, - ) - ) { - throw new Response(null, { status: 400 }); - } + const usersBuilds = await BuildRepository.allByUserId({ + userId: user.id, + showPrivate: true, + }); - db.builds.deleteById(data.buildToDeleteId); + validate(usersBuilds.some((build) => build.id === data.buildToDeleteId)); + + await BuildRepository.deleteById(data.buildToDeleteId); return null; }; @@ -55,11 +55,13 @@ export const handle: SendouRouteHandle = { export const loader = async ({ params, request }: LoaderArgs) => { const loggedInUser = await getUserId(request); const { identifier } = userParamsSchema.parse(params); - const user = notFoundIfFalsy(db.users.findByIdentifier(identifier)); + const user = notFoundIfFalsy( + await UserRepository.identifierToUserId(identifier), + ); - const builds = buildsByUserId({ + const builds = await BuildRepository.allByUserId({ userId: user.id, - loggedInUserId: loggedInUser?.id, + showPrivate: loggedInUser?.id === user.id, }); if (builds.length === 0 && loggedInUser?.id !== user.id) { @@ -161,6 +163,8 @@ function WeaponFilters({ const { t } = useTranslation(["weapons", "builds"]); const data = useLoaderData(); + if (data.builds.length === 0) return null; + return (