236 lines
8.7 KiB
TypeScript
236 lines
8.7 KiB
TypeScript
#!/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,
|
|
isAllDay: true,
|
|
},
|
|
orderBy: [
|
|
{ calendarId: 'asc' },
|
|
{ title: 'asc' },
|
|
{ start: 'asc' },
|
|
],
|
|
});
|
|
|
|
// Group events by title + start + end + calendarId
|
|
// For all-day events, compare by date only (YYYY-MM-DD), not exact time
|
|
const eventGroups = new Map<string, DuplicateGroup>();
|
|
|
|
for (const event of allEvents) {
|
|
// Normalize dates to compare: for all-day events, use date only
|
|
// For timed events, round to nearest minute to handle small time differences
|
|
const startDate = new Date(event.start);
|
|
const endDate = new Date(event.end);
|
|
|
|
// Check if it's an all-day event (use isAllDay field or detect by duration)
|
|
const duration = endDate.getTime() - startDate.getTime();
|
|
const isAllDay = event.isAllDay ||
|
|
(duration >= 23 * 60 * 60 * 1000 && duration <= 25 * 60 * 60 * 1000); // 23-25 hours
|
|
|
|
let startKey: string;
|
|
let endKey: string;
|
|
|
|
if (isAllDay) {
|
|
// For all-day events, compare by date only (YYYY-MM-DD)
|
|
// Use UTC date to avoid timezone issues
|
|
const startUTC = new Date(Date.UTC(
|
|
startDate.getUTCFullYear(),
|
|
startDate.getUTCMonth(),
|
|
startDate.getUTCDate()
|
|
));
|
|
const endUTC = new Date(Date.UTC(
|
|
endDate.getUTCFullYear(),
|
|
endDate.getUTCMonth(),
|
|
endDate.getUTCDate()
|
|
));
|
|
startKey = `${startUTC.getUTCFullYear()}-${String(startUTC.getUTCMonth() + 1).padStart(2, '0')}-${String(startUTC.getUTCDate()).padStart(2, '0')}`;
|
|
endKey = `${endUTC.getUTCFullYear()}-${String(endUTC.getUTCMonth() + 1).padStart(2, '0')}-${String(endUTC.getUTCDate()).padStart(2, '0')}`;
|
|
} else {
|
|
// For timed events, round to nearest minute to handle small differences
|
|
const startRounded = new Date(startDate);
|
|
startRounded.setSeconds(0, 0);
|
|
const endRounded = new Date(endDate);
|
|
endRounded.setSeconds(0, 0);
|
|
startKey = startRounded.toISOString();
|
|
endKey = endRounded.toISOString();
|
|
}
|
|
|
|
const key = `${event.calendarId}|${event.title}|${startKey}|${endKey}`;
|
|
|
|
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`);
|
|
|
|
// Debug: Show all events for specific titles to understand grouping
|
|
const debugTitles = ['Vendredi saint', 'Dimanche de Pâques'];
|
|
for (const title of debugTitles) {
|
|
const titleEvents = allEvents.filter(e => e.title === title);
|
|
if (titleEvents.length > 0) {
|
|
console.log(`\n🔍 Debug: Events for "${title}":`);
|
|
for (const evt of titleEvents) {
|
|
const startDate = new Date(evt.start);
|
|
const endDate = new Date(evt.end);
|
|
const duration = endDate.getTime() - startDate.getTime();
|
|
const isAllDay = evt.isAllDay || (duration >= 23 * 60 * 60 * 1000 && duration <= 25 * 60 * 60 * 1000);
|
|
console.log(` - ID: ${evt.id}`);
|
|
console.log(` Calendar: ${evt.calendarId}`);
|
|
console.log(` Start: ${evt.start.toISOString()} (${startDate.toLocaleDateString()})`);
|
|
console.log(` End: ${evt.end.toISOString()} (${endDate.toLocaleDateString()})`);
|
|
console.log(` Duration: ${duration / (60 * 60 * 1000)} hours`);
|
|
console.log(` isAllDay: ${evt.isAllDay}, detected: ${isAllDay}`);
|
|
console.log(` externalEventId: ${evt.externalEventId ? 'YES' : 'NO'}`);
|
|
|
|
// Show what key would be generated
|
|
let startKey: string;
|
|
let endKey: string;
|
|
if (isAllDay) {
|
|
const startUTC = new Date(Date.UTC(
|
|
startDate.getUTCFullYear(),
|
|
startDate.getUTCMonth(),
|
|
startDate.getUTCDate()
|
|
));
|
|
const endUTC = new Date(Date.UTC(
|
|
endDate.getUTCFullYear(),
|
|
endDate.getUTCMonth(),
|
|
endDate.getUTCDate()
|
|
));
|
|
startKey = `${startUTC.getUTCFullYear()}-${String(startUTC.getUTCMonth() + 1).padStart(2, '0')}-${String(startUTC.getUTCDate()).padStart(2, '0')}`;
|
|
endKey = `${endUTC.getUTCFullYear()}-${String(endUTC.getUTCMonth() + 1).padStart(2, '0')}-${String(endUTC.getUTCDate()).padStart(2, '0')}`;
|
|
} else {
|
|
const startRounded = new Date(startDate);
|
|
startRounded.setSeconds(0, 0);
|
|
const endRounded = new Date(endDate);
|
|
endRounded.setSeconds(0, 0);
|
|
startKey = startRounded.toISOString();
|
|
endKey = endRounded.toISOString();
|
|
}
|
|
const key = `${evt.calendarId}|${evt.title}|${startKey}|${endKey}`;
|
|
console.log(` Group key: ${key}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (duplicates.length === 0) {
|
|
console.log('\n✅ No duplicates found!');
|
|
console.log(' (Check debug output above to see why events are not grouped as duplicates)');
|
|
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();
|