feat: add scripts for player deduplication and user management
This commit is contained in:
@@ -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 };
|
||||
Reference in New Issue
Block a user