diff --git a/scripts/merge-duplicate-players.js b/scripts/merge-duplicate-players.js new file mode 100644 index 0000000..bbebe3a --- /dev/null +++ b/scripts/merge-duplicate-players.js @@ -0,0 +1,314 @@ +#!/usr/bin/env node + +/** + * Script to merge duplicate players with timestamp suffixes + * This script identifies players with the same base name (e.g., "Emily") + * and merges them into a single player with combined stats. + */ + +const { PrismaClient } = require('@prisma/client'); +const prisma = new PrismaClient(); + +// Pattern to extract base name from "Name-Timestamp-RandomString" format +const NAME_PATTERN = /^(.+?)-(\d{13})-([a-z0-9]{6})$/; + +function extractBaseName(name) { + const match = name.match(NAME_PATTERN); + if (match) { + return match[1].trim(); // Return the base name (everything before the timestamp) + } + return name.trim(); // Return as-is if no pattern matches +} + +async function mergeDuplicatePlayers() { + console.log('šŸ” Scanning for duplicate players...\n'); + + // Get all players + const allPlayers = await prisma.player.findMany({ + orderBy: { id: 'asc' }, + }); + + // Group players by base name + const playersByBaseName = new Map(); + + for (const player of allPlayers) { + const baseName = extractBaseName(player.name); + if (!playersByBaseName.has(baseName)) { + playersByBaseName.set(baseName, []); + } + playersByBaseName.get(baseName).push(player); + } + + // Find groups with duplicates (more than 1 player per base name) + const duplicateGroups = Array.from(playersByBaseName.entries()) + .filter(([_, players]) => players.length > 1); + + console.log(`Found ${duplicateGroups.length} groups of duplicate players\n`); + + if (duplicateGroups.length === 0) { + console.log('āœ… No duplicate players found!'); + return; + } + + // Process each group + for (const [baseName, players] of duplicateGroups) { + console.log(`\nšŸ“Š Processing group: "${baseName}" (${players.length} players)`); + + // Sort players by ID (earliest created is the canonical player) + const [canonicalPlayer, ...duplicatePlayers] = players.sort((a, b) => a.id - b.id); + + console.log(` Canonical player: ${canonicalPlayer.name} (ID: ${canonicalPlayer.id})`); + + // Merge stats from duplicates into canonical player + let totalGamesPlayed = canonicalPlayer.gamesPlayed; + let totalWins = canonicalPlayer.wins; + let totalLosses = canonicalPlayer.losses; + let totalEloChange = 0; // We'll calculate this from Elo snapshots + + for (const dupPlayer of duplicatePlayers) { + console.log(` Merging duplicate: ${dupPlayer.name} (ID: ${dupPlayer.id})`); + totalGamesPlayed += dupPlayer.gamesPlayed; + totalWins += dupPlayer.wins; + totalLosses += dupPlayer.losses; + } + + // Update canonical player with merged stats + await prisma.player.update({ + where: { id: canonicalPlayer.id }, + data: { + gamesPlayed: totalGamesPlayed, + wins: totalWins, + losses: totalLosses, + // Keep the original name (without suffix) for display + name: baseName, + normalizedName: baseName.toLowerCase(), + }, + }); + + console.log(` Updated canonical player with merged stats:`); + console.log(` Games: ${totalGamesPlayed}, Wins: ${totalWins}, Losses: ${totalLosses}`); + + // Update all foreign key references to point to canonical player + const updates = []; + + // Update matches + updates.push( + prisma.match.updateMany({ + where: { team1P1Id: { in: duplicatePlayers.map(p => p.id) } }, + data: { team1P1Id: canonicalPlayer.id }, + }), + prisma.match.updateMany({ + where: { team1P2Id: { in: duplicatePlayers.map(p => p.id) } }, + data: { team1P2Id: canonicalPlayer.id }, + }), + prisma.match.updateMany({ + where: { team2P1Id: { in: duplicatePlayers.map(p => p.id) } }, + data: { team2P1Id: canonicalPlayer.id }, + }), + prisma.match.updateMany({ + where: { team2P2Id: { in: duplicatePlayers.map(p => p.id) } }, + data: { team2P2Id: canonicalPlayer.id }, + }) + ); + + // Update event participants - need to handle unique constraint + // First, check for conflicts and delete duplicates + for (const dupPlayer of duplicatePlayers) { + const dupParticipants = await prisma.eventParticipant.findMany({ + where: { playerId: dupPlayer.id }, + }); + + for (const dupParticipant of dupParticipants) { + // Check if canonical player already participates in this event + const existingParticipant = await prisma.eventParticipant.findFirst({ + where: { + eventId: dupParticipant.eventId, + playerId: canonicalPlayer.id, + }, + }); + + if (existingParticipant) { + // Delete the duplicate participant + await prisma.eventParticipant.delete({ + where: { id: dupParticipant.id }, + }); + } else { + // Update the participant to use canonical player + await prisma.eventParticipant.update({ + where: { id: dupParticipant.id }, + data: { playerId: canonicalPlayer.id }, + }); + } + } + } + + // Update teams - need to handle unique constraint on (player1Id, player2Id) + for (const dupPlayer of duplicatePlayers) { + // Update teams where dupPlayer is player1 + const teamsAsPlayer1 = await prisma.team.findMany({ + where: { player1Id: dupPlayer.id }, + }); + + for (const team of teamsAsPlayer1) { + // Check if a team already exists with canonical player as player1 and same player2 + const existingTeam = await prisma.team.findFirst({ + where: { + eventId: team.eventId, + player1Id: canonicalPlayer.id, + player2Id: team.player2Id, + }, + }); + + if (existingTeam) { + // Delete the duplicate team + await prisma.team.delete({ + where: { id: team.id }, + }); + } else { + // Update the team to use canonical player + await prisma.team.update({ + where: { id: team.id }, + data: { player1Id: canonicalPlayer.id }, + }); + } + } + + // Update teams where dupPlayer is player2 + const teamsAsPlayer2 = await prisma.team.findMany({ + where: { player2Id: dupPlayer.id }, + }); + + for (const team of teamsAsPlayer2) { + // Check if a team already exists with canonical player as player2 and same player1 + const existingTeam = await prisma.team.findFirst({ + where: { + eventId: team.eventId, + player1Id: team.player1Id, + player2Id: canonicalPlayer.id, + }, + }); + + if (existingTeam) { + // Delete the duplicate team + await prisma.team.delete({ + where: { id: team.id }, + }); + } else { + // Update the team to use canonical player + await prisma.team.update({ + where: { id: team.id }, + data: { player2Id: canonicalPlayer.id }, + }); + } + } + } + + // Update partnership games + updates.push( + prisma.partnershipGame.updateMany({ + where: { player1Id: { in: duplicatePlayers.map(p => p.id) } }, + data: { player1Id: canonicalPlayer.id }, + }), + prisma.partnershipGame.updateMany({ + where: { player2Id: { in: duplicatePlayers.map(p => p.id) } }, + data: { player2Id: canonicalPlayer.id }, + }) + ); + + // Update partnership stats - this is more complex as we need to merge them + for (const dupPlayer of duplicatePlayers) { + // Find partnerships involving this duplicate player + const partnerships = await prisma.partnershipStat.findMany({ + where: { + OR: [ + { player1Id: dupPlayer.id }, + { player2Id: dupPlayer.id }, + ], + }, + }); + + for (const partnership of partnerships) { + const otherPlayerId = partnership.player1Id === dupPlayer.id + ? partnership.player2Id + : partnership.player1Id; + + // Check if a partnership already exists between canonical and other player + const existingPartnership = await prisma.partnershipStat.findFirst({ + where: { + OR: [ + { + player1Id: canonicalPlayer.id, + player2Id: otherPlayerId + }, + { + player1Id: otherPlayerId, + player2Id: canonicalPlayer.id + }, + ], + }, + }); + + if (existingPartnership) { + // Merge stats into existing partnership and delete the old one + await prisma.partnershipStat.update({ + where: { id: existingPartnership.id }, + data: { + gamesPlayed: { increment: partnership.gamesPlayed }, + wins: { increment: partnership.wins }, + losses: { increment: partnership.losses }, + totalEloChange: { increment: partnership.totalEloChange }, + lastPlayed: partnership.lastPlayed > existingPartnership.lastPlayed + ? partnership.lastPlayed + : existingPartnership.lastPlayed, + }, + }); + // Delete the duplicate partnership + await prisma.partnershipStat.delete({ + where: { id: partnership.id }, + }); + } else { + // Update partnership to use canonical player + const isPlayer1 = partnership.player1Id === dupPlayer.id; + await prisma.partnershipStat.update({ + where: { id: partnership.id }, + data: isPlayer1 + ? { player1Id: canonicalPlayer.id } + : { player2Id: canonicalPlayer.id }, + }); + } + } + } + + // Execute all updates + await Promise.all(updates); + console.log(` Updated foreign key references`); + + // Delete duplicate players + for (const dupPlayer of duplicatePlayers) { + await prisma.player.delete({ + where: { id: dupPlayer.id }, + }); + } + console.log(` Deleted ${duplicatePlayers.length} duplicate players`); + } + + console.log('\nāœ… Merge complete!'); +} + +async function main() { + try { + await mergeDuplicatePlayers(); + } catch (error) { + console.error('āŒ Error merging players:', error); + process.exit(1); + } finally { + await prisma.$disconnect(); + } +} + +// Run if called directly +if (require.main === module) { + main(); +} + +module.exports = { mergeDuplicatePlayers }; diff --git a/scripts/update-user-name.js b/scripts/update-user-name.js new file mode 100755 index 0000000..2dc1fe2 --- /dev/null +++ b/scripts/update-user-name.js @@ -0,0 +1,74 @@ +#!/usr/bin/env node + +/** + * Script to update a user's name (for superuser/admin use) + * Usage: node scripts/update-user-name.js + */ + +const { PrismaClient } = require('@prisma/client'); +const prisma = new PrismaClient(); + +async function updateUserName(userId, newName) { + if (!userId || !newName) { + console.error('Usage: node scripts/update-user-name.js '); + console.error('Example: node scripts/update-user-name.js "user-123" "John Doe"'); + process.exit(1); + } + + try { + // Check if user exists + const user = await prisma.user.findUnique({ + where: { id: userId }, + include: { player: true }, + }); + + if (!user) { + console.error(`āŒ User with ID "${userId}" not found`); + process.exit(1); + } + + console.log(`Found user: ${user.name} (${user.email})`); + + // Update user name + const updatedUser = await prisma.user.update({ + where: { id: userId }, + data: { + name: newName, + ...(user.player && { + player: { + update: { + name: newName, + normalizedName: newName.toLowerCase(), + }, + }, + }), + }, + include: { player: true }, + }); + + console.log(`āœ… Successfully updated user name to "${newName}"`); + console.log(` User ID: ${updatedUser.id}`); + console.log(` Email: ${updatedUser.email}`); + console.log(` New Name: ${updatedUser.name}`); + + if (updatedUser.player) { + console.log(` Player Name: ${updatedUser.player.name}`); + } + + return updatedUser; + } catch (error) { + console.error('āŒ Error updating user name:', error.message); + process.exit(1); + } finally { + await prisma.$disconnect(); + } +} + +// Run if called directly +if (require.main === module) { + const userId = process.argv[2]; + const newName = process.argv[3]; + updateUserName(userId, newName); +} + +module.exports = { updateUserName };