mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-10-27 04:54:25 -05:00
Repository initial
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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"),
|
||||
|
||||
|
||||
BIN
db-test.sqlite3
BIN
db-test.sqlite3
Binary file not shown.
Reference in New Issue
Block a user