From 6d56b75962f79d31f0790558d68496bebadc8613 Mon Sep 17 00:00:00 2001 From: Guillim Date: Thu, 19 Jun 2025 17:04:21 +0200 Subject: [PATCH] 12690-error-unknown-error-importing-calendar-events-reafcto-required (#12711) Why : we had an issue impoting events du to CalendarEvents not yet existing while inserting the CalendarChannelAssociation due to inverted method in the service This PR refactors the calendar event import logic by - renaming - splitting utility functions for better clarity and maintainability. - adding TSDoc comments to explain the purpose and uniqueness of the `eventExternalId` field in calendar event associations Fixes #12690 --------- Co-authored-by: Charles Bochet --- .../format-google-calendar-event.util.spec.ts | 2 +- .../format-google-calendar-event.util.ts | 22 +- ...icrosoft-calendar-import-events.service.ts | 4 +- ...rmat-microsoft-calendar-event.util.spec.ts | 2 +- .../format-microsoft-calendar-event.util.ts | 24 +- .../calendar-events-import.service.ts | 8 +- .../services/calendar-get-events.service.ts | 4 +- .../services/calendar-save-events.service.ts | 327 ++++++++++-------- .../utils/calendar-event-mapper.util.ts | 12 + .../utils/filter-events.util.ts | 12 +- .../filter-out-blocklisted-events.util.ts | 4 +- .../inject-ids-in-calendar-events.util.ts | 31 -- .../calendar-event-participant.service.ts | 62 +++- ...nnel-event-association.workspace-entity.ts | 7 +- .../calendar/common/types/calendar-event.ts | 50 --- .../common/types/fetched-calendar-event.ts | 26 ++ 16 files changed, 327 insertions(+), 270 deletions(-) create mode 100644 packages/twenty-server/src/modules/calendar/calendar-event-import-manager/utils/calendar-event-mapper.util.ts delete mode 100644 packages/twenty-server/src/modules/calendar/calendar-event-import-manager/utils/inject-ids-in-calendar-events.util.ts delete mode 100644 packages/twenty-server/src/modules/calendar/common/types/calendar-event.ts create mode 100644 packages/twenty-server/src/modules/calendar/common/types/fetched-calendar-event.ts diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/utils/__tests__/format-google-calendar-event.util.spec.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/utils/__tests__/format-google-calendar-event.util.spec.ts index 7124f069d..278ab7b06 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/utils/__tests__/format-google-calendar-event.util.spec.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/utils/__tests__/format-google-calendar-event.util.spec.ts @@ -59,7 +59,7 @@ describe('formatGoogleCalendarEvents', () => { expect(formattedEvent.isFullDay).toBe(false); expect(formattedEvent.startsAt).toBe('2023-01-15T14:00:00Z'); expect(formattedEvent.endsAt).toBe('2023-01-15T15:00:00Z'); - expect(formattedEvent.externalId).toBe('event123'); + expect(formattedEvent.id).toBe('event123'); expect(formattedEvent.conferenceSolution).toBe('hangoutsMeet'); expect(formattedEvent.conferenceLinkUrl).toBe( 'https://meet.google.com/abc-defg-hij', diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/utils/format-google-calendar-event.util.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/utils/format-google-calendar-event.util.ts index eef52c0ca..3ab408778 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/utils/format-google-calendar-event.util.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/utils/format-google-calendar-event.util.ts @@ -2,17 +2,17 @@ import { calendar_v3 as calendarV3 } from 'googleapis'; import { sanitizeCalendarEvent } from 'src/modules/calendar/calendar-event-import-manager/drivers/utils/sanitizeCalendarEvent'; import { CalendarEventParticipantResponseStatus } from 'src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity'; -import { CalendarEventWithParticipants } from 'src/modules/calendar/common/types/calendar-event'; +import { FetchedCalendarEvent } from 'src/modules/calendar/common/types/fetched-calendar-event'; export const formatGoogleCalendarEvents = ( events: calendarV3.Schema$Event[], -): CalendarEventWithParticipants[] => { +): FetchedCalendarEvent[] => { return events.map(formatGoogleCalendarEvent); }; const formatGoogleCalendarEvent = ( event: calendarV3.Schema$Event, -): CalendarEventWithParticipants => { +): FetchedCalendarEvent => { const formatResponseStatus = (status: string | null | undefined) => { switch (status) { case 'accepted': @@ -27,15 +27,15 @@ const formatGoogleCalendarEvent = ( }; // Create the event object - const calendarEvent: CalendarEventWithParticipants = { + const calendarEvent: FetchedCalendarEvent = { title: event.summary ?? '', isCanceled: event.status === 'cancelled', isFullDay: event.start?.dateTime == null, - startsAt: event.start?.dateTime ?? event.start?.date ?? null, - endsAt: event.end?.dateTime ?? event.end?.date ?? null, - externalId: event.id ?? '', - externalCreatedAt: event.created ?? null, - externalUpdatedAt: event.updated ?? null, + startsAt: event.start?.dateTime ?? event.start?.date ?? '', + endsAt: event.end?.dateTime ?? event.end?.date ?? '', + id: event.id ?? '', + externalCreatedAt: event.created ?? '', + externalUpdatedAt: event.updated ?? '', description: event.description ?? '', location: event.location ?? '', iCalUID: event.iCalUID ?? '', @@ -54,11 +54,11 @@ const formatGoogleCalendarEvent = ( status: event.status ?? '', }; - const propertiesToSanitize: (keyof CalendarEventWithParticipants)[] = [ + const propertiesToSanitize: (keyof FetchedCalendarEvent)[] = [ 'title', 'startsAt', 'endsAt', - 'externalId', + 'id', 'externalCreatedAt', 'externalUpdatedAt', 'description', diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/services/microsoft-calendar-import-events.service.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/services/microsoft-calendar-import-events.service.ts index 7f6ac5223..31aa76f33 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/services/microsoft-calendar-import-events.service.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/services/microsoft-calendar-import-events.service.ts @@ -4,7 +4,7 @@ import { Event } from '@microsoft/microsoft-graph-types'; import { formatMicrosoftCalendarEvents } from 'src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/format-microsoft-calendar-event.util'; import { parseMicrosoftCalendarError } from 'src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/parse-microsoft-calendar-error.util'; -import { CalendarEventWithParticipants } from 'src/modules/calendar/common/types/calendar-event'; +import { FetchedCalendarEvent } from 'src/modules/calendar/common/types/fetched-calendar-event'; import { MicrosoftOAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/drivers/microsoft/microsoft-oauth2-client-manager.service'; import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; @@ -20,7 +20,7 @@ export class MicrosoftCalendarImportEventsService { 'provider' | 'refreshToken' | 'id' >, changedEventIds: string[], - ): Promise { + ): Promise { try { const microsoftClient = await this.microsoftOAuth2ClientManagerService.getOAuth2Client( diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/__tests__/format-microsoft-calendar-event.util.spec.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/__tests__/format-microsoft-calendar-event.util.spec.ts index fd48a90e3..aaa922d60 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/__tests__/format-microsoft-calendar-event.util.spec.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/__tests__/format-microsoft-calendar-event.util.spec.ts @@ -66,7 +66,7 @@ describe('formatMicrosoftCalendarEvents', () => { expect(formattedEvent.isFullDay).toBe(false); expect(formattedEvent.startsAt).toBe('2023-01-15T14:00:00Z'); expect(formattedEvent.endsAt).toBe('2023-01-15T15:00:00Z'); - expect(formattedEvent.externalId).toBe('event123'); + expect(formattedEvent.id).toBe('event123'); expect(formattedEvent.conferenceSolution).toBe('teamsForBusiness'); expect(formattedEvent.conferenceLinkUrl).toBe( 'https://teams.microsoft.com/l/meetup-join/abc123', diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/format-microsoft-calendar-event.util.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/format-microsoft-calendar-event.util.ts index 0beca6c24..49b4011cb 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/format-microsoft-calendar-event.util.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/format-microsoft-calendar-event.util.ts @@ -6,17 +6,15 @@ import { import { sanitizeCalendarEvent } from 'src/modules/calendar/calendar-event-import-manager/drivers/utils/sanitizeCalendarEvent'; import { CalendarEventParticipantResponseStatus } from 'src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity'; -import { CalendarEventWithParticipants } from 'src/modules/calendar/common/types/calendar-event'; +import { FetchedCalendarEvent } from 'src/modules/calendar/common/types/fetched-calendar-event'; export const formatMicrosoftCalendarEvents = ( events: Event[], -): CalendarEventWithParticipants[] => { +): FetchedCalendarEvent[] => { return events.map(formatMicrosoftCalendarEvent); }; -const formatMicrosoftCalendarEvent = ( - event: Event, -): CalendarEventWithParticipants => { +const formatMicrosoftCalendarEvent = (event: Event): FetchedCalendarEvent => { const formatResponseStatus = ( status: NullableOption | undefined, ) => { @@ -33,15 +31,15 @@ const formatMicrosoftCalendarEvent = ( } }; - const calendarEvent: CalendarEventWithParticipants = { + const calendarEvent: FetchedCalendarEvent = { title: event.subject ?? '', isCanceled: !!event.isCancelled, isFullDay: !!event.isAllDay, - startsAt: event.start?.dateTime ?? null, - endsAt: event.end?.dateTime ?? null, - externalId: event.id ?? '', - externalCreatedAt: event.createdDateTime ?? null, - externalUpdatedAt: event.lastModifiedDateTime ?? null, + startsAt: event.start?.dateTime ?? '', + endsAt: event.end?.dateTime ?? '', + id: event.id ?? '', + externalCreatedAt: event.createdDateTime ?? '', + externalUpdatedAt: event.lastModifiedDateTime ?? '', description: event.body?.content ?? '', location: event.location?.displayName ?? '', iCalUID: event.iCalUId ?? '', @@ -59,11 +57,11 @@ const formatMicrosoftCalendarEvent = ( status: '', }; - const propertiesToSanitize: (keyof CalendarEventWithParticipants)[] = [ + const propertiesToSanitize: (keyof FetchedCalendarEvent)[] = [ 'title', 'startsAt', 'endsAt', - 'externalId', + 'id', 'externalCreatedAt', 'externalUpdatedAt', 'description', diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-events-import.service.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-events-import.service.ts index dfe0e9154..20062f676 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-events-import.service.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-events-import.service.ts @@ -21,7 +21,7 @@ import { filterEventsAndReturnCancelledEvents } from 'src/modules/calendar/calen import { CalendarChannelSyncStatusService } from 'src/modules/calendar/common/services/calendar-channel-sync-status.service'; import { CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity'; import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; -import { CalendarEventWithParticipants } from 'src/modules/calendar/common/types/calendar-event'; +import { FetchedCalendarEvent } from 'src/modules/calendar/common/types/fetched-calendar-event'; import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; @Injectable() @@ -43,13 +43,13 @@ export class CalendarEventsImportService { calendarChannel: CalendarChannelWorkspaceEntity, connectedAccount: ConnectedAccountWorkspaceEntity, workspaceId: string, - fetchedCalendarEvents?: CalendarEventWithParticipants[], + fetchedCalendarEvents?: FetchedCalendarEvent[], ): Promise { await this.calendarChannelSyncStatusService.markAsCalendarEventsImportOngoing( [calendarChannel.id], ); - let calendarEvents: CalendarEventWithParticipants[] = []; + let calendarEvents: FetchedCalendarEvent[] = []; try { if (fetchedCalendarEvents) { @@ -103,7 +103,7 @@ export class CalendarEventsImportService { ); const cancelledEventExternalIds = cancelledEvents.map( - (event) => event.externalId, + (event) => event.id, ); const BATCH_SIZE = 1000; diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-get-events.service.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-get-events.service.ts index 550f47257..cf8f13fcd 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-get-events.service.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-get-events.service.ts @@ -8,12 +8,12 @@ import { CalendarEventImportException, CalendarEventImportExceptionCode, } from 'src/modules/calendar/calendar-event-import-manager/exceptions/calendar-event-import.exception'; -import { CalendarEventWithParticipants } from 'src/modules/calendar/common/types/calendar-event'; +import { FetchedCalendarEvent } from 'src/modules/calendar/common/types/fetched-calendar-event'; import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; export type GetCalendarEventsResponse = { fullEvents: boolean; - calendarEvents?: CalendarEventWithParticipants[]; + calendarEvents?: FetchedCalendarEvent[]; calendarEventIds?: string[]; nextSyncCursor: string; }; diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-save-events.service.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-save-events.service.ts index d393220f7..181442cb1 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-save-events.service.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-save-events.service.ts @@ -2,35 +2,30 @@ import { Injectable } from '@nestjs/common'; import { Any } from 'typeorm'; -import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator'; -import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; -import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; -import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; import { WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; -import { injectIdsInCalendarEvents } from 'src/modules/calendar/calendar-event-import-manager/utils/inject-ids-in-calendar-events.util'; import { CalendarEventParticipantService } from 'src/modules/calendar/calendar-event-participant-manager/services/calendar-event-participant.service'; import { CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity'; import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; import { CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity'; -import { CalendarEventWithParticipants } from 'src/modules/calendar/common/types/calendar-event'; +import { FetchedCalendarEvent } from 'src/modules/calendar/common/types/fetched-calendar-event'; import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; -import { - CreateCompanyAndContactJob, - CreateCompanyAndContactJobData, -} from 'src/modules/contact-creation-manager/jobs/create-company-and-contact.job'; + +type FetchedCalendarEventWithDBEvent = { + fetchedCalendarEvent: FetchedCalendarEvent; + existingCalendarEvent: CalendarEventWorkspaceEntity | null; + newlyCreatedCalendarEvent: CalendarEventWorkspaceEntity | null; +}; @Injectable() export class CalendarSaveEventsService { constructor( private readonly twentyORMManager: TwentyORMManager, private readonly calendarEventParticipantService: CalendarEventParticipantService, - @InjectMessageQueue(MessageQueue.contactCreationQueue) - private readonly messageQueueService: MessageQueueService, ) {} public async saveCalendarEventsAndEnqueueContactCreationJob( - filteredEvents: CalendarEventWithParticipants[], + fetchedCalendarEvents: FetchedCalendarEvent[], calendarChannel: CalendarChannelWorkspaceEntity, connectedAccount: ConnectedAccountWorkspaceEntity, workspaceId: string, @@ -40,158 +35,220 @@ export class CalendarSaveEventsService { 'calendarEvent', ); - const existingCalendarEvents = await calendarEventRepository.find({ - where: { - iCalUID: Any(filteredEvents.map((event) => event.iCalUID as string)), - }, - }); - - const iCalUIDCalendarEventIdMap = new Map( - existingCalendarEvents.map((calendarEvent) => [ - calendarEvent.iCalUID, - calendarEvent.id, - ]), - ); - - const calendarEventsWithIds = injectIdsInCalendarEvents( - filteredEvents, - iCalUIDCalendarEventIdMap, - ); - - // TODO: When we will be able to add unicity contraint on iCalUID, we will do a INSERT ON CONFLICT DO UPDATE - - const existingEventsICalUIDs = existingCalendarEvents.map( - (calendarEvent) => calendarEvent.iCalUID, - ); - - const eventsToSave = calendarEventsWithIds.filter( - (calendarEvent) => - !existingEventsICalUIDs.includes(calendarEvent.iCalUID), - ); - - const eventsToUpdate = calendarEventsWithIds.filter((calendarEvent) => - existingEventsICalUIDs.includes(calendarEvent.iCalUID), - ); - const calendarChannelEventAssociationRepository = await this.twentyORMManager.getRepository( 'calendarChannelEventAssociation', ); - const existingCalendarChannelEventAssociations = - await calendarChannelEventAssociationRepository.find({ - where: { - eventExternalId: Any( - calendarEventsWithIds.map((calendarEvent) => calendarEvent.id), - ), - calendarChannel: { - id: calendarChannel.id, - }, - }, + const existingCalendarEvents = await calendarEventRepository.find({ + where: { + iCalUID: Any( + fetchedCalendarEvents.map((event) => event.iCalUID as string), + ), + }, + }); + + const fetchedCalendarEventsWithDBEvents: FetchedCalendarEventWithDBEvent[] = + fetchedCalendarEvents.map((event): FetchedCalendarEventWithDBEvent => { + const existingEventWithSameiCalUID = existingCalendarEvents.find( + (existingEvent) => existingEvent.iCalUID === event.iCalUID, + ); + + return { + fetchedCalendarEvent: event, + existingCalendarEvent: existingEventWithSameiCalUID ?? null, + newlyCreatedCalendarEvent: null, + }; }); - const calendarChannelEventAssociationsToSave = calendarEventsWithIds - .filter( - (calendarEvent) => - !existingCalendarChannelEventAssociations.some( - (association) => association.eventExternalId === calendarEvent.id, - ), - ) - .map((calendarEvent) => ({ - calendarEventId: calendarEvent.id, - eventExternalId: calendarEvent.externalId, - calendarChannelId: calendarChannel.id, - recurringEventExternalId: calendarEvent.recurringEventExternalId, - })); - - const participantsToSave = eventsToSave.flatMap( - (event) => event.participants, - ); - - const participantsToUpdate = eventsToUpdate.flatMap( - (event) => event.participants, - ); - const workspaceDataSource = await this.twentyORMManager.getDatasource(); - await workspaceDataSource?.transaction( + await workspaceDataSource.transaction( async (transactionManager: WorkspaceEntityManager) => { - await calendarEventRepository.save( - eventsToSave.map( - (calendarEvent) => - ({ - id: calendarEvent.id, - iCalUID: calendarEvent.iCalUID, - title: calendarEvent.title, - description: calendarEvent.description, - startsAt: calendarEvent.startsAt, - endsAt: calendarEvent.endsAt, - location: calendarEvent.location, - isFullDay: calendarEvent.isFullDay, - isCanceled: calendarEvent.isCanceled, - conferenceSolution: calendarEvent.conferenceSolution, - conferenceLink: { - primaryLinkLabel: calendarEvent.conferenceLinkLabel, - primaryLinkUrl: calendarEvent.conferenceLinkUrl, - }, - externalCreatedAt: calendarEvent.externalCreatedAt, - externalUpdatedAt: calendarEvent.externalUpdatedAt, - }) satisfies DeepPartial, - ), + const savedCalendarEvents = await calendarEventRepository.save( + fetchedCalendarEventsWithDBEvents + .filter( + ({ existingCalendarEvent }) => existingCalendarEvent === null, + ) + .map( + ({ fetchedCalendarEvent }) => + ({ + iCalUID: fetchedCalendarEvent.iCalUID, + title: fetchedCalendarEvent.title, + description: fetchedCalendarEvent.description, + startsAt: fetchedCalendarEvent.startsAt, + endsAt: fetchedCalendarEvent.endsAt, + location: fetchedCalendarEvent.location, + isFullDay: fetchedCalendarEvent.isFullDay, + isCanceled: fetchedCalendarEvent.isCanceled, + conferenceSolution: fetchedCalendarEvent.conferenceSolution, + conferenceLink: { + primaryLinkLabel: fetchedCalendarEvent.conferenceLinkLabel, + primaryLinkUrl: fetchedCalendarEvent.conferenceLinkUrl, + secondaryLinks: [], + }, + externalCreatedAt: fetchedCalendarEvent.externalCreatedAt, + externalUpdatedAt: fetchedCalendarEvent.externalUpdatedAt, + }) satisfies Omit< + CalendarEventWorkspaceEntity, + | 'id' + | 'calendarChannelEventAssociations' + | 'calendarEventParticipants' + | 'createdAt' + | 'updatedAt' + | 'deletedAt' + >, + ), {}, transactionManager, ); + const fetchedCalendarEventsWithDBEventsEnrichedWithSavedEvents: FetchedCalendarEventWithDBEvent[] = + fetchedCalendarEventsWithDBEvents.map( + ({ fetchedCalendarEvent, existingCalendarEvent }) => { + const savedCalendarEvent = savedCalendarEvents.find( + (savedCalendarEvent) => + savedCalendarEvent.iCalUID === fetchedCalendarEvent.iCalUID, + ); + + return { + fetchedCalendarEvent, + existingCalendarEvent: existingCalendarEvent, + newlyCreatedCalendarEvent: savedCalendarEvent ?? null, + }; + }, + ); + await calendarEventRepository.save( - eventsToUpdate.map( - (calendarEvent) => - ({ - id: calendarEvent.id, - iCalUID: calendarEvent.iCalUID, - title: calendarEvent.title, - description: calendarEvent.description, - startsAt: calendarEvent.startsAt, - endsAt: calendarEvent.endsAt, - location: calendarEvent.location, - isFullDay: calendarEvent.isFullDay, - isCanceled: calendarEvent.isCanceled, - conferenceSolution: calendarEvent.conferenceSolution, + fetchedCalendarEventsWithDBEventsEnrichedWithSavedEvents + .filter( + ({ existingCalendarEvent }) => existingCalendarEvent !== null, + ) + .map(({ fetchedCalendarEvent, existingCalendarEvent }) => { + if (!existingCalendarEvent) { + throw new Error( + `Existing calendar event with iCalUID ${fetchedCalendarEvent.iCalUID} not found - should never happen`, + ); + } + + return { + id: existingCalendarEvent.id, + iCalUID: fetchedCalendarEvent.iCalUID, + title: fetchedCalendarEvent.title, + description: fetchedCalendarEvent.description, + startsAt: fetchedCalendarEvent.startsAt, + endsAt: fetchedCalendarEvent.endsAt, + location: fetchedCalendarEvent.location, + isFullDay: fetchedCalendarEvent.isFullDay, + isCanceled: fetchedCalendarEvent.isCanceled, + conferenceSolution: fetchedCalendarEvent.conferenceSolution, conferenceLink: { - primaryLinkLabel: calendarEvent.conferenceLinkLabel, - primaryLinkUrl: calendarEvent.conferenceLinkUrl, + primaryLinkLabel: fetchedCalendarEvent.conferenceLinkLabel, + primaryLinkUrl: fetchedCalendarEvent.conferenceLinkUrl, + secondaryLinks: [], }, - externalCreatedAt: calendarEvent.externalCreatedAt, - externalUpdatedAt: calendarEvent.externalUpdatedAt, - }) satisfies DeepPartial, - ), + externalCreatedAt: fetchedCalendarEvent.externalCreatedAt, + externalUpdatedAt: fetchedCalendarEvent.externalUpdatedAt, + } satisfies Omit< + CalendarEventWorkspaceEntity, + | 'calendarChannelEventAssociations' + | 'calendarEventParticipants' + | 'createdAt' + | 'updatedAt' + | 'deletedAt' + >; + }), {}, transactionManager, ); + const calendarChannelEventAssociationsToSave: Pick< + CalendarChannelEventAssociationWorkspaceEntity, + | 'calendarEventId' + | 'eventExternalId' + | 'calendarChannelId' + | 'recurringEventExternalId' + >[] = fetchedCalendarEventsWithDBEventsEnrichedWithSavedEvents.map( + ({ + fetchedCalendarEvent, + existingCalendarEvent, + newlyCreatedCalendarEvent, + }) => { + const calendarEventId = + existingCalendarEvent?.id ?? newlyCreatedCalendarEvent?.id; + + if (!calendarEventId) { + throw new Error( + `Calendar event id not found for event with iCalUID ${fetchedCalendarEvent.iCalUID} - should never happen`, + ); + } + + return { + calendarEventId, + eventExternalId: fetchedCalendarEvent.id, + calendarChannelId: calendarChannel.id, + recurringEventExternalId: + fetchedCalendarEvent.recurringEventExternalId ?? '', + }; + }, + ); + await calendarChannelEventAssociationRepository.save( calendarChannelEventAssociationsToSave, {}, transactionManager, ); + const participantsToSave = + fetchedCalendarEventsWithDBEventsEnrichedWithSavedEvents + .filter( + ({ newlyCreatedCalendarEvent }) => + newlyCreatedCalendarEvent !== null, + ) + .flatMap(({ newlyCreatedCalendarEvent, fetchedCalendarEvent }) => { + if (!newlyCreatedCalendarEvent?.id) { + throw new Error( + `Newly created calendar event with iCalUID ${fetchedCalendarEvent.iCalUID} not found - should never happen`, + ); + } + + return fetchedCalendarEvent.participants.map((participant) => ({ + ...participant, + calendarEventId: newlyCreatedCalendarEvent.id, + })); + }); + + // todo: we should prevent duplicate rows on calendarEventAssociation by creating + // an index on calendarChannelId and calendarEventId + const participantsToUpdate = + fetchedCalendarEventsWithDBEventsEnrichedWithSavedEvents + .filter( + ({ existingCalendarEvent }) => existingCalendarEvent !== null, + ) + .flatMap(({ fetchedCalendarEvent, existingCalendarEvent }) => { + if (!existingCalendarEvent?.id) { + throw new Error( + `Existing calendar event with iCalUID ${fetchedCalendarEvent.iCalUID} not found - should never happen`, + ); + } + + return fetchedCalendarEvent.participants.map((participant) => ({ + ...participant, + calendarEventId: existingCalendarEvent.id, + })); + }); + await this.calendarEventParticipantService.upsertAndDeleteCalendarEventParticipants( - participantsToSave, - participantsToUpdate, - transactionManager, + { + participantsToSave, + participantsToUpdate, + transactionManager, + calendarChannel, + connectedAccount, + workspaceId, + }, ); }, ); - - if (calendarChannel.isContactAutoCreationEnabled) { - await this.messageQueueService.add( - CreateCompanyAndContactJob.name, - { - workspaceId, - connectedAccount, - contactsToCreate: participantsToSave, - source: FieldActorSource.CALENDAR, - }, - ); - } } } diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/utils/calendar-event-mapper.util.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/utils/calendar-event-mapper.util.ts new file mode 100644 index 000000000..cb69b793b --- /dev/null +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/utils/calendar-event-mapper.util.ts @@ -0,0 +1,12 @@ +import { CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity'; + +export const mapCalendarEventsByICalUID = ( + existingCalendarEvents: CalendarEventWorkspaceEntity[], +): Map => { + return new Map( + existingCalendarEvents.map((calendarEvent) => [ + calendarEvent.iCalUID, + calendarEvent.id, + ]), + ); +}; diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/utils/filter-events.util.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/utils/filter-events.util.ts index d705f81c0..3a7a65daa 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/utils/filter-events.util.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/utils/filter-events.util.ts @@ -1,13 +1,13 @@ import { filterOutBlocklistedEvents } from 'src/modules/calendar/calendar-event-import-manager/utils/filter-out-blocklisted-events.util'; -import { CalendarEventWithParticipants } from 'src/modules/calendar/common/types/calendar-event'; +import { FetchedCalendarEvent } from 'src/modules/calendar/common/types/fetched-calendar-event'; export const filterEventsAndReturnCancelledEvents = ( calendarChannelHandles: string[], - events: CalendarEventWithParticipants[], + events: FetchedCalendarEvent[], blocklist: string[], ): { - filteredEvents: CalendarEventWithParticipants[]; - cancelledEvents: CalendarEventWithParticipants[]; + filteredEvents: FetchedCalendarEvent[]; + cancelledEvents: FetchedCalendarEvent[]; } => { const filteredEvents = filterOutBlocklistedEvents( calendarChannelHandles, @@ -18,8 +18,8 @@ export const filterEventsAndReturnCancelledEvents = ( return filteredEvents.reduce( ( acc: { - filteredEvents: CalendarEventWithParticipants[]; - cancelledEvents: CalendarEventWithParticipants[]; + filteredEvents: FetchedCalendarEvent[]; + cancelledEvents: FetchedCalendarEvent[]; }, event, ) => { diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/utils/filter-out-blocklisted-events.util.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/utils/filter-out-blocklisted-events.util.ts index d6a82a978..4e4a63243 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/utils/filter-out-blocklisted-events.util.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/utils/filter-out-blocklisted-events.util.ts @@ -1,9 +1,9 @@ import { isEmailBlocklisted } from 'src/modules/blocklist/utils/is-email-blocklisted.util'; -import { CalendarEventWithParticipants } from 'src/modules/calendar/common/types/calendar-event'; +import { FetchedCalendarEvent } from 'src/modules/calendar/common/types/fetched-calendar-event'; export const filterOutBlocklistedEvents = ( calendarChannelHandles: string[], - events: CalendarEventWithParticipants[], + events: FetchedCalendarEvent[], blocklist: string[], ) => { return events.filter((event) => { diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/utils/inject-ids-in-calendar-events.util.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/utils/inject-ids-in-calendar-events.util.ts deleted file mode 100644 index b41e329dc..000000000 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/utils/inject-ids-in-calendar-events.util.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { v4 } from 'uuid'; - -import { - CalendarEventWithParticipants, - CalendarEventWithParticipantsAndCalendarEventId, -} from 'src/modules/calendar/common/types/calendar-event'; - -export const injectIdsInCalendarEvents = ( - calendarEvents: CalendarEventWithParticipants[], - iCalUIDCalendarEventIdMap: Map, -): CalendarEventWithParticipantsAndCalendarEventId[] => { - return calendarEvents.map((calendarEvent) => { - const id = iCalUIDCalendarEventIdMap.get(calendarEvent.iCalUID) ?? v4(); - - return injectIdInCalendarEvent(calendarEvent, id); - }); -}; - -const injectIdInCalendarEvent = ( - calendarEvent: CalendarEventWithParticipants, - id: string, -): CalendarEventWithParticipantsAndCalendarEventId => { - return { - ...calendarEvent, - id, - participants: calendarEvent.participants.map((participant) => ({ - ...participant, - calendarEventId: id, - })), - }; -}; diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/services/calendar-event-participant.service.ts b/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/services/calendar-event-participant.service.ts index 19a9540b4..f66e8e12a 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/services/calendar-event-participant.service.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/services/calendar-event-participant.service.ts @@ -4,24 +4,51 @@ import { isDefined } from 'class-validator'; import differenceWith from 'lodash.differencewith'; import { Any } from 'typeorm'; +import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator'; +import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; +import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; +import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; import { WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; +import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity'; -import { CalendarEventParticipantWithCalendarEventId } from 'src/modules/calendar/common/types/calendar-event'; +import { FetchedCalendarEventParticipant } from 'src/modules/calendar/common/types/fetched-calendar-event'; +import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; +import { + CreateCompanyAndContactJob, + CreateCompanyAndContactJobData, +} from 'src/modules/contact-creation-manager/jobs/create-company-and-contact.job'; import { MatchParticipantService } from 'src/modules/match-participant/match-participant.service'; +type FetchedCalendarEventParticipantWithCalendarEventId = + FetchedCalendarEventParticipant & { + calendarEventId: string; + }; + @Injectable() export class CalendarEventParticipantService { constructor( private readonly twentyORMManager: TwentyORMManager, private readonly matchParticipantService: MatchParticipantService, + @InjectMessageQueue(MessageQueue.contactCreationQueue) + private readonly messageQueueService: MessageQueueService, ) {} - public async upsertAndDeleteCalendarEventParticipants( - participantsToSave: CalendarEventParticipantWithCalendarEventId[], - participantsToUpdate: CalendarEventParticipantWithCalendarEventId[], - transactionManager?: WorkspaceEntityManager, - ): Promise { + public async upsertAndDeleteCalendarEventParticipants({ + participantsToSave, + participantsToUpdate, + transactionManager, + calendarChannel, + connectedAccount, + workspaceId, + }: { + participantsToSave: FetchedCalendarEventParticipantWithCalendarEventId[]; + participantsToUpdate: FetchedCalendarEventParticipantWithCalendarEventId[]; + transactionManager?: WorkspaceEntityManager; + calendarChannel: CalendarChannelWorkspaceEntity; + connectedAccount: ConnectedAccountWorkspaceEntity; + workspaceId: string; + }): Promise { const calendarEventParticipantRepository = await this.twentyORMManager.getRepository( 'calendarEventParticipant', @@ -39,7 +66,10 @@ export class CalendarEventParticipantService { }); const { calendarEventParticipantsToUpdate, newCalendarEventParticipants } = - participantsToUpdate.reduce( + participantsToUpdate.reduce<{ + calendarEventParticipantsToUpdate: FetchedCalendarEventParticipantWithCalendarEventId[]; + newCalendarEventParticipants: FetchedCalendarEventParticipantWithCalendarEventId[]; + }>( (acc, calendarEventParticipant) => { const existingCalendarEventParticipant = existingCalendarEventParticipants.find( @@ -61,10 +91,8 @@ export class CalendarEventParticipantService { return acc; }, { - calendarEventParticipantsToUpdate: - [] as CalendarEventParticipantWithCalendarEventId[], - newCalendarEventParticipants: - [] as CalendarEventParticipantWithCalendarEventId[], + calendarEventParticipantsToUpdate: [], + newCalendarEventParticipants: [], }, ); @@ -110,6 +138,18 @@ export class CalendarEventParticipantService { transactionManager, ); + if (calendarChannel.isContactAutoCreationEnabled) { + await this.messageQueueService.add( + CreateCompanyAndContactJob.name, + { + workspaceId, + connectedAccount, + contactsToCreate: savedParticipants, + source: FieldActorSource.CALENDAR, + }, + ); + } + await this.matchParticipantService.matchParticipants({ participants: savedParticipants, objectMetadataName: 'calendarEventParticipant', diff --git a/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity.ts b/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity.ts index 010997a16..d7295eafc 100644 --- a/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity.ts +++ b/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity.ts @@ -1,9 +1,9 @@ import { msg } from '@lingui/core/macro'; import { FieldMetadataType } from 'twenty-shared/types'; +import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; -import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; @@ -29,6 +29,11 @@ import { CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standa @WorkspaceIsSystem() @WorkspaceIsNotAuditLogged() export class CalendarChannelEventAssociationWorkspaceEntity extends BaseWorkspaceEntity { + /** + * External ID of the calendar event. Comes from the provider's API, is unique per connected account. + * Used by the provider to identify the event in their system. + * So two External ID can be related to the same event sharing the same iCalUID. + */ @WorkspaceField({ standardId: CALENDAR_CHANNEL_EVENT_ASSOCIATION_STANDARD_FIELD_IDS.eventExternalId, diff --git a/packages/twenty-server/src/modules/calendar/common/types/calendar-event.ts b/packages/twenty-server/src/modules/calendar/common/types/calendar-event.ts deleted file mode 100644 index 50517314f..000000000 --- a/packages/twenty-server/src/modules/calendar/common/types/calendar-event.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity'; -import { CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity'; - -export type CalendarEvent = Omit< - CalendarEventWorkspaceEntity, - | 'createdAt' - | 'updatedAt' - | 'deletedAt' - | 'calendarChannelEventAssociations' - | 'calendarEventParticipants' - | 'conferenceLink' - | 'id' -> & { - conferenceLinkLabel: string; - conferenceLinkUrl: string; -}; - -export type CalendarEventParticipant = Omit< - CalendarEventParticipantWorkspaceEntity, - | 'id' - | 'createdAt' - | 'updatedAt' - | 'deletedAt' - | 'personId' - | 'workspaceMemberId' - | 'person' - | 'workspaceMember' - | 'calendarEvent' - | 'calendarEventId' ->; - -export type CalendarEventParticipantWithCalendarEventId = - CalendarEventParticipant & { - calendarEventId: string; - }; - -export type CalendarEventWithParticipants = CalendarEvent & { - externalId: string; - recurringEventExternalId?: string; - participants: CalendarEventParticipant[]; - status: string; -}; - -export type CalendarEventWithParticipantsAndCalendarEventId = CalendarEvent & { - id: string; - externalId: string; - recurringEventExternalId?: string; - participants: CalendarEventParticipantWithCalendarEventId[]; - status: string; -}; diff --git a/packages/twenty-server/src/modules/calendar/common/types/fetched-calendar-event.ts b/packages/twenty-server/src/modules/calendar/common/types/fetched-calendar-event.ts new file mode 100644 index 000000000..85239d585 --- /dev/null +++ b/packages/twenty-server/src/modules/calendar/common/types/fetched-calendar-event.ts @@ -0,0 +1,26 @@ +export type FetchedCalendarEventParticipant = { + displayName: string; + responseStatus: string; + handle: string; + isOrganizer: boolean; +}; + +export type FetchedCalendarEvent = { + id: string; + title: string; + iCalUID: string; + description: string; + startsAt: string; + endsAt: string; + location: string; + isFullDay: boolean; + isCanceled: boolean; + conferenceLinkLabel: string; + conferenceLinkUrl: string; + externalCreatedAt: string; + externalUpdatedAt: string; + conferenceSolution: string; + recurringEventExternalId?: string; + participants: FetchedCalendarEventParticipant[]; + status: string; +};