mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-10-02 08:07:40 -05:00
Move app to apps/web-react in pnpm workspace layout
This commit is contained in:
742
apps/web-react/app/features/team/TeamRepository.server.ts
Normal file
742
apps/web-react/app/features/team/TeamRepository.server.ts
Normal file
@@ -0,0 +1,742 @@
|
||||
import { type Insertable, sql, type Transaction } from "kysely";
|
||||
import { db } from "~/db/sql";
|
||||
import type { DB, Tables } from "~/db/tables";
|
||||
import type { CustomTheme, UserMapModePreferences } from "~/db/tables-json";
|
||||
import { actorId } from "~/features/auth/core/user.server";
|
||||
import * as LFGRepository from "~/features/lfg/LFGRepository.server";
|
||||
import * as MatchProfileRepository from "~/features/match-profile/MatchProfileRepository.server";
|
||||
import { NON_PLAYER_TEAM_ROLES } from "~/features/team/team-constants";
|
||||
import { subsOfResult } from "~/features/team/team-utils";
|
||||
import { databaseTimestampNow } from "~/utils/dates";
|
||||
import { shortNanoid } from "~/utils/id";
|
||||
import invariant from "~/utils/invariant";
|
||||
import {
|
||||
commonUserSelect,
|
||||
concatUserSubmittedImagePrefix,
|
||||
jsonArrayFrom,
|
||||
tournamentLogoOrNull,
|
||||
userProfileWeapons,
|
||||
} from "~/utils/kysely.server";
|
||||
import { toDBBoolean } from "~/utils/sql";
|
||||
import { mySlugify } from "~/utils/urls";
|
||||
|
||||
export function findAllUndisbanded() {
|
||||
return db
|
||||
.selectFrom("Team")
|
||||
.leftJoin("UserSubmittedImage", "UserSubmittedImage.id", "Team.avatarImgId")
|
||||
.select(({ eb }) => [
|
||||
"Team.customUrl",
|
||||
"Team.name",
|
||||
"Team.tag",
|
||||
concatUserSubmittedImagePrefix(eb.ref("UserSubmittedImage.url")).as(
|
||||
"avatarUrl",
|
||||
),
|
||||
jsonArrayFrom(
|
||||
eb
|
||||
// AllTeamMember directly instead of the TeamMemberWithSecondary view:
|
||||
// the view's team-existence check is redundant when joining to Team
|
||||
.selectFrom("AllTeamMember")
|
||||
.innerJoin("User", "User.id", "AllTeamMember.userId")
|
||||
.leftJoin("PlusTier", "PlusTier.userId", "User.id")
|
||||
.select(["User.id", "User.username", "PlusTier.tier as plusTier"])
|
||||
.whereRef("AllTeamMember.teamId", "=", "Team.id")
|
||||
.where("AllTeamMember.leftAt", "is", null)
|
||||
.orderBy("AllTeamMember.order", "asc"),
|
||||
).as("members"),
|
||||
])
|
||||
.execute();
|
||||
}
|
||||
|
||||
export function searchByName({
|
||||
query,
|
||||
limit,
|
||||
}: {
|
||||
query: string;
|
||||
limit: number;
|
||||
}) {
|
||||
return db
|
||||
.selectFrom("Team")
|
||||
.leftJoin("UserSubmittedImage", "UserSubmittedImage.id", "Team.avatarImgId")
|
||||
.select(({ eb }) => [
|
||||
"Team.id",
|
||||
"Team.customUrl",
|
||||
"Team.name",
|
||||
concatUserSubmittedImagePrefix(eb.ref("UserSubmittedImage.url")).as(
|
||||
"avatarUrl",
|
||||
),
|
||||
jsonArrayFrom(
|
||||
eb
|
||||
.selectFrom("TeamMemberWithSecondary")
|
||||
.innerJoin("User", "User.id", "TeamMemberWithSecondary.userId")
|
||||
.select(["User.id", "User.username", "User.tournamentName"])
|
||||
.whereRef("TeamMemberWithSecondary.teamId", "=", "Team.id")
|
||||
.where((eb2) =>
|
||||
eb2.and([
|
||||
eb2.or([
|
||||
eb2("TeamMemberWithSecondary.role", "is", null),
|
||||
eb2(
|
||||
"TeamMemberWithSecondary.role",
|
||||
"not in",
|
||||
NON_PLAYER_TEAM_ROLES,
|
||||
),
|
||||
]),
|
||||
eb2.or([
|
||||
eb2("TeamMemberWithSecondary.roleType", "is", null),
|
||||
eb2("TeamMemberWithSecondary.roleType", "!=", "OTHER"),
|
||||
]),
|
||||
]),
|
||||
)
|
||||
.orderBy("TeamMemberWithSecondary.order", "asc"),
|
||||
).as("members"),
|
||||
])
|
||||
.where("Team.name", "like", `%${query}%`)
|
||||
.orderBy("Team.name", "asc")
|
||||
.limit(limit)
|
||||
.execute();
|
||||
}
|
||||
|
||||
export function findById(teamId: number) {
|
||||
return db
|
||||
.selectFrom("AllTeam")
|
||||
.select(["AllTeam.id", "AllTeam.name"])
|
||||
.where("AllTeam.id", "=", teamId)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
export function findAllMemberOfByUserId(userId: number) {
|
||||
return db
|
||||
.selectFrom("TeamMemberWithSecondary")
|
||||
.innerJoin("Team", "Team.id", "TeamMemberWithSecondary.teamId")
|
||||
.leftJoin("UserSubmittedImage", "UserSubmittedImage.id", "Team.avatarImgId")
|
||||
.select(({ eb }) => [
|
||||
"Team.id",
|
||||
"Team.customUrl",
|
||||
"Team.name",
|
||||
"Team.mapModePreferences",
|
||||
"TeamMemberWithSecondary.role",
|
||||
"TeamMemberWithSecondary.customRole",
|
||||
"TeamMemberWithSecondary.isOwner",
|
||||
"TeamMemberWithSecondary.isManager",
|
||||
concatUserSubmittedImagePrefix(eb.ref("UserSubmittedImage.url")).as(
|
||||
"logoUrl",
|
||||
),
|
||||
])
|
||||
.where("TeamMemberWithSecondary.userId", "=", userId)
|
||||
.orderBy("TeamMemberWithSecondary.isMainTeam", "desc")
|
||||
.orderBy("Team.name", "asc")
|
||||
.execute();
|
||||
}
|
||||
|
||||
export type findByCustomUrl = NonNullable<
|
||||
Awaited<ReturnType<typeof findByCustomUrl>>
|
||||
>;
|
||||
|
||||
export async function findByCustomUrl(
|
||||
customUrl: string,
|
||||
{
|
||||
includeInviteCode = false,
|
||||
includeUnvalidatedImages = false,
|
||||
includeMapModePreferences = false,
|
||||
} = {},
|
||||
) {
|
||||
// join the unvalidated table (instead of the validated-only `UserSubmittedImage` view) so the
|
||||
// edit page can preview images still pending moderation; for everyone else the url is gated on
|
||||
// `validatedAt` so pending images stay hidden
|
||||
const row = await db
|
||||
.selectFrom("Team")
|
||||
.leftJoin(
|
||||
"UnvalidatedUserSubmittedImage as AvatarImage",
|
||||
"AvatarImage.id",
|
||||
"Team.avatarImgId",
|
||||
)
|
||||
.leftJoin(
|
||||
"UnvalidatedUserSubmittedImage as BannerImage",
|
||||
"BannerImage.id",
|
||||
"Team.bannerImgId",
|
||||
)
|
||||
.select(({ eb }) => [
|
||||
"Team.id",
|
||||
"Team.name",
|
||||
"Team.bsky",
|
||||
"Team.bio",
|
||||
"Team.tag",
|
||||
"Team.customUrl",
|
||||
"Team.customTheme",
|
||||
"Team.avatarImgId",
|
||||
"Team.bannerImgId",
|
||||
concatUserSubmittedImagePrefix(
|
||||
includeUnvalidatedImages
|
||||
? eb.ref("AvatarImage.url")
|
||||
: eb.fn<string | null>("iif", [
|
||||
eb("AvatarImage.validatedAt", "is not", null),
|
||||
eb.ref("AvatarImage.url"),
|
||||
sql`null`,
|
||||
]),
|
||||
).as("avatarUrl"),
|
||||
concatUserSubmittedImagePrefix(
|
||||
includeUnvalidatedImages
|
||||
? eb.ref("BannerImage.url")
|
||||
: eb.fn<string | null>("iif", [
|
||||
eb("BannerImage.validatedAt", "is not", null),
|
||||
eb.ref("BannerImage.url"),
|
||||
sql`null`,
|
||||
]),
|
||||
).as("bannerUrl"),
|
||||
jsonArrayFrom(
|
||||
eb
|
||||
.selectFrom("TeamMemberWithSecondary")
|
||||
.innerJoin("User", "User.id", "TeamMemberWithSecondary.userId")
|
||||
.select(({ eb: innerEb }) => [
|
||||
...commonUserSelect(innerEb),
|
||||
"TeamMemberWithSecondary.role",
|
||||
"TeamMemberWithSecondary.customRole",
|
||||
"TeamMemberWithSecondary.roleType",
|
||||
"TeamMemberWithSecondary.isOwner",
|
||||
"TeamMemberWithSecondary.isManager",
|
||||
"TeamMemberWithSecondary.isMainTeam",
|
||||
"User.country",
|
||||
"User.patronTier",
|
||||
userProfileWeapons(innerEb).as("weapons"),
|
||||
])
|
||||
.whereRef("TeamMemberWithSecondary.teamId", "=", "Team.id")
|
||||
.orderBy("TeamMemberWithSecondary.order", "asc"),
|
||||
).as("members"),
|
||||
])
|
||||
.$if(includeInviteCode, (qb) => qb.select("Team.inviteCode"))
|
||||
.$if(includeMapModePreferences, (qb) =>
|
||||
qb.select("Team.mapModePreferences"),
|
||||
)
|
||||
.where("Team.customUrl", "=", customUrl.toLowerCase())
|
||||
.executeTakeFirst();
|
||||
|
||||
if (!row) return;
|
||||
|
||||
const managerIds = row.members
|
||||
.filter((member) => member.isOwner || member.isManager)
|
||||
.map((member) => member.id);
|
||||
|
||||
return {
|
||||
...row,
|
||||
permissions: {
|
||||
EDIT: managerIds,
|
||||
MANAGE_ROSTER: managerIds,
|
||||
DELETE: row.members
|
||||
.filter((member) => member.isOwner)
|
||||
.map((member) => member.id),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export type FindResultPlacementsById = NonNullable<
|
||||
Awaited<ReturnType<typeof findResultPlacementsById>>
|
||||
>;
|
||||
|
||||
export function findResultPlacementsById(teamId: number) {
|
||||
return db
|
||||
.selectFrom("TournamentTeam")
|
||||
.innerJoin(
|
||||
"TournamentResult",
|
||||
"TournamentResult.tournamentTeamId",
|
||||
"TournamentTeam.id",
|
||||
)
|
||||
.select(["TournamentResult.placement"])
|
||||
.where("teamId", "=", teamId)
|
||||
.groupBy("TournamentResult.tournamentId")
|
||||
.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves tournament results for a given team by its ID.
|
||||
*/
|
||||
export async function findResultsById(teamId: number) {
|
||||
const rows = await db
|
||||
.with("results", (db) =>
|
||||
db
|
||||
.selectFrom("TournamentTeam")
|
||||
.innerJoin(
|
||||
"TournamentResult",
|
||||
"TournamentResult.tournamentTeamId",
|
||||
"TournamentTeam.id",
|
||||
)
|
||||
.select([
|
||||
"TournamentResult.userId",
|
||||
"TournamentResult.tournamentTeamId",
|
||||
"TournamentResult.tournamentId",
|
||||
"TournamentResult.placement",
|
||||
"TournamentResult.participantCount",
|
||||
])
|
||||
.where("teamId", "=", teamId)
|
||||
.groupBy("TournamentResult.tournamentId"),
|
||||
)
|
||||
.selectFrom("results")
|
||||
.innerJoin(
|
||||
"CalendarEvent",
|
||||
"CalendarEvent.tournamentId",
|
||||
"results.tournamentId",
|
||||
)
|
||||
.innerJoin(
|
||||
"CalendarEventDate",
|
||||
"CalendarEventDate.eventId",
|
||||
"CalendarEvent.id",
|
||||
)
|
||||
.innerJoin("Tournament", "Tournament.id", "results.tournamentId")
|
||||
.select((eb) => [
|
||||
"results.placement",
|
||||
"results.tournamentId",
|
||||
"results.participantCount",
|
||||
"results.tournamentTeamId",
|
||||
"CalendarEvent.name as tournamentName",
|
||||
"CalendarEventDate.startsAt",
|
||||
"Tournament.tier",
|
||||
tournamentLogoOrNull(eb).as("logoUrl"),
|
||||
jsonArrayFrom(
|
||||
eb
|
||||
.selectFrom("results as results2")
|
||||
.innerJoin("TournamentResult", (join) =>
|
||||
join
|
||||
.onRef(
|
||||
"TournamentResult.tournamentTeamId",
|
||||
"=",
|
||||
"results2.tournamentTeamId",
|
||||
)
|
||||
.onRef(
|
||||
"TournamentResult.tournamentId",
|
||||
"=",
|
||||
"results2.tournamentId",
|
||||
),
|
||||
)
|
||||
.innerJoin("User", "User.id", "TournamentResult.userId")
|
||||
.whereRef("results2.tournamentId", "=", "results.tournamentId")
|
||||
.select((eb) => commonUserSelect(eb)),
|
||||
).as("participants"),
|
||||
])
|
||||
.orderBy("CalendarEventDate.startsAt", "desc")
|
||||
.execute();
|
||||
|
||||
const members = await allMembersById(teamId);
|
||||
|
||||
return rows.map((row) => {
|
||||
const subs = subsOfResult(row, members);
|
||||
|
||||
return {
|
||||
...row,
|
||||
subs,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// reads AllTeamMember instead of the TeamMemberWithSecondary view because the
|
||||
// view filters out members who have left, and past members are exactly what
|
||||
// subsOfResult needs to tell substitutes apart from since-departed roster members
|
||||
function allMembersById(teamId: number) {
|
||||
return db
|
||||
.selectFrom("AllTeamMember")
|
||||
.select([
|
||||
"AllTeamMember.userId",
|
||||
"AllTeamMember.leftAt",
|
||||
"AllTeamMember.createdAt",
|
||||
])
|
||||
.where("AllTeamMember.teamId", "=", teamId)
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function findAllByMemberUserId(
|
||||
userId: number,
|
||||
trx?: Transaction<DB>,
|
||||
) {
|
||||
return (trx ?? db)
|
||||
.selectFrom("TeamMemberWithSecondary")
|
||||
.innerJoin("Team", "Team.id", "TeamMemberWithSecondary.teamId")
|
||||
.select((eb) => [
|
||||
"TeamMemberWithSecondary.teamId as id",
|
||||
"Team.name",
|
||||
"TeamMemberWithSecondary.isOwner",
|
||||
"TeamMemberWithSecondary.isMainTeam",
|
||||
jsonArrayFrom(
|
||||
eb
|
||||
.selectFrom("TeamMemberWithSecondary as m2")
|
||||
.innerJoin("User", "User.id", "m2.userId")
|
||||
.select((eb) => [...commonUserSelect(eb), "m2.role", "m2.roleType"])
|
||||
.whereRef("TeamMemberWithSecondary.teamId", "=", "m2.teamId")
|
||||
.orderBy("m2.order", "asc"),
|
||||
).as("members"),
|
||||
])
|
||||
.where("userId", "=", userId)
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function insert(
|
||||
args: Pick<Insertable<Tables["Team"]>, "name"> & {
|
||||
ownerUserId: number;
|
||||
isMainTeam: boolean;
|
||||
},
|
||||
) {
|
||||
const customUrl = mySlugify(args.name);
|
||||
|
||||
return db.transaction().execute(async (trx) => {
|
||||
const team = await trx
|
||||
.insertInto("AllTeam")
|
||||
.values({
|
||||
name: args.name,
|
||||
customUrl,
|
||||
inviteCode: shortNanoid(),
|
||||
})
|
||||
.returning(["id", "customUrl"])
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
await trx
|
||||
.insertInto("AllTeamMember")
|
||||
.values({
|
||||
userId: args.ownerUserId,
|
||||
teamId: team.id,
|
||||
isOwner: 1,
|
||||
isMainTeam: toDBBoolean(args.isMainTeam),
|
||||
})
|
||||
.execute();
|
||||
|
||||
return team;
|
||||
});
|
||||
}
|
||||
|
||||
export async function update({
|
||||
id,
|
||||
name,
|
||||
bio,
|
||||
bsky,
|
||||
tag,
|
||||
avatarImgId,
|
||||
bannerImgId,
|
||||
}: Pick<
|
||||
Insertable<Tables["Team"]>,
|
||||
"id" | "name" | "bio" | "bsky" | "tag" | "avatarImgId" | "bannerImgId"
|
||||
>) {
|
||||
const customUrl = mySlugify(name);
|
||||
|
||||
return db.transaction().execute(async (trx) => {
|
||||
const current = await trx
|
||||
.selectFrom("Team")
|
||||
.select(["avatarImgId", "bannerImgId"])
|
||||
.where("id", "=", id)
|
||||
.executeTakeFirst();
|
||||
|
||||
// images that got removed or replaced are no longer referenced by anything,
|
||||
// so their submitted image rows are cleaned up
|
||||
const orphanedImageIds: number[] = [];
|
||||
if (current?.avatarImgId && current.avatarImgId !== avatarImgId) {
|
||||
orphanedImageIds.push(current.avatarImgId);
|
||||
}
|
||||
if (current?.bannerImgId && current.bannerImgId !== bannerImgId) {
|
||||
orphanedImageIds.push(current.bannerImgId);
|
||||
}
|
||||
|
||||
if (orphanedImageIds.length > 0) {
|
||||
await trx
|
||||
.deleteFrom("UnvalidatedUserSubmittedImage")
|
||||
.where("id", "in", orphanedImageIds)
|
||||
.execute();
|
||||
}
|
||||
|
||||
return trx
|
||||
.updateTable("AllTeam")
|
||||
.set({
|
||||
name,
|
||||
customUrl,
|
||||
bio,
|
||||
bsky,
|
||||
tag,
|
||||
avatarImgId,
|
||||
bannerImgId,
|
||||
})
|
||||
.where("id", "=", id)
|
||||
.returningAll()
|
||||
.executeTakeFirstOrThrow();
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateCustomTheme({
|
||||
id,
|
||||
customTheme,
|
||||
}: {
|
||||
id: number;
|
||||
customTheme: CustomTheme | null;
|
||||
}) {
|
||||
await db
|
||||
.updateTable("AllTeam")
|
||||
.set({
|
||||
customTheme: customTheme ? JSON.stringify(customTheme) : null,
|
||||
})
|
||||
.where("id", "=", id)
|
||||
.execute();
|
||||
}
|
||||
|
||||
/** Updates the team's SendouQ map/mode preferences, or clears them when passed `null`. Keeps existing map pools of modes missing from the new value. */
|
||||
export async function updateMapModePreferences({
|
||||
id,
|
||||
mapModePreferences,
|
||||
}: {
|
||||
id: number;
|
||||
mapModePreferences: UserMapModePreferences | null;
|
||||
}) {
|
||||
if (!mapModePreferences) {
|
||||
await db
|
||||
.updateTable("AllTeam")
|
||||
.set({ mapModePreferences: null })
|
||||
.where("id", "=", id)
|
||||
.execute();
|
||||
return;
|
||||
}
|
||||
|
||||
const current = await db
|
||||
.selectFrom("Team")
|
||||
.select("Team.mapModePreferences")
|
||||
.where("Team.id", "=", id)
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
const merged: UserMapModePreferences = {
|
||||
...mapModePreferences,
|
||||
pool: MatchProfileRepository.mergeExcludedModePreferences(
|
||||
mapModePreferences.pool,
|
||||
current.mapModePreferences?.pool,
|
||||
),
|
||||
};
|
||||
|
||||
await db
|
||||
.updateTable("AllTeam")
|
||||
.set({ mapModePreferences: JSON.stringify(merged) })
|
||||
.where("id", "=", id)
|
||||
.execute();
|
||||
}
|
||||
|
||||
export function switchOwnMainTeam(teamId: number) {
|
||||
const userId = actorId();
|
||||
return db.transaction().execute(async (trx) => {
|
||||
const currentTeams = await findAllByMemberUserId(userId, trx);
|
||||
|
||||
const teamToSwitchTo = currentTeams.find((team) => team.id === teamId);
|
||||
invariant(teamToSwitchTo, "User is not a member of this team");
|
||||
|
||||
await trx
|
||||
.updateTable("AllTeamMember")
|
||||
.set({
|
||||
isMainTeam: 0,
|
||||
})
|
||||
.where("userId", "=", userId)
|
||||
.execute();
|
||||
|
||||
await trx
|
||||
.updateTable("AllTeamMember")
|
||||
.set({
|
||||
isMainTeam: 1,
|
||||
})
|
||||
.where("userId", "=", userId)
|
||||
.where("teamId", "=", teamId)
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteById(teamId: number) {
|
||||
return db.transaction().execute(async (trx) => {
|
||||
const members = await trx
|
||||
.selectFrom("TeamMember")
|
||||
.select(["TeamMember.userId"])
|
||||
.where("teamId", "=", teamId)
|
||||
.execute();
|
||||
|
||||
// switch main team to another if they at least one secondary team
|
||||
for (const member of members) {
|
||||
const currentTeams = await findAllByMemberUserId(member.userId, trx);
|
||||
|
||||
const teamToSwitchTo = currentTeams.find((team) => team.id !== teamId);
|
||||
|
||||
if (!teamToSwitchTo) continue;
|
||||
|
||||
await trx
|
||||
.updateTable("AllTeamMember")
|
||||
.set({
|
||||
isMainTeam: 1,
|
||||
})
|
||||
.where("userId", "=", member.userId)
|
||||
.where("teamId", "=", teamToSwitchTo.id)
|
||||
.execute();
|
||||
}
|
||||
|
||||
await trx
|
||||
.updateTable("AllTeamMember")
|
||||
.set({
|
||||
isMainTeam: 0,
|
||||
})
|
||||
.where("AllTeamMember.teamId", "=", teamId)
|
||||
.execute();
|
||||
|
||||
await LFGRepository.deletePostsByTeamId(teamId, trx);
|
||||
|
||||
await trx
|
||||
.updateTable("AllTeam")
|
||||
.set({
|
||||
deletedAt: databaseTimestampNow(),
|
||||
})
|
||||
.where("id", "=", teamId)
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
|
||||
export function resetInviteCode(teamId: number) {
|
||||
return db
|
||||
.updateTable("AllTeam")
|
||||
.set({
|
||||
inviteCode: shortNanoid(),
|
||||
})
|
||||
.where("id", "=", teamId)
|
||||
.execute();
|
||||
}
|
||||
|
||||
export function insertOwnMembership({
|
||||
teamId,
|
||||
maxTeamsAllowed,
|
||||
}: {
|
||||
teamId: number;
|
||||
maxTeamsAllowed: number;
|
||||
}) {
|
||||
const userId = actorId();
|
||||
return db.transaction().execute(async (trx) => {
|
||||
const teamCount = (await findAllByMemberUserId(userId, trx)).length;
|
||||
|
||||
if (teamCount >= maxTeamsAllowed) {
|
||||
throw new Error("Trying to exceed allowed team count");
|
||||
}
|
||||
|
||||
const isMainTeam = toDBBoolean(teamCount === 0);
|
||||
|
||||
const maxOrder = await trx
|
||||
.selectFrom("AllTeamMember")
|
||||
.select((eb) =>
|
||||
eb.fn.coalesce(eb.fn.max("order"), sql<number>`-1`).as("maxOrder"),
|
||||
)
|
||||
.where("teamId", "=", teamId)
|
||||
.where("leftAt", "is", null)
|
||||
.executeTakeFirst();
|
||||
const order = (maxOrder?.maxOrder ?? -1) + 1;
|
||||
|
||||
await trx
|
||||
.insertInto("AllTeamMember")
|
||||
.values({ userId, teamId, isMainTeam, order })
|
||||
.onConflict((oc) =>
|
||||
oc.columns(["userId", "teamId"]).doUpdateSet({
|
||||
leftAt: null,
|
||||
isMainTeam,
|
||||
order,
|
||||
}),
|
||||
)
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
|
||||
export function handleMemberLeaving({
|
||||
userId,
|
||||
teamId,
|
||||
newOwnerUserId,
|
||||
}: {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
newOwnerUserId?: number;
|
||||
}) {
|
||||
return db
|
||||
.transaction()
|
||||
.execute((trx) => memberLeave(trx, { userId, teamId, newOwnerUserId }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a roster edit in a single transaction: updates each kept member's role
|
||||
* & editor status and removes (kicks) the members in `kickedUserIds`.
|
||||
*/
|
||||
export function updateRoster({
|
||||
teamId,
|
||||
members,
|
||||
kickedUserIds,
|
||||
}: {
|
||||
teamId: number;
|
||||
members: Array<{
|
||||
userId: number;
|
||||
role: Tables["TeamMember"]["role"];
|
||||
customRole: Tables["TeamMember"]["customRole"];
|
||||
roleType: Tables["TeamMember"]["roleType"];
|
||||
isManager: boolean;
|
||||
order: number;
|
||||
}>;
|
||||
kickedUserIds: number[];
|
||||
}) {
|
||||
return db.transaction().execute(async (trx) => {
|
||||
for (const userId of kickedUserIds) {
|
||||
await memberLeave(trx, { userId, teamId });
|
||||
}
|
||||
|
||||
for (const member of members) {
|
||||
await trx
|
||||
.updateTable("AllTeamMember")
|
||||
.set({
|
||||
role: member.role,
|
||||
customRole: member.customRole,
|
||||
roleType: member.roleType,
|
||||
isManager: member.isManager ? 1 : 0,
|
||||
order: member.order,
|
||||
})
|
||||
.where("teamId", "=", teamId)
|
||||
.where("userId", "=", member.userId)
|
||||
.execute();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function memberLeave(
|
||||
trx: Transaction<DB>,
|
||||
{
|
||||
userId,
|
||||
teamId,
|
||||
newOwnerUserId,
|
||||
}: { userId: number; teamId: number; newOwnerUserId?: number },
|
||||
) {
|
||||
const currentTeams = await findAllByMemberUserId(userId, trx);
|
||||
|
||||
const teamToLeave = currentTeams.find((team) => team.id === teamId);
|
||||
invariant(teamToLeave, "User is not a member of this team");
|
||||
invariant(
|
||||
!teamToLeave.isOwner || newOwnerUserId,
|
||||
"New owner id must be provided when old is leaving",
|
||||
);
|
||||
|
||||
const wasMainTeam = teamToLeave.isMainTeam;
|
||||
const newMainTeam = currentTeams.find((team) => team.id !== teamId);
|
||||
if (wasMainTeam && newMainTeam) {
|
||||
await trx
|
||||
.updateTable("AllTeamMember")
|
||||
.set({
|
||||
isMainTeam: 1,
|
||||
})
|
||||
.where("userId", "=", userId)
|
||||
.where("teamId", "=", newMainTeam.id)
|
||||
.execute();
|
||||
}
|
||||
|
||||
await trx
|
||||
.updateTable("AllTeamMember")
|
||||
.set({
|
||||
leftAt: databaseTimestampNow(),
|
||||
isMainTeam: 0,
|
||||
isOwner: 0,
|
||||
isManager: 0,
|
||||
})
|
||||
.where("userId", "=", userId)
|
||||
.where("teamId", "=", teamId)
|
||||
.execute();
|
||||
if (newOwnerUserId) {
|
||||
await trx
|
||||
.updateTable("AllTeamMember")
|
||||
.set({
|
||||
isOwner: 1,
|
||||
isManager: 0,
|
||||
})
|
||||
.where("userId", "=", newOwnerUserId)
|
||||
.where("teamId", "=", teamId)
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user