From 4159a2aadb3ee2184d42131c64d1828f38e674f0 Mon Sep 17 00:00:00 2001 From: richardtekula Date: Thu, 20 Nov 2025 08:05:27 +0100 Subject: [PATCH] Add script to fix duplicate contacts in database --- src/scripts/fix-duplicate-contacts.js | 90 +++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 src/scripts/fix-duplicate-contacts.js diff --git a/src/scripts/fix-duplicate-contacts.js b/src/scripts/fix-duplicate-contacts.js new file mode 100644 index 0000000..1bc9dc4 --- /dev/null +++ b/src/scripts/fix-duplicate-contacts.js @@ -0,0 +1,90 @@ +import { db } from '../config/database.js'; +import { contacts, emails } from '../db/schema.js'; +import { eq, and, sql } from 'drizzle-orm'; +import { logger } from '../utils/logger.js'; + +/** + * Fix duplicate contacts by merging them + * - Finds contacts with the same email address + * - Keeps the newest contact + * - Updates all emails to use the newest contact ID + * - Deletes old duplicate contacts + */ +async function fixDuplicateContacts() { + try { + logger.info('šŸ” Finding duplicate contacts...'); + + // Find duplicate contacts (same userId + email) + const duplicates = await db + .select({ + userId: contacts.userId, + email: contacts.email, + count: sql`count(*)::int`, + ids: sql`array_agg(${contacts.id} ORDER BY ${contacts.createdAt} DESC)`, + }) + .from(contacts) + .groupBy(contacts.userId, contacts.email) + .having(sql`count(*) > 1`); + + if (duplicates.length === 0) { + logger.success('āœ… No duplicate contacts found!'); + return; + } + + logger.info(`Found ${duplicates.length} sets of duplicate contacts`); + + let totalFixed = 0; + let totalDeleted = 0; + + for (const dup of duplicates) { + const contactIds = dup.ids; + const newestContactId = contactIds[0]; // First one (ordered by createdAt DESC) + const oldContactIds = contactIds.slice(1); // Rest are duplicates + + logger.info(`\nšŸ“§ Fixing duplicates for ${dup.email}:`); + logger.info(` - Keeping contact: ${newestContactId}`); + logger.info(` - Merging ${oldContactIds.length} duplicate(s): ${oldContactIds.join(', ')}`); + + // Update all emails from old contacts to use the newest contact ID + for (const oldContactId of oldContactIds) { + const updateResult = await db + .update(emails) + .set({ contactId: newestContactId, updatedAt: new Date() }) + .where(eq(emails.contactId, oldContactId)) + .returning(); + + if (updateResult.length > 0) { + logger.success(` āœ… Updated ${updateResult.length} emails from ${oldContactId} → ${newestContactId}`); + totalFixed += updateResult.length; + } + + // Delete the old duplicate contact + await db + .delete(contacts) + .where(eq(contacts.id, oldContactId)); + + logger.success(` šŸ—‘ļø Deleted duplicate contact: ${oldContactId}`); + totalDeleted++; + } + } + + logger.success(`\nāœ… Cleanup complete!`); + logger.success(` - Fixed ${totalFixed} emails`); + logger.success(` - Deleted ${totalDeleted} duplicate contacts`); + + } catch (error) { + logger.error('āŒ Error fixing duplicate contacts:', error); + throw error; + } +} + +// Run the script +fixDuplicateContacts() + .then(() => { + logger.success('šŸŽ‰ Script completed successfully!'); + process.exit(0); + }) + .catch((error) => { + logger.error('šŸ’„ Script failed:', error); + process.exit(1); + });