sendou.ink/app/features/tournament-organization/TournamentOrganizationRepository.server.ts
Kalle baa4b43855
Some checks are pending
Tests and checks on push / run-checks-and-tests (push) Waiting to run
Updates translation progress / update-translation-progress-issue (push) Waiting to run
Docker setup for development (#2460)
2025-07-20 16:58:21 +03:00

505 lines
12 KiB
TypeScript

import { sql } from "kysely";
import { jsonArrayFrom, jsonObjectFrom } from "kysely/helpers/sqlite";
import { db } from "~/db/sql";
import type { Tables, TablesInsertable } from "~/db/tables";
import { databaseTimestampNow, dateToDatabaseTimestamp } from "~/utils/dates";
import { COMMON_USER_FIELDS } from "~/utils/kysely.server";
import { mySlugify } from "~/utils/urls";
import { userSubmittedImage } from "~/utils/urls-img";
import { HACKY_resolvePicture } from "../tournament/tournament-utils";
import { TOURNAMENT_SERIES_EVENTS_PER_PAGE } from "./tournament-organization-constants";
interface CreateArgs {
ownerId: number;
name: string;
}
export function create(args: CreateArgs) {
return db.transaction().execute(async (trx) => {
const org = await trx
.insertInto("TournamentOrganization")
.values({
name: args.name,
slug: mySlugify(args.name),
})
.returning("id")
.executeTakeFirstOrThrow();
return trx
.insertInto("TournamentOrganizationMember")
.values({
organizationId: org.id,
userId: args.ownerId,
role: "ADMIN",
})
.execute();
});
}
export async function findBySlug(slug: string) {
const organization = await db
.selectFrom("TournamentOrganization")
.leftJoin(
"UserSubmittedImage",
"UserSubmittedImage.id",
"TournamentOrganization.avatarImgId",
)
.select(({ eb }) => [
"TournamentOrganization.id",
"TournamentOrganization.name",
"TournamentOrganization.description",
"TournamentOrganization.socials",
"TournamentOrganization.slug",
"UserSubmittedImage.url as avatarUrl",
jsonArrayFrom(
eb
.selectFrom("TournamentOrganizationMember")
.innerJoin("User", "User.id", "TournamentOrganizationMember.userId")
.select([
"TournamentOrganizationMember.role",
"TournamentOrganizationMember.roleDisplayName",
...COMMON_USER_FIELDS,
])
.whereRef(
"TournamentOrganizationMember.organizationId",
"=",
"TournamentOrganization.id",
),
).as("members"),
jsonArrayFrom(
eb
.selectFrom("TournamentOrganizationSeries")
.select([
"TournamentOrganizationSeries.id",
"TournamentOrganizationSeries.name",
"TournamentOrganizationSeries.substringMatches",
"TournamentOrganizationSeries.showLeaderboard",
"TournamentOrganizationSeries.description",
])
.whereRef(
"TournamentOrganizationSeries.organizationId",
"=",
"TournamentOrganization.id",
),
).as("series"),
jsonArrayFrom(
eb
.selectFrom("TournamentOrganizationBadge")
.innerJoin("Badge", "Badge.id", "TournamentOrganizationBadge.badgeId")
.select(["Badge.id", "Badge.displayName", "Badge.code", "Badge.hue"])
.whereRef(
"TournamentOrganizationBadge.organizationId",
"=",
"TournamentOrganization.id",
),
).as("badges"),
])
.where("TournamentOrganization.slug", "=", slug)
.executeTakeFirst();
if (!organization) return null;
const orgAdminUserIds = organization.members
.filter((member) => member.role === "ADMIN")
.map((member) => member.id);
return {
...organization,
permissions: {
EDIT: orgAdminUserIds,
BAN: orgAdminUserIds,
},
};
}
export function findByOrganizerUserId(userId: number) {
return db
.selectFrom("TournamentOrganizationMember")
.innerJoin(
"TournamentOrganization",
"TournamentOrganization.id",
"TournamentOrganizationMember.organizationId",
)
.select(["TournamentOrganization.id", "TournamentOrganization.name"])
.where("TournamentOrganizationMember.userId", "=", userId)
.where((eb) =>
eb("TournamentOrganizationMember.role", "=", "ADMIN").or(
"TournamentOrganizationMember.role",
"=",
"ORGANIZER",
),
)
.orderBy("TournamentOrganization.id", "asc")
.execute();
}
interface FindEventsByMonthArgs {
month: number;
year: number;
organizationId: number;
}
const findEventsBaseQuery = (organizationId: number) =>
db
.selectFrom("CalendarEvent")
.innerJoin(
"CalendarEventDate",
"CalendarEventDate.eventId",
"CalendarEvent.id",
)
.select(({ eb }) => [
"CalendarEvent.id as eventId",
"CalendarEvent.name",
"CalendarEvent.tournamentId",
eb.fn.min("CalendarEventDate.startTime").as("startTime"),
eb
.selectFrom("UserSubmittedImage")
.select(["UserSubmittedImage.url"])
.whereRef("CalendarEvent.avatarImgId", "=", "UserSubmittedImage.id")
.as("logoUrl"),
jsonObjectFrom(
eb
.selectFrom("TournamentResult")
.innerJoin(
"TournamentTeam",
"TournamentTeam.id",
"TournamentResult.tournamentTeamId",
)
.leftJoin("AllTeam", "TournamentTeam.teamId", "AllTeam.id")
.leftJoin("UserSubmittedImage as u1", "AllTeam.avatarImgId", "u1.id")
.leftJoin(
"UserSubmittedImage as u2",
"TournamentTeam.avatarImgId",
"u2.id",
)
.select(({ eb: innerEb }) => [
"TournamentTeam.name",
innerEb.fn.coalesce("u1.url", "u2.url").as("avatarUrl"),
jsonArrayFrom(
innerEb
.selectFrom("TournamentTeamMember")
.innerJoin("User", "User.id", "TournamentTeamMember.userId")
.select(["User.discordAvatar", "User.discordId"])
.whereRef(
"TournamentTeamMember.tournamentTeamId",
"=",
"TournamentTeam.id",
)
.orderBy("User.id", "asc"),
).as("members"),
])
.whereRef(
"TournamentResult.tournamentId",
"=",
"CalendarEvent.tournamentId",
)
.where("TournamentResult.placement", "=", 1),
).as("tournamentWinners"),
jsonObjectFrom(
eb
.selectFrom("CalendarEventResultTeam")
.select(({ eb: innerEb }) => [
"CalendarEventResultTeam.name",
sql<null>`null`.as("avatarUrl"),
jsonArrayFrom(
innerEb
.selectFrom("CalendarEventResultPlayer")
.innerJoin(
"User",
"User.id",
"CalendarEventResultPlayer.userId",
)
.select(["User.discordAvatar", "User.discordId"])
.whereRef(
"CalendarEventResultPlayer.teamId",
"=",
"CalendarEventResultTeam.id",
)
.orderBy("User.id", "asc"),
).as("members"),
])
.whereRef("CalendarEventResultTeam.eventId", "=", "CalendarEvent.id")
.where("CalendarEventResultTeam.placement", "=", 1),
).as("eventWinners"),
])
.where("CalendarEvent.organizationId", "=", organizationId)
.where("CalendarEvent.hidden", "=", 0)
.groupBy("CalendarEvent.id");
const mapEvent = <
T extends {
tournamentId: number | null;
logoUrl: string | null;
name: string;
},
>(
event: T,
) => {
return {
...event,
logoUrl: !event.tournamentId
? null
: event.logoUrl
? userSubmittedImage(event.logoUrl)
: HACKY_resolvePicture(event),
};
};
export async function findEventsByMonth({
month,
year,
organizationId,
}: FindEventsByMonthArgs) {
const firstDayOfTheMonth = new Date(Date.UTC(year, month, 1));
const lastDayOfTheMonth = new Date(Date.UTC(year, month + 1, 0));
// a bit of margin for timezones, filtered in the frontend code
firstDayOfTheMonth.setUTCDate(firstDayOfTheMonth.getUTCDate() - 1);
lastDayOfTheMonth.setUTCDate(lastDayOfTheMonth.getUTCDate() + 1);
const events = await findEventsBaseQuery(organizationId)
.where(
"CalendarEventDate.startTime",
">=",
dateToDatabaseTimestamp(firstDayOfTheMonth),
)
.where(
"CalendarEventDate.startTime",
"<=",
dateToDatabaseTimestamp(lastDayOfTheMonth),
)
.orderBy("CalendarEventDate.startTime", "asc")
.execute();
return events.map(mapEvent);
}
export function findAllUnfinalizedEvents(organizationId: number) {
return db
.selectFrom("Tournament")
.innerJoin("CalendarEvent", "CalendarEvent.tournamentId", "Tournament.id")
.select(["Tournament.id"])
.where("Tournament.isFinalized", "=", 0)
.where("CalendarEvent.organizationId", "=", organizationId)
.execute();
}
const findSeriesEventsBaseQuery = ({
organizationId,
substringMatches,
}: {
organizationId: number;
substringMatches: string[];
}) =>
findEventsBaseQuery(organizationId)
.where((eb) =>
eb.or(
substringMatches.map((match) =>
eb("CalendarEvent.name", "like", `%${match}%`),
),
),
)
.orderBy("CalendarEventDate.startTime", "desc");
export async function findPaginatedEventsBySeries({
organizationId,
substringMatches,
page,
}: {
organizationId: number;
substringMatches: string[];
page: number;
}) {
const events = await findSeriesEventsBaseQuery({
organizationId,
substringMatches,
})
.limit(TOURNAMENT_SERIES_EVENTS_PER_PAGE)
.offset((page - 1) * TOURNAMENT_SERIES_EVENTS_PER_PAGE)
.execute();
return events.map(mapEvent);
}
export async function findAllEventsBySeries({
organizationId,
substringMatches,
}: {
organizationId: number;
substringMatches: string[];
}) {
const events = await findSeriesEventsBaseQuery({
organizationId,
substringMatches,
}).execute();
return events.map(mapEvent);
}
interface UpdateArgs
extends Pick<
Tables["TournamentOrganization"],
"id" | "name" | "description" | "socials"
> {
members: Array<
Pick<
Tables["TournamentOrganizationMember"],
"role" | "roleDisplayName" | "userId"
>
>;
series: Array<
Pick<Tables["TournamentOrganizationSeries"], "description" | "name"> & {
showLeaderboard: boolean;
}
>;
badges: number[];
}
export function update({
id,
name,
description,
socials,
members,
series,
badges,
}: UpdateArgs) {
return db.transaction().execute(async (trx) => {
const updatedOrg = await trx
.updateTable("TournamentOrganization")
.set({
name,
description,
slug: mySlugify(name),
socials: socials ? JSON.stringify(socials) : null,
})
.where("id", "=", id)
.returningAll()
.executeTakeFirstOrThrow();
await trx
.deleteFrom("TournamentOrganizationMember")
.where("organizationId", "=", id)
.execute();
await trx
.insertInto("TournamentOrganizationMember")
.values(
members.map((member) => ({
organizationId: id,
...member,
})),
)
.execute();
await trx
.deleteFrom("TournamentOrganizationSeries")
.where("organizationId", "=", id)
.execute();
if (series.length > 0) {
await trx
.insertInto("TournamentOrganizationSeries")
.values(
series.map((s) => ({
organizationId: id,
name: s.name,
description: s.description,
substringMatches: JSON.stringify([s.name.toLowerCase()]),
showLeaderboard: Number(s.showLeaderboard),
})),
)
.execute();
}
await trx
.deleteFrom("TournamentOrganizationBadge")
.where("TournamentOrganizationBadge.organizationId", "=", id)
.execute();
if (badges.length > 0) {
await trx
.insertInto("TournamentOrganizationBadge")
.values(
badges.map((badgeId) => ({
organizationId: id,
badgeId,
})),
)
.execute();
}
return updatedOrg;
});
}
/**
* Inserts a user to the banned list for a tournament organization or updates the existing entry if already exists.
*/
export function upsertBannedUser(
args: Omit<TablesInsertable["TournamentOrganizationBannedUser"], "updatedAt">,
) {
return db
.insertInto("TournamentOrganizationBannedUser")
.values({ ...args, updatedAt: databaseTimestampNow() })
.execute();
}
/**
* Removes a user from the banned list for a tournament organization
*/
export function unbanUser({
organizationId,
userId,
}: {
organizationId: number;
userId: number;
}) {
return db
.deleteFrom("TournamentOrganizationBannedUser")
.where("organizationId", "=", organizationId)
.where("userId", "=", userId)
.execute();
}
/**
* Returns all banned users for a specific tournament organization
*/
export function allBannedUsersByOrganizationId(organizationId: number) {
return db
.selectFrom("TournamentOrganizationBannedUser")
.innerJoin("User", "User.id", "TournamentOrganizationBannedUser.userId")
.select([
"TournamentOrganizationBannedUser.privateNote",
"TournamentOrganizationBannedUser.updatedAt",
...COMMON_USER_FIELDS,
])
.where(
"TournamentOrganizationBannedUser.organizationId",
"=",
organizationId,
)
.orderBy("TournamentOrganizationBannedUser.updatedAt", "desc")
.execute();
}
/**
* Checks if a user is banned by a specific organization
*/
export async function isUserBannedByOrganization({
organizationId,
userId,
}: {
organizationId: number;
userId: number;
}) {
const result = await db
.selectFrom("TournamentOrganizationBannedUser")
.select("userId")
.where("organizationId", "=", organizationId)
.where("userId", "=", userId)
.executeTakeFirst();
return Boolean(result);
}