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; +};