From 94ad0e33ec648a62df8b7388681924f14fd79181 Mon Sep 17 00:00:00 2001 From: bosiraphael <71827178+bosiraphael@users.noreply.github.com> Date: Wed, 14 Feb 2024 17:30:17 +0100 Subject: [PATCH] 3889 activate settingsaccountsemailsinboxsettings (#3962) * update email visibility in settings * improve styling * Add contact auto creation toggle to inbox settings * re move soonpill * update Icon * create job * Add logic to create contacts and companies for message participants without personId and workspaceMemberId * add listener * wip * wip * refactoring * improve structure * Add isContactAutoCreationEnabled method to MessageChannelService * wip * wip * clean * add job * fix bug * contact creation is working * wip * working * improve code * improve typing * resolve conflicts * fix * create company repository * move util * wip * fix --- ...nboxSettingsContactAutoCreationSection.tsx | 4 +- ...AccountsInboxSettingsVisibilitySection.tsx | 24 +-- .../SettingsAccountsEmailsInboxSettings.tsx | 15 +- .../auth/services/google-gmail.service.ts | 4 +- .../integrations/message-queue/jobs.module.ts | 9 + ...e-companies-and-contacts-after-sync.job.ts | 67 +++++++ ...-contact-auto-creation-enabled-listener.ts | 40 ++++ .../workspace/messaging/messaging.module.ts | 8 + .../repositories/company/company.module.ts | 12 ++ .../repositories/company/company.service.ts | 52 +++++ .../message-channel.service.ts | 29 +++ .../message-participant.module.ts | 9 +- .../message-participant.service.ts | 177 +++++++++++------- .../repositories/message/message.module.ts | 4 + .../repositories/message/message.service.ts | 21 ++- .../repositories/person/person.module.ts | 12 ++ .../repositories/person/person.service.ts | 52 +++++ .../create-companies-and-contacts.module.ts | 17 ++ .../create-companies-and-contacts.service.ts | 76 ++++++++ .../create-company/create-company.module.ts | 4 +- .../create-company/create-company.service.ts | 39 ++-- .../create-contact/create-contact.module.ts | 4 +- .../create-contact/create-contact.service.ts | 59 ++---- .../messaging/types/gmail-message.ts | 4 + .../utils/get-domain-name-from-handle.util.ts | 3 + ...-name-from-handle-and-display-name.util.ts | 18 ++ .../message-channel.object-metadata.ts | 8 + 27 files changed, 607 insertions(+), 164 deletions(-) create mode 100644 packages/twenty-server/src/workspace/messaging/jobs/create-companies-and-contacts-after-sync.job.ts create mode 100644 packages/twenty-server/src/workspace/messaging/listeners/is-contact-auto-creation-enabled-listener.ts create mode 100644 packages/twenty-server/src/workspace/messaging/repositories/company/company.module.ts create mode 100644 packages/twenty-server/src/workspace/messaging/repositories/company/company.service.ts create mode 100644 packages/twenty-server/src/workspace/messaging/repositories/person/person.module.ts create mode 100644 packages/twenty-server/src/workspace/messaging/repositories/person/person.service.ts create mode 100644 packages/twenty-server/src/workspace/messaging/services/create-companies-and-contacts/create-companies-and-contacts.module.ts create mode 100644 packages/twenty-server/src/workspace/messaging/services/create-companies-and-contacts/create-companies-and-contacts.service.ts create mode 100644 packages/twenty-server/src/workspace/messaging/utils/get-domain-name-from-handle.util.ts create mode 100644 packages/twenty-server/src/workspace/messaging/utils/get-first-name-and-last-name-from-handle-and-display-name.util.ts diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsInboxSettingsContactAutoCreationSection.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsInboxSettingsContactAutoCreationSection.tsx index 5d8a2a47c..176b3efc5 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsInboxSettingsContactAutoCreationSection.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsInboxSettingsContactAutoCreationSection.tsx @@ -3,7 +3,7 @@ import styled from '@emotion/styled'; import { MessageChannel } from '@/accounts/types/MessageChannel'; import { SettingsAccountsInboxSettingsCardMedia } from '@/settings/accounts/components/SettingsAccountsInboxSettingsCardMedia'; -import { IconSend } from '@/ui/display/icon'; +import { IconUser } from '@/ui/display/icon'; import { H2Title } from '@/ui/display/typography/components/H2Title'; import { Toggle } from '@/ui/input/components/Toggle'; import { Card } from '@/ui/layout/card/components/Card'; @@ -43,7 +43,7 @@ export const SettingsAccountsInboxSettingsContactAutoCreateSection = ({ - + Auto-creation theme.spacing(4)}; - opacity: 0.56; + cursor: pointer; + + &:hover { + background: ${({ theme }) => theme.background.transparent.lighter}; + } `; const StyledCardMedia = styled(SettingsAccountsInboxSettingsCardMedia)` @@ -61,16 +64,6 @@ const StyledRadio = styled(Radio)` margin-left: auto; `; -const StyledSoonPill = styled(SoonPill)` - position: absolute; - right: 0; - top: 0; -`; - -const StyledSection = styled(Section)` - position: relative; -`; - const inboxSettingsVisibilityOptions = [ { title: 'Everything', @@ -108,12 +101,11 @@ export const SettingsAccountsInboxSettingsVisibilitySection = ({ onChange, value = InboxSettingsVisibilityValue.Everything, }: SettingsAccountsInboxSettingsVisibilitySectionProps) => ( - +
- {inboxSettingsVisibilityOptions.map( ( @@ -123,6 +115,7 @@ export const SettingsAccountsInboxSettingsVisibilitySection = ({ onChange(optionValue)} > @@ -137,11 +130,10 @@ export const SettingsAccountsInboxSettingsVisibilitySection = ({ value={optionValue} onCheckedChange={() => onChange(optionValue)} checked={value === optionValue} - disabled={true} /> ), )} - +
); diff --git a/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsEmailsInboxSettings.tsx b/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsEmailsInboxSettings.tsx index 7d2713a5f..6260dbeee 100644 --- a/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsEmailsInboxSettings.tsx +++ b/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsEmailsInboxSettings.tsx @@ -4,6 +4,7 @@ import { useNavigate, useParams } from 'react-router-dom'; import { MessageChannel } from '@/accounts/types/MessageChannel'; import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; +import { SettingsAccountsInboxSettingsContactAutoCreateSection } from '@/settings/accounts/components/SettingsAccountsInboxSettingsContactAutoCreationSection'; import { InboxSettingsVisibilityValue, SettingsAccountsInboxSettingsVisibilitySection, @@ -36,6 +37,15 @@ export const SettingsAccountsEmailsInboxSettings = () => { }); }; + const handleContactAutoCreationToggle = (value: boolean) => { + updateOneRecord({ + idToUpdate: messageChannelId, + updateOneRecordInput: { + isContactAutoCreationEnabled: value, + }, + }); + }; + useEffect(() => { if (!loading && !messageChannel) navigate(AppPath.NotFound); }, [loading, messageChannel, navigate]); @@ -61,11 +71,10 @@ export const SettingsAccountsEmailsInboxSettings = () => { value={messageChannel?.visibility} onChange={handleVisibilityChange} /> - {/* TODO : Add this section when the backend will be ready to auto create contacts */} - {/* */} + /> ); diff --git a/packages/twenty-server/src/core/auth/services/google-gmail.service.ts b/packages/twenty-server/src/core/auth/services/google-gmail.service.ts index 25e33c208..9ddd138be 100644 --- a/packages/twenty-server/src/core/auth/services/google-gmail.service.ts +++ b/packages/twenty-server/src/core/auth/services/google-gmail.service.ts @@ -67,8 +67,8 @@ export class GoogleGmailService { ); await manager.query( - `INSERT INTO ${dataSourceMetadata.schema}."messageChannel" ("visibility", "handle", "connectedAccountId", "type") VALUES ($1, $2, $3, $4)`, - ['share_everything', handle, connectedAccountId, 'email'], + `INSERT INTO ${dataSourceMetadata.schema}."messageChannel" ("visibility", "handle", "connectedAccountId", "type", "isContactAutoCreationEnabled") VALUES ($1, $2, $3, $4, $5)`, + ['share_everything', handle, connectedAccountId, 'email', true], ); }); diff --git a/packages/twenty-server/src/integrations/message-queue/jobs.module.ts b/packages/twenty-server/src/integrations/message-queue/jobs.module.ts index f8e474904..adaba9f10 100644 --- a/packages/twenty-server/src/integrations/message-queue/jobs.module.ts +++ b/packages/twenty-server/src/integrations/message-queue/jobs.module.ts @@ -20,6 +20,9 @@ import { FeatureFlagEntity } from 'src/core/feature-flag/feature-flag.entity'; import { FetchAllWorkspacesMessagesJob } from 'src/workspace/messaging/commands/crons/fetch-all-workspaces-messages.job'; import { ConnectedAccountModule } from 'src/workspace/messaging/repositories/connected-account/connected-account.module'; import { MatchMessageParticipantJob } from 'src/workspace/messaging/jobs/match-message-participant.job'; +import { CreateCompaniesAndContactsAfterSyncJob } from 'src/workspace/messaging/jobs/create-companies-and-contacts-after-sync.job'; +import { CreateCompaniesAndContactsModule } from 'src/workspace/messaging/services/create-companies-and-contacts/create-companies-and-contacts.module'; +import { MessageChannelModule } from 'src/workspace/messaging/repositories/message-channel/message-channel.module'; import { MessageParticipantModule } from 'src/workspace/messaging/repositories/message-participant/message-participant.module'; @Module({ @@ -36,6 +39,8 @@ import { MessageParticipantModule } from 'src/workspace/messaging/repositories/m TypeOrmModule.forFeature([FeatureFlagEntity], 'core'), ConnectedAccountModule, MessageParticipantModule, + CreateCompaniesAndContactsModule, + MessageChannelModule, ], providers: [ { @@ -67,6 +72,10 @@ import { MessageParticipantModule } from 'src/workspace/messaging/repositories/m provide: MatchMessageParticipantJob.name, useClass: MatchMessageParticipantJob, }, + { + provide: CreateCompaniesAndContactsAfterSyncJob.name, + useClass: CreateCompaniesAndContactsAfterSyncJob, + }, ], }) export class JobsModule { diff --git a/packages/twenty-server/src/workspace/messaging/jobs/create-companies-and-contacts-after-sync.job.ts b/packages/twenty-server/src/workspace/messaging/jobs/create-companies-and-contacts-after-sync.job.ts new file mode 100644 index 000000000..efe6504ec --- /dev/null +++ b/packages/twenty-server/src/workspace/messaging/jobs/create-companies-and-contacts-after-sync.job.ts @@ -0,0 +1,67 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import { CreateContactsAndCompaniesAfterSyncJobData } from 'packages/twenty-server/dist/src/workspace/messaging/jobs/create-contacts-and-companies-after-sync.job'; + +import { MessageQueueJob } from 'src/integrations/message-queue/interfaces/message-queue-job.interface'; + +import { CreateCompaniesAndContactsService } from 'src/workspace/messaging/services/create-companies-and-contacts/create-companies-and-contacts.service'; +import { MessageChannelService } from 'src/workspace/messaging/repositories/message-channel/message-channel.service'; +import { MessageParticipantService } from 'src/workspace/messaging/repositories/message-participant/message-participant.service'; + +export type CreateCompaniesAndContactsAfterSyncJobData = { + workspaceId: string; + messageChannelId: string; +}; + +@Injectable() +export class CreateCompaniesAndContactsAfterSyncJob + implements MessageQueueJob +{ + private readonly logger = new Logger( + CreateCompaniesAndContactsAfterSyncJob.name, + ); + constructor( + private readonly createCompaniesAndContactsService: CreateCompaniesAndContactsService, + private readonly messageChannelService: MessageChannelService, + private readonly messageParticipantService: MessageParticipantService, + ) {} + + async handle( + data: CreateContactsAndCompaniesAfterSyncJobData, + ): Promise { + this.logger.log( + `create contacts and companies after sync for workspace ${data.workspaceId} and messageChannel ${data.messageChannelId}`, + ); + const { workspaceId, messageChannelId } = data; + + const isContactAutoCreationEnabled = + await this.messageChannelService.getIsContactAutoCreationEnabledByMessageChannelId( + messageChannelId, + workspaceId, + ); + + if (!isContactAutoCreationEnabled) { + return; + } + + const messageParticipantsWithoutPersonIdAndWorkspaceMemberId = + await this.messageParticipantService.getByMessageChannelIdWithoutPersonIdAndWorkspaceMemberId( + messageChannelId, + workspaceId, + ); + + await this.createCompaniesAndContactsService.createCompaniesAndContacts( + messageParticipantsWithoutPersonIdAndWorkspaceMemberId, + workspaceId, + ); + + await this.messageParticipantService.updateMessageParticipantsAfterPeopleCreation( + messageParticipantsWithoutPersonIdAndWorkspaceMemberId, + workspaceId, + ); + + this.logger.log( + `create contacts and companies after sync for workspace ${data.workspaceId} and messageChannel ${data.messageChannelId} done`, + ); + } +} diff --git a/packages/twenty-server/src/workspace/messaging/listeners/is-contact-auto-creation-enabled-listener.ts b/packages/twenty-server/src/workspace/messaging/listeners/is-contact-auto-creation-enabled-listener.ts new file mode 100644 index 000000000..72d5cee63 --- /dev/null +++ b/packages/twenty-server/src/workspace/messaging/listeners/is-contact-auto-creation-enabled-listener.ts @@ -0,0 +1,40 @@ +import { Injectable, Inject } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; + +import { CreateContactsAndCompaniesAfterSyncJobData } from 'packages/twenty-server/dist/src/workspace/messaging/jobs/create-contacts-and-companies-after-sync.job'; + +import { ObjectRecordUpdateEvent } from 'src/integrations/event-emitter/types/object-record-update.event'; +import { MessageQueue } from 'src/integrations/message-queue/message-queue.constants'; +import { MessageQueueService } from 'src/integrations/message-queue/services/message-queue.service'; +import { MessageChannelObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/message-channel.object-metadata'; +import { objectRecordChangedProperties as objectRecordUpdateEventChangedProperties } from 'src/integrations/event-emitter/utils/object-record-changed-properties.util'; +import { CreateCompaniesAndContactsAfterSyncJob } from 'src/workspace/messaging/jobs/create-companies-and-contacts-after-sync.job'; + +@Injectable() +export class IsContactAutoCreationEnabledListener { + constructor( + @Inject(MessageQueue.messagingQueue) + private readonly messageQueueService: MessageQueueService, + ) {} + + @OnEvent('messageChannel.updated') + handleUpdatedEvent( + payload: ObjectRecordUpdateEvent, + ) { + if ( + objectRecordUpdateEventChangedProperties( + payload.previousRecord, + payload.updatedRecord, + ).includes('isContactAutoCreationEnabled') && + payload.updatedRecord.isContactAutoCreationEnabled + ) { + this.messageQueueService.add( + CreateCompaniesAndContactsAfterSyncJob.name, + { + workspaceId: payload.workspaceId, + messageChannelId: payload.updatedRecord.id, + }, + ); + } + } +} diff --git a/packages/twenty-server/src/workspace/messaging/messaging.module.ts b/packages/twenty-server/src/workspace/messaging/messaging.module.ts index e5f0a3969..f7079e4fb 100644 --- a/packages/twenty-server/src/workspace/messaging/messaging.module.ts +++ b/packages/twenty-server/src/workspace/messaging/messaging.module.ts @@ -18,10 +18,14 @@ import { GmailRefreshAccessTokenService } from 'src/workspace/messaging/services import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module'; import { MessageParticipantModule } from 'src/workspace/messaging/repositories/message-participant/message-participant.module'; import { MessagingWorkspaceMemberListener } from 'src/workspace/messaging/listeners/messaging-workspace-member.listener'; +import { IsContactAutoCreationEnabledListener } from 'src/workspace/messaging/listeners/is-contact-auto-creation-enabled-listener'; import { MessagingMessageChannelListener } from 'src/workspace/messaging/listeners/messaging-message-channel.listener'; import { MessageService } from 'src/workspace/messaging/repositories/message/message.service'; import { WorkspaceMemberModule } from 'src/workspace/messaging/repositories/workspace-member/workspace-member.module'; import { FeatureFlagEntity } from 'src/core/feature-flag/feature-flag.entity'; +import { CreateCompaniesAndContactsModule } from 'src/workspace/messaging/services/create-companies-and-contacts/create-companies-and-contacts.module'; +import { CompanyModule } from 'src/workspace/messaging/repositories/company/company.module'; +import { PersonModule } from 'src/workspace/messaging/repositories/person/person.module'; @Module({ imports: [ EnvironmentModule, @@ -32,8 +36,11 @@ import { FeatureFlagEntity } from 'src/core/feature-flag/feature-flag.entity'; MessageModule, MessageThreadModule, MessageParticipantModule, + CreateCompaniesAndContactsModule, WorkspaceMemberModule, TypeOrmModule.forFeature([FeatureFlagEntity], 'core'), + CompanyModule, + PersonModule, ], providers: [ GmailFullSyncService, @@ -45,6 +52,7 @@ import { FeatureFlagEntity } from 'src/core/feature-flag/feature-flag.entity'; CreateCompanyService, MessagingPersonListener, MessagingWorkspaceMemberListener, + IsContactAutoCreationEnabledListener, MessagingMessageChannelListener, MessageService, ], diff --git a/packages/twenty-server/src/workspace/messaging/repositories/company/company.module.ts b/packages/twenty-server/src/workspace/messaging/repositories/company/company.module.ts new file mode 100644 index 000000000..bab9d7b82 --- /dev/null +++ b/packages/twenty-server/src/workspace/messaging/repositories/company/company.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; + +import { CompanyService } from 'src/workspace/messaging/repositories/company/company.service'; +import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module'; + +// TODO: Move outside of the messaging module +@Module({ + imports: [WorkspaceDataSourceModule], + providers: [CompanyService], + exports: [CompanyService], +}) +export class CompanyModule {} diff --git a/packages/twenty-server/src/workspace/messaging/repositories/company/company.service.ts b/packages/twenty-server/src/workspace/messaging/repositories/company/company.service.ts new file mode 100644 index 000000000..e3f69253b --- /dev/null +++ b/packages/twenty-server/src/workspace/messaging/repositories/company/company.service.ts @@ -0,0 +1,52 @@ +import { Injectable } from '@nestjs/common'; + +import { EntityManager } from 'typeorm'; + +import { WorkspaceDataSourceService } from 'src/workspace/workspace-datasource/workspace-datasource.service'; + +// TODO: Move outside of the messaging module +@Injectable() +export class CompanyService { + constructor( + private readonly workspaceDataSourceService: WorkspaceDataSourceService, + ) {} + + public async getExistingCompaniesByDomainNames( + domainNames: string[], + workspaceId: string, + transactionManager?: EntityManager, + ): Promise<{ id: string; domainName: string }[]> { + const dataSourceSchema = + this.workspaceDataSourceService.getSchemaName(workspaceId); + + const existingCompanies = + await this.workspaceDataSourceService.executeRawQuery( + `SELECT id, "domainName" FROM ${dataSourceSchema}.company WHERE "domainName" = ANY($1)`, + [domainNames], + workspaceId, + transactionManager, + ); + + return existingCompanies; + } + + public async createCompany( + id: string, + name: string, + domainName: string, + city: string, + workspaceId: string, + transactionManager?: EntityManager, + ): Promise { + const dataSourceSchema = + this.workspaceDataSourceService.getSchemaName(workspaceId); + + await this.workspaceDataSourceService.executeRawQuery( + `INSERT INTO ${dataSourceSchema}.company (id, name, "domainName", address) + VALUES ($1, $2, $3, $4)`, + [id, name, domainName, city], + workspaceId, + transactionManager, + ); + } +} diff --git a/packages/twenty-server/src/workspace/messaging/repositories/message-channel/message-channel.service.ts b/packages/twenty-server/src/workspace/messaging/repositories/message-channel/message-channel.service.ts index 8f0673df8..1fb4f6736 100644 --- a/packages/twenty-server/src/workspace/messaging/repositories/message-channel/message-channel.service.ts +++ b/packages/twenty-server/src/workspace/messaging/repositories/message-channel/message-channel.service.ts @@ -44,6 +44,35 @@ export class MessageChannelService { return messageChannels[0]; } + public async getIsContactAutoCreationEnabledByConnectedAccountIdOrFail( + connectedAccountId: string, + workspaceId: string, + ): Promise { + const messageChannel = await this.getFirstByConnectedAccountIdOrFail( + connectedAccountId, + workspaceId, + ); + + return messageChannel.isContactAutoCreationEnabled; + } + + public async getIsContactAutoCreationEnabledByMessageChannelId( + messageChannelId: string, + workspaceId: string, + ): Promise { + const dataSourceSchema = + this.workspaceDataSourceService.getSchemaName(workspaceId); + + const messageChannels = + await this.workspaceDataSourceService.executeRawQuery( + `SELECT * FROM ${dataSourceSchema}."messageChannel" WHERE "id" = $1 LIMIT 1`, + [messageChannelId], + workspaceId, + ); + + return messageChannels[0]?.isContactAutoCreationEnabled; + } + public async getByIds( ids: string[], workspaceId: string, diff --git a/packages/twenty-server/src/workspace/messaging/repositories/message-participant/message-participant.module.ts b/packages/twenty-server/src/workspace/messaging/repositories/message-participant/message-participant.module.ts index 162505973..24864f192 100644 --- a/packages/twenty-server/src/workspace/messaging/repositories/message-participant/message-participant.module.ts +++ b/packages/twenty-server/src/workspace/messaging/repositories/message-participant/message-participant.module.ts @@ -1,16 +1,11 @@ import { Module } from '@nestjs/common'; -import { CreateCompanyModule } from 'src/workspace/messaging/services/create-company/create-company.module'; -import { CreateContactModule } from 'src/workspace/messaging/services/create-contact/create-contact.module'; +import { CreateCompaniesAndContactsModule } from 'src/workspace/messaging/services/create-companies-and-contacts/create-companies-and-contacts.module'; import { MessageParticipantService } from 'src/workspace/messaging/repositories/message-participant/message-participant.service'; import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module'; @Module({ - imports: [ - WorkspaceDataSourceModule, - CreateContactModule, - CreateCompanyModule, - ], + imports: [WorkspaceDataSourceModule, CreateCompaniesAndContactsModule], providers: [MessageParticipantService], exports: [MessageParticipantService], }) diff --git a/packages/twenty-server/src/workspace/messaging/repositories/message-participant/message-participant.service.ts b/packages/twenty-server/src/workspace/messaging/repositories/message-participant/message-participant.service.ts index ce2c0fb8c..d380bd729 100644 --- a/packages/twenty-server/src/workspace/messaging/repositories/message-participant/message-participant.service.ts +++ b/packages/twenty-server/src/workspace/messaging/repositories/message-participant/message-participant.service.ts @@ -5,17 +5,15 @@ import { EntityManager } from 'typeorm'; import { WorkspaceDataSourceService } from 'src/workspace/workspace-datasource/workspace-datasource.service'; import { MessageParticipantObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/message-participant.object-metadata'; import { ObjectRecord } from 'src/workspace/workspace-sync-metadata/types/object-record'; -import { DataSourceEntity } from 'src/metadata/data-source/data-source.entity'; -import { Participant } from 'src/workspace/messaging/types/gmail-message'; -import { CreateContactService } from 'src/workspace/messaging/services/create-contact/create-contact.service'; -import { CreateCompanyService } from 'src/workspace/messaging/services/create-company/create-company.service'; +import { + ParticipantWithId, + Participant, +} from 'src/workspace/messaging/types/gmail-message'; @Injectable() export class MessageParticipantService { constructor( private readonly workspaceDataSourceService: WorkspaceDataSourceService, - private readonly createContactService: CreateContactService, - private readonly createCompaniesService: CreateCompanyService, ) {} public async getByHandles( @@ -68,79 +66,71 @@ export class MessageParticipantService { ); } + public async getByMessageChannelIdWithoutPersonIdAndWorkspaceMemberId( + messageChannelId: string, + workspaceId: string, + transactionManager?: EntityManager, + ): Promise { + if (!messageChannelId || !workspaceId) { + return []; + } + + const dataSourceSchema = + this.workspaceDataSourceService.getSchemaName(workspaceId); + + const messageParticipants: ParticipantWithId[] = + await this.workspaceDataSourceService.executeRawQuery( + `SELECT "messageParticipant".id, + "messageParticipant"."role", + "messageParticipant"."handle", + "messageParticipant"."displayName", + "messageParticipant"."personId", + "messageParticipant"."workspaceMemberId", + "messageParticipant"."messageId" + FROM ${dataSourceSchema}."messageParticipant" "messageParticipant" + LEFT JOIN ${dataSourceSchema}."message" ON "messageParticipant"."messageId" = ${dataSourceSchema}."message"."id" + LEFT JOIN ${dataSourceSchema}."messageChannelMessageAssociation" ON ${dataSourceSchema}."messageChannelMessageAssociation"."messageId" = ${dataSourceSchema}."message"."id" + WHERE ${dataSourceSchema}."messageChannelMessageAssociation"."messageChannelId" = $1 + AND "messageParticipant"."personId" IS NULL + AND "messageParticipant"."workspaceMemberId" IS NULL`, + [messageChannelId], + workspaceId, + transactionManager, + ); + + return messageParticipants; + } + public async saveMessageParticipants( participants: Participant[], messageId: string, - dataSourceMetadata: DataSourceEntity, - manager: EntityManager, + workspaceId: string, + transactionManager?: EntityManager, ): Promise { if (!participants) return; - const alreadyCreatedContacts = await manager.query( - `SELECT email FROM ${dataSourceMetadata.schema}."person" WHERE "email" = ANY($1)`, - [participants.map((participant) => participant.handle)], - ); - - const alreadyCreatedContactEmails: string[] = alreadyCreatedContacts?.map( - ({ email }) => email, - ); - - const filteredParticipants = participants.filter( - (participant) => - !alreadyCreatedContactEmails.includes(participant.handle) && - participant.handle.includes('@'), - ); - - const filteredParticipantsWihCompanyDomainNames = filteredParticipants?.map( - (participant) => ({ - handle: participant.handle, - displayName: participant.displayName, - companyDomainName: participant.handle - .split('@')?.[1] - .split('.') - .slice(-2) - .join('.') - .toLowerCase(), - }), - ); - - const domainNamesToCreate = filteredParticipantsWihCompanyDomainNames.map( - (participant) => participant.companyDomainName, - ); - - const companiesObject = await this.createCompaniesService.createCompanies( - domainNamesToCreate, - dataSourceMetadata, - manager, - ); - - const contactsToCreate = filteredParticipantsWihCompanyDomainNames.map( - (participant) => ({ - handle: participant.handle, - displayName: participant.displayName, - companyId: companiesObject[participant.companyDomainName], - }), - ); - - await this.createContactService.createContacts( - contactsToCreate, - dataSourceMetadata, - manager, - ); + const dataSourceSchema = + this.workspaceDataSourceService.getSchemaName(workspaceId); const handles = participants.map((participant) => participant.handle); - const participantPersonIds = await manager.query( - `SELECT id, email FROM ${dataSourceMetadata.schema}."person" WHERE "email" = ANY($1)`, - [handles], - ); + const participantPersonIds = + await this.workspaceDataSourceService.executeRawQuery( + `SELECT id, email FROM ${dataSourceSchema}."person" WHERE "email" = ANY($1)`, + [handles], + workspaceId, + transactionManager, + ); - const participantWorkspaceMemberIds = await manager.query( - `SELECT "workspaceMember"."id", "connectedAccount"."handle" AS email FROM ${dataSourceMetadata.schema}."workspaceMember" - JOIN ${dataSourceMetadata.schema}."connectedAccount" ON ${dataSourceMetadata.schema}."workspaceMember"."id" = ${dataSourceMetadata.schema}."connectedAccount"."accountOwnerId" - WHERE ${dataSourceMetadata.schema}."connectedAccount"."handle" = ANY($1)`, - [handles], - ); + const participantWorkspaceMemberIds = + await this.workspaceDataSourceService.executeRawQuery( + `SELECT "workspaceMember"."id", "connectedAccount"."handle" AS email FROM ${dataSourceSchema}."workspaceMember" + JOIN ${dataSourceSchema}."connectedAccount" ON ${dataSourceSchema}."workspaceMember"."id" = ${dataSourceSchema}."connectedAccount"."accountOwnerId" + WHERE ${dataSourceSchema}."connectedAccount"."handle" = ANY($1)`, + [handles], + workspaceId, + transactionManager, + ); const messageParticipantsToSave = participants.map((participant) => [ messageId, @@ -163,9 +153,54 @@ export class MessageParticipantService { if (messageParticipantsToSave.length === 0) return; - await manager.query( - `INSERT INTO ${dataSourceMetadata.schema}."messageParticipant" ("messageId", "role", "handle", "displayName", "personId", "workspaceMemberId") VALUES ${valuesString}`, + await this.workspaceDataSourceService.executeRawQuery( + `INSERT INTO ${dataSourceSchema}."messageParticipant" ("messageId", "role", "handle", "displayName", "personId", "workspaceMemberId") VALUES ${valuesString}`, messageParticipantsToSave.flat(), + workspaceId, + transactionManager, + ); + } + + public async updateMessageParticipantsAfterPeopleCreation( + participants: ParticipantWithId[], + workspaceId: string, + transactionManager?: EntityManager, + ): Promise { + if (!participants) return; + + const dataSourceSchema = + this.workspaceDataSourceService.getSchemaName(workspaceId); + + const handles = participants.map((participant) => participant.handle); + + const participantPersonIds = + await this.workspaceDataSourceService.executeRawQuery( + `SELECT id, email FROM ${dataSourceSchema}."person" WHERE "email" = ANY($1)`, + [handles], + workspaceId, + transactionManager, + ); + + const messageParticipantsToUpdate = participants.map((participant) => [ + participant.id, + participantPersonIds.find( + (e: { id: string; email: string }) => e.email === participant.handle, + )?.id, + ]); + + if (messageParticipantsToUpdate.length === 0) return; + + const valuesString = messageParticipantsToUpdate + .map((_, index) => `($${index * 2 + 1}::uuid, $${index * 2 + 2}::uuid)`) + .join(', '); + + await this.workspaceDataSourceService.executeRawQuery( + `UPDATE ${dataSourceSchema}."messageParticipant" AS "messageParticipant" SET "personId" = "data"."personId" + FROM (VALUES ${valuesString}) AS "data"("id", "personId") + WHERE "messageParticipant"."id" = "data"."id"`, + messageParticipantsToUpdate.flat(), + workspaceId, + transactionManager, ); } } diff --git a/packages/twenty-server/src/workspace/messaging/repositories/message/message.module.ts b/packages/twenty-server/src/workspace/messaging/repositories/message/message.module.ts index ce9a95db9..e7b1fed3d 100644 --- a/packages/twenty-server/src/workspace/messaging/repositories/message/message.module.ts +++ b/packages/twenty-server/src/workspace/messaging/repositories/message/message.module.ts @@ -1,10 +1,12 @@ import { Module } from '@nestjs/common'; +import { MessageChannelModule } from 'src/workspace/messaging/repositories/message-channel/message-channel.module'; import { MessageChannelMessageAssociationModule } from 'src/workspace/messaging/repositories/message-channel-message-association/message-channel-message-assocation.module'; import { MessageParticipantModule } from 'src/workspace/messaging/repositories/message-participant/message-participant.module'; import { MessageThreadModule } from 'src/workspace/messaging/repositories/message-thread/message-thread.module'; import { MessageService } from 'src/workspace/messaging/repositories/message/message.service'; import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module'; +import { CreateCompaniesAndContactsModule } from 'src/workspace/messaging/services/create-companies-and-contacts/create-companies-and-contacts.module'; @Module({ imports: [ @@ -12,6 +14,8 @@ import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/wo MessageThreadModule, MessageParticipantModule, MessageChannelMessageAssociationModule, + MessageChannelModule, + CreateCompaniesAndContactsModule, ], providers: [MessageService], exports: [MessageService], diff --git a/packages/twenty-server/src/workspace/messaging/repositories/message/message.service.ts b/packages/twenty-server/src/workspace/messaging/repositories/message/message.service.ts index 8f5ab9008..08a59632b 100644 --- a/packages/twenty-server/src/workspace/messaging/repositories/message/message.service.ts +++ b/packages/twenty-server/src/workspace/messaging/repositories/message/message.service.ts @@ -9,10 +9,12 @@ import { ObjectRecord } from 'src/workspace/workspace-sync-metadata/types/object import { DataSourceEntity } from 'src/metadata/data-source/data-source.entity'; import { GmailMessage } from 'src/workspace/messaging/types/gmail-message'; import { ConnectedAccountObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/connected-account.object-metadata'; +import { MessageChannelService } from 'src/workspace/messaging/repositories/message-channel/message-channel.service'; import { MessageChannelMessageAssociationService } from 'src/workspace/messaging/repositories/message-channel-message-association/message-channel-message-association.service'; import { MessageParticipantService } from 'src/workspace/messaging/repositories/message-participant/message-participant.service'; import { MessageThreadService } from 'src/workspace/messaging/repositories/message-thread/message-thread.service'; import { isPersonEmail } from 'src/workspace/messaging/utils/is-person-email.util'; +import { CreateCompaniesAndContactsService } from 'src/workspace/messaging/services/create-companies-and-contacts/create-companies-and-contacts.service'; @Injectable() export class MessageService { constructor( @@ -20,6 +22,8 @@ export class MessageService { private readonly messageChannelMessageAssociationService: MessageChannelMessageAssociationService, private readonly messageThreadService: MessageThreadService, private readonly messageParticipantService: MessageParticipantService, + private readonly messageChannelService: MessageChannelService, + private readonly createCompaniesAndContactsService: CreateCompaniesAndContactsService, ) {} public async getFirstByHeaderMessageId( @@ -193,11 +197,24 @@ export class MessageService { ], ); + const isContactAutoCreationEnabled = + await this.messageChannelService.getIsContactAutoCreationEnabledByConnectedAccountIdOrFail( + connectedAccount.id, + workspaceId, + ); + + if (isContactAutoCreationEnabled) { + await this.createCompaniesAndContactsService.createCompaniesAndContacts( + message.participants, + workspaceId, + manager, + ); + } + await this.messageParticipantService.saveMessageParticipants( message.participants, newMessageId, - dataSourceMetadata, - manager, + workspaceId, ); return Promise.resolve(newMessageId); diff --git a/packages/twenty-server/src/workspace/messaging/repositories/person/person.module.ts b/packages/twenty-server/src/workspace/messaging/repositories/person/person.module.ts new file mode 100644 index 000000000..d74ef65b1 --- /dev/null +++ b/packages/twenty-server/src/workspace/messaging/repositories/person/person.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; + +import { PersonService } from 'src/workspace/messaging/repositories/person/person.service'; +import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module'; + +// TODO: Move outside of the messaging module +@Module({ + imports: [WorkspaceDataSourceModule], + providers: [PersonService], + exports: [PersonService], +}) +export class PersonModule {} diff --git a/packages/twenty-server/src/workspace/messaging/repositories/person/person.service.ts b/packages/twenty-server/src/workspace/messaging/repositories/person/person.service.ts new file mode 100644 index 000000000..50f18aba8 --- /dev/null +++ b/packages/twenty-server/src/workspace/messaging/repositories/person/person.service.ts @@ -0,0 +1,52 @@ +import { Injectable } from '@nestjs/common'; + +import { EntityManager } from 'typeorm'; + +import { WorkspaceDataSourceService } from 'src/workspace/workspace-datasource/workspace-datasource.service'; + +// TODO: Move outside of the messaging module +@Injectable() +export class PersonService { + constructor( + private readonly workspaceDataSourceService: WorkspaceDataSourceService, + ) {} + + async createPeople( + peopleToCreate: { + id: string; + handle: string; + firstName: string; + lastName: string; + companyId: string; + }[], + workspaceId: string, + transactionManager?: EntityManager, + ): Promise { + const dataSourceSchema = + this.workspaceDataSourceService.getSchemaName(workspaceId); + + const valuesString = peopleToCreate + .map( + (_, index) => + `($${index * 5 + 1}, $${index * 5 + 2}, $${index * 5 + 3}, $${ + index * 5 + 4 + }, $${index * 5 + 5})`, + ) + .join(', '); + + return await this.workspaceDataSourceService.executeRawQuery( + `INSERT INTO ${dataSourceSchema}.person (id, email, "nameFirstName", "nameLastName", "companyId") VALUES ${valuesString}`, + peopleToCreate + .map((contact) => [ + contact.id, + contact.handle, + contact.firstName, + contact.lastName, + contact.companyId, + ]) + .flat(), + workspaceId, + transactionManager, + ); + } +} diff --git a/packages/twenty-server/src/workspace/messaging/services/create-companies-and-contacts/create-companies-and-contacts.module.ts b/packages/twenty-server/src/workspace/messaging/services/create-companies-and-contacts/create-companies-and-contacts.module.ts new file mode 100644 index 000000000..c997b155d --- /dev/null +++ b/packages/twenty-server/src/workspace/messaging/services/create-companies-and-contacts/create-companies-and-contacts.module.ts @@ -0,0 +1,17 @@ +import { Module } from '@nestjs/common'; + +import { CreateCompaniesAndContactsService } from 'src/workspace/messaging/services/create-companies-and-contacts/create-companies-and-contacts.service'; +import { CreateCompanyModule } from 'src/workspace/messaging/services/create-company/create-company.module'; +import { CreateContactModule } from 'src/workspace/messaging/services/create-contact/create-contact.module'; +import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module'; + +@Module({ + imports: [ + WorkspaceDataSourceModule, + CreateContactModule, + CreateCompanyModule, + ], + providers: [CreateCompaniesAndContactsService], + exports: [CreateCompaniesAndContactsService], +}) +export class CreateCompaniesAndContactsModule {} diff --git a/packages/twenty-server/src/workspace/messaging/services/create-companies-and-contacts/create-companies-and-contacts.service.ts b/packages/twenty-server/src/workspace/messaging/services/create-companies-and-contacts/create-companies-and-contacts.service.ts new file mode 100644 index 000000000..9d953df9e --- /dev/null +++ b/packages/twenty-server/src/workspace/messaging/services/create-companies-and-contacts/create-companies-and-contacts.service.ts @@ -0,0 +1,76 @@ +import { Injectable } from '@nestjs/common'; + +import { EntityManager } from 'typeorm'; + +import { WorkspaceDataSourceService } from 'src/workspace/workspace-datasource/workspace-datasource.service'; +import { Participant } from 'src/workspace/messaging/types/gmail-message'; +import { getDomainNameFromHandle } from 'src/workspace/messaging/utils/get-domain-name-from-handle.util'; +import { CreateCompanyService } from 'src/workspace/messaging/services/create-company/create-company.service'; +import { CreateContactService } from 'src/workspace/messaging/services/create-contact/create-contact.service'; + +@Injectable() +export class CreateCompaniesAndContactsService { + constructor( + private readonly workspaceDataSourceService: WorkspaceDataSourceService, + private readonly createContactService: CreateContactService, + private readonly createCompaniesService: CreateCompanyService, + ) {} + + async createCompaniesAndContacts( + participants: Participant[], + workspaceId: string, + transactionManager?: EntityManager, + ) { + const dataSourceSchema = + this.workspaceDataSourceService.getSchemaName(workspaceId); + + const alreadyCreatedContacts = + await this.workspaceDataSourceService.executeRawQuery( + `SELECT email FROM ${dataSourceSchema}."person" WHERE "email" = ANY($1)`, + [participants.map((participant) => participant.handle)], + workspaceId, + transactionManager, + ); + + const alreadyCreatedContactEmails: string[] = alreadyCreatedContacts?.map( + ({ email }) => email, + ); + + const filteredParticipants = participants.filter( + (participant) => + !alreadyCreatedContactEmails.includes(participant.handle) && + participant.handle.includes('@'), + ); + + const filteredParticipantsWithCompanyDomainNames = + filteredParticipants?.map((participant) => ({ + handle: participant.handle, + displayName: participant.displayName, + companyDomainName: getDomainNameFromHandle(participant.handle), + })); + + const domainNamesToCreate = filteredParticipantsWithCompanyDomainNames.map( + (participant) => participant.companyDomainName, + ); + + const companiesObject = await this.createCompaniesService.createCompanies( + domainNamesToCreate, + workspaceId, + transactionManager, + ); + + const contactsToCreate = filteredParticipantsWithCompanyDomainNames.map( + (participant) => ({ + handle: participant.handle, + displayName: participant.displayName, + companyId: companiesObject[participant.companyDomainName], + }), + ); + + await this.createContactService.createContacts( + contactsToCreate, + workspaceId, + transactionManager, + ); + } +} diff --git a/packages/twenty-server/src/workspace/messaging/services/create-company/create-company.module.ts b/packages/twenty-server/src/workspace/messaging/services/create-company/create-company.module.ts index 85df4a1c5..b9ce318f7 100644 --- a/packages/twenty-server/src/workspace/messaging/services/create-company/create-company.module.ts +++ b/packages/twenty-server/src/workspace/messaging/services/create-company/create-company.module.ts @@ -1,9 +1,11 @@ import { Module } from '@nestjs/common'; +import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module'; import { CreateCompanyService } from 'src/workspace/messaging/services/create-company/create-company.service'; +import { CompanyModule } from 'src/workspace/messaging/repositories/company/company.module'; @Module({ - imports: [], + imports: [WorkspaceDataSourceModule, CompanyModule], providers: [CreateCompanyService], exports: [CreateCompanyService], }) diff --git a/packages/twenty-server/src/workspace/messaging/services/create-company/create-company.service.ts b/packages/twenty-server/src/workspace/messaging/services/create-company/create-company.service.ts index 67d45e770..cf3077e52 100644 --- a/packages/twenty-server/src/workspace/messaging/services/create-company/create-company.service.ts +++ b/packages/twenty-server/src/workspace/messaging/services/create-company/create-company.service.ts @@ -1,16 +1,16 @@ import { Injectable } from '@nestjs/common'; import { EntityManager } from 'typeorm'; -import axios, { AxiosInstance } from 'axios'; import { v4 } from 'uuid'; +import axios, { AxiosInstance } from 'axios'; -import { DataSourceEntity } from 'src/metadata/data-source/data-source.entity'; +import { CompanyService } from 'src/workspace/messaging/repositories/company/company.service'; import { capitalize } from 'src/utils/capitalize'; @Injectable() export class CreateCompanyService { private readonly httpService: AxiosInstance; - constructor() { + constructor(private readonly companyService: CompanyService) { this.httpService = axios.create({ baseURL: 'https://companies.twenty.com', }); @@ -18,17 +18,19 @@ export class CreateCompanyService { async createCompanies( domainNames: string[], - dataSourceMetadata: DataSourceEntity, - manager: EntityManager, + workspaceId: string, + transactionManager?: EntityManager, ): Promise<{ [domainName: string]: string; }> { const uniqueDomainNames = [...new Set(domainNames)]; - const existingCompanies = await manager.query( - `SELECT id, "domainName" FROM ${dataSourceMetadata.schema}.company WHERE "domainName" = ANY($1)`, - [uniqueDomainNames], - ); + const existingCompanies = + await this.companyService.getExistingCompaniesByDomainNames( + uniqueDomainNames, + workspaceId, + transactionManager, + ); const companiesObject = existingCompanies.reduce( ( @@ -57,8 +59,8 @@ export class CreateCompanyService { for (const domainName of filteredDomainNames) { companiesObject[domainName] = await this.createCompany( domainName, - dataSourceMetadata, - manager, + workspaceId, + transactionManager, ); } @@ -67,17 +69,20 @@ export class CreateCompanyService { async createCompany( domainName: string, - dataSourceMetadata: DataSourceEntity, - manager: EntityManager, + workspaceId: string, + transactionManager?: EntityManager, ): Promise { const companyId = v4(); const { name, city } = await this.getCompanyInfoFromDomainName(domainName); - await manager.query( - `INSERT INTO ${dataSourceMetadata.schema}.company (id, name, "domainName", address) - VALUES ($1, $2, $3, $4)`, - [companyId, name, domainName, city], + this.companyService.createCompany( + companyId, + name, + domainName, + city, + workspaceId, + transactionManager, ); return companyId; diff --git a/packages/twenty-server/src/workspace/messaging/services/create-contact/create-contact.module.ts b/packages/twenty-server/src/workspace/messaging/services/create-contact/create-contact.module.ts index 0c7200784..6a20b9fe6 100644 --- a/packages/twenty-server/src/workspace/messaging/services/create-contact/create-contact.module.ts +++ b/packages/twenty-server/src/workspace/messaging/services/create-contact/create-contact.module.ts @@ -1,9 +1,11 @@ import { Module } from '@nestjs/common'; +import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module'; import { CreateContactService } from 'src/workspace/messaging/services/create-contact/create-contact.service'; +import { PersonModule } from 'src/workspace/messaging/repositories/person/person.module'; @Module({ - imports: [], + imports: [WorkspaceDataSourceModule, PersonModule], providers: [CreateContactService], exports: [CreateContactService], }) diff --git a/packages/twenty-server/src/workspace/messaging/services/create-contact/create-contact.service.ts b/packages/twenty-server/src/workspace/messaging/services/create-contact/create-contact.service.ts index 478aa2524..e10ee7034 100644 --- a/packages/twenty-server/src/workspace/messaging/services/create-contact/create-contact.service.ts +++ b/packages/twenty-server/src/workspace/messaging/services/create-contact/create-contact.service.ts @@ -3,8 +3,8 @@ import { Injectable } from '@nestjs/common'; import { EntityManager } from 'typeorm'; import { v4 } from 'uuid'; -import { DataSourceEntity } from 'src/metadata/data-source/data-source.entity'; -import { capitalize } from 'src/utils/capitalize'; +import { PersonService } from 'src/workspace/messaging/repositories/person/person.service'; +import { getFirstNameAndLastNameFromHandleAndDisplayName } from 'src/workspace/messaging/utils/get-first-name-and-last-name-from-handle-and-display-name.util'; type ContactToCreate = { handle: string; @@ -22,67 +22,42 @@ type FormattedContactToCreate = { @Injectable() export class CreateContactService { - constructor() {} + constructor(private readonly personService: PersonService) {} - formatContacts( + public formatContacts( contactsToCreate: ContactToCreate[], ): FormattedContactToCreate[] { return contactsToCreate.map((contact) => { + const id = v4(); + const { handle, displayName, companyId } = contact; - const contactFirstName = displayName.split(' ')[0]; - const contactLastName = displayName.split(' ')[1]; - - const contactFullNameFromHandle = handle.split('@')[0]; - const contactFirstNameFromHandle = - contactFullNameFromHandle.split('.')[0]; - const contactLastNameFromHandle = contactFullNameFromHandle.split('.')[1]; - - const id = v4(); + const { firstName, lastName } = + getFirstNameAndLastNameFromHandleAndDisplayName(handle, displayName); return { id, handle, - firstName: capitalize( - contactFirstName || contactFirstNameFromHandle || '', - ), - lastName: capitalize( - contactLastName || contactLastNameFromHandle || '', - ), + firstName, + lastName, companyId, }; }); } - async createContacts( + public async createContacts( contactsToCreate: ContactToCreate[], - dataSourceMetadata: DataSourceEntity, - manager: EntityManager, + workspaceId: string, + transactionManager?: EntityManager, ): Promise { if (contactsToCreate.length === 0) return; const formattedContacts = this.formatContacts(contactsToCreate); - const valuesString = formattedContacts - .map( - (_, index) => - `($${index * 5 + 1}, $${index * 5 + 2}, $${index * 5 + 3}, $${ - index * 5 + 4 - }, $${index * 5 + 5})`, - ) - .join(', '); - - await manager.query( - `INSERT INTO ${dataSourceMetadata.schema}.person (id, email, "nameFirstName", "nameLastName", "companyId") VALUES ${valuesString}`, - formattedContacts - .map((contact) => [ - contact.id, - contact.handle, - contact.firstName, - contact.lastName, - contact.companyId, - ]) - .flat(), + await this.personService.createPeople( + formattedContacts, + workspaceId, + transactionManager, ); } } diff --git a/packages/twenty-server/src/workspace/messaging/types/gmail-message.ts b/packages/twenty-server/src/workspace/messaging/types/gmail-message.ts index 006398a5a..1bf268e7f 100644 --- a/packages/twenty-server/src/workspace/messaging/types/gmail-message.ts +++ b/packages/twenty-server/src/workspace/messaging/types/gmail-message.ts @@ -20,3 +20,7 @@ export type Participant = { handle: string; displayName: string; }; + +export type ParticipantWithId = Participant & { + id: string; +}; diff --git a/packages/twenty-server/src/workspace/messaging/utils/get-domain-name-from-handle.util.ts b/packages/twenty-server/src/workspace/messaging/utils/get-domain-name-from-handle.util.ts new file mode 100644 index 000000000..75bc941d7 --- /dev/null +++ b/packages/twenty-server/src/workspace/messaging/utils/get-domain-name-from-handle.util.ts @@ -0,0 +1,3 @@ +export function getDomainNameFromHandle(handle: string): string { + return handle.split('@')?.[1].split('.').slice(-2).join('.').toLowerCase(); +} diff --git a/packages/twenty-server/src/workspace/messaging/utils/get-first-name-and-last-name-from-handle-and-display-name.util.ts b/packages/twenty-server/src/workspace/messaging/utils/get-first-name-and-last-name-from-handle-and-display-name.util.ts new file mode 100644 index 000000000..c5655e7b5 --- /dev/null +++ b/packages/twenty-server/src/workspace/messaging/utils/get-first-name-and-last-name-from-handle-and-display-name.util.ts @@ -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 || ''), + }; +} diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/message-channel.object-metadata.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/message-channel.object-metadata.ts index eb0cd14fe..e6208cf14 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/message-channel.object-metadata.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/standard-objects/message-channel.object-metadata.ts @@ -71,6 +71,14 @@ export class MessageChannelObjectMetadata extends BaseObjectMetadata { }) type: string; + @FieldMetadata({ + type: FieldMetadataType.BOOLEAN, + label: 'Is Contact Auto Creation Enabled', + description: 'Is Contact Auto Creation Enabled', + icon: 'IconUserCircle', + }) + isContactAutoCreationEnabled: boolean; + @FieldMetadata({ type: FieldMetadataType.RELATION, label: 'Message Channel Association',