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 <charles@twenty.com>
This commit is contained in:
Guillim
2025-06-19 17:04:21 +02:00
committed by GitHub
parent e1393c4887
commit 6d56b75962
16 changed files with 327 additions and 270 deletions

View File

@ -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',

View File

@ -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',

View File

@ -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<CalendarEventWithParticipants[]> {
): Promise<FetchedCalendarEvent[]> {
try {
const microsoftClient =
await this.microsoftOAuth2ClientManagerService.getOAuth2Client(

View File

@ -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',

View File

@ -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<ResponseType> | 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',

View File

@ -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<void> {
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;

View File

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

View File

@ -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<CalendarChannelEventAssociationWorkspaceEntity>(
'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<CalendarEventWorkspaceEntity>,
),
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<CalendarEventWorkspaceEntity>,
),
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<CreateCompanyAndContactJobData>(
CreateCompanyAndContactJob.name,
{
workspaceId,
connectedAccount,
contactsToCreate: participantsToSave,
source: FieldActorSource.CALENDAR,
},
);
}
}
}

View File

@ -0,0 +1,12 @@
import { CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity';
export const mapCalendarEventsByICalUID = (
existingCalendarEvents: CalendarEventWorkspaceEntity[],
): Map<string, string> => {
return new Map<string, string>(
existingCalendarEvents.map((calendarEvent) => [
calendarEvent.iCalUID,
calendarEvent.id,
]),
);
};

View File

@ -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,
) => {

View File

@ -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) => {

View File

@ -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<string, string>,
): 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,
})),
};
};

View File

@ -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<CalendarEventParticipantWorkspaceEntity>,
@InjectMessageQueue(MessageQueue.contactCreationQueue)
private readonly messageQueueService: MessageQueueService,
) {}
public async upsertAndDeleteCalendarEventParticipants(
participantsToSave: CalendarEventParticipantWithCalendarEventId[],
participantsToUpdate: CalendarEventParticipantWithCalendarEventId[],
transactionManager?: WorkspaceEntityManager,
): Promise<void> {
public async upsertAndDeleteCalendarEventParticipants({
participantsToSave,
participantsToUpdate,
transactionManager,
calendarChannel,
connectedAccount,
workspaceId,
}: {
participantsToSave: FetchedCalendarEventParticipantWithCalendarEventId[];
participantsToUpdate: FetchedCalendarEventParticipantWithCalendarEventId[];
transactionManager?: WorkspaceEntityManager;
calendarChannel: CalendarChannelWorkspaceEntity;
connectedAccount: ConnectedAccountWorkspaceEntity;
workspaceId: string;
}): Promise<void> {
const calendarEventParticipantRepository =
await this.twentyORMManager.getRepository<CalendarEventParticipantWorkspaceEntity>(
'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<CreateCompanyAndContactJobData>(
CreateCompanyAndContactJob.name,
{
workspaceId,
connectedAccount,
contactsToCreate: savedParticipants,
source: FieldActorSource.CALENDAR,
},
);
}
await this.matchParticipantService.matchParticipants({
participants: savedParticipants,
objectMetadataName: 'calendarEventParticipant',

View File

@ -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,

View File

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

View File

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