diff --git a/app/features/tournament-lfg/TournamentLFGRepository.server.test.ts b/app/features/tournament-lfg/TournamentLFGRepository.server.test.ts new file mode 100644 index 000000000..97205e502 --- /dev/null +++ b/app/features/tournament-lfg/TournamentLFGRepository.server.test.ts @@ -0,0 +1,631 @@ +import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { db } from "~/db/sql"; +import { dbInsertUsers, dbReset } from "~/utils/Test"; +import * as TournamentLFGRepository from "./TournamentLFGRepository.server"; + +const createTournament = () => + db + .insertInto("Tournament") + .values({ + mapPickingStyle: "TO", + settings: JSON.stringify({ bracketProgression: [] }), + }) + .returning("id") + .executeTakeFirstOrThrow(); + +const createGroup = (tournamentId: number, userId: number) => + TournamentLFGRepository.createGroup({ tournamentId, userId }); + +describe("createGroup", () => { + beforeEach(async () => { + await dbInsertUsers(2); + }); + + afterEach(() => { + dbReset(); + }); + + test("creates a group with owner member", async () => { + const tournament = await createTournament(); + const group = await createGroup(tournament.id, 1); + + const groups = await TournamentLFGRepository.findGroupsByTournamentId( + tournament.id, + ); + + expect(groups).toHaveLength(1); + expect(groups[0].id).toBe(group.id); + }); + + test("owner has OWNER role", async () => { + const tournament = await createTournament(); + await createGroup(tournament.id, 1); + + const groups = await TournamentLFGRepository.findGroupsByTournamentId( + tournament.id, + ); + const members = groups[0].members; + + expect(members[0].role).toBe("OWNER"); + }); +}); + +describe("findGroupsByTournamentId", () => { + beforeEach(async () => { + await dbInsertUsers(3); + }); + + afterEach(() => { + dbReset(); + }); + + test("returns groups for given tournament", async () => { + const tournament = await createTournament(); + await createGroup(tournament.id, 1); + await createGroup(tournament.id, 2); + + const groups = await TournamentLFGRepository.findGroupsByTournamentId( + tournament.id, + ); + + expect(groups).toHaveLength(2); + }); + + test("returns empty array when no groups exist", async () => { + const tournament = await createTournament(); + + const groups = await TournamentLFGRepository.findGroupsByTournamentId( + tournament.id, + ); + + expect(groups).toHaveLength(0); + }); + + test("returns groups with member data", async () => { + const tournament = await createTournament(); + await createGroup(tournament.id, 1); + + const groups = await TournamentLFGRepository.findGroupsByTournamentId( + tournament.id, + ); + const members = groups[0].members; + + expect(members[0].id).toBe(1); + expect(members[0].username).toBeDefined(); + expect(members[0].role).toBe("OWNER"); + }); + + test("does not return groups from other tournaments", async () => { + const tournament1 = await createTournament(); + const tournament2 = await createTournament(); + await createGroup(tournament1.id, 1); + await createGroup(tournament2.id, 2); + + const groups = await TournamentLFGRepository.findGroupsByTournamentId( + tournament1.id, + ); + + expect(groups).toHaveLength(1); + }); +}); + +describe("addLike / deleteLike", () => { + beforeEach(async () => { + await dbInsertUsers(2); + }); + + afterEach(() => { + dbReset(); + }); + + test("adds a like between two groups", async () => { + const tournament = await createTournament(); + const group1 = await createGroup(tournament.id, 1); + const group2 = await createGroup(tournament.id, 2); + + await TournamentLFGRepository.addLike({ + likerGroupId: group1.id, + targetGroupId: group2.id, + }); + + const likes = await TournamentLFGRepository.allLikesByGroupId(group1.id); + + expect(likes.given).toHaveLength(1); + expect(likes.given[0].groupId).toBe(group2.id); + }); + + test("duplicate like does not throw", async () => { + const tournament = await createTournament(); + const group1 = await createGroup(tournament.id, 1); + const group2 = await createGroup(tournament.id, 2); + + await TournamentLFGRepository.addLike({ + likerGroupId: group1.id, + targetGroupId: group2.id, + }); + await TournamentLFGRepository.addLike({ + likerGroupId: group1.id, + targetGroupId: group2.id, + }); + + const likes = await TournamentLFGRepository.allLikesByGroupId(group1.id); + + expect(likes.given).toHaveLength(1); + }); + + test("deleteLike removes the like", async () => { + const tournament = await createTournament(); + const group1 = await createGroup(tournament.id, 1); + const group2 = await createGroup(tournament.id, 2); + + await TournamentLFGRepository.addLike({ + likerGroupId: group1.id, + targetGroupId: group2.id, + }); + await TournamentLFGRepository.deleteLike({ + likerGroupId: group1.id, + targetGroupId: group2.id, + }); + + const likes = await TournamentLFGRepository.allLikesByGroupId(group1.id); + + expect(likes.given).toHaveLength(0); + }); +}); + +describe("allLikesByGroupId", () => { + beforeEach(async () => { + await dbInsertUsers(3); + }); + + afterEach(() => { + dbReset(); + }); + + test("separates likes into given and received", async () => { + const tournament = await createTournament(); + const group1 = await createGroup(tournament.id, 1); + const group2 = await createGroup(tournament.id, 2); + const group3 = await createGroup(tournament.id, 3); + + await TournamentLFGRepository.addLike({ + likerGroupId: group1.id, + targetGroupId: group2.id, + }); + await TournamentLFGRepository.addLike({ + likerGroupId: group3.id, + targetGroupId: group1.id, + }); + + const likes = await TournamentLFGRepository.allLikesByGroupId(group1.id); + + expect(likes.given).toHaveLength(1); + expect(likes.given[0].groupId).toBe(group2.id); + expect(likes.received).toHaveLength(1); + expect(likes.received[0].groupId).toBe(group3.id); + }); + + test("returns empty given/received when no likes", async () => { + const tournament = await createTournament(); + const group = await createGroup(tournament.id, 1); + + const likes = await TournamentLFGRepository.allLikesByGroupId(group.id); + + expect(likes.given).toHaveLength(0); + expect(likes.received).toHaveLength(0); + }); +}); + +describe("morphGroups", () => { + beforeEach(async () => { + await dbInsertUsers(5); + }); + + afterEach(() => { + dbReset(); + }); + + test("merges two groups, other group is deleted", async () => { + const tournament = await createTournament(); + const group1 = await createGroup(tournament.id, 1); + const group2 = await createGroup(tournament.id, 2); + + await TournamentLFGRepository.morphGroups({ + survivingGroupId: group1.id, + otherGroupId: group2.id, + maxGroupSize: 4, + }); + + const groups = await TournamentLFGRepository.findGroupsByTournamentId( + tournament.id, + ); + + expect(groups).toHaveLength(1); + expect(groups[0].id).toBe(group1.id); + const members = groups[0].members; + expect(members).toHaveLength(2); + }); + + test("demotes other group's OWNER to MANAGER", async () => { + const tournament = await createTournament(); + const group1 = await createGroup(tournament.id, 1); + const group2 = await createGroup(tournament.id, 2); + + await TournamentLFGRepository.morphGroups({ + survivingGroupId: group1.id, + otherGroupId: group2.id, + maxGroupSize: 4, + }); + + const groups = await TournamentLFGRepository.findGroupsByTournamentId( + tournament.id, + ); + const members = groups[0].members; + const user2Member = members.find((m) => m.id === 2); + + expect(user2Member?.role).toBe("MANAGER"); + }); + + test("throws when merged size exceeds maxGroupSize", async () => { + const tournament = await createTournament(); + const group1 = await createGroup(tournament.id, 1); + const group2 = await createGroup(tournament.id, 2); + + await expect( + TournamentLFGRepository.morphGroups({ + survivingGroupId: group1.id, + otherGroupId: group2.id, + maxGroupSize: 1, + }), + ).rejects.toThrow("Group has too many members after merge"); + }); + + test("clears likes on surviving group after merge", async () => { + const tournament = await createTournament(); + const group1 = await createGroup(tournament.id, 1); + const group2 = await createGroup(tournament.id, 2); + const group3 = await createGroup(tournament.id, 3); + + await TournamentLFGRepository.addLike({ + likerGroupId: group1.id, + targetGroupId: group3.id, + }); + await TournamentLFGRepository.addLike({ + likerGroupId: group3.id, + targetGroupId: group1.id, + }); + + await TournamentLFGRepository.morphGroups({ + survivingGroupId: group1.id, + otherGroupId: group2.id, + maxGroupSize: 4, + }); + + const likes = await TournamentLFGRepository.allLikesByGroupId(group1.id); + + expect(likes.given).toHaveLength(0); + expect(likes.received).toHaveLength(0); + }); +}); + +describe("updateMemberNote", () => { + beforeEach(async () => { + await dbInsertUsers(1); + }); + + afterEach(() => { + dbReset(); + }); + + test("sets and clears a member note", async () => { + const tournament = await createTournament(); + const group = await createGroup(tournament.id, 1); + + await TournamentLFGRepository.updateMemberNote({ + groupId: group.id, + userId: 1, + value: "Looking for support", + }); + + let groups = await TournamentLFGRepository.findGroupsByTournamentId( + tournament.id, + ); + let members = groups[0].members; + expect(members[0].note).toBe("Looking for support"); + + await TournamentLFGRepository.updateMemberNote({ + groupId: group.id, + userId: 1, + value: null, + }); + + groups = await TournamentLFGRepository.findGroupsByTournamentId( + tournament.id, + ); + members = groups[0].members; + expect(members[0].note).toBeNull(); + }); +}); + +describe("updateMemberRole", () => { + beforeEach(async () => { + await dbInsertUsers(2); + }); + + afterEach(() => { + dbReset(); + }); + + test("changes role from REGULAR to MANAGER", async () => { + const tournament = await createTournament(); + const group = await createGroup(tournament.id, 1); + + await db + .insertInto("TournamentLFGGroupMember") + .values({ + groupId: group.id, + tournamentId: tournament.id, + userId: 2, + role: "REGULAR", + }) + .execute(); + + await TournamentLFGRepository.updateMemberRole({ + userId: 2, + groupId: group.id, + role: "MANAGER", + }); + + const groups = await TournamentLFGRepository.findGroupsByTournamentId( + tournament.id, + ); + const members = groups[0].members; + const user2Member = members.find((m) => m.id === 2); + + expect(user2Member?.role).toBe("MANAGER"); + }); + + test("throws when attempting to set OWNER role", () => { + expect(() => + TournamentLFGRepository.updateMemberRole({ + userId: 1, + groupId: 1, + role: "OWNER", + }), + ).toThrow("Can't set role to OWNER with this function"); + }); +}); + +describe("updateStayAsSub", () => { + beforeEach(async () => { + await dbInsertUsers(1); + }); + + afterEach(() => { + dbReset(); + }); + + test("toggles isStayAsSub on/off", async () => { + const tournament = await createTournament(); + const group = await createGroup(tournament.id, 1); + + await TournamentLFGRepository.updateStayAsSub({ + groupId: group.id, + userId: 1, + value: true, + }); + + let subs = await TournamentLFGRepository.getSubsForTournament( + tournament.id, + ); + expect(subs).toContain(1); + + await TournamentLFGRepository.updateStayAsSub({ + groupId: group.id, + userId: 1, + value: false, + }); + + subs = await TournamentLFGRepository.getSubsForTournament(tournament.id); + expect(subs).not.toContain(1); + }); +}); + +describe("leaveGroup", () => { + beforeEach(async () => { + await dbInsertUsers(3); + }); + + afterEach(() => { + dbReset(); + }); + + test("removes member from group", async () => { + const tournament = await createTournament(); + const group = await createGroup(tournament.id, 1); + + await db + .insertInto("TournamentLFGGroupMember") + .values({ + groupId: group.id, + tournamentId: tournament.id, + userId: 2, + role: "REGULAR", + }) + .execute(); + + await TournamentLFGRepository.leaveGroup({ + userId: 2, + tournamentId: tournament.id, + }); + + const groups = await TournamentLFGRepository.findGroupsByTournamentId( + tournament.id, + ); + const members = groups[0].members; + + expect(members).toHaveLength(1); + expect(members[0].id).toBe(1); + }); + + test("deletes group when last member leaves", async () => { + const tournament = await createTournament(); + await createGroup(tournament.id, 1); + + await TournamentLFGRepository.leaveGroup({ + userId: 1, + tournamentId: tournament.id, + }); + + const groups = await TournamentLFGRepository.findGroupsByTournamentId( + tournament.id, + ); + + expect(groups).toHaveLength(0); + }); + + test("promotes MANAGER to OWNER when owner leaves", async () => { + const tournament = await createTournament(); + const group = await createGroup(tournament.id, 1); + + await db + .insertInto("TournamentLFGGroupMember") + .values({ + groupId: group.id, + tournamentId: tournament.id, + userId: 2, + role: "MANAGER", + }) + .execute(); + + await db + .insertInto("TournamentLFGGroupMember") + .values({ + groupId: group.id, + tournamentId: tournament.id, + userId: 3, + role: "REGULAR", + }) + .execute(); + + await TournamentLFGRepository.leaveGroup({ + userId: 1, + tournamentId: tournament.id, + }); + + const groups = await TournamentLFGRepository.findGroupsByTournamentId( + tournament.id, + ); + const members = groups[0].members; + const newOwner = members.find((m) => m.id === 2); + + expect(newOwner?.role).toBe("OWNER"); + }); + + test("promotes first member to OWNER when owner leaves and no manager exists", async () => { + const tournament = await createTournament(); + const group = await createGroup(tournament.id, 1); + + await db + .insertInto("TournamentLFGGroupMember") + .values({ + groupId: group.id, + tournamentId: tournament.id, + userId: 2, + role: "REGULAR", + }) + .execute(); + + await TournamentLFGRepository.leaveGroup({ + userId: 1, + tournamentId: tournament.id, + }); + + const groups = await TournamentLFGRepository.findGroupsByTournamentId( + tournament.id, + ); + const members = groups[0].members; + + expect(members[0].role).toBe("OWNER"); + }); +}); + +describe("getSubsForTournament", () => { + beforeEach(async () => { + await dbInsertUsers(2); + }); + + afterEach(() => { + dbReset(); + }); + + test("returns userIds with isStayAsSub", async () => { + const tournament = await createTournament(); + await TournamentLFGRepository.createGroup({ + tournamentId: tournament.id, + userId: 1, + isStayAsSub: true, + }); + await createGroup(tournament.id, 2); + + const subs = await TournamentLFGRepository.getSubsForTournament( + tournament.id, + ); + + expect(subs).toEqual([1]); + }); + + test("returns empty when nobody opted in", async () => { + const tournament = await createTournament(); + await createGroup(tournament.id, 1); + + const subs = await TournamentLFGRepository.getSubsForTournament( + tournament.id, + ); + + expect(subs).toHaveLength(0); + }); +}); + +describe("cleanupForTournamentStart", () => { + beforeEach(async () => { + await dbInsertUsers(2); + }); + + afterEach(() => { + dbReset(); + }); + + test("deletes all groups for a tournament", async () => { + const tournament = await createTournament(); + await createGroup(tournament.id, 1); + await createGroup(tournament.id, 2); + + await TournamentLFGRepository.cleanupForTournamentStart(tournament.id); + + const groups = await TournamentLFGRepository.findGroupsByTournamentId( + tournament.id, + ); + + expect(groups).toHaveLength(0); + }); + + test("does not affect groups from other tournaments", async () => { + const tournament1 = await createTournament(); + const tournament2 = await createTournament(); + await createGroup(tournament1.id, 1); + await createGroup(tournament2.id, 2); + + await TournamentLFGRepository.cleanupForTournamentStart(tournament1.id); + + const groups1 = await TournamentLFGRepository.findGroupsByTournamentId( + tournament1.id, + ); + const groups2 = await TournamentLFGRepository.findGroupsByTournamentId( + tournament2.id, + ); + + expect(groups1).toHaveLength(0); + expect(groups2).toHaveLength(1); + }); +}); diff --git a/app/features/tournament-lfg/TournamentLFGRepository.server.ts b/app/features/tournament-lfg/TournamentLFGRepository.server.ts index 66431f27b..59d85ba65 100644 --- a/app/features/tournament-lfg/TournamentLFGRepository.server.ts +++ b/app/features/tournament-lfg/TournamentLFGRepository.server.ts @@ -1,2 +1,361 @@ -// Phase 4: Implement repository functions here -// See SQGroupRepository.server.ts for pattern reference +import type { Transaction } from "kysely"; +import { jsonBuildObject } from "kysely/helpers/sqlite"; +import { db } from "~/db/sql"; +import type { DB, Tables } from "~/db/tables"; +import { shortNanoid } from "~/utils/id"; +import invariant from "~/utils/invariant"; +import { userChatNameColorForJson } from "~/utils/kysely.server"; +import { errorIsSqliteForeignKeyConstraintFailure } from "~/utils/sql"; + +type CreateGroupArgs = { + tournamentId: number; + userId: number; + isStayAsSub?: boolean; +}; +export function createGroup(args: CreateGroupArgs) { + return db.transaction().execute(async (trx) => { + const createdGroup = await trx + .insertInto("TournamentLFGGroup") + .values({ + tournamentId: args.tournamentId, + chatCode: shortNanoid(), + }) + .returning("id") + .executeTakeFirstOrThrow(); + + await trx + .insertInto("TournamentLFGGroupMember") + .values({ + groupId: createdGroup.id, + tournamentId: args.tournamentId, + userId: args.userId, + role: "OWNER", + isStayAsSub: args.isStayAsSub ? 1 : 0, + }) + .execute(); + + return createdGroup; + }); +} + +type TournamentLFGMemberObject = { + id: Tables["User"]["id"]; + username: Tables["User"]["username"]; + discordId: Tables["User"]["discordId"]; + discordAvatar: Tables["User"]["discordAvatar"]; + customUrl: Tables["User"]["customUrl"]; + languages: Tables["User"]["languages"]; + vc: Tables["User"]["vc"]; + role: Tables["TournamentLFGGroupMember"]["role"]; + note: Tables["TournamentLFGGroupMember"]["note"]; + isStayAsSub: Tables["TournamentLFGGroupMember"]["isStayAsSub"]; + weapons: Tables["User"]["qWeaponPool"]; + chatNameColor: string | null; + plusTier: Tables["PlusTier"]["tier"] | null; +}; + +export async function findGroupsByTournamentId(tournamentId: number) { + return db + .selectFrom("TournamentLFGGroup") + .innerJoin( + "TournamentLFGGroupMember", + "TournamentLFGGroupMember.groupId", + "TournamentLFGGroup.id", + ) + .innerJoin("User", "User.id", "TournamentLFGGroupMember.userId") + .leftJoin("PlusTier", "PlusTier.userId", "User.id") + .select(({ fn, eb }) => [ + "TournamentLFGGroup.id", + "TournamentLFGGroup.chatCode", + "TournamentLFGGroup.tournamentTeamId", + "TournamentLFGGroup.visibility", + fn + .agg("json_group_array", [ + jsonBuildObject({ + id: eb.ref("User.id"), + username: eb.ref("User.username"), + discordId: eb.ref("User.discordId"), + discordAvatar: eb.ref("User.discordAvatar"), + customUrl: eb.ref("User.customUrl"), + languages: eb.ref("User.languages"), + vc: eb.ref("User.vc"), + role: eb.ref("TournamentLFGGroupMember.role"), + note: eb.ref("TournamentLFGGroupMember.note"), + isStayAsSub: eb.ref("TournamentLFGGroupMember.isStayAsSub"), + weapons: eb.ref("User.qWeaponPool"), + chatNameColor: userChatNameColorForJson, + plusTier: eb.ref("PlusTier.tier"), + }), + ]) + .$castTo() + .as("members"), + ]) + .where("TournamentLFGGroup.tournamentId", "=", tournamentId) + .groupBy("TournamentLFGGroup.id") + .execute(); +} + +export function morphGroups({ + survivingGroupId, + otherGroupId, + maxGroupSize, +}: { + survivingGroupId: number; + otherGroupId: number; + maxGroupSize: number; +}) { + return db.transaction().execute(async (trx) => { + await trx + .updateTable("TournamentLFGGroup") + .set({ chatCode: shortNanoid() }) + .where("TournamentLFGGroup.id", "=", survivingGroupId) + .execute(); + + const otherGroupMembers = await trx + .selectFrom("TournamentLFGGroupMember") + .select([ + "TournamentLFGGroupMember.userId", + "TournamentLFGGroupMember.role", + ]) + .where("TournamentLFGGroupMember.groupId", "=", otherGroupId) + .execute(); + + for (const member of otherGroupMembers) { + await trx + .updateTable("TournamentLFGGroupMember") + .set({ + role: member.role === "OWNER" ? "MANAGER" : member.role, + groupId: survivingGroupId, + }) + .where("TournamentLFGGroupMember.groupId", "=", otherGroupId) + .where("TournamentLFGGroupMember.userId", "=", member.userId) + .execute(); + } + + await deleteLikesByGroupId(survivingGroupId, trx); + + await trx + .deleteFrom("TournamentLFGGroup") + .where("TournamentLFGGroup.id", "=", otherGroupId) + .execute(); + + invariant( + await isGroupCorrect(survivingGroupId, trx, maxGroupSize), + "Group has too many members after merge", + ); + }); +} + +export async function addLike({ + likerGroupId, + targetGroupId, +}: { + likerGroupId: number; + targetGroupId: number; +}) { + try { + await db + .insertInto("TournamentLFGLike") + .values({ likerGroupId, targetGroupId }) + .onConflict((oc) => + oc.columns(["likerGroupId", "targetGroupId"]).doNothing(), + ) + .execute(); + } catch (error) { + if (errorIsSqliteForeignKeyConstraintFailure(error)) { + return; + } + throw error; + } +} + +export function deleteLike({ + likerGroupId, + targetGroupId, +}: { + likerGroupId: number; + targetGroupId: number; +}) { + return db + .deleteFrom("TournamentLFGLike") + .where("likerGroupId", "=", likerGroupId) + .where("targetGroupId", "=", targetGroupId) + .execute(); +} + +export async function allLikesByGroupId(groupId: number) { + const rows = await db + .selectFrom("TournamentLFGLike") + .select([ + "TournamentLFGLike.likerGroupId", + "TournamentLFGLike.targetGroupId", + ]) + .where((eb) => + eb.or([ + eb("TournamentLFGLike.likerGroupId", "=", groupId), + eb("TournamentLFGLike.targetGroupId", "=", groupId), + ]), + ) + .execute(); + + return { + given: rows + .filter((row) => row.likerGroupId === groupId) + .map((row) => ({ groupId: row.targetGroupId })), + received: rows + .filter((row) => row.targetGroupId === groupId) + .map((row) => ({ groupId: row.likerGroupId })), + }; +} + +export function updateMemberNote({ + groupId, + userId, + value, +}: { + groupId: number; + userId: number; + value: string | null; +}) { + return db + .updateTable("TournamentLFGGroupMember") + .set({ note: value }) + .where("groupId", "=", groupId) + .where("userId", "=", userId) + .execute(); +} + +export function updateMemberRole({ + userId, + groupId, + role, +}: { + userId: number; + groupId: number; + role: Tables["TournamentLFGGroupMember"]["role"]; +}) { + if (role === "OWNER") { + throw new Error("Can't set role to OWNER with this function"); + } + + return db + .updateTable("TournamentLFGGroupMember") + .set({ role }) + .where("userId", "=", userId) + .where("groupId", "=", groupId) + .execute(); +} + +export function updateStayAsSub({ + groupId, + userId, + value, +}: { + groupId: number; + userId: number; + value: boolean; +}) { + return db + .updateTable("TournamentLFGGroupMember") + .set({ isStayAsSub: value ? 1 : 0 }) + .where("groupId", "=", groupId) + .where("userId", "=", userId) + .execute(); +} + +export function leaveGroup({ + userId, + tournamentId, +}: { + userId: number; + tournamentId: number; +}) { + return db.transaction().execute(async (trx) => { + const userGroup = await trx + .selectFrom("TournamentLFGGroupMember") + .select([ + "TournamentLFGGroupMember.groupId", + "TournamentLFGGroupMember.role", + ]) + .where("TournamentLFGGroupMember.userId", "=", userId) + .where("TournamentLFGGroupMember.tournamentId", "=", tournamentId) + .executeTakeFirstOrThrow(); + + await trx + .deleteFrom("TournamentLFGGroupMember") + .where("userId", "=", userId) + .where("groupId", "=", userGroup.groupId) + .execute(); + + const remainingMembers = await trx + .selectFrom("TournamentLFGGroupMember") + .select(["userId", "role"]) + .where("groupId", "=", userGroup.groupId) + .execute(); + + if (remainingMembers.length === 0) { + await trx + .deleteFrom("TournamentLFGGroup") + .where("id", "=", userGroup.groupId) + .execute(); + return; + } + + if (userGroup.role === "OWNER") { + const newOwner = + remainingMembers.find((m) => m.role === "MANAGER") ?? + remainingMembers[0]; + + await trx + .updateTable("TournamentLFGGroupMember") + .set({ role: "OWNER" }) + .where("userId", "=", newOwner.userId) + .where("groupId", "=", userGroup.groupId) + .execute(); + } + }); +} + +export function cleanupForTournamentStart(tournamentId: number) { + return db + .deleteFrom("TournamentLFGGroup") + .where("TournamentLFGGroup.tournamentId", "=", tournamentId) + .execute(); +} + +export async function getSubsForTournament(tournamentId: number) { + const rows = await db + .selectFrom("TournamentLFGGroupMember") + .select("TournamentLFGGroupMember.userId") + .where("TournamentLFGGroupMember.tournamentId", "=", tournamentId) + .where("TournamentLFGGroupMember.isStayAsSub", "=", 1) + .execute(); + + return rows.map((row) => row.userId); +} + +function deleteLikesByGroupId(groupId: number, trx: Transaction) { + return trx + .deleteFrom("TournamentLFGLike") + .where((eb) => + eb.or([ + eb("TournamentLFGLike.likerGroupId", "=", groupId), + eb("TournamentLFGLike.targetGroupId", "=", groupId), + ]), + ) + .execute(); +} + +async function isGroupCorrect( + groupId: number, + trx: Transaction, + maxGroupSize: number, +): Promise { + const members = await trx + .selectFrom("TournamentLFGGroupMember") + .select("TournamentLFGGroupMember.userId") + .where("TournamentLFGGroupMember.groupId", "=", groupId) + .execute(); + + return members.length <= maxGroupSize; +} diff --git a/app/features/tournament-lfg/tournament-lfg-schemas.server.ts b/app/features/tournament-lfg/tournament-lfg-schemas.server.ts index 664e68748..56ab4cb2c 100644 --- a/app/features/tournament-lfg/tournament-lfg-schemas.server.ts +++ b/app/features/tournament-lfg/tournament-lfg-schemas.server.ts @@ -33,10 +33,7 @@ export const lookingSchema = z.union([ value: z.preprocess( falsyToNull, // xxx: use SendouForm style? (but would need SQ to be migrated too) - z - .string() - .max(TOURNAMENT_LFG.PUBLIC_NOTE_MAX_LENGTH) - .nullable(), + z.string().max(TOURNAMENT_LFG.PUBLIC_NOTE_MAX_LENGTH).nullable(), ), }), z.object({ diff --git a/app/routes.ts b/app/routes.ts index 88468af24..be93c3bbc 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -108,6 +108,8 @@ export default [ route("results", "features/tournament/routes/to.$id.results.tsx"), route("streams", "features/tournament/routes/to.$id.streams.tsx"), + route("looking", "features/tournament-lfg/routes/to.$id.looking.tsx"), + route("subs", "features/tournament-subs/routes/to.$id.subs.tsx"), route("subs/new", "features/tournament-subs/routes/to.$id.subs.new.tsx"), diff --git a/db-test.sqlite3 b/db-test.sqlite3 index 7861ee433..87474164b 100644 Binary files a/db-test.sqlite3 and b/db-test.sqlite3 differ