Add /api/badges/leaderboard endpoint for Nitro v3 badge leaderboard

This commit is contained in:
openhands committed 2026-07-07 16:00:46 +02:00
1 parent efb9758cba
commit 2f0233a0c0
1 file changed
+334
+334
View File
@@ -0,0 +1,334 @@
import { apiJson } from "@/lib/api";
import { bearerUserId } from "@/lib/api-auth";
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
export const dynamic = "force-dynamic";
type BadgeRarityKey = "common" | "rare" | "epic" | "legendary" | "mythical" | "unique";
interface BadgeLeaderboardEntry {
userId: number;
username: string;
figure: string;
score: number;
rank: number;
}
interface BadgeLeaderboardBoard {
entries: BadgeLeaderboardEntry[];
totalPlayers: number;
viewerEntry?: Partial<BadgeLeaderboardEntry>;
}
interface BadgeLeaderboardStat {
badgeCode: string;
ownerCount: number;
rarity: BadgeRarityKey;
}
const THRESHOLDS = {
commonMinOwners: 500,
rareMinOwners: 100,
epicMinOwners: 20,
legendaryMinOwners: 5,
mythicalMinOwners: 2,
uniqueOwners: 1,
};
function assignRarity(ownerCount: number): BadgeRarityKey {
if (ownerCount >= THRESHOLDS.commonMinOwners) return "common";
if (ownerCount >= THRESHOLDS.rareMinOwners) return "rare";
if (ownerCount >= THRESHOLDS.epicMinOwners) return "epic";
if (ownerCount >= THRESHOLDS.legendaryMinOwners) return "legendary";
if (ownerCount >= THRESHOLDS.mythicalMinOwners) return "mythical";
return "unique";
}
function rankEntries(entries: BadgeLeaderboardEntry[]): BadgeLeaderboardEntry[] {
return entries
.sort((a, b) => b.score - a.score)
.map((e, i) => ({ ...e, rank: i + 1 }));
}
async function loadTotalBadgesBoard(userId: number | null): Promise<BadgeLeaderboardBoard> {
const rows = await prisma.$queryRawUnsafe<Array<{ userId: number; username: string; look: string; cnt: bigint }>>(
`SELECT ub.user_id AS userId, u.username, u.look, COUNT(*) AS cnt
FROM users_badges ub
JOIN users u ON u.id = ub.user_id
GROUP BY ub.user_id
ORDER BY cnt DESC
LIMIT 20`
);
const entries = rankEntries(
rows.map((r) => ({
userId: Number(r.userId),
username: r.username,
figure: r.look,
score: Number(r.cnt),
rank: 0,
}))
);
const totalPlayers = entries.length > 0
? Number(
(
await prisma.$queryRawUnsafe<Array<{ cnt: bigint }>>(
`SELECT COUNT(DISTINCT user_id) AS cnt FROM users_badges`
)
)[0]?.cnt ?? 0
)
: 0;
let viewerEntry: Partial<BadgeLeaderboardEntry> | undefined;
if (userId) {
const viewerRow = await prisma.$queryRawUnsafe<Array<{ cnt: bigint }>>(
`SELECT COUNT(*) AS cnt FROM users_badges WHERE user_id = ?`,
userId
);
const viewerScore = Number(viewerRow[0]?.cnt ?? 0);
if (viewerScore > 0) {
const viewerRank = entries.length > 0
? Number(
(
await prisma.$queryRawUnsafe<Array<{ cnt: bigint }>>(
`SELECT COUNT(*) + 1 AS cnt
FROM (SELECT user_id, COUNT(*) AS total FROM users_badges GROUP BY user_id) t
WHERE t.total > ?`,
viewerScore
)
)[0]?.cnt ?? entries.length + 1
)
: 1;
const viewerUser = await prisma.user.findUnique({
where: { id: userId },
select: { username: true, look: true },
});
if (viewerUser) {
viewerEntry = {
userId,
username: viewerUser.username,
figure: viewerUser.look,
score: viewerScore,
rank: viewerRank,
};
}
}
}
return { entries, totalPlayers, viewerEntry };
}
async function loadAchievementBoard(userId: number | null): Promise<BadgeLeaderboardBoard> {
const rows = await prisma.$queryRawUnsafe<Array<{ userId: number; username: string; look: string; score: number }>>(
`SELECT us.user_id AS userId, u.username, u.look, us.achievement_score AS score
FROM users_settings us
JOIN users u ON u.id = us.user_id
ORDER BY us.achievement_score DESC
LIMIT 20`
);
const entries = rankEntries(
rows.map((r) => ({
userId: Number(r.userId),
username: r.username,
figure: r.look,
score: r.score,
rank: 0,
}))
);
const totalPlayers = entries.length > 0
? Number(
(
await prisma.$queryRawUnsafe<Array<{ cnt: bigint }>>(
`SELECT COUNT(*) AS cnt FROM users_settings WHERE achievement_score > 0`
)
)[0]?.cnt ?? 0
)
: 0;
let viewerEntry: Partial<BadgeLeaderboardEntry> | undefined;
if (userId) {
const viewerUser = await prisma.user.findUnique({
where: { id: userId },
select: { username: true, look: true },
});
if (viewerUser) {
const viewerSettings = await prisma.usersSettings.findUnique({
where: { userId },
select: { achievementScore: true },
});
if (viewerSettings && viewerSettings.achievementScore > 0) {
const viewerRank = Number(
(
await prisma.$queryRawUnsafe<Array<{ cnt: bigint }>>(
`SELECT COUNT(*) + 1 AS cnt FROM users_settings WHERE achievement_score > ?`,
viewerSettings.achievementScore
)
)[0]?.cnt ?? entries.length + 1
);
viewerEntry = {
userId,
username: viewerUser.username,
figure: viewerUser.look,
score: viewerSettings.achievementScore,
rank: viewerRank,
};
}
}
}
return { entries, totalPlayers, viewerEntry };
}
async function loadRarityBoard(
rarity: BadgeRarityKey,
badgeCodes: string[],
userId: number | null
): Promise<BadgeLeaderboardBoard> {
if (badgeCodes.length === 0) return { entries: [], totalPlayers: 0 };
const placeholders = badgeCodes.map(() => "?").join(",");
const rows = await prisma.$queryRawUnsafe<Array<{ userId: number; username: string; look: string; cnt: bigint }>>(
`SELECT ub.user_id AS userId, u.username, u.look, COUNT(*) AS cnt
FROM users_badges ub
JOIN users u ON u.id = ub.user_id
WHERE ub.badge_code IN (${placeholders})
GROUP BY ub.user_id
ORDER BY cnt DESC
LIMIT 20`,
...badgeCodes
);
const entries = rankEntries(
rows.map((r) => ({
userId: Number(r.userId),
username: r.username,
figure: r.look,
score: Number(r.cnt),
rank: 0,
}))
);
const totalPlayers = rows.length > 0
? Number(
(
await prisma.$queryRawUnsafe<Array<{ cnt: bigint }>>(
`SELECT COUNT(DISTINCT user_id) AS cnt FROM users_badges WHERE badge_code IN (${placeholders})`,
...badgeCodes
)
)[0]?.cnt ?? 0
)
: 0;
let viewerEntry: Partial<BadgeLeaderboardEntry> | undefined;
if (userId) {
const viewerRow = await prisma.$queryRawUnsafe<Array<{ cnt: bigint }>>(
`SELECT COUNT(*) AS cnt FROM users_badges WHERE user_id = ? AND badge_code IN (${placeholders})`,
userId,
...badgeCodes
);
const viewerScore = Number(viewerRow[0]?.cnt ?? 0);
if (viewerScore > 0) {
const viewerUser = await prisma.user.findUnique({
where: { id: userId },
select: { username: true, look: true },
});
if (viewerUser) {
const viewerRank = Number(
(
await prisma.$queryRawUnsafe<Array<{ cnt: bigint }>>(
`SELECT COUNT(*) + 1 AS cnt
FROM (SELECT user_id, COUNT(*) AS total FROM users_badges WHERE badge_code IN (${placeholders}) GROUP BY user_id) t
WHERE t.total > ?`,
...badgeCodes,
viewerScore
)
)[0]?.cnt ?? entries.length + 1
);
viewerEntry = {
userId,
username: viewerUser.username,
figure: viewerUser.look,
score: viewerScore,
rank: viewerRank,
};
}
}
}
return { entries, totalPlayers, viewerEntry };
}
export async function GET(req: Request) {
try {
let userId: number | null = await bearerUserId(req);
if (!userId) {
const session = await auth();
userId = session?.user?.id ? Number(session.user.id) : null;
}
// Gather badge ownership stats
const badgeStatsRaw = await prisma.$queryRawUnsafe<
Array<{ badgeCode: string; ownerCount: bigint }>
>(
`SELECT badge_code AS badgeCode, COUNT(DISTINCT user_id) AS ownerCount
FROM users_badges
GROUP BY badge_code`
);
const badgeStats: BadgeLeaderboardStat[] = badgeStatsRaw.map((r) => ({
badgeCode: r.badgeCode,
ownerCount: Number(r.ownerCount),
rarity: assignRarity(Number(r.ownerCount)),
}));
// Group badge codes by rarity
const badgesByRarity = new Map<BadgeRarityKey, string[]>();
for (const stat of badgeStats) {
const list = badgesByRarity.get(stat.rarity) ?? [];
list.push(stat.badgeCode);
badgesByRarity.set(stat.rarity, list);
}
const rarityKeys: BadgeRarityKey[] = ["common", "rare", "epic", "legendary", "mythical", "unique"];
const [totalBadges, achievementLevel, ...rarityBoards] = await Promise.all([
loadTotalBadgesBoard(userId),
loadAchievementBoard(userId),
...rarityKeys.map((rk) => loadRarityBoard(rk, badgesByRarity.get(rk) ?? [], userId)),
]);
const rarity = Object.fromEntries(
rarityKeys.map((rk, i) => [rk, rarityBoards[i]])
) as Record<BadgeRarityKey, BadgeLeaderboardBoard>;
return apiJson({
viewerUserId: userId ?? 0,
badgeStats,
thresholds: THRESHOLDS,
leaderboards: { totalBadges, achievementLevel, rarity },
});
} catch (err) {
console.error("Badge leaderboard error:", err);
return apiJson({
viewerUserId: 0,
badgeStats: [],
thresholds: THRESHOLDS,
leaderboards: {
totalBadges: { entries: [], totalPlayers: 0 },
achievementLevel: { entries: [], totalPlayers: 0 },
rarity: {
common: { entries: [], totalPlayers: 0 },
rare: { entries: [], totalPlayers: 0 },
epic: { entries: [], totalPlayers: 0 },
legendary: { entries: [], totalPlayers: 0 },
mythical: { entries: [], totalPlayers: 0 },
unique: { entries: [], totalPlayers: 0 },
},
},
});
}
}