diff --git a/app/features/lobbies/LobbyRepository.server.test.ts b/app/features/lobbies/LobbyRepository.server.test.ts index 28b9f8b49..b0177aa00 100644 --- a/app/features/lobbies/LobbyRepository.server.test.ts +++ b/app/features/lobbies/LobbyRepository.server.test.ts @@ -13,6 +13,7 @@ import * as LobbySet from "./core/LobbySet"; import "./core/LobbyProgression.server"; import * as LobbyMatchRepository from "./LobbyMatchRepository.server"; import * as LobbyRepository from "./LobbyRepository.server"; +import { editArgs } from "./tests/fixtures"; const users = UserFactory.pool(); @@ -205,14 +206,12 @@ describe("LobbyRepository visibility", () => { expect(Boolean(found)).toBe(expected); }); - test("updateVisibility: opening a lobby to everyone shows it to outsiders", async () => { + test("edit: opening a lobby to everyone shows it to outsiders", async () => { const lobby = await associationLobby(); - await LobbyRepository.updateVisibility({ - id: lobby.id, - visibility: null, - ownerId: hostId(), - }); + await LobbyRepository.edit( + editArgs({ id: lobby.id, ownerId: hostId(), visibility: null }), + ); const found = await withUserId(outsiderId(), () => LobbyRepository.findById(lobby.id), @@ -220,15 +219,13 @@ describe("LobbyRepository visibility", () => { expect(found?.isPrivate).toBe(false); }); - test("updateVisibility: making a lobby link-only keeps its share key", async () => { + test("edit: making a lobby link-only keeps its share key", async () => { const lobby = await LobbyFactory.create({ hostUserId: hostId() }); const shareKey = await hostShareKeyOf(lobby.id); - await LobbyRepository.updateVisibility({ - id: lobby.id, - visibility: "LINK_ONLY", - ownerId: hostId(), - }); + await LobbyRepository.edit( + editArgs({ id: lobby.id, ownerId: hostId(), visibility: "LINK_ONLY" }), + ); expect(await LobbyRepository.isLinkOnlyById(lobby.id)).toBe(true); expect(await hostShareKeyOf(lobby.id)).toBe(shareKey); diff --git a/app/features/lobbies/LobbyRepository.server.ts b/app/features/lobbies/LobbyRepository.server.ts index c21000f6c..eecb4983b 100644 --- a/app/features/lobbies/LobbyRepository.server.ts +++ b/app/features/lobbies/LobbyRepository.server.ts @@ -752,8 +752,7 @@ export function insert({ .insertInto("Lobby") .values({ ...args, - modes: JSON.stringify(modes), - mapPool: mapPool ? JSON.stringify(mapPool) : null, + ...serializeMaps({ modes, mapPool }), isApplicationRequired: toDBBoolean(isApplicationRequired), chatRoomId: chatRoom.id, shareKey: shortNanoid(), @@ -761,16 +760,14 @@ export function insert({ .returning(["id", "createdAt"]) .executeTakeFirstOrThrow(); - if (visibility !== "LINK_ONLY") { - await trx - .insertInto("ContentVisibility") - .values( - Visibility.toRows(visibility, { - createdAt: lobby.createdAt, - ownerId: args.hostUserId, - }).map((row) => ({ ...row, lobbyId: lobby.id })), - ) - .execute(); + const rows = visibilityRows({ + visibility, + lobbyId: lobby.id, + ownerId: args.hostUserId, + createdAt: lobby.createdAt, + }); + if (rows.length > 0) { + await trx.insertInto("ContentVisibility").values(rows).execute(); } await trx @@ -984,73 +981,59 @@ export function kickParticipant(args: ParticipantRemoval) { }); } -/** Updates the settings the host can change after creating the lobby. */ -export async function updateSettings({ - id, - isApplicationRequired, - ...values -}: Pick< +type EditArgs = Pick< Tables["Lobby"], - "id" | "name" | "description" | "label" | "bestOf" | "weaponRandomizer" + | "id" + | "name" + | "description" + | "label" + | "bestOf" + | "weaponRandomizer" + | "modes" + | "mapPool" + | "mapsTournamentId" > & { isApplicationRequired: boolean; -}) { - await db - .updateTable("Lobby") - .set({ - ...values, - isApplicationRequired: toDBBoolean(isApplicationRequired), - }) - .where("Lobby.id", "=", id) - .execute(); -} - -/** Replaces where the lobby's sets get their maps from: the modes of the players' preferences or a map pool. */ -export async function updateMaps({ - id, - modes, - mapPool, - mapsTournamentId, -}: Pick) { - await db - .updateTable("Lobby") - .set({ - modes: JSON.stringify(modes), - mapPool: mapPool ? JSON.stringify(mapPool) : null, - mapsTournamentId, - }) - .where("Lobby.id", "=", id) - .execute(); -} - -/** Replaces who sees the lobby, its new audience in effect right away. Its share key keeps working. */ -export function updateVisibility({ - id, - visibility, - ownerId, -}: { - id: number; visibility: InsertArgs["visibility"]; /** The host, whose friends a friends only lobby is visible to */ ownerId: number; -}) { +}; + +/** Saves the settings the host can change after creating the lobby, the new audience in effect right away. Its share key keeps working. */ +export function edit({ + id, + ownerId, + visibility, + modes, + mapPool, + isApplicationRequired, + ...values +}: EditArgs) { return db.transaction().execute(async (trx) => { + await trx + .updateTable("Lobby") + .set({ + ...values, + ...serializeMaps({ modes, mapPool }), + isApplicationRequired: toDBBoolean(isApplicationRequired), + }) + .where("Lobby.id", "=", id) + .execute(); + await trx .deleteFrom("ContentVisibility") .where("ContentVisibility.lobbyId", "=", id) .execute(); - if (visibility === "LINK_ONLY") return; - - await trx - .insertInto("ContentVisibility") - .values( - Visibility.toRows(visibility, { - createdAt: databaseTimestampNow(), - ownerId, - }).map((row) => ({ ...row, lobbyId: id })), - ) - .execute(); + const rows = visibilityRows({ + visibility, + lobbyId: id, + ownerId, + createdAt: databaseTimestampNow(), + }); + if (rows.length > 0) { + await trx.insertInto("ContentVisibility").values(rows).execute(); + } }); } @@ -1264,3 +1247,32 @@ async function deleteParticipantEndingSet( return setEnding; } + +function serializeMaps({ + modes, + mapPool, +}: Pick) { + return { + modes: JSON.stringify(modes), + mapPool: mapPool ? JSON.stringify(mapPool) : null, + }; +} + +function visibilityRows({ + visibility, + lobbyId, + ownerId, + createdAt, +}: { + visibility: InsertArgs["visibility"]; + lobbyId: number; + ownerId: number; + createdAt: number; +}) { + if (visibility === "LINK_ONLY") return []; + + return Visibility.toRows(visibility, { createdAt, ownerId }).map((row) => ({ + ...row, + lobbyId, + })); +} diff --git a/app/features/lobbies/actions/lobbies.$id.server.ts b/app/features/lobbies/actions/lobbies.$id.server.ts index 37ffb0007..94763543b 100644 --- a/app/features/lobbies/actions/lobbies.$id.server.ts +++ b/app/features/lobbies/actions/lobbies.$id.server.ts @@ -312,7 +312,9 @@ export const action = defineAction( }); if (issue) return { fieldErrors: { [issue.path[0]]: issue.message } }; - await LobbyRepository.updateSettings({ + const maps = await mapsFromForm(body); + + await LobbyRepository.edit({ id, name: body.name ?? `${lobby.hostUsername}'s lobby`, description: body.description, @@ -320,13 +322,7 @@ export const action = defineAction( bestOf: Number(body.bestOf) as LobbyBestOf, weaponRandomizer: body.weaponRandomizer, isApplicationRequired: body.isApplicationRequired, - }); - await LobbyRepository.updateMaps({ - id, - ...(await mapsFromForm(body)), - }); - await LobbyRepository.updateVisibility({ - id, + ...maps, visibility: visibilityFromForm(body), ownerId: lobby.hostUserId, }); diff --git a/app/features/lobbies/core/LobbyProgression.server.test.ts b/app/features/lobbies/core/LobbyProgression.server.test.ts index de7582640..044af63cd 100644 --- a/app/features/lobbies/core/LobbyProgression.server.test.ts +++ b/app/features/lobbies/core/LobbyProgression.server.test.ts @@ -9,6 +9,7 @@ import { withUserId } from "~/utils/Test"; import * as LobbyMatchRepository from "../LobbyMatchRepository.server"; import * as LobbyRepository from "../LobbyRepository.server"; import type { LobbyWeaponRandomizer } from "../lobbies-constants"; +import { editArgs } from "../tests/fixtures"; import * as LobbyProgression from "./LobbyProgression.server"; import * as LobbySet from "./LobbySet"; @@ -186,15 +187,9 @@ describe("LobbyProgression.advance", () => { }); const set = await latestSet(lobbyId); - await LobbyRepository.updateSettings({ - id: lobbyId, - name: "Lobby", - description: null, - label: "CASUAL", - bestOf: 3, - weaponRandomizer: "OFF", - isApplicationRequired: false, - }); + await LobbyRepository.edit( + editArgs({ id: lobbyId, ownerId: hostId(), weaponRandomizer: "OFF" }), + ); await LobbyProgression.advance(lobbyId); expect((await latestSet(lobbyId)).rolledWeapons).toEqual(set.rolledWeapons); @@ -212,12 +207,9 @@ describe("LobbyProgression.advance", () => { { mode: "TC" as const, stageId: 3 as const }, ]; - await LobbyRepository.updateMaps({ - id: lobbyId, - modes: ["TC"], - mapPool, - mapsTournamentId: null, - }); + await LobbyRepository.edit( + editArgs({ id: lobbyId, ownerId: hostId(), modes: ["TC"], mapPool }), + ); await playOut(lobbyId); const { games } = await latestSet(lobbyId); diff --git a/app/features/lobbies/tests/fixtures.ts b/app/features/lobbies/tests/fixtures.ts new file mode 100644 index 000000000..ee3b05a5b --- /dev/null +++ b/app/features/lobbies/tests/fixtures.ts @@ -0,0 +1,26 @@ +import type * as LobbyRepository from "../LobbyRepository.server"; + +type EditArgs = Parameters[0]; + +/** `LobbyRepository.edit` args of a public lobby with `LobbyFactory`'s default settings, overridden by the given ones. */ +export function editArgs({ + id, + ownerId, + ...overrides +}: Pick & Partial): EditArgs { + return { + id, + ownerId, + name: "Lobby", + description: null, + label: "CASUAL", + bestOf: 3, + weaponRandomizer: "OFF", + isApplicationRequired: false, + modes: ["SZ", "TC", "RM", "CB"], + mapPool: null, + mapsTournamentId: null, + visibility: null, + ...overrides, + }; +}