mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-06-10 02:32:05 -05:00
TournamentBadgeOwner previously stored manual grants as N identical rows to represent "this user has this badge N times" — one user had 12,345 duplicate rows for a single badge. This adds a `count` column, collapses NULL-tournamentId duplicates in the backfill, and rewrites the BadgeOwner view to expose count via SUM. The table shrinks from 75,596 to 24,204 rows. Also adds an index on TournamentBadgeOwner(userId) as defense-in-depth for direct lookups, and updates replaceOwners to dedupe ownerIds at insert time. Profile-page query for the worst-case user drops from ~4 ms to ~1–2 ms warm.
256 lines
5.9 KiB
TypeScript
256 lines
5.9 KiB
TypeScript
import type { ExpressionBuilder, NotNull } from "kysely";
|
|
import { jsonArrayFrom, jsonObjectFrom } from "kysely/helpers/sqlite";
|
|
import { db } from "~/db/sql";
|
|
import type { DB } from "~/db/tables";
|
|
import { sortBadgesByFavorites } from "~/features/user-page/core/badge-sorting.server";
|
|
import invariant from "~/utils/invariant";
|
|
import { COMMON_USER_FIELDS } from "~/utils/kysely.server";
|
|
import { SPLATOON_3_XP_BADGE_VALUES } from "./badges-constants";
|
|
import { findSplatoon3XpBadgeValue } from "./badges-utils";
|
|
|
|
const addPermissions = <T extends { managers: { userId: number }[] }>(
|
|
row: T,
|
|
) => ({
|
|
...row,
|
|
permissions: {
|
|
MANAGE: row.managers.map((m) => m.userId),
|
|
},
|
|
});
|
|
|
|
const withAuthor = (eb: ExpressionBuilder<DB, "Badge">) => {
|
|
return jsonObjectFrom(
|
|
eb
|
|
.selectFrom("User")
|
|
.select(COMMON_USER_FIELDS)
|
|
.whereRef("User.id", "=", "Badge.authorId"),
|
|
).as("author");
|
|
};
|
|
|
|
const withManagers = (eb: ExpressionBuilder<DB, "Badge">) => {
|
|
return jsonArrayFrom(
|
|
eb
|
|
.selectFrom("BadgeManager")
|
|
.innerJoin("User", "BadgeManager.userId", "User.id")
|
|
.select(["userId", ...COMMON_USER_FIELDS])
|
|
.whereRef("BadgeManager.badgeId", "=", "Badge.id"),
|
|
).as("managers");
|
|
};
|
|
|
|
const withOwners = (eb: ExpressionBuilder<DB, "Badge">) => {
|
|
return jsonArrayFrom(
|
|
eb
|
|
.selectFrom("BadgeOwner")
|
|
.innerJoin("User", "BadgeOwner.userId", "User.id")
|
|
.select(({ fn }) => [
|
|
fn.sum<number>("BadgeOwner.count").as("count"),
|
|
"User.id",
|
|
"User.discordId",
|
|
"User.username",
|
|
])
|
|
.whereRef("BadgeOwner.badgeId", "=", "Badge.id")
|
|
.groupBy("User.id")
|
|
.orderBy("count", "desc"),
|
|
).as("owners");
|
|
};
|
|
|
|
export async function all() {
|
|
const rows = await db
|
|
.selectFrom("Badge")
|
|
.select(({ eb }) => [
|
|
"id",
|
|
"displayName",
|
|
"code",
|
|
"hue",
|
|
withManagers(eb),
|
|
withAuthor(eb),
|
|
])
|
|
.execute();
|
|
|
|
return rows.map(addPermissions);
|
|
}
|
|
|
|
export async function findById(badgeId: number) {
|
|
const row = await db
|
|
.selectFrom("Badge")
|
|
.select((eb) => [
|
|
"Badge.id",
|
|
"Badge.displayName",
|
|
"Badge.code",
|
|
"Badge.hue",
|
|
withAuthor(eb),
|
|
withManagers(eb),
|
|
withOwners(eb),
|
|
])
|
|
.where("id", "=", badgeId)
|
|
.executeTakeFirst();
|
|
|
|
if (!row) {
|
|
return null;
|
|
}
|
|
|
|
return addPermissions(row);
|
|
}
|
|
|
|
export function findByManagersList(userIds: number[]) {
|
|
return db
|
|
.selectFrom("Badge")
|
|
.select(["Badge.id", "Badge.code", "Badge.displayName", "Badge.hue"])
|
|
.innerJoin("BadgeManager", "Badge.id", "BadgeManager.badgeId")
|
|
.where("BadgeManager.userId", "in", userIds)
|
|
.orderBy("Badge.id", "asc")
|
|
.groupBy("Badge.id")
|
|
.execute();
|
|
}
|
|
|
|
export function findManagedByUserId(userId: number) {
|
|
return db
|
|
.selectFrom("BadgeManager")
|
|
.innerJoin("Badge", "Badge.id", "BadgeManager.badgeId")
|
|
.select(["Badge.id", "Badge.code", "Badge.displayName", "Badge.hue"])
|
|
.where("BadgeManager.userId", "=", userId)
|
|
.execute();
|
|
}
|
|
|
|
export async function findByOwnerUserId(userId: number) {
|
|
const rows = await db
|
|
.selectFrom("BadgeOwner")
|
|
.innerJoin("Badge", "Badge.id", "BadgeOwner.badgeId")
|
|
.innerJoin("User", "User.id", "BadgeOwner.userId")
|
|
.select(({ fn }) => [
|
|
fn.sum<number>("BadgeOwner.count").as("count"),
|
|
"Badge.id",
|
|
"Badge.displayName",
|
|
"Badge.code",
|
|
"Badge.hue",
|
|
"User.favoriteBadgeIds",
|
|
"User.patronTier",
|
|
])
|
|
.where("BadgeOwner.userId", "=", userId)
|
|
.groupBy("BadgeOwner.badgeId")
|
|
.execute();
|
|
|
|
if (rows.length === 0) return [];
|
|
|
|
const { favoriteBadgeIds, patronTier } = rows[0];
|
|
|
|
return sortBadgesByFavorites({
|
|
favoriteBadgeIds,
|
|
badges: rows.map(
|
|
({ favoriteBadgeIds: _, patronTier: __, ...badge }) => badge,
|
|
),
|
|
patronTier,
|
|
}).badges;
|
|
}
|
|
|
|
export function findByAuthorUserId(userId: number) {
|
|
return db
|
|
.selectFrom("Badge")
|
|
.select(["Badge.id", "Badge.displayName", "Badge.code", "Badge.hue"])
|
|
.where("Badge.authorId", "=", userId)
|
|
.groupBy("Badge.id")
|
|
.execute();
|
|
}
|
|
|
|
export function replaceManagers({
|
|
badgeId,
|
|
managerIds,
|
|
}: {
|
|
badgeId: number;
|
|
managerIds: number[];
|
|
}) {
|
|
return db.transaction().execute(async (trx) => {
|
|
await trx
|
|
.deleteFrom("BadgeManager")
|
|
.where("badgeId", "=", badgeId)
|
|
.execute();
|
|
|
|
if (managerIds.length > 0) {
|
|
await trx
|
|
.insertInto("BadgeManager")
|
|
.values(
|
|
managerIds.map((userId) => ({
|
|
badgeId,
|
|
userId,
|
|
})),
|
|
)
|
|
.execute();
|
|
}
|
|
});
|
|
}
|
|
|
|
export function replaceOwners({
|
|
badgeId,
|
|
ownerIds,
|
|
}: {
|
|
badgeId: number;
|
|
ownerIds: number[];
|
|
}) {
|
|
return db.transaction().execute(async (trx) => {
|
|
await trx
|
|
.deleteFrom("TournamentBadgeOwner")
|
|
.where("badgeId", "=", badgeId)
|
|
.execute();
|
|
|
|
if (ownerIds.length > 0) {
|
|
const counts = new Map<number, number>();
|
|
for (const userId of ownerIds) {
|
|
counts.set(userId, (counts.get(userId) ?? 0) + 1);
|
|
}
|
|
|
|
await trx
|
|
.insertInto("TournamentBadgeOwner")
|
|
.values(
|
|
Array.from(counts, ([userId, count]) => ({
|
|
badgeId,
|
|
userId,
|
|
count,
|
|
})),
|
|
)
|
|
.execute();
|
|
}
|
|
});
|
|
}
|
|
|
|
export async function syncXPBadges() {
|
|
return db.transaction().execute(async (trx) => {
|
|
for (const value of SPLATOON_3_XP_BADGE_VALUES) {
|
|
const badge = await trx
|
|
.selectFrom("Badge")
|
|
.select("id")
|
|
.where("code", "=", String(value))
|
|
.executeTakeFirst();
|
|
|
|
invariant(badge, `Badge ${value} not found`);
|
|
|
|
await trx
|
|
.deleteFrom("TournamentBadgeOwner")
|
|
.where("badgeId", "=", badge.id)
|
|
.execute();
|
|
}
|
|
|
|
const userTopXPowers = await trx
|
|
.selectFrom("SplatoonPlayer")
|
|
.select(["userId", "peakXp"])
|
|
.where("userId", "is not", null)
|
|
.where("peakXp", "is not", null)
|
|
.$narrowType<{ userId: NotNull; peakXp: NotNull }>()
|
|
.execute();
|
|
|
|
for (const { userId, peakXp } of userTopXPowers) {
|
|
const badgeValue = findSplatoon3XpBadgeValue(peakXp!);
|
|
if (!badgeValue) continue;
|
|
|
|
await trx
|
|
.insertInto("TournamentBadgeOwner")
|
|
.values((eb) => ({
|
|
badgeId: eb
|
|
.selectFrom("Badge")
|
|
.select("id")
|
|
.where("code", "=", String(badgeValue)),
|
|
userId,
|
|
}))
|
|
.execute();
|
|
}
|
|
});
|
|
}
|