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 7124f069d3..278ab7b06b 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 eef52c0ca0..3ab4087787 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 7f6ac52231..31aa76f335 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 fd48a90e38..aaa922d60c 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 0beca6c24a..49b4011cb9 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 dfe0e91543..20062f676f 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 550f472573..cf8f13fcd8 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 d393220f7b..181442cb17 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 0000000000..cb69b793bc --- /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 d705f81c0b..3a7a65daaf 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 d6a82a978a..4e4a632436 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 b41e329dc0..0000000000 --- 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 19a9540b41..f66e8e12aa 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 010997a164..d7295eafce 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 50517314f4..0000000000 --- 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 0000000000..85239d585b --- /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; +};