Files
eagler-tiers/server/routes/rankings.js
starified 08bf320b57 uploaded
2026-04-21 22:03:19 -04:00

177 lines
4.4 KiB
JavaScript

const express = require("express");
const { pool } = require("../db");
const { GAMEMODES, avatarUrl, normalizeGamemode } = require("../config");
const { ValidationError, validateRequest } = require("../middleware/validation");
const { sendError } = require("../utils/http");
const router = express.Router();
const PER_PAGE = 20;
const TIER_POINTS_SQL = `
CASE pr.tier
WHEN 'HT1' THEN 60
WHEN 'LT1' THEN 45
WHEN 'HT2' THEN 30
WHEN 'LT2' THEN 20
WHEN 'HT3' THEN 10
WHEN 'LT3' THEN 6
WHEN 'HT4' THEN 4
WHEN 'LT4' THEN 3
WHEN 'HT5' THEN 2
WHEN 'LT5' THEN 1
ELSE 0
END`;
const GM_POINTS_SQL = `
CASE gm.tier
WHEN 'HT1' THEN 60
WHEN 'LT1' THEN 45
WHEN 'HT2' THEN 30
WHEN 'LT2' THEN 20
WHEN 'HT3' THEN 10
WHEN 'LT3' THEN 6
WHEN 'HT4' THEN 4
WHEN 'LT4' THEN 3
WHEN 'HT5' THEN 2
WHEN 'LT5' THEN 1
ELSE 0
END`;
const schema = {
params: {
fields: {
gamemode: {
type: "string",
required: true,
minLength: 3,
maxLength: 32,
toLowerCase: true,
},
},
allowUnknown: false,
},
query: {
fields: {
page: { type: "int", min: 1, max: 50000 },
perPage: { type: "int", min: 1, max: 500 },
},
allowUnknown: false,
},
};
function validateOrReply(req, res) {
try {
return validateRequest(req, schema);
} catch (error) {
if (error instanceof ValidationError) {
sendError(res, 400, "VALIDATION_ERROR", error.message, error.details);
return null;
}
sendError(res, 400, "VALIDATION_ERROR", "Invalid request payload.");
return null;
}
}
function buildDefaultTiers() {
return Object.fromEntries(GAMEMODES.map((mode) => [mode, null]));
}
router.get("/rankings/:gamemode", async (req, res) => {
const validated = validateOrReply(req, res);
if (!validated) {
return;
}
try {
const gamemode = normalizeGamemode(validated.params.gamemode);
const page = validated.query.page || 1;
const perPage = validated.query.perPage || PER_PAGE;
if (!GAMEMODES.includes(gamemode)) {
return sendError(res, 400, "VALIDATION_ERROR", "Invalid gamemode");
}
const [countRows] = await pool.query(
"SELECT COUNT(*) AS total FROM player_ranks WHERE gamemode = ?",
[gamemode]
);
const total = Number(countRows[0]?.total || 0);
const totalPages = Math.max(Math.ceil(total / perPage), 1);
const start = (page - 1) * perPage;
if (total === 0) {
return res.json({
gamemode,
data: [],
pagination: { page, perPage, total, totalPages },
});
}
const [pageRows] = await pool.query(
`SELECT
p.id,
p.username,
p.region,
gm.tier AS gm_tier,
COALESCE(SUM(${TIER_POINTS_SQL}), 0) AS total_points,
${GM_POINTS_SQL} AS gamemode_points
FROM players p
JOIN player_ranks gm ON gm.player_id = p.id AND gm.gamemode = ?
LEFT JOIN player_ranks pr ON pr.player_id = p.id
GROUP BY p.id, p.username, p.region, gm.tier
ORDER BY gamemode_points DESC, total_points DESC, p.username ASC
LIMIT ? OFFSET ?`,
[gamemode, perPage, start]
);
const playerIds = pageRows.map((row) => row.id);
const [allTierRows] = playerIds.length
? await pool.query(
"SELECT player_id, gamemode, tier FROM player_ranks WHERE player_id IN (?)",
[playerIds]
)
: [[]];
const byId = new Map();
for (let i = 0; i < pageRows.length; i += 1) {
const row = pageRows[i];
byId.set(row.id, {
id: row.id,
username: row.username,
region: row.region,
avatarUrl: avatarUrl(row.username),
tiers: buildDefaultTiers(),
totalPoints: Number(row.total_points || 0),
gamemodePoints: Number(row.gamemode_points || 0),
position: start + i + 1,
});
}
for (const row of allTierRows) {
const target = byId.get(row.player_id);
if (!target) {
continue;
}
target.tiers[row.gamemode] = row.tier;
}
return res.json({
gamemode,
data: Array.from(byId.values()),
pagination: {
page,
perPage,
total,
totalPages,
},
});
} catch (error) {
console.error("GET /api/rankings/:gamemode failed:", error?.message || error);
return sendError(res, 500, "RANKINGS_FETCH_FAILED", "Failed to fetch rankings");
}
});
module.exports = router;