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 };
|
||||||
Executable
+74
@@ -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 <userId> <newName>
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 <userId> <newName>');
|
||||||
|
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 };
|
||||||
Reference in New Issue
Block a user