From d051afba82b4e4f56807585edecefbda717bfc41 Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Tue, 31 Mar 2026 03:38:25 -0700 Subject: [PATCH] fix: update admin scripts to use PostgreSQL adapter and dotenv --- scripts/create-admin-better-auth.js | 8 +- scripts/create-admin-via-api.js | 24 ++- scripts/list-users.js | 8 +- scripts/merge-duplicate-players.js | 9 +- scripts/recalculate-elo.js | 283 +++++++++++++++++++++++++++- scripts/seed.js | 8 +- scripts/update-admin-password.js | 8 +- scripts/update-user-name.js | 9 +- 8 files changed, 347 insertions(+), 10 deletions(-) diff --git a/scripts/create-admin-better-auth.js b/scripts/create-admin-better-auth.js index 15b3d3a..6a13036 100644 --- a/scripts/create-admin-better-auth.js +++ b/scripts/create-admin-better-auth.js @@ -1,7 +1,13 @@ const { PrismaClient } = require('@prisma/client'); const bcrypt = require('bcrypt'); +const { PrismaPg } = require('@prisma/adapter-pg'); -const prisma = new PrismaClient(); +// Load environment variables from .env file +require('dotenv').config(); + +// Create PrismaClient with PostgreSQL adapter (matching src/lib/prisma.ts) +const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL }); +const prisma = new PrismaClient({ adapter }); async function main() { try { diff --git a/scripts/create-admin-via-api.js b/scripts/create-admin-via-api.js index ccf6ac4..e593dab 100644 --- a/scripts/create-admin-via-api.js +++ b/scripts/create-admin-via-api.js @@ -1,22 +1,42 @@ const { PrismaClient } = require('@prisma/client'); const { betterAuth } = require('better-auth'); const { prismaAdapter } = require('better-auth/adapters/prisma'); +const { PrismaPg } = require('@prisma/adapter-pg'); -const prisma = new PrismaClient(); +// Load environment variables from .env file +require('dotenv').config(); + +// Check if DATABASE_URL is set +if (!process.env.DATABASE_URL) { + console.error('❌ DATABASE_URL environment variable is not set'); + console.error(' Please set DATABASE_URL in your environment or .env file'); + console.error(' Example: DATABASE_URL="postgresql://user:password@localhost:5432/dbname"'); + process.exit(1); +} + +// Create PrismaClient with adapter (matching src/lib/prisma.ts) +const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL }); +const prisma = new PrismaClient({ adapter }); // Create Better Auth instance const auth = betterAuth({ database: prismaAdapter(prisma, { - provider: 'sqlite', + provider: 'postgresql', }), emailAndPassword: { enabled: true, }, baseURL: process.env.BETTER_AUTH_URL || 'http://localhost:3000', + secret: process.env.BETTER_AUTH_SECRET || process.env.NEXTAUTH_SECRET || 'your-secret-key-here-change-this-in-production', }); async function main() { try { + // Test database connection first + console.log('Connecting to database...'); + await prisma.$connect(); + console.log('✅ Connected to database'); + // Check if user exists const existing = await prisma.user.findUnique({ where: { email: 'david@dhg.lol' } diff --git a/scripts/list-users.js b/scripts/list-users.js index ef8851f..148fd7a 100644 --- a/scripts/list-users.js +++ b/scripts/list-users.js @@ -1,6 +1,12 @@ const { PrismaClient } = require('@prisma/client'); +const { PrismaPg } = require('@prisma/adapter-pg'); -const prisma = new PrismaClient(); +// Load environment variables from .env file +require('dotenv').config(); + +// Create PrismaClient with PostgreSQL adapter (matching src/lib/prisma.ts) +const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL }); +const prisma = new PrismaClient({ adapter }); async function listUsers() { try { diff --git a/scripts/merge-duplicate-players.js b/scripts/merge-duplicate-players.js index bbebe3a..b91ee22 100644 --- a/scripts/merge-duplicate-players.js +++ b/scripts/merge-duplicate-players.js @@ -7,7 +7,14 @@ */ const { PrismaClient } = require('@prisma/client'); -const prisma = new PrismaClient(); +const { PrismaPg } = require('@prisma/adapter-pg'); + +// Load environment variables from .env file +require('dotenv').config(); + +// Create PrismaClient with PostgreSQL adapter (matching src/lib/prisma.ts) +const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL }); +const prisma = new PrismaClient({ adapter }); // Pattern to extract base name from "Name-Timestamp-RandomString" format const NAME_PATTERN = /^(.+?)-(\d{13})-([a-z0-9]{6})$/; diff --git a/scripts/recalculate-elo.js b/scripts/recalculate-elo.js index 8923f3e..f53a985 100644 --- a/scripts/recalculate-elo.js +++ b/scripts/recalculate-elo.js @@ -8,9 +8,288 @@ */ const { PrismaClient } = require('@prisma/client'); -const { recalculateAllElo } = require('../src/lib/elo-utils'); +const { PrismaPg } = require('@prisma/adapter-pg'); -const prisma = new PrismaClient(); +// Load environment variables from .env file +require('dotenv').config(); + +// Create PrismaClient with PostgreSQL adapter (matching src/lib/prisma.ts) +const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL }); +const prisma = new PrismaClient({ adapter }); + +// Simple Elo calculation functions (copied from src/lib/elo-utils.ts) +function calculateTeamElo(rating1, rating2) { + return (rating1 + rating2) / 2; +} + +function calculateExpectedScore(ratingA, ratingB) { + const ratingDiff = ratingA - ratingB; + return 1 / (1 + Math.pow(10, -ratingDiff / 400)); +} + +function calculateEloChange(rating, opponentRating, actualScore, kFactor = 32) { + const expectedScore = calculateExpectedScore(rating, opponentRating); + return kFactor * (actualScore - expectedScore); +} + +function calculateTeamEloChange(team1Rating, team2Rating, team1Score, kFactor = 32) { + return calculateEloChange(team1Rating, team2Rating, team1Score, kFactor); +} + +// Recalculate all Elo ratings +async function recalculateAllElo(prisma) { + // Get all matches in chronological order + const matches = await prisma.match.findMany({ + orderBy: { playedAt: 'asc' }, + include: { + team1P1: true, + team1P2: true, + team2P1: true, + team2P2: true, + }, + }); + + // Reset all player stats to zero + await prisma.player.updateMany({ + data: { + currentElo: 1000, + gamesPlayed: 0, + wins: 0, + losses: 0, + }, + }); + + // Delete all existing elo snapshots + await prisma.eloSnapshot.deleteMany({}); + + // Delete all existing partnership stats (will be rebuilt) + await prisma.partnershipStat.deleteMany({}); + + // Initialize in-memory tracking for all players + const playerStats = new Map(); + + // Initialize partnership tracking + const partnershipStats = new Map(); + + // Helper to get/create player stats + const getPlayerStats = (playerId) => { + if (!playerStats.has(playerId)) { + playerStats.set(playerId, { rating: 1000, gamesPlayed: 0, wins: 0, losses: 0 }); + } + return playerStats.get(playerId); + }; + + // Helper to get partnership key (sorted player IDs) + const getPartnershipKey = (p1Id, p2Id) => { + return [Math.min(p1Id, p2Id), Math.max(p1Id, p2Id)].join('-'); + }; + + // Helper to get/update partnership stats + const getPartnershipStats = (p1Id, p2Id) => { + const key = getPartnershipKey(p1Id, p2Id); + if (!partnershipStats.has(key)) { + partnershipStats.set(key, { gamesPlayed: 0, wins: 0, losses: 0, totalEloChange: 0, lastPlayed: null }); + } + return partnershipStats.get(key); + }; + + // Process each match in chronological order + for (const match of matches) { + const { team1P1, team1P2, team2P1, team2P2, team1Score, team2Score, id: matchId, playedAt } = match; + + // Get current ratings for all players + const p1Rating = getPlayerStats(team1P1.id).rating; + const p2Rating = getPlayerStats(team1P2.id).rating; + const p3Rating = getPlayerStats(team2P1.id).rating; + const p4Rating = getPlayerStats(team2P2.id).rating; + + // Calculate team ratings + const team1Rating = calculateTeamElo(p1Rating, p2Rating); + const team2Rating = calculateTeamElo(p3Rating, p4Rating); + + // Determine match result (team1 score based on points) + const team1ScoreNormalized = team1Score > team2Score ? 1 : team1Score === team2Score ? 0.5 : 0; + + // Calculate team ELO changes + const team1Change = calculateTeamEloChange(team1Rating, team2Rating, team1ScoreNormalized); + const team2Change = -team1Change; // Equal and opposite + + // Split changes evenly between partners + const p1Change = Math.round(team1Change / 2); + const p2Change = Math.round(team1Change / 2); + const p3Change = Math.round(team2Change / 2); + const p4Change = Math.round(team2Change / 2); + + // Update player stats + const team1Won = team1Score > team2Score; + const team2Won = team2Score > team1Score; + const isTie = team1Score === team2Score; + + // Update Player 1 (team 1, player 1) + const stats1 = getPlayerStats(team1P1.id); + stats1.rating += p1Change; + stats1.gamesPlayed += 1; + if (isTie) { + // For ties, don't increment wins or losses (gamesPlayed is already incremented) + } else if (team1Won) { + stats1.wins += 1; + } else { + stats1.losses += 1; + } + + // Update Player 2 (team 1, player 2) + const stats2 = getPlayerStats(team1P2.id); + stats2.rating += p2Change; + stats2.gamesPlayed += 1; + if (isTie) { + // For ties, don't increment wins or losses (gamesPlayed is already incremented) + } else if (team1Won) { + stats2.wins += 1; + } else { + stats2.losses += 1; + } + + // Update Player 3 (team 2, player 1) + const stats3 = getPlayerStats(team2P1.id); + stats3.rating += p3Change; + stats3.gamesPlayed += 1; + if (isTie) { + // For ties, don't increment wins or losses (gamesPlayed is already incremented) + } else if (team2Won) { + stats3.wins += 1; + } else { + stats3.losses += 1; + } + + // Update Player 4 (team 2, player 2) + const stats4 = getPlayerStats(team2P2.id); + stats4.rating += p4Change; + stats4.gamesPlayed += 1; + if (isTie) { + // For ties, don't increment wins or losses (gamesPlayed is already incremented) + } else if (team2Won) { + stats4.wins += 1; + } else { + stats4.losses += 1; + } + + // Update partnership stats for team 1 + const partnership1 = getPartnershipStats(team1P1.id, team1P2.id); + partnership1.gamesPlayed += 1; + if (isTie) { + // For ties, don't increment wins or losses (gamesPlayed is already incremented) + } else if (team1Won) { + partnership1.wins += 1; + } else { + partnership1.losses += 1; + } + partnership1.totalEloChange += p1Change + p2Change; + if (playedAt && (!partnership1.lastPlayed || playedAt > partnership1.lastPlayed)) { + partnership1.lastPlayed = playedAt; + } + + // Update partnership stats for team 2 + const partnership2 = getPartnershipStats(team2P1.id, team2P2.id); + partnership2.gamesPlayed += 1; + if (isTie) { + // For ties, don't increment wins or losses (gamesPlayed is already incremented) + } else if (team2Won) { + partnership2.wins += 1; + } else { + partnership2.losses += 1; + } + partnership2.totalEloChange += p3Change + p4Change; + if (playedAt && (!partnership2.lastPlayed || playedAt > partnership2.lastPlayed)) { + partnership2.lastPlayed = playedAt; + } + + // Create elo snapshots for all players + const snapshotData = [ + { playerId: team1P1.id, ratingBefore: p1Rating, ratingChange: p1Change }, + { playerId: team1P2.id, ratingBefore: p2Rating, ratingChange: p2Change }, + { playerId: team2P1.id, ratingBefore: p3Rating, ratingChange: p3Change }, + { playerId: team2P2.id, ratingBefore: p4Rating, ratingChange: p4Change }, + ]; + + for (const snapshot of snapshotData) { + await prisma.eloSnapshot.create({ + data: { + playerId: snapshot.playerId, + matchId: matchId, + ratingBefore: snapshot.ratingBefore, + ratingAfter: snapshot.ratingBefore + snapshot.ratingChange, + ratingChange: snapshot.ratingChange, + }, + }); + } + } + + // Write all updated player stats to database + const playerUpdatePromises = Array.from(playerStats.entries()).map( + async ([playerId, stats]) => + await prisma.player.update({ + where: { id: playerId }, + data: { + currentElo: stats.rating, + gamesPlayed: stats.gamesPlayed, + wins: stats.wins, + losses: stats.losses, + }, + }) + ); + + await Promise.all(playerUpdatePromises); + + // Write all partnership stats to database + const partnershipUpdatePromises = Array.from(partnershipStats.entries()).map(async ([key, stats]) => { + const [player1Id, player2Id] = key.split('-').map(Number); + + // Check if the partnership stat already exists + const existingStat = await prisma.partnershipStat.findFirst({ + where: { + player1Id, + player2Id, + }, + }); + + if (existingStat) { + // Update existing record + return await prisma.partnershipStat.update({ + where: { + id: existingStat.id, + }, + data: { + gamesPlayed: stats.gamesPlayed, + wins: stats.wins, + losses: stats.losses, + totalEloChange: stats.totalEloChange, + lastPlayed: stats.lastPlayed, + }, + }); + } else { + // Create new record + return await prisma.partnershipStat.create({ + data: { + player1Id, + player2Id, + gamesPlayed: stats.gamesPlayed, + wins: stats.wins, + losses: stats.losses, + totalEloChange: stats.totalEloChange, + lastPlayed: stats.lastPlayed, + }, + }); + } + }); + + await Promise.all(partnershipUpdatePromises); + + return { + matchesProcessed: matches.length, + playersUpdated: playerStats.size, + partnershipsUpdated: partnershipStats.size, + }; +} async function main() { console.log('Starting ELO recalculation...'); diff --git a/scripts/seed.js b/scripts/seed.js index 4c0948d..455cbdb 100755 --- a/scripts/seed.js +++ b/scripts/seed.js @@ -7,8 +7,14 @@ const { PrismaClient } = require('@prisma/client'); const bcrypt = require('bcrypt'); +const { PrismaPg } = require('@prisma/adapter-pg'); -const prisma = new PrismaClient(); +// Load environment variables from .env file +require('dotenv').config(); + +// Create PrismaClient with PostgreSQL adapter (matching src/lib/prisma.ts) +const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL }); +const prisma = new PrismaClient({ adapter }); async function main() { console.log('🌱 Seeding database...\n'); diff --git a/scripts/update-admin-password.js b/scripts/update-admin-password.js index 34a9efe..7f66b01 100644 --- a/scripts/update-admin-password.js +++ b/scripts/update-admin-password.js @@ -1,7 +1,13 @@ const { PrismaClient } = require('@prisma/client'); const bcrypt = require('bcryptjs'); +const { PrismaPg } = require('@prisma/adapter-pg'); -const prisma = new PrismaClient(); +// Load environment variables from .env file +require('dotenv').config(); + +// Create PrismaClient with PostgreSQL adapter (matching src/lib/prisma.ts) +const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL }); +const prisma = new PrismaClient({ adapter }); async function main() { try { diff --git a/scripts/update-user-name.js b/scripts/update-user-name.js index 2dc1fe2..a970f23 100755 --- a/scripts/update-user-name.js +++ b/scripts/update-user-name.js @@ -6,7 +6,14 @@ */ const { PrismaClient } = require('@prisma/client'); -const prisma = new PrismaClient(); +const { PrismaPg } = require('@prisma/adapter-pg'); + +// Load environment variables from .env file +require('dotenv').config(); + +// Create PrismaClient with PostgreSQL adapter (matching src/lib/prisma.ts) +const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL }); +const prisma = new PrismaClient({ adapter }); async function updateUserName(userId, newName) { if (!userId || !newName) {