Compare commits

...

3 Commits

Author SHA1 Message Date
Marie 1a84f5df7f Revert "fix(signinup): several issues (#12698)" (#12745)
This reverts commit 9612a4928d.

(Causes a google auth sign-up issue)
2025-06-19 18:16:24 +02:00
Guillim 2c144715b4 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>
2025-06-19 18:16:02 +02:00
Paul Rastoin 51fa78c0b4 0.60.0 upgrade commands (#12735)
# Introduction
Even tho there's no commands we need to add the `0.60.0` entry within
the records
2025-06-19 18:15:56 +02:00
22 changed files with 361 additions and 317 deletions
@@ -5,7 +5,7 @@ import { Avatar } from 'twenty-ui/display';
import { UndecoratedLink } from 'twenty-ui/navigation';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
import { useRedirectToDefaultDomain } from '~/modules/domain-manager/hooks/useRedirectToDefaultDomain';
import { useReadDefaultDomainFromConfiguration } from '~/modules/domain-manager/hooks/useReadDefaultDomainFromConfiguration';
import { AppPath } from '~/modules/types/AppPath';
type LogoProps = {
primaryLogo?: string | null;
@@ -59,8 +59,6 @@ export const Logo = ({
const { redirectToDefaultDomain } = useRedirectToDefaultDomain();
const defaultPrimaryLogoUrl = `${window.location.origin}/images/icons/android/android-launchericon-192-192.png`;
const { defaultUrl } = useReadDefaultDomainFromConfiguration();
const primaryLogoUrl = getImageAbsoluteURI({
imageUrl: primaryLogo ?? defaultPrimaryLogoUrl,
baseUrl: REACT_APP_SERVER_BASE_URL,
@@ -79,7 +77,7 @@ export const Logo = ({
<StyledContainer onClick={() => onClick?.()}>
{isUsingDefaultLogo ? (
<UndecoratedLink
to={defaultUrl.toString()}
to={AppPath.SignInUp}
onClick={redirectToDefaultDomain}
>
<StyledPrimaryLogo src={primaryLogoUrl} />
@@ -3,18 +3,15 @@ import {
signInUpStepState,
} from '@/auth/states/signInUpStepState';
import { useEffect } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { useAuth } from '@/auth/hooks/useAuth';
import { useSearchParams } from 'react-router-dom';
import { availableWorkspacesState } from '@/auth/states/availableWorkspacesState';
import { countAvailableWorkspaces } from '@/auth/utils/availableWorkspacesUtils';
export const SignInUpGlobalScopeFormEffect = () => {
const setSignInUpStep = useSetRecoilState(signInUpStepState);
const [searchParams, setSearchParams] = useSearchParams();
const { setAuthTokens, loadCurrentUser } = useAuth();
const availableWorkspaces = useRecoilValue(availableWorkspacesState);
useEffect(() => {
const tokenPair = searchParams.get('tokenPair');
@@ -23,9 +20,6 @@ export const SignInUpGlobalScopeFormEffect = () => {
searchParams.delete('tokenPair');
setSearchParams(searchParams);
loadCurrentUser();
}
if (countAvailableWorkspaces(availableWorkspaces) > 1) {
setSignInUpStep(SignInUpStep.WorkspaceSelection);
}
}, [
@@ -34,7 +28,6 @@ export const SignInUpGlobalScopeFormEffect = () => {
setSignInUpStep,
loadCurrentUser,
setAuthTokens,
availableWorkspaces,
]);
return <></>;
@@ -10,11 +10,7 @@ export const useReadDefaultDomainFromConfiguration = () => {
? `${domainConfiguration.defaultSubdomain}.${domainConfiguration.frontDomain}`
: domainConfiguration.frontDomain;
const defaultUrl = new URL(window.location.href);
defaultUrl.hostname = defaultDomain;
return {
defaultDomain,
defaultUrl,
};
};
@@ -1,8 +1,11 @@
import { useSignInUp } from '@/auth/sign-in-up/hooks/useSignInUp';
import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
import { SignInUpStep } from '@/auth/states/signInUpStepState';
import {
SignInUpStep,
signInUpStepState,
} from '@/auth/states/signInUpStepState';
import { workspacePublicDataState } from '@/auth/states/workspacePublicDataState';
import { useRecoilValue } from 'recoil';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { Logo } from '@/auth/components/Logo';
import { Title } from '@/auth/components/Title';
@@ -27,7 +30,6 @@ import { isDefined } from 'twenty-shared/utils';
import { AnimatedEaseIn } from 'twenty-ui/utilities';
import { PublicWorkspaceDataOutput } from '~/generated/graphql';
import { SignInUpGlobalScopeFormEffect } from '@/auth/sign-in-up/components/internal/SignInUpGlobalScopeFormEffect';
import { useAuth } from '@/auth/hooks/useAuth';
const StandardContent = ({
workspacePublicData,
@@ -60,6 +62,7 @@ const StandardContent = ({
export const SignInUp = () => {
const { t } = useLingui();
const setSignInUpStep = useSetRecoilState(signInUpStepState);
const { form } = useSignInUpForm();
const { signInUpStep } = useSignInUp(form);
@@ -70,14 +73,11 @@ export const SignInUp = () => {
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
const { workspaceInviteHash, workspace: workspaceFromInviteHash } =
useWorkspaceFromInviteHash();
const { signOut } = useAuth();
const [searchParams] = useSearchParams();
const onClickOnLogo = () => {
if (!isOnAWorkspace && signInUpStep === SignInUpStep.WorkspaceSelection) {
signOut();
}
setSignInUpStep(SignInUpStep.Init);
};
const title = useMemo(() => {
@@ -165,10 +165,16 @@ export class UpgradeCommand extends UpgradeCommandRunner {
afterSyncMetadata: [],
};
const commands_060: VersionCommands = {
afterSyncMetadata: [],
beforeSyncMetadata: [],
};
this.allCommands = {
'0.53.0': commands_053,
'0.54.0': commands_054,
'0.55.0': commands_055,
'0.60.0': commands_060,
};
}
@@ -399,24 +399,23 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspace> {
displayName: workspace.displayName,
workspaceUrls: this.domainManagerService.getWorkspaceUrls(workspace),
logo: workspace.logo,
sso:
workspace.workspaceSSOIdentityProviders?.reduce(
(acc, identityProvider) =>
acc.concat(
identityProvider.status === 'Inactive'
? []
: [
{
id: identityProvider.id,
name: identityProvider.name,
issuer: identityProvider.issuer,
type: identityProvider.type,
status: identityProvider.status,
},
],
),
[] as AvailableWorkspace['sso'],
) ?? [],
sso: workspace.workspaceSSOIdentityProviders.reduce(
(acc, identityProvider) =>
acc.concat(
identityProvider.status === 'Inactive'
? []
: [
{
id: identityProvider.id,
name: identityProvider.name,
issuer: identityProvider.issuer,
type: identityProvider.type,
status: identityProvider.status,
},
],
),
[] as AvailableWorkspace['sso'],
),
};
}
@@ -440,12 +439,7 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspace> {
({ workspace, appToken }) => {
return {
...this.castWorkspaceToAvailableWorkspace(workspace),
...(appToken
? {
personalInviteToken: appToken.value,
inviteHash: workspace.inviteHash,
}
: {}),
...(appToken ? { personalInviteToken: appToken.value } : {}),
};
},
),
@@ -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;
};