Repository initial

This commit is contained in:
Kalle
2026-02-21 12:26:41 +02:00
parent a0b55c2359
commit d52fe7c45a
5 changed files with 995 additions and 6 deletions

View File

@@ -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);
});
});

View File

@@ -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<TournamentLFGMemberObject[]>()
.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<DB>) {
return trx
.deleteFrom("TournamentLFGLike")
.where((eb) =>
eb.or([
eb("TournamentLFGLike.likerGroupId", "=", groupId),
eb("TournamentLFGLike.targetGroupId", "=", groupId),
]),
)
.execute();
}
async function isGroupCorrect(
groupId: number,
trx: Transaction<DB>,
maxGroupSize: number,
): Promise<boolean> {
const members = await trx
.selectFrom("TournamentLFGGroupMember")
.select("TournamentLFGGroupMember.userId")
.where("TournamentLFGGroupMember.groupId", "=", groupId)
.execute();
return members.length <= maxGroupSize;
}

View File

@@ -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({

View File

@@ -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"),

Binary file not shown.