Files
pdga-rating/src/models/player.js
T

310 lines
9.0 KiB
JavaScript

const { db } = require('../db');
const { parseDate } = require('../services/rating-calculator');
function getPlayerFromDB(pdgaNumber) {
return new Promise((resolve, reject) => {
db.get(
'SELECT * FROM players WHERE pdga_number = ?',
[pdgaNumber],
(err, row) => {
if (err) reject(err);
else resolve(row);
}
);
});
}
function savePlayerToDB(playerData) {
return new Promise((resolve, reject) => {
db.run(
`INSERT OR REPLACE INTO players (pdga_number, name, current_rating, rating_change, last_updated)
VALUES (?, ?, ?, ?, datetime('now'))`,
[playerData.pdgaNumber, playerData.name, playerData.rating, playerData.ratingChange],
function(err) {
if (err) reject(err);
else resolve(this.lastID);
}
);
});
}
function getRatingHistoryFromDB(pdgaNumber) {
return new Promise((resolve, reject) => {
db.get('SELECT id FROM players WHERE pdga_number = ?', [pdgaNumber], (err, player) => {
if (err) return reject(err);
if (!player) return resolve(null);
db.all(
'SELECT * FROM rating_history WHERE player_id = ? ORDER BY date ASC',
[player.id],
(err, rows) => {
if (err) reject(err);
else resolve(rows);
}
);
});
});
}
function saveRatingHistoryToDB(pdgaNumber, ratingHistory) {
return new Promise((resolve, reject) => {
db.get('SELECT id FROM players WHERE pdga_number = ?', [pdgaNumber], (err, player) => {
if (err) return reject(err);
if (!player) return reject(new Error('Player not found'));
db.run('DELETE FROM rating_history WHERE player_id = ?', [player.id], (err) => {
if (err) return reject(err);
if (ratingHistory.length === 0) {
return resolve();
}
let completed = 0;
const total = ratingHistory.length;
ratingHistory.forEach(entry => {
const parsedDate = parseDate(entry.date);
db.run(
'INSERT INTO rating_history (player_id, date, rating) VALUES (?, ?, ?)',
[player.id, parsedDate.toISOString().split('T')[0], entry.rating],
(err) => {
if (err) return reject(err);
completed++;
if (completed === total) {
resolve();
}
}
);
});
});
});
});
}
function getRoundHistoryFromDB(pdgaNumber) {
return new Promise((resolve, reject) => {
db.get('SELECT id FROM players WHERE pdga_number = ?', [pdgaNumber], (err, player) => {
if (err) return reject(err);
if (!player) return resolve([]);
db.all(
'SELECT * FROM round_history WHERE player_id = ? ORDER BY date DESC',
[player.id],
(err, rows) => {
if (err) reject(err);
else resolve(rows);
}
);
});
});
}
function getLastRoundUpdateDate(pdgaNumber) {
return new Promise((resolve, reject) => {
db.get(
'SELECT last_round_update FROM players WHERE pdga_number = ?',
[pdgaNumber],
(err, row) => {
if (err) reject(err);
else resolve(row ? row.last_round_update : null);
}
);
});
}
function updateLastRoundUpdateDate(pdgaNumber) {
return new Promise((resolve, reject) => {
db.run(
'UPDATE players SET last_round_update = CURRENT_TIMESTAMP WHERE pdga_number = ?',
[pdgaNumber],
function(err) {
if (err) reject(err);
else resolve();
}
);
});
}
function saveRoundHistoryToDB(pdgaNumber, roundData, isIncremental = false) {
return new Promise((resolve, reject) => {
db.get('SELECT id FROM players WHERE pdga_number = ?', [pdgaNumber], (err, player) => {
if (err) return reject(err);
if (!player) return reject(new Error('Player not found'));
const processRounds = () => {
if (roundData.length === 0) {
db.run('UPDATE players SET last_round_update = datetime("now") WHERE pdga_number = ?', [pdgaNumber], (err) => {
if (err) reject(err);
else resolve();
});
return;
}
const stmt = db.prepare('INSERT OR REPLACE INTO round_history (player_id, date, competition_name, rating) VALUES (?, ?, ?, ?)');
for (const round of roundData) {
stmt.run([player.id, round.date.toISOString().split('T')[0], round.competition || 'Unknown', round.rating]);
}
stmt.finalize((err) => {
if (err) {
reject(err);
} else {
db.run('UPDATE players SET last_round_update = datetime("now") WHERE pdga_number = ?', [pdgaNumber], (updateErr) => {
if (updateErr) reject(updateErr);
else resolve();
});
}
});
};
if (!isIncremental) {
db.run('DELETE FROM round_history WHERE player_id = ?', [player.id], (err) => {
if (err) return reject(err);
processRounds();
});
} else {
processRounds();
}
});
});
}
function savePredictedRatingToDB(pdgaNumber, predictedRating, stdDev = null, excludedRoundsCount = null, cutoffRating = null) {
return new Promise((resolve, reject) => {
db.run(
'UPDATE players SET predicted_rating = ?, std_dev = ?, excluded_rounds_count = ?, cutoff_rating = ? WHERE pdga_number = ?',
[predictedRating, stdDev, excludedRoundsCount, cutoffRating, pdgaNumber],
function(err) {
if (err) reject(err);
else resolve();
}
);
});
}
/**
* Returns monthly rating snapshots for one player (latest entry per calendar month),
* ordered oldest → newest. At most `months` entries; [] if none.
*/
function getMonthlyHistory(pdgaNumber, months = 12) {
return new Promise((resolve, reject) => {
db.get('SELECT id FROM players WHERE pdga_number = ?', [pdgaNumber], (err, player) => {
if (err) return reject(err);
if (!player) return resolve([]);
db.all(
`SELECT rating
FROM rating_history
WHERE player_id = ?
AND date IN (
SELECT MAX(date)
FROM rating_history
WHERE player_id = ?
GROUP BY strftime('%Y-%m', date)
)
ORDER BY date DESC
LIMIT ?`,
[player.id, player.id, months],
(err, rows) => {
if (err) return reject(err);
resolve(rows.map(r => r.rating).reverse());
}
);
});
});
}
/**
* Fetches the last `months` monthly rating snapshots for ALL players in one query.
* Returns a Map<pdgaNumber, number[]> (oldest → newest per player).
* Use this in bulk-fetch paths to avoid N+1 queries.
*/
function getAllMonthlyHistoriesFromDB(months = 12) {
return new Promise((resolve, reject) => {
db.all(
`SELECT p.pdga_number, rh.date, rh.rating
FROM rating_history rh
JOIN players p ON rh.player_id = p.id
INNER JOIN (
SELECT player_id, MAX(date) AS max_date
FROM rating_history
GROUP BY player_id, strftime('%Y-%m', date)
) latest ON rh.player_id = latest.player_id AND rh.date = latest.max_date
ORDER BY p.pdga_number, rh.date ASC`,
[],
(err, rows) => {
if (err) return reject(err);
const map = new Map();
for (const row of rows) {
if (!map.has(row.pdga_number)) map.set(row.pdga_number, []);
map.get(row.pdga_number).push(row.rating);
}
// Trim each player's history to the requested window
for (const [key, arr] of map) {
if (arr.length > months) map.set(key, arr.slice(-months));
}
resolve(map);
}
);
});
}
/**
* Fetches the full rating history for ALL players in one query.
* Returns Map<pdgaNumber, {rating, date}[]> ordered chronologically (oldest → newest).
* Mirrors getAllMonthlyHistoriesFromDB but returns every point, not monthly snapshots.
*/
function getAllRatingHistoriesFromDB() {
return new Promise((resolve, reject) => {
db.all(
`SELECT p.pdga_number, rh.date, rh.rating
FROM rating_history rh
JOIN players p ON rh.player_id = p.id
ORDER BY p.pdga_number, rh.date ASC`,
[],
(err, rows) => {
if (err) return reject(err);
const map = new Map();
for (const row of rows) {
if (!map.has(row.pdga_number)) map.set(row.pdga_number, []);
map.get(row.pdga_number).push({ date: row.date, rating: row.rating });
}
resolve(map);
}
);
});
}
function getLastRefresh() {
return new Promise((resolve, reject) => {
db.get(
'SELECT MAX(last_updated) AS lastRefresh FROM players',
[],
(err, row) => {
if (err) reject(err);
else resolve(row ? row.lastRefresh : null);
}
);
});
}
module.exports = {
getPlayerFromDB,
savePlayerToDB,
getRatingHistoryFromDB,
saveRatingHistoryToDB,
getRoundHistoryFromDB,
getLastRoundUpdateDate,
updateLastRoundUpdateDate,
saveRoundHistoryToDB,
savePredictedRatingToDB,
getLastRefresh,
getMonthlyHistory,
getAllMonthlyHistoriesFromDB,
getAllRatingHistoriesFromDB
};