NeahStable/scripts/cleanup-duplicate-calendars.ts
2026-01-15 14:04:38 +01:00

123 lines
3.7 KiB
TypeScript

#!/usr/bin/env ts-node
/**
* Script to clean up duplicate calendars created from failed Microsoft account installations
* Removes calendars that have no events and no active sync config
*/
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function cleanupDuplicateCalendars() {
try {
console.log('🔍 Searching for duplicate calendars...\n');
// Find all calendars named "Privée" with Microsoft sync
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`);
// Group by email (from mailCredential)
const calendarsByEmail = new Map<string, typeof privateCalendars>();
for (const calendar of privateCalendars) {
const email = calendar.syncConfig?.mailCredential?.email || 'unknown';
if (!calendarsByEmail.has(email)) {
calendarsByEmail.set(email, []);
}
calendarsByEmail.get(email)!.push(calendar);
}
let totalDeleted = 0;
let totalKept = 0;
for (const [email, calendars] of calendarsByEmail.entries()) {
if (calendars.length <= 1) {
continue; // No duplicates for this email
}
console.log(`\n📧 Processing email: ${email} (${calendars.length} calendars)`);
// Sort calendars by:
// 1. Has events (prefer calendars with events)
// 2. Has active sync (prefer calendars with active sync)
// 3. Created date (prefer newest)
calendars.sort((a, b) => {
// First: prefer calendars with events
if (a._count.events !== b._count.events) {
return b._count.events - a._count.events;
}
// Second: prefer calendars with active sync
const aHasActiveSync = a.syncConfig?.syncEnabled === true;
const bHasActiveSync = b.syncConfig?.syncEnabled === true;
if (aHasActiveSync !== bHasActiveSync) {
return bHasActiveSync ? 1 : -1;
}
// Third: prefer newest
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
});
const toKeep = calendars[0];
const toDelete = calendars.slice(1);
console.log(` ✅ Keeping: ${toKeep.id}`);
console.log(` Events: ${toKeep._count.events}`);
console.log(` Sync enabled: ${toKeep.syncConfig?.syncEnabled || false}`);
console.log(` Created: ${toKeep.createdAt.toISOString()}`);
for (const calendar of toDelete) {
console.log(` 🗑️ Deleting: ${calendar.id}`);
console.log(` Events: ${calendar._count.events}`);
console.log(` Sync enabled: ${calendar.syncConfig?.syncEnabled || false}`);
console.log(` Created: ${calendar.createdAt.toISOString()}`);
// Delete sync config first (if exists)
if (calendar.syncConfig) {
await prisma.calendarSync.delete({
where: { id: calendar.syncConfig.id },
});
}
// Delete calendar (events will be cascade deleted)
await prisma.calendar.delete({
where: { id: calendar.id },
});
totalDeleted++;
}
totalKept++;
}
console.log(`\n✅ Cleanup completed!`);
console.log(` Kept: ${totalKept} calendars`);
console.log(` Deleted: ${totalDeleted} duplicate calendars`);
} catch (error) {
console.error('❌ Error cleaning up duplicate calendars:', error);
throw error;
} finally {
await prisma.$disconnect();
}
}
cleanupDuplicateCalendars();