'use strict'; /** * GET /api/players/search (Session 60, night2/E — audit fix 4.1). * * The scan search box's canonical resolver. Before this route existed, MLB * search 404'd at Express and NBA/WNBA hit the (usually offline) Python * service — "Ohtani" returned nothing while his tile sat on the page. * * MLB: fuzzy match against the cached statsapi player list (free, 24h * cache) — case/diacritic-insensitive, nickname/suffix-aware via nameKey. * Other sports: cache-only match against the names the platform already * knows (rosterlogs blob + tonight's graded slate). Never an upstream call * for non-MLB; empty is a valid answer. */ const express = require('express'); const { createRateLimit } = require('../middleware/rateLimit'); const { nameKey } = require('../utils/playerName'); const router = express.Router(); router.use(createRateLimit({ windowMs: 60_000, max: 60 })); async function cachedNames(sport) { const { cacheGet } = require('../utils/redis'); const out = new Map(); try { const blob = await cacheGet(`rosterlogs:${sport}`); for (const p of Array.isArray(blob) ? blob : []) { if (p && p.name) out.set(nameKey(p.name), { full_name: p.name, team: p.team || null }); } } catch { /* cache-only, degrade */ } try { const env = await cacheGet(`grades:${sport}`); for (const g of (env && env.grades) || []) { const n = g.player || g.player_name; if (n && !out.has(nameKey(n))) out.set(nameKey(n), { full_name: n, team: g.team || null }); } } catch { /* cache-only, degrade */ } return [...out.values()]; } router.get('/search', async (req, res) => { const sport = String(req.query.sport || 'NBA').toUpperCase(); const q = String(req.query.q || '').trim(); if (q.length < 2) return res.json({ players: [] }); try { if (sport === 'MLB') { const { searchPlayers } = require('../services/adapters/mlbStatsAdapter'); const hits = await searchPlayers(q, { limit: 12 }); return res.json({ players: hits.map((h) => ({ id: String(h.id), full_name: h.fullName, team: h.team || undefined, position: h.position || undefined })), }); } // NBA/WNBA/soccer — the names the platform already carries (cache-only). const names = await cachedNames(sport.toLowerCase()); const qKey = nameKey(q); const qLast = qKey.split(' ').pop(); const players = names .filter((p) => { const k = nameKey(p.full_name); return k === qKey || k.includes(qKey) || (qLast.length >= 3 && k.split(' ').some((w) => w.startsWith(qLast))); }) .slice(0, 12) .map((p, i) => ({ id: `${sport}-${i}-${nameKey(p.full_name)}`, full_name: p.full_name, team: p.team || undefined })); return res.json({ players }); } catch (err) { console.error('[players/search]', err.message); return res.json({ players: [] }); } }); module.exports = router;