Refactor calendar to use new sync statuses and stages (#6141)

- Refactor calendar modules and some messaging modules to better
organize them by business rules and decouple them
- Work toward a common architecture for the different calendar providers
by introducing interfaces for the drivers
- Modify cron job to use the new sync statuses and stages
This commit is contained in:
bosiraphael
2024-07-08 17:01:06 +02:00
committed by GitHub
parent 1c3ea9b106
commit f458322303
69 changed files with 1300 additions and 884 deletions

View File

@ -9,10 +9,9 @@ import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/perso
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { MessagingCommonModule } from 'src/modules/messaging/common/messaging-common.module';
import { CalendarEventParticipantModule } from 'src/modules/calendar/calendar-event-participant-manager/calendar-event-participant.module';
import { AutoCompaniesAndContactsCreationMessageChannelListener } from 'src/modules/connected-account/auto-companies-and-contacts-creation/listeners/auto-companies-and-contacts-creation-message-channel.listener';
import { AutoCompaniesAndContactsCreationCalendarChannelListener } from 'src/modules/connected-account/auto-companies-and-contacts-creation/listeners/auto-companies-and-contacts-creation-calendar-channel.listener';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
@Module({
imports: [
@ -22,10 +21,9 @@ import { AutoCompaniesAndContactsCreationCalendarChannelListener } from 'src/mod
PersonWorkspaceEntity,
WorkspaceMemberWorkspaceEntity,
]),
MessagingCommonModule,
WorkspaceDataSourceModule,
CalendarEventParticipantModule,
TypeOrmModule.forFeature([FeatureFlagEntity], 'core'),
TypeOrmModule.forFeature([ObjectMetadataEntity], 'metadata'),
],
providers: [
CreateCompanyAndContactService,

View File

@ -5,9 +5,9 @@ import { v4 } from 'uuid';
import axios, { AxiosInstance } from 'axios';
import { CompanyRepository } from 'src/modules/company/repositories/company.repository';
import { getCompanyNameFromDomainName } from 'src/modules/calendar-messaging-participant/utils/get-company-name-from-domain-name.util';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
import { CompanyWorkspaceEntity } from 'src/modules/company/standard-objects/company.workspace-entity';
import { getCompanyNameFromDomainName } from 'src/modules/connected-account/auto-companies-and-contacts-creation/utils/get-company-name-from-domain-name.util';
@Injectable()
export class CreateCompanyService {
private readonly httpService: AxiosInstance;

View File

@ -4,7 +4,7 @@ import { EntityManager } from 'typeorm';
import { v4 } from 'uuid';
import { PersonRepository } from 'src/modules/person/repositories/person.repository';
import { getFirstNameAndLastNameFromHandleAndDisplayName } from 'src/modules/calendar-messaging-participant/utils/get-first-name-and-last-name-from-handle-and-display-name.util';
import { getFirstNameAndLastNameFromHandleAndDisplayName } from 'src/modules/connected-account/auto-companies-and-contacts-creation/utils/get-first-name-and-last-name-from-handle-and-display-name.util';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';

View File

@ -6,11 +6,11 @@ import { objectRecordChangedProperties } from 'src/engine/integrations/event-emi
import { InjectMessageQueue } from 'src/engine/integrations/message-queue/decorators/message-queue.decorator';
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service';
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import {
CalendarCreateCompanyAndContactAfterSyncJobData,
CalendarCreateCompanyAndContactAfterSyncJob,
} from 'src/modules/messaging/message-participants-manager/jobs/calendar-create-company-and-contact-after-sync.job';
} from 'src/modules/calendar/calendar-event-participant-manager/jobs/calendar-create-company-and-contact-after-sync.job';
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
@Injectable()
export class AutoCompaniesAndContactsCreationCalendarChannelListener {

View File

@ -10,7 +10,7 @@ import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/stan
import {
MessagingCreateCompanyAndContactAfterSyncJobData,
MessagingCreateCompanyAndContactAfterSyncJob,
} from 'src/modules/messaging/message-participants-manager/jobs/messaging-create-company-and-contact-after-sync.job';
} from 'src/modules/messaging/message-participant-manager/jobs/messaging-create-company-and-contact-after-sync.job';
@Injectable()
export class AutoCompaniesAndContactsCreationMessageChannelListener {

View File

@ -1,25 +1,23 @@
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { InjectRepository } from '@nestjs/typeorm';
import chunk from 'lodash.chunk';
import compact from 'lodash.compact';
import { EntityManager } from 'typeorm';
import { EntityManager, Repository } from 'typeorm';
import { ObjectRecordCreateEvent } from 'src/engine/integrations/event-emitter/types/object-record-create.event';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource';
import { InjectWorkspaceDatasource } from 'src/engine/twenty-orm/decorators/inject-workspace-datasource.decorator';
import { getDomainNameFromHandle } from 'src/modules/calendar-messaging-participant/utils/get-domain-name-from-handle.util';
import { CalendarEventParticipantService } from 'src/modules/calendar/calendar-event-participant-manager/services/calendar-event-participant.service';
import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity';
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
import { CONTACTS_CREATION_BATCH_SIZE } from 'src/modules/connected-account/auto-companies-and-contacts-creation/constants/contacts-creation-batch-size.constant';
import { CreateCompanyService } from 'src/modules/connected-account/auto-companies-and-contacts-creation/create-company/create-company.service';
import { CreateContactService } from 'src/modules/connected-account/auto-companies-and-contacts-creation/create-contact/create-contact.service';
import { Contact } from 'src/modules/connected-account/auto-companies-and-contacts-creation/types/contact.type';
import { filterOutSelfAndContactsFromCompanyOrWorkspace } from 'src/modules/connected-account/auto-companies-and-contacts-creation/utils/filter-out-contacts-from-company-or-workspace.util';
import { getDomainNameFromHandle } from 'src/modules/connected-account/auto-companies-and-contacts-creation/utils/get-domain-name-from-handle.util';
import { getUniqueContactsAndHandles } from 'src/modules/connected-account/auto-companies-and-contacts-creation/utils/get-unique-contacts-and-handles.util';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { MessagingMessageParticipantService } from 'src/modules/messaging/common/services/messaging-message-participant.service';
import { MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity';
import { PersonRepository } from 'src/modules/person/repositories/person.repository';
import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
import { WorkspaceMemberRepository } from 'src/modules/workspace-member/repositories/workspace-member.repository';
@ -35,14 +33,12 @@ export class CreateCompanyAndContactService {
private readonly personRepository: PersonRepository,
@InjectObjectMetadataRepository(WorkspaceMemberWorkspaceEntity)
private readonly workspaceMemberRepository: WorkspaceMemberRepository,
@InjectWorkspaceDatasource()
private readonly workspaceDataSource: WorkspaceDataSource,
private readonly messageParticipantService: MessagingMessageParticipantService,
private readonly calendarEventParticipantService: CalendarEventParticipantService,
private readonly eventEmitter: EventEmitter2,
@InjectRepository(ObjectMetadataEntity, 'metadata')
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
) {}
async createCompaniesAndPeople(
private async createCompaniesAndPeople(
connectedAccount: ConnectedAccountWorkspaceEntity,
contactsToCreate: Contact[],
workspaceId: string,
@ -137,47 +133,37 @@ export class CreateCompanyAndContactService {
CONTACTS_CREATION_BATCH_SIZE,
);
// TODO: Remove this when events are emitted directly inside TwentyORM
const objectMetadata = await this.objectMetadataRepository.findOne({
where: {
standardId: STANDARD_OBJECT_IDS.person,
workspaceId,
},
});
if (!objectMetadata) {
throw new Error('Object metadata not found');
}
for (const contactsBatch of contactsBatches) {
let updatedMessageParticipants: MessageParticipantWorkspaceEntity[] = [];
let updatedCalendarEventParticipants: CalendarEventParticipantWorkspaceEntity[] =
[];
await this.workspaceDataSource?.transaction(
async (transactionManager: EntityManager) => {
const createdPeople = await this.createCompaniesAndPeople(
connectedAccount,
contactsBatch,
workspaceId,
transactionManager,
);
updatedMessageParticipants =
await this.messageParticipantService.updateMessageParticipantsAfterPeopleCreation(
createdPeople,
workspaceId,
transactionManager,
);
updatedCalendarEventParticipants =
await this.calendarEventParticipantService.updateCalendarEventParticipantsAfterPeopleCreation(
createdPeople,
workspaceId,
transactionManager,
);
},
const createdPeople = await this.createCompaniesAndPeople(
connectedAccount,
contactsBatch,
workspaceId,
);
this.eventEmitter.emit(`messageParticipant.matched`, {
workspaceId,
workspaceMemberId: connectedAccount.accountOwnerId,
messageParticipants: updatedMessageParticipants,
});
this.eventEmitter.emit(`calendarEventParticipant.matched`, {
workspaceId,
workspaceMemberId: connectedAccount.accountOwnerId,
calendarEventParticipants: updatedCalendarEventParticipants,
});
for (const createdPerson of createdPeople) {
this.eventEmitter.emit('person.created', {
name: 'person.created',
workspaceId,
recordId: createdPerson.id,
objectMetadata,
properties: {
after: createdPerson,
},
} satisfies ObjectRecordCreateEvent<any>);
}
}
}
}

View File

@ -1,4 +1,4 @@
import { getDomainNameFromHandle } from 'src/modules/calendar-messaging-participant/utils/get-domain-name-from-handle.util';
import { getDomainNameFromHandle } from 'src/modules/connected-account/auto-companies-and-contacts-creation/utils/get-domain-name-from-handle.util';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { Contact } from 'src/modules/connected-account/auto-companies-and-contacts-creation/types/contact.type';

View File

@ -0,0 +1,9 @@
import psl from 'psl';
import { capitalize } from 'src/utils/capitalize';
export const getCompanyNameFromDomainName = (domainName: string) => {
const { sld } = psl.parse(domainName);
return sld ? capitalize(sld) : '';
};

View File

@ -0,0 +1,9 @@
import psl from 'psl';
export const getDomainNameFromHandle = (handle: string): string => {
const wholeDomain = handle?.split('@')?.[1] || '';
const { domain } = psl.parse(wholeDomain);
return domain || '';
};

View File

@ -0,0 +1,18 @@
import { capitalize } from 'src/utils/capitalize';
export function getFirstNameAndLastNameFromHandleAndDisplayName(
handle: string,
displayName: string,
): { firstName: string; lastName: string } {
const firstName = displayName.split(' ')[0];
const lastName = displayName.split(' ')[1];
const contactFullNameFromHandle = handle.split('@')[0];
const firstNameFromHandle = contactFullNameFromHandle.split('.')[0];
const lastNameFromHandle = contactFullNameFromHandle.split('.')[1];
return {
firstName: capitalize(firstName || firstNameFromHandle || ''),
lastName: capitalize(lastName || lastNameFromHandle || ''),
};
}

View File

@ -12,7 +12,10 @@ export class OAuth2ClientManagerService {
) {}
public async getOAuth2Client(
connectedAccount: ConnectedAccountWorkspaceEntity,
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
'provider' | 'refreshToken'
>,
): Promise<OAuth2Client> {
const { refreshToken } = connectedAccount;

View File

@ -0,0 +1,25 @@
import { MESSAGING_THROTTLE_DURATION } from 'src/modules/messaging/common/constants/messaging-throttle-duration';
export const isThrottled = (
syncStageStartedAt: string | null,
throttleFailureCount: number,
): boolean => {
if (!syncStageStartedAt) {
return false;
}
return (
computeThrottlePauseUntil(syncStageStartedAt, throttleFailureCount) >
new Date()
);
};
const computeThrottlePauseUntil = (
syncStageStartedAt: string,
throttleFailureCount: number,
): Date => {
return new Date(
new Date(syncStageStartedAt).getTime() +
MESSAGING_THROTTLE_DURATION * Math.pow(2, throttleFailureCount - 1),
);
};