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:
@ -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',
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
]),
|
||||
);
|
||||
};
|
||||
@ -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,
|
||||
) => {
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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,
|
||||
})),
|
||||
};
|
||||
};
|
||||
@ -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',
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user