From d52fe7c45a9f69c75f6d0f89cc0ef5cee37310d3 Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Sat, 21 Feb 2026 12:26:41 +0200 Subject: [PATCH] Repository initial --- .../TournamentLFGRepository.server.test.ts | 631 ++++++++++++++++++ .../TournamentLFGRepository.server.ts | 363 +++++++++- .../tournament-lfg-schemas.server.ts | 5 +- app/routes.ts | 2 + db-test.sqlite3 | Bin 1130496 -> 1179648 bytes 5 files changed, 995 insertions(+), 6 deletions(-) create mode 100644 app/features/tournament-lfg/TournamentLFGRepository.server.test.ts 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 7861ee43343bf086976acd388850e3eee8e9bf9f..87474164b4c63d53094d7e9eed90d3a07b370725 100644 GIT binary patch delta 2619 zcmds3|8Emz81H+%>-Fy1-i@(d*D-c2-!@oTyDh*5b?aa-k`0Ojw?SgvSi!PyrR{+E z3+=gp#9%-}9ulK`5i~*hA_cm{L{JlmQN9@wV^BBUPyS#6Mva7k@7?+pwm(2GdEQ*! ze%|MK?)f~==WR6F7>za^D9Zaxqd}4;{h*@<%d7C_!u<~^yqU)z#VeY5FenlH(fDbt z1H~_+F{1@jPPQb%ftL!Xs=1HZ3+18--1WjmD4Ad^8PcfuE%#E}?0fPu|M&zMklD3x|k=H=n<-OlxO~fcEkpl3C3Z#q+#3 z^&REANX_CZm#fAR85j)pclP=FBaYrJTNiE%S2`;fIh>V_N|)qv);gz?v_h#gWc!K__mgfbEL}`-{yhS2YKE{65$EbUBh+sCam%?50Jxn|N(^q_eBn zp95JH8W;?=`1`v2p{e$f1#bUJTFlsC7TPYAvJ!sa*j;Ki9Eo!jygaTefUn2NJQ%&e zl5qAMYlkCu4KtcR|I0$&~%9YM6qnjFA^d8n6l_|Avb@dczp^2+wU37 zvkK*6H&(-~d0IXEe8(Utdn=)CgquF#muqOT%0|SHS_Nh9d=qo$Q{MyK z8}L?}I*Eu~{(PfneWSE`&5Fi$X%eh7&@b6D2XILgkW!3ld5KP3WrJfyyrm<9@VPSY zh>_>Zi)qnjBZiCBie-MwydixuiS?d_=Ej_8QYqXzAy^^0i4ounFJotL`%ZiecxG!w zn3!i(BB>n5ox$Vv&g1njy;)CMSlX)LH=9NbZtgd( zfqh4RSl>#I&{iyxk99L+R(S<2wpI`^_av2K|rzNUWlhWN;kPbTss_IUm&MQBsOKEa%F6=1=vx61>U+<_{|BcJWnqmr58A)G<3ZD~ySN;X3U3a_y delta 687 zcmZo@@Mvgooggi^mw|y%1cXK$4MrVu8Zu)y(>g zoA)z5Gy!fS47C*?^cGh&h0m z6NtHhm>YV{$V`ArP<_nmv zVIVMd`voHbFUIZK<^oqaSQr?X`nSuO2pndtk7?3m6W5nzs?5wwNv%jM$c!(^&rZ#Y zFD*_jiqA}OEXWK2@fo?CgIpa$TopY1TwEg*Fw`mJ=P4+`6f0>cfwX(3C}}o%HL0${g0tQ#CBmpep@C+uFZ@J|K%4M z2td%p0Cq;E?Hm&Xe)4OxzG2{J-p{}v&9{x~4yP!4Jv%pBKkHYPT-L2DO3eFtV|X=r zHZZq