From 946a11005412b8563a29f245da43c1997b95149d Mon Sep 17 00:00:00 2001 From: alma Date: Thu, 15 Jan 2026 12:46:55 +0100 Subject: [PATCH] Agenda refactor --- lib/services/caldav-sync.ts | 40 +++++++++--- lib/services/microsoft-calendar-sync.ts | 54 +++++++++++++---- scripts/verify-and-fix-prisma-client.ts | 81 +++++++++++++++++++++++++ 3 files changed, 153 insertions(+), 22 deletions(-) create mode 100644 scripts/verify-and-fix-prisma-client.ts diff --git a/lib/services/caldav-sync.ts b/lib/services/caldav-sync.ts index d53452d..2e1f2cb 100644 --- a/lib/services/caldav-sync.ts +++ b/lib/services/caldav-sync.ts @@ -418,11 +418,14 @@ export async function syncInfomaniakCalendar( }); // Create a map of existing events by externalEventId (UID) for fast lookup + // Use type assertion to handle case where Prisma client doesn't recognize externalEventId yet type EventType = typeof existingEvents[number]; const existingEventsByExternalId = new Map(); for (const event of existingEvents) { - if (event.externalEventId) { - existingEventsByExternalId.set(event.externalEventId, event); + // Access externalEventId safely (may not be in Prisma type if client not regenerated) + const externalId = (event as any).externalEventId; + if (externalId) { + existingEventsByExternalId.set(externalId, event); } } @@ -441,7 +444,9 @@ export async function syncInfomaniakCalendar( if (!existingEvent) { existingEvent = existingEvents.find( (e: EventType) => { - if (!e.externalEventId && // Only match events that don't have externalEventId yet + // Access externalEventId safely (may not be in Prisma type if client not regenerated) + const hasExternalId = !!(e as any).externalEventId; + if (!hasExternalId && // Only match events that don't have externalEventId yet e.title === caldavEvent.summary) { const timeDiff = Math.abs(new Date(e.start).getTime() - caldavEvent.start.getTime()); return timeDiff < 60000; // Within 1 minute @@ -478,10 +483,18 @@ export async function syncInfomaniakCalendar( }); updated++; } catch (updateError: any) { - // If externalEventId field doesn't exist, retry without it - if (updateError?.message?.includes('externalEventId') || updateError?.code === 'P2009') { - logger.warn('externalEventId field not available, updating without it', { + // If externalEventId field doesn't exist in Prisma client (even though it exists in DB), + // retry without it. This can happen if Prisma client wasn't regenerated after migration. + const errorMessage = updateError?.message || ''; + const errorCode = updateError?.code || ''; + + if (errorMessage.includes('externalEventId') || + errorMessage.includes('Unknown argument') || + errorCode === 'P2009' || + errorCode === 'P1012') { + logger.warn('externalEventId field not recognized by Prisma client, updating without it', { eventId: existingEvent.id, + error: errorMessage.substring(0, 200), }); const { externalEventId, ...dataWithoutExternalId } = baseEventData; await prisma.event.update({ @@ -505,9 +518,18 @@ export async function syncInfomaniakCalendar( }); created++; } catch (createError: any) { - // If externalEventId field doesn't exist, retry without it - if (createError?.message?.includes('externalEventId') || createError?.code === 'P2009') { - logger.warn('externalEventId field not available, creating without it'); + // If externalEventId field doesn't exist in Prisma client (even though it exists in DB), + // retry without it. This can happen if Prisma client wasn't regenerated after migration. + const errorMessage = createError?.message || ''; + const errorCode = createError?.code || ''; + + if (errorMessage.includes('externalEventId') || + errorMessage.includes('Unknown argument') || + errorCode === 'P2009' || + errorCode === 'P1012') { + logger.warn('externalEventId field not recognized by Prisma client, creating without it', { + error: errorMessage.substring(0, 200), + }); const { externalEventId, ...dataWithoutExternalId } = baseEventData; await prisma.event.create({ data: { diff --git a/lib/services/microsoft-calendar-sync.ts b/lib/services/microsoft-calendar-sync.ts index ed01f94..ef324d7 100644 --- a/lib/services/microsoft-calendar-sync.ts +++ b/lib/services/microsoft-calendar-sync.ts @@ -440,10 +440,13 @@ export async function syncMicrosoftCalendar( }); // Create a map of existing events by externalEventId (Microsoft ID) for fast lookup + // Use type assertion to handle case where Prisma client doesn't recognize externalEventId yet const existingEventsByExternalId = new Map(); for (const event of existingEvents) { - if (event.externalEventId) { - existingEventsByExternalId.set(event.externalEventId, event); + // Access externalEventId safely (may not be in Prisma type if client not regenerated) + const externalId = (event as any).externalEventId; + if (externalId) { + existingEventsByExternalId.set(externalId, event); } } @@ -477,7 +480,9 @@ export async function syncMicrosoftCalendar( // Priority 2: Fallback to checking description for [MS_ID:xxx] (backward compatibility) if (!existingEvent && microsoftId) { existingEvent = existingEvents.find((e) => { - if (!e.externalEventId && e.description && e.description.includes(`[MS_ID:${microsoftId}]`)) { + // Access externalEventId safely (may not be in Prisma type if client not regenerated) + const hasExternalId = !!(e as any).externalEventId; + if (!hasExternalId && e.description && e.description.includes(`[MS_ID:${microsoftId}]`)) { return true; } return false; @@ -487,10 +492,16 @@ export async function syncMicrosoftCalendar( // Priority 3: Fallback to title + date matching for events without externalEventId if (!existingEvent) { existingEvent = existingEvents.find( - (e) => - !e.externalEventId && // Only match events that don't have externalEventId yet - e.title === caldavEvent.summary && - Math.abs(new Date(e.start).getTime() - caldavEvent.start.getTime()) < 60000 // Within 1 minute + (e) => { + // Access externalEventId safely (may not be in Prisma type if client not regenerated) + const hasExternalId = !!(e as any).externalEventId; + if (!hasExternalId && // Only match events that don't have externalEventId yet + e.title === caldavEvent.summary) { + const timeDiff = Math.abs(new Date(e.start).getTime() - caldavEvent.start.getTime()); + return timeDiff < 60000; // Within 1 minute + } + return false; + } ); } @@ -529,10 +540,18 @@ export async function syncMicrosoftCalendar( microsoftId, }); } catch (updateError: any) { - // If externalEventId field doesn't exist, retry without it - if (updateError?.message?.includes('externalEventId') || updateError?.code === 'P2009') { - logger.warn('externalEventId field not available, updating without it', { + // If externalEventId field doesn't exist in Prisma client (even though it exists in DB), + // retry without it. This can happen if Prisma client wasn't regenerated after migration. + const errorMessage = updateError?.message || ''; + const errorCode = updateError?.code || ''; + + if (errorMessage.includes('externalEventId') || + errorMessage.includes('Unknown argument') || + errorCode === 'P2009' || + errorCode === 'P1012') { + logger.warn('externalEventId field not recognized by Prisma client, updating without it', { eventId: existingEvent.id, + error: errorMessage.substring(0, 200), }); const { externalEventId, ...dataWithoutExternalId } = baseEventData; await prisma.event.update({ @@ -562,9 +581,18 @@ export async function syncMicrosoftCalendar( start: caldavEvent.start.toISOString(), }); } catch (createError: any) { - // If externalEventId field doesn't exist, retry without it - if (createError?.message?.includes('externalEventId') || createError?.code === 'P2009') { - logger.warn('externalEventId field not available, creating without it'); + // If externalEventId field doesn't exist in Prisma client (even though it exists in DB), + // retry without it. This can happen if Prisma client wasn't regenerated after migration. + const errorMessage = createError?.message || ''; + const errorCode = createError?.code || ''; + + if (errorMessage.includes('externalEventId') || + errorMessage.includes('Unknown argument') || + errorCode === 'P2009' || + errorCode === 'P1012') { + logger.warn('externalEventId field not recognized by Prisma client, creating without it', { + error: errorMessage.substring(0, 200), + }); const { externalEventId, ...dataWithoutExternalId } = baseEventData; const newEvent = await prisma.event.create({ data: { diff --git a/scripts/verify-and-fix-prisma-client.ts b/scripts/verify-and-fix-prisma-client.ts new file mode 100644 index 0000000..207579b --- /dev/null +++ b/scripts/verify-and-fix-prisma-client.ts @@ -0,0 +1,81 @@ +#!/usr/bin/env ts-node + +/** + * Script to verify Prisma client is up to date with schema + * Run this to check if externalEventId field is available in Prisma client + */ + +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +async function verifyPrismaClient() { + try { + console.log('Checking if externalEventId field is available in Prisma client...\n'); + + // Try to access the externalEventId field in the Event model + // If it doesn't exist, TypeScript/Prisma will throw an error at compile time + // But we can check at runtime by trying to query with it + + // Check if we can select externalEventId + try { + const testEvent = await prisma.event.findFirst({ + select: { + id: true, + title: true, + externalEventId: true, + externalEventUrl: true, + }, + }); + + if (testEvent !== null) { + console.log('āœ… Prisma client recognizes externalEventId and externalEventUrl fields'); + console.log(' Sample event:', { + id: testEvent.id, + title: testEvent.title, + hasExternalEventId: 'externalEventId' in testEvent, + hasExternalEventUrl: 'externalEventUrl' in testEvent, + }); + } else { + console.log('āš ļø No events found, but Prisma client seems to recognize the fields'); + } + } catch (error: any) { + if (error.message?.includes('externalEventId') || error.message?.includes('Unknown argument')) { + console.error('āŒ Prisma client does NOT recognize externalEventId field'); + console.error(' Error:', error.message); + console.log('\nšŸ”§ Solution:'); + console.log(' 1. Run: npx prisma generate'); + console.log(' 2. Restart your Next.js server'); + } else { + throw error; + } + } + + // Also check database directly + const dbColumns = await prisma.$queryRaw>` + SELECT column_name, data_type + FROM information_schema.columns + WHERE table_name = 'Event' + AND column_name IN ('externalEventId', 'externalEventUrl') + `; + + console.log('\nšŸ“Š Database columns:'); + if (dbColumns.length === 0) { + console.log(' āŒ externalEventId and externalEventUrl columns NOT found in database'); + console.log(' šŸ”§ Run the migration: npx prisma migrate deploy'); + } else { + console.log(' āœ… Database columns exist:'); + dbColumns.forEach(col => { + console.log(` - ${col.column_name} (${col.data_type})`); + }); + } + + await prisma.$disconnect(); + } catch (error) { + console.error('Error:', error); + await prisma.$disconnect(); + process.exit(1); + } +} + +verifyPrismaClient();