#!/usr/bin/env ts-node /** * Script to consolidate duplicate "Privée" calendars * - Identifies the calendar to keep (with active sync and most events) * - Migrates events from duplicate calendars to the one to keep * - Deletes duplicate calendars and their sync configs */ import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); async function consolidatePrivateCalendars() { try { console.log('🔍 Searching for duplicate "Privée" calendars...\n'); // Find all calendars named "Privée" const privateCalendars = await prisma.calendar.findMany({ where: { name: 'Privée', }, include: { syncConfig: { include: { mailCredential: true, }, }, _count: { select: { events: true, }, }, }, }); console.log(`Found ${privateCalendars.length} calendars named "Privée"\n`); if (privateCalendars.length <= 1) { console.log('✅ No duplicates found. Nothing to consolidate.\n'); return; } // Find the calendar to keep: // 1. Has active sync (syncEnabled = true) // 2. Has the most events // 3. Most recent const calendarsToSort = [...privateCalendars]; calendarsToSort.sort((a, b) => { // Priority 1: Active sync const aHasActiveSync = a.syncConfig?.syncEnabled === true; const bHasActiveSync = b.syncConfig?.syncEnabled === true; if (aHasActiveSync && !bHasActiveSync) return -1; if (!aHasActiveSync && bHasActiveSync) return 1; // Priority 2: Most events const aEventCount = a._count.events; const bEventCount = b._count.events; if (aEventCount !== bEventCount) { return bEventCount - aEventCount; // Descending } // Priority 3: Most recent return b.createdAt.getTime() - a.createdAt.getTime(); // Descending }); const calendarToKeep = calendarsToSort[0]; const calendarsToDelete = calendarsToSort.slice(1); console.log('📋 Calendar to KEEP:'); console.log(` ID: ${calendarToKeep.id}`); console.log(` Events: ${calendarToKeep._count.events}`); console.log(` Sync enabled: ${calendarToKeep.syncConfig?.syncEnabled || false}`); console.log(` Provider: ${calendarToKeep.syncConfig?.provider || 'none'}`); console.log(` Created: ${calendarToKeep.createdAt}\n`); console.log(`🗑️ Calendars to DELETE (${calendarsToDelete.length}):`); for (const cal of calendarsToDelete) { console.log(` ID: ${cal.id}`); console.log(` Events: ${cal._count.events}`); console.log(` Sync enabled: ${cal.syncConfig?.syncEnabled || false}`); console.log(` Created: ${cal.createdAt}\n`); } // Migrate events from calendars to delete to the calendar to keep let totalMigrated = 0; for (const calToDelete of calendarsToDelete) { const eventsToMigrate = await prisma.event.findMany({ where: { calendarId: calToDelete.id, }, }); console.log(`📦 Migrating ${eventsToMigrate.length} events from calendar ${calToDelete.id}...`); for (const event of eventsToMigrate) { // Check if event with same externalEventId already exists in target calendar const externalId = (event as any).externalEventId; if (externalId) { // Check if event with this externalEventId already exists in target calendar const existingEvent = await prisma.event.findFirst({ where: { calendarId: calendarToKeep.id, externalEventId: externalId, }, }); if (existingEvent) { console.log(` ⚠️ Skipping event "${event.title}" - already exists in target calendar (externalEventId: ${externalId.substring(0, 50)}...)`); // Delete the duplicate event await prisma.event.delete({ where: { id: event.id }, }); continue; } } else { // For events without externalEventId, check by title + date const existingEvent = await prisma.event.findFirst({ where: { calendarId: calendarToKeep.id, title: event.title, start: event.start, }, }); if (existingEvent) { console.log(` ⚠️ Skipping event "${event.title}" - already exists in target calendar (title + date match)`); // Delete the duplicate event await prisma.event.delete({ where: { id: event.id }, }); continue; } } // Migrate event to target calendar try { await prisma.event.update({ where: { id: event.id }, data: { calendarId: calendarToKeep.id, }, }); totalMigrated++; console.log(` ✅ Migrated: "${event.title}"`); } catch (error) { console.error(` ❌ Error migrating event "${event.title}":`, error); } } } console.log(`\n✅ Migrated ${totalMigrated} events to calendar ${calendarToKeep.id}\n`); // Delete duplicate calendars and their sync configs for (const calToDelete of calendarsToDelete) { console.log(`🗑️ Deleting calendar ${calToDelete.id}...`); // Delete sync config if exists if (calToDelete.syncConfig) { await prisma.calendarSync.delete({ where: { id: calToDelete.syncConfig.id }, }); console.log(` ✅ Deleted sync config ${calToDelete.syncConfig.id}`); } // Delete calendar (events are already migrated or deleted) await prisma.calendar.delete({ where: { id: calToDelete.id }, }); console.log(` ✅ Deleted calendar ${calToDelete.id}`); } console.log(`\n✅ Consolidation completed!`); console.log(` Kept: 1 calendar (${calendarToKeep.id})`); console.log(` Deleted: ${calendarsToDelete.length} duplicate calendars`); console.log(` Migrated: ${totalMigrated} events\n`); } catch (error) { console.error('❌ Error consolidating calendars:', error); throw error; } finally { await prisma.$disconnect(); } } // Run the script consolidatePrivateCalendars() .then(() => { console.log('✅ Script completed successfully'); process.exit(0); }) .catch((error) => { console.error('❌ Script failed:', error); process.exit(1); });