283 lines
8.0 KiB
JavaScript
283 lines
8.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) {
|
|
return new Promise((resolve, reject) => {
|
|
db.run(
|
|
'UPDATE players SET predicted_rating = ?, std_dev = ? WHERE pdga_number = ?',
|
|
[predictedRating, stdDev, 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);
|
|
}
|
|
);
|
|
});
|
|
}
|
|
|
|
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
|
|
};
|