Simplify match history with filters and persistent pagination
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import { db } from '../db/index.js';
|
||||
import { MIN_RECORDED_ROUND, type MatchDetail, type MatchPlayer, type ServerMatch } from '../../match-history.js';
|
||||
import { MIN_RECORDED_ROUND, type MatchDetail, type MatchPlayer, type MatchHistoryResponse, type ServerMatch } from '../../match-history.js';
|
||||
|
||||
const matchSummary = `m.id, m.server, m.server_deleted_at, m.map_name AS map, m.started_at AS startedAt, m.status,
|
||||
import { readMatchQuery, type MatchHistoryQuery } from '../../match-history-query.js';
|
||||
|
||||
const matchSummary = `m.id, m.game, m.highest_round AS observedRound, m.server, m.server_deleted_at, m.map_name AS map, m.started_at AS startedAt, m.status,
|
||||
CASE WHEN m.status = 'completed' THEN m.ended_at END AS endedAt,
|
||||
m.start_players AS startPlayers,
|
||||
(SELECT COUNT(*) FROM leaderboard_match_players p WHERE p.match_id = m.id) AS uniquePlayers,
|
||||
@@ -12,22 +14,38 @@ const matchSummary = `m.id, m.server, m.server_deleted_at, m.map_name AS map, m.
|
||||
MAX(0, ROUND((julianday(m.ended_at) - julianday(m.started_at)) * 86400000)) END AS durationMs,
|
||||
CASE WHEN m.status = 'completed' THEN m.highest_round END AS finalRound`;
|
||||
|
||||
export function serverMatchHistory(server?: string, limit = 25): { matches: ServerMatch[]; total: number } {
|
||||
export function serverMatchHistory(server?: string, limit = 25, query: MatchHistoryQuery = readMatchQuery(new URLSearchParams()), now = Date.now()): MatchHistoryResponse {
|
||||
const handle = db();
|
||||
const where = `WHERE m.highest_round >= ${MIN_RECORDED_ROUND}${server === undefined ? '' : ' AND m.server = ?'}`;
|
||||
const parameters = server === undefined ? [] : [server];
|
||||
const clauses = [`m.highest_round >= ${MIN_RECORDED_ROUND}`];
|
||||
const parameters: Array<string | number> = [];
|
||||
if (server !== undefined) { clauses.push('m.server = ?'); parameters.push(server); }
|
||||
if (query.game) { clauses.push('m.game = ?'); parameters.push(query.game); }
|
||||
const maps = handle.prepare(`SELECT m.map_key AS key, MAX(m.map_name) AS name, m.game
|
||||
FROM leaderboard_matches m WHERE ${clauses.join(' AND ')} GROUP BY m.map_key, m.game ORDER BY name COLLATE NOCASE, game`)
|
||||
.all(...parameters) as MatchHistoryResponse['maps'];
|
||||
if (query.map) { clauses.push('m.map_key = ?'); parameters.push(query.map); }
|
||||
if (query.status) { clauses.push('m.status = ?'); parameters.push(query.status); }
|
||||
if (query.days) { clauses.push('julianday(m.started_at) >= julianday(?)'); parameters.push(new Date(now - query.days * 86400000).toISOString()); }
|
||||
// Search readable instance names as well as their stored identifiers. Tokens allow "Town Lobby 1".
|
||||
for (const token of query.q.toLowerCase().replace(/\blobby\s+(?=\d+\b)/g, '').split(/[\s_]+/).filter(Boolean)) {
|
||||
clauses.push("instr(lower(m.server || ' ' || m.map_name), ?) > 0"); parameters.push(token);
|
||||
}
|
||||
const where = `WHERE ${clauses.join(' AND ')}`;
|
||||
const total = (handle.prepare(`SELECT COUNT(*) AS total FROM leaderboard_matches m ${where}`).get(...parameters) as { total: number }).total;
|
||||
const page = Math.min(query.page, Math.max(1, Math.ceil(total / limit)));
|
||||
const column = { started: 'startedAt', round: 'observedRound', duration: 'durationMs', players: 'uniquePlayers' }[query.sort];
|
||||
const direction = query.direction === 'asc' ? 'ASC' : 'DESC';
|
||||
const matches = handle.prepare(`SELECT ${matchSummary} FROM leaderboard_matches m
|
||||
${where} ORDER BY m.started_at DESC, m.id DESC LIMIT ?`)
|
||||
.all(...parameters, limit) as ServerMatch[];
|
||||
return { matches, total };
|
||||
${where} ORDER BY ${column} IS NULL ASC, ${column} ${direction}, m.id DESC LIMIT ? OFFSET ?`)
|
||||
.all(...parameters, limit, (page - 1) * limit) as ServerMatch[];
|
||||
return { matches, total, page, limit, maps };
|
||||
}
|
||||
|
||||
/** Read durable match records without depending on a current server or scoreboard. */
|
||||
export function getMatchDetail(id: number): MatchDetail | null {
|
||||
if (!Number.isSafeInteger(id) || id < 1) return null;
|
||||
const handle = db();
|
||||
const match = handle.prepare(`SELECT ${matchSummary}, m.game, m.start_observed AS startObserved
|
||||
const match = handle.prepare(`SELECT ${matchSummary}, m.start_observed AS startObserved
|
||||
FROM leaderboard_matches m WHERE m.id = ? AND m.status = 'completed'
|
||||
AND m.highest_round >= ${MIN_RECORDED_ROUND}`)
|
||||
.get(id) as (Omit<MatchDetail, 'players' | 'startObserved'> & { startObserved: number }) | undefined;
|
||||
|
||||
Reference in New Issue
Block a user