#!/usr/bin/env ts-node /** * Script to clean up duplicate events in the database * Keeps events with externalEventId, removes duplicates without externalEventId */ import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); interface DuplicateGroup { title: string; start: Date; end: Date; calendarId: string; events: Array<{ id: string; externalEventId: string | null; }>; } async function cleanupDuplicateEvents() { try { console.log('šŸ” Searching for duplicate events...\n'); // Find all events grouped by title, start, end, and calendarId const allEvents = await prisma.event.findMany({ select: { id: true, title: true, start: true, end: true, calendarId: true, externalEventId: true, }, orderBy: [ { calendarId: 'asc' }, { title: 'asc' }, { start: 'asc' }, ], }); // Group events by title + start + end + calendarId const eventGroups = new Map(); for (const event of allEvents) { const key = `${event.calendarId}|${event.title}|${event.start.toISOString()}|${event.end.toISOString()}`; if (!eventGroups.has(key)) { eventGroups.set(key, { title: event.title, start: event.start, end: event.end, calendarId: event.calendarId, events: [], }); } eventGroups.get(key)!.events.push({ id: event.id, externalEventId: event.externalEventId, }); } // Find duplicates (groups with more than 1 event) const duplicates: DuplicateGroup[] = []; for (const group of eventGroups.values()) { if (group.events.length > 1) { duplicates.push(group); } } console.log(`Found ${duplicates.length} groups of duplicate events\n`); if (duplicates.length === 0) { console.log('āœ… No duplicates found!'); return; } let totalDeleted = 0; let totalKept = 0; for (const group of duplicates) { console.log(`\nšŸ“‹ Processing: "${group.title}" (${group.events.length} duplicates)`); console.log(` Calendar: ${group.calendarId}`); console.log(` Date: ${group.start.toISOString()}`); // Separate events with and without externalEventId const withExternalId = group.events.filter(e => e.externalEventId); const withoutExternalId = group.events.filter(e => !e.externalEventId); if (withExternalId.length > 1) { // Multiple events with externalEventId - keep the first one, delete others console.log(` āš ļø Warning: Multiple events with externalEventId found!`); const toKeep = withExternalId[0]; const toDelete = [...withExternalId.slice(1), ...withoutExternalId]; console.log(` āœ… Keeping: ${toKeep.id} (has externalEventId)`); for (const event of toDelete) { console.log(` šŸ—‘ļø Deleting: ${event.id}`); await prisma.event.delete({ where: { id: event.id } }); totalDeleted++; } totalKept++; } else if (withExternalId.length === 1) { // One event with externalEventId - keep it, delete others const toKeep = withExternalId[0]; const toDelete = withoutExternalId; console.log(` āœ… Keeping: ${toKeep.id} (has externalEventId)`); for (const event of toDelete) { console.log(` šŸ—‘ļø Deleting: ${event.id}`); await prisma.event.delete({ where: { id: event.id } }); totalDeleted++; } totalKept++; } else { // No events with externalEventId - keep the first one, delete others const toKeep = group.events[0]; const toDelete = group.events.slice(1); console.log(` āœ… Keeping: ${toKeep.id} (no externalEventId, keeping first)`); for (const event of toDelete) { console.log(` šŸ—‘ļø Deleting: ${event.id}`); await prisma.event.delete({ where: { id: event.id } }); totalDeleted++; } totalKept++; } } console.log(`\nāœ… Cleanup completed!`); console.log(` Kept: ${totalKept} events`); console.log(` Deleted: ${totalDeleted} duplicate events`); } catch (error) { console.error('āŒ Error cleaning up duplicates:', error); throw error; } finally { await prisma.$disconnect(); } } cleanupDuplicateEvents();