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
This commit is contained in:
@ -3,7 +3,7 @@ import styled from '@emotion/styled';
|
|||||||
|
|
||||||
import { MessageChannel } from '@/accounts/types/MessageChannel';
|
import { MessageChannel } from '@/accounts/types/MessageChannel';
|
||||||
import { SettingsAccountsInboxSettingsCardMedia } from '@/settings/accounts/components/SettingsAccountsInboxSettingsCardMedia';
|
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 { H2Title } from '@/ui/display/typography/components/H2Title';
|
||||||
import { Toggle } from '@/ui/input/components/Toggle';
|
import { Toggle } from '@/ui/input/components/Toggle';
|
||||||
import { Card } from '@/ui/layout/card/components/Card';
|
import { Card } from '@/ui/layout/card/components/Card';
|
||||||
@ -43,7 +43,7 @@ export const SettingsAccountsInboxSettingsContactAutoCreateSection = ({
|
|||||||
<Card>
|
<Card>
|
||||||
<StyledCardContent>
|
<StyledCardContent>
|
||||||
<SettingsAccountsInboxSettingsCardMedia>
|
<SettingsAccountsInboxSettingsCardMedia>
|
||||||
<IconSend size={theme.icon.size.sm} stroke={theme.icon.stroke.lg} />
|
<IconUser size={theme.icon.size.sm} stroke={theme.icon.stroke.lg} />
|
||||||
</SettingsAccountsInboxSettingsCardMedia>
|
</SettingsAccountsInboxSettingsCardMedia>
|
||||||
<StyledTitle>Auto-creation</StyledTitle>
|
<StyledTitle>Auto-creation</StyledTitle>
|
||||||
<Toggle
|
<Toggle
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { SoonPill } from 'tsup.ui.index';
|
|
||||||
|
|
||||||
import { SettingsAccountsInboxSettingsCardMedia } from '@/settings/accounts/components/SettingsAccountsInboxSettingsCardMedia';
|
import { SettingsAccountsInboxSettingsCardMedia } from '@/settings/accounts/components/SettingsAccountsInboxSettingsCardMedia';
|
||||||
import { H2Title } from '@/ui/display/typography/components/H2Title';
|
import { H2Title } from '@/ui/display/typography/components/H2Title';
|
||||||
@ -23,7 +22,11 @@ const StyledCardContent = styled(CardContent)`
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: ${({ theme }) => theme.spacing(4)};
|
gap: ${({ theme }) => theme.spacing(4)};
|
||||||
opacity: 0.56;
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: ${({ theme }) => theme.background.transparent.lighter};
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledCardMedia = styled(SettingsAccountsInboxSettingsCardMedia)`
|
const StyledCardMedia = styled(SettingsAccountsInboxSettingsCardMedia)`
|
||||||
@ -61,16 +64,6 @@ const StyledRadio = styled(Radio)`
|
|||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledSoonPill = styled(SoonPill)`
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledSection = styled(Section)`
|
|
||||||
position: relative;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const inboxSettingsVisibilityOptions = [
|
const inboxSettingsVisibilityOptions = [
|
||||||
{
|
{
|
||||||
title: 'Everything',
|
title: 'Everything',
|
||||||
@ -108,12 +101,11 @@ export const SettingsAccountsInboxSettingsVisibilitySection = ({
|
|||||||
onChange,
|
onChange,
|
||||||
value = InboxSettingsVisibilityValue.Everything,
|
value = InboxSettingsVisibilityValue.Everything,
|
||||||
}: SettingsAccountsInboxSettingsVisibilitySectionProps) => (
|
}: SettingsAccountsInboxSettingsVisibilitySectionProps) => (
|
||||||
<StyledSection>
|
<Section>
|
||||||
<H2Title
|
<H2Title
|
||||||
title="Email visibility"
|
title="Email visibility"
|
||||||
description="Define what will be visible to other users in your workspace"
|
description="Define what will be visible to other users in your workspace"
|
||||||
/>
|
/>
|
||||||
<StyledSoonPill />
|
|
||||||
<Card>
|
<Card>
|
||||||
{inboxSettingsVisibilityOptions.map(
|
{inboxSettingsVisibilityOptions.map(
|
||||||
(
|
(
|
||||||
@ -123,6 +115,7 @@ export const SettingsAccountsInboxSettingsVisibilitySection = ({
|
|||||||
<StyledCardContent
|
<StyledCardContent
|
||||||
key={optionValue}
|
key={optionValue}
|
||||||
divider={index < inboxSettingsVisibilityOptions.length - 1}
|
divider={index < inboxSettingsVisibilityOptions.length - 1}
|
||||||
|
onClick={() => onChange(optionValue)}
|
||||||
>
|
>
|
||||||
<StyledCardMedia>
|
<StyledCardMedia>
|
||||||
<StyledMetadataSkeleton isActive={visibleElements.metadata} />
|
<StyledMetadataSkeleton isActive={visibleElements.metadata} />
|
||||||
@ -137,11 +130,10 @@ export const SettingsAccountsInboxSettingsVisibilitySection = ({
|
|||||||
value={optionValue}
|
value={optionValue}
|
||||||
onCheckedChange={() => onChange(optionValue)}
|
onCheckedChange={() => onChange(optionValue)}
|
||||||
checked={value === optionValue}
|
checked={value === optionValue}
|
||||||
disabled={true}
|
|
||||||
/>
|
/>
|
||||||
</StyledCardContent>
|
</StyledCardContent>
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
</StyledSection>
|
</Section>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { useNavigate, useParams } from 'react-router-dom';
|
|||||||
import { MessageChannel } from '@/accounts/types/MessageChannel';
|
import { MessageChannel } from '@/accounts/types/MessageChannel';
|
||||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||||
|
import { SettingsAccountsInboxSettingsContactAutoCreateSection } from '@/settings/accounts/components/SettingsAccountsInboxSettingsContactAutoCreationSection';
|
||||||
import {
|
import {
|
||||||
InboxSettingsVisibilityValue,
|
InboxSettingsVisibilityValue,
|
||||||
SettingsAccountsInboxSettingsVisibilitySection,
|
SettingsAccountsInboxSettingsVisibilitySection,
|
||||||
@ -36,6 +37,15 @@ export const SettingsAccountsEmailsInboxSettings = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleContactAutoCreationToggle = (value: boolean) => {
|
||||||
|
updateOneRecord({
|
||||||
|
idToUpdate: messageChannelId,
|
||||||
|
updateOneRecordInput: {
|
||||||
|
isContactAutoCreationEnabled: value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loading && !messageChannel) navigate(AppPath.NotFound);
|
if (!loading && !messageChannel) navigate(AppPath.NotFound);
|
||||||
}, [loading, messageChannel, navigate]);
|
}, [loading, messageChannel, navigate]);
|
||||||
@ -61,11 +71,10 @@ export const SettingsAccountsEmailsInboxSettings = () => {
|
|||||||
value={messageChannel?.visibility}
|
value={messageChannel?.visibility}
|
||||||
onChange={handleVisibilityChange}
|
onChange={handleVisibilityChange}
|
||||||
/>
|
/>
|
||||||
{/* TODO : Add this section when the backend will be ready to auto create contacts */}
|
<SettingsAccountsInboxSettingsContactAutoCreateSection
|
||||||
{/* <SettingsAccountsInboxSettingsContactAutoCreateSection
|
|
||||||
messageChannel={messageChannel}
|
messageChannel={messageChannel}
|
||||||
onToggle={handleContactAutoCreationToggle}
|
onToggle={handleContactAutoCreationToggle}
|
||||||
/> */}
|
/>
|
||||||
</SettingsPageContainer>
|
</SettingsPageContainer>
|
||||||
</SubMenuTopBarContainer>
|
</SubMenuTopBarContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -67,8 +67,8 @@ export class GoogleGmailService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await manager.query(
|
await manager.query(
|
||||||
`INSERT INTO ${dataSourceMetadata.schema}."messageChannel" ("visibility", "handle", "connectedAccountId", "type") VALUES ($1, $2, $3, $4)`,
|
`INSERT INTO ${dataSourceMetadata.schema}."messageChannel" ("visibility", "handle", "connectedAccountId", "type", "isContactAutoCreationEnabled") VALUES ($1, $2, $3, $4, $5)`,
|
||||||
['share_everything', handle, connectedAccountId, 'email'],
|
['share_everything', handle, connectedAccountId, 'email', true],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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 { 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 { ConnectedAccountModule } from 'src/workspace/messaging/repositories/connected-account/connected-account.module';
|
||||||
import { MatchMessageParticipantJob } from 'src/workspace/messaging/jobs/match-message-participant.job';
|
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';
|
import { MessageParticipantModule } from 'src/workspace/messaging/repositories/message-participant/message-participant.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@ -36,6 +39,8 @@ import { MessageParticipantModule } from 'src/workspace/messaging/repositories/m
|
|||||||
TypeOrmModule.forFeature([FeatureFlagEntity], 'core'),
|
TypeOrmModule.forFeature([FeatureFlagEntity], 'core'),
|
||||||
ConnectedAccountModule,
|
ConnectedAccountModule,
|
||||||
MessageParticipantModule,
|
MessageParticipantModule,
|
||||||
|
CreateCompaniesAndContactsModule,
|
||||||
|
MessageChannelModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
@ -67,6 +72,10 @@ import { MessageParticipantModule } from 'src/workspace/messaging/repositories/m
|
|||||||
provide: MatchMessageParticipantJob.name,
|
provide: MatchMessageParticipantJob.name,
|
||||||
useClass: MatchMessageParticipantJob,
|
useClass: MatchMessageParticipantJob,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: CreateCompaniesAndContactsAfterSyncJob.name,
|
||||||
|
useClass: CreateCompaniesAndContactsAfterSyncJob,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class JobsModule {
|
export class JobsModule {
|
||||||
|
|||||||
@ -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<CreateCompaniesAndContactsAfterSyncJobData>
|
||||||
|
{
|
||||||
|
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<void> {
|
||||||
|
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`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<MessageChannelObjectMetadata>,
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
objectRecordUpdateEventChangedProperties(
|
||||||
|
payload.previousRecord,
|
||||||
|
payload.updatedRecord,
|
||||||
|
).includes('isContactAutoCreationEnabled') &&
|
||||||
|
payload.updatedRecord.isContactAutoCreationEnabled
|
||||||
|
) {
|
||||||
|
this.messageQueueService.add<CreateContactsAndCompaniesAfterSyncJobData>(
|
||||||
|
CreateCompaniesAndContactsAfterSyncJob.name,
|
||||||
|
{
|
||||||
|
workspaceId: payload.workspaceId,
|
||||||
|
messageChannelId: payload.updatedRecord.id,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -18,10 +18,14 @@ import { GmailRefreshAccessTokenService } from 'src/workspace/messaging/services
|
|||||||
import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module';
|
import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module';
|
||||||
import { MessageParticipantModule } from 'src/workspace/messaging/repositories/message-participant/message-participant.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 { 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 { MessagingMessageChannelListener } from 'src/workspace/messaging/listeners/messaging-message-channel.listener';
|
||||||
import { MessageService } from 'src/workspace/messaging/repositories/message/message.service';
|
import { MessageService } from 'src/workspace/messaging/repositories/message/message.service';
|
||||||
import { WorkspaceMemberModule } from 'src/workspace/messaging/repositories/workspace-member/workspace-member.module';
|
import { WorkspaceMemberModule } from 'src/workspace/messaging/repositories/workspace-member/workspace-member.module';
|
||||||
import { FeatureFlagEntity } from 'src/core/feature-flag/feature-flag.entity';
|
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({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
EnvironmentModule,
|
EnvironmentModule,
|
||||||
@ -32,8 +36,11 @@ import { FeatureFlagEntity } from 'src/core/feature-flag/feature-flag.entity';
|
|||||||
MessageModule,
|
MessageModule,
|
||||||
MessageThreadModule,
|
MessageThreadModule,
|
||||||
MessageParticipantModule,
|
MessageParticipantModule,
|
||||||
|
CreateCompaniesAndContactsModule,
|
||||||
WorkspaceMemberModule,
|
WorkspaceMemberModule,
|
||||||
TypeOrmModule.forFeature([FeatureFlagEntity], 'core'),
|
TypeOrmModule.forFeature([FeatureFlagEntity], 'core'),
|
||||||
|
CompanyModule,
|
||||||
|
PersonModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
GmailFullSyncService,
|
GmailFullSyncService,
|
||||||
@ -45,6 +52,7 @@ import { FeatureFlagEntity } from 'src/core/feature-flag/feature-flag.entity';
|
|||||||
CreateCompanyService,
|
CreateCompanyService,
|
||||||
MessagingPersonListener,
|
MessagingPersonListener,
|
||||||
MessagingWorkspaceMemberListener,
|
MessagingWorkspaceMemberListener,
|
||||||
|
IsContactAutoCreationEnabledListener,
|
||||||
MessagingMessageChannelListener,
|
MessagingMessageChannelListener,
|
||||||
MessageService,
|
MessageService,
|
||||||
],
|
],
|
||||||
|
|||||||
@ -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 {}
|
||||||
@ -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<void> {
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -44,6 +44,35 @@ export class MessageChannelService {
|
|||||||
return messageChannels[0];
|
return messageChannels[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getIsContactAutoCreationEnabledByConnectedAccountIdOrFail(
|
||||||
|
connectedAccountId: string,
|
||||||
|
workspaceId: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const messageChannel = await this.getFirstByConnectedAccountIdOrFail(
|
||||||
|
connectedAccountId,
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
return messageChannel.isContactAutoCreationEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getIsContactAutoCreationEnabledByMessageChannelId(
|
||||||
|
messageChannelId: string,
|
||||||
|
workspaceId: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
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(
|
public async getByIds(
|
||||||
ids: string[],
|
ids: string[],
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
|
|||||||
@ -1,16 +1,11 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { CreateCompanyModule } from 'src/workspace/messaging/services/create-company/create-company.module';
|
import { CreateCompaniesAndContactsModule } from 'src/workspace/messaging/services/create-companies-and-contacts/create-companies-and-contacts.module';
|
||||||
import { CreateContactModule } from 'src/workspace/messaging/services/create-contact/create-contact.module';
|
|
||||||
import { MessageParticipantService } from 'src/workspace/messaging/repositories/message-participant/message-participant.service';
|
import { MessageParticipantService } from 'src/workspace/messaging/repositories/message-participant/message-participant.service';
|
||||||
import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module';
|
import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [WorkspaceDataSourceModule, CreateCompaniesAndContactsModule],
|
||||||
WorkspaceDataSourceModule,
|
|
||||||
CreateContactModule,
|
|
||||||
CreateCompanyModule,
|
|
||||||
],
|
|
||||||
providers: [MessageParticipantService],
|
providers: [MessageParticipantService],
|
||||||
exports: [MessageParticipantService],
|
exports: [MessageParticipantService],
|
||||||
})
|
})
|
||||||
|
|||||||
@ -5,17 +5,15 @@ import { EntityManager } from 'typeorm';
|
|||||||
import { WorkspaceDataSourceService } from 'src/workspace/workspace-datasource/workspace-datasource.service';
|
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 { 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 { ObjectRecord } from 'src/workspace/workspace-sync-metadata/types/object-record';
|
||||||
import { DataSourceEntity } from 'src/metadata/data-source/data-source.entity';
|
import {
|
||||||
import { Participant } from 'src/workspace/messaging/types/gmail-message';
|
ParticipantWithId,
|
||||||
import { CreateContactService } from 'src/workspace/messaging/services/create-contact/create-contact.service';
|
Participant,
|
||||||
import { CreateCompanyService } from 'src/workspace/messaging/services/create-company/create-company.service';
|
} from 'src/workspace/messaging/types/gmail-message';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MessageParticipantService {
|
export class MessageParticipantService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
||||||
private readonly createContactService: CreateContactService,
|
|
||||||
private readonly createCompaniesService: CreateCompanyService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async getByHandles(
|
public async getByHandles(
|
||||||
@ -68,79 +66,71 @@ export class MessageParticipantService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getByMessageChannelIdWithoutPersonIdAndWorkspaceMemberId(
|
||||||
|
messageChannelId: string,
|
||||||
|
workspaceId: string,
|
||||||
|
transactionManager?: EntityManager,
|
||||||
|
): Promise<ParticipantWithId[]> {
|
||||||
|
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(
|
public async saveMessageParticipants(
|
||||||
participants: Participant[],
|
participants: Participant[],
|
||||||
messageId: string,
|
messageId: string,
|
||||||
dataSourceMetadata: DataSourceEntity,
|
workspaceId: string,
|
||||||
manager: EntityManager,
|
transactionManager?: EntityManager,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!participants) return;
|
if (!participants) return;
|
||||||
|
|
||||||
const alreadyCreatedContacts = await manager.query(
|
const dataSourceSchema =
|
||||||
`SELECT email FROM ${dataSourceMetadata.schema}."person" WHERE "email" = ANY($1)`,
|
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||||
[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 handles = participants.map((participant) => participant.handle);
|
const handles = participants.map((participant) => participant.handle);
|
||||||
|
|
||||||
const participantPersonIds = await manager.query(
|
const participantPersonIds =
|
||||||
`SELECT id, email FROM ${dataSourceMetadata.schema}."person" WHERE "email" = ANY($1)`,
|
await this.workspaceDataSourceService.executeRawQuery(
|
||||||
[handles],
|
`SELECT id, email FROM ${dataSourceSchema}."person" WHERE "email" = ANY($1)`,
|
||||||
);
|
[handles],
|
||||||
|
workspaceId,
|
||||||
|
transactionManager,
|
||||||
|
);
|
||||||
|
|
||||||
const participantWorkspaceMemberIds = await manager.query(
|
const participantWorkspaceMemberIds =
|
||||||
`SELECT "workspaceMember"."id", "connectedAccount"."handle" AS email FROM ${dataSourceMetadata.schema}."workspaceMember"
|
await this.workspaceDataSourceService.executeRawQuery(
|
||||||
JOIN ${dataSourceMetadata.schema}."connectedAccount" ON ${dataSourceMetadata.schema}."workspaceMember"."id" = ${dataSourceMetadata.schema}."connectedAccount"."accountOwnerId"
|
`SELECT "workspaceMember"."id", "connectedAccount"."handle" AS email FROM ${dataSourceSchema}."workspaceMember"
|
||||||
WHERE ${dataSourceMetadata.schema}."connectedAccount"."handle" = ANY($1)`,
|
JOIN ${dataSourceSchema}."connectedAccount" ON ${dataSourceSchema}."workspaceMember"."id" = ${dataSourceSchema}."connectedAccount"."accountOwnerId"
|
||||||
[handles],
|
WHERE ${dataSourceSchema}."connectedAccount"."handle" = ANY($1)`,
|
||||||
);
|
[handles],
|
||||||
|
workspaceId,
|
||||||
|
transactionManager,
|
||||||
|
);
|
||||||
|
|
||||||
const messageParticipantsToSave = participants.map((participant) => [
|
const messageParticipantsToSave = participants.map((participant) => [
|
||||||
messageId,
|
messageId,
|
||||||
@ -163,9 +153,54 @@ export class MessageParticipantService {
|
|||||||
|
|
||||||
if (messageParticipantsToSave.length === 0) return;
|
if (messageParticipantsToSave.length === 0) return;
|
||||||
|
|
||||||
await manager.query(
|
await this.workspaceDataSourceService.executeRawQuery(
|
||||||
`INSERT INTO ${dataSourceMetadata.schema}."messageParticipant" ("messageId", "role", "handle", "displayName", "personId", "workspaceMemberId") VALUES ${valuesString}`,
|
`INSERT INTO ${dataSourceSchema}."messageParticipant" ("messageId", "role", "handle", "displayName", "personId", "workspaceMemberId") VALUES ${valuesString}`,
|
||||||
messageParticipantsToSave.flat(),
|
messageParticipantsToSave.flat(),
|
||||||
|
workspaceId,
|
||||||
|
transactionManager,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateMessageParticipantsAfterPeopleCreation(
|
||||||
|
participants: ParticipantWithId[],
|
||||||
|
workspaceId: string,
|
||||||
|
transactionManager?: EntityManager,
|
||||||
|
): Promise<void> {
|
||||||
|
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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
import { Module } from '@nestjs/common';
|
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 { 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 { MessageParticipantModule } from 'src/workspace/messaging/repositories/message-participant/message-participant.module';
|
||||||
import { MessageThreadModule } from 'src/workspace/messaging/repositories/message-thread/message-thread.module';
|
import { MessageThreadModule } from 'src/workspace/messaging/repositories/message-thread/message-thread.module';
|
||||||
import { MessageService } from 'src/workspace/messaging/repositories/message/message.service';
|
import { MessageService } from 'src/workspace/messaging/repositories/message/message.service';
|
||||||
import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module';
|
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({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -12,6 +14,8 @@ import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/wo
|
|||||||
MessageThreadModule,
|
MessageThreadModule,
|
||||||
MessageParticipantModule,
|
MessageParticipantModule,
|
||||||
MessageChannelMessageAssociationModule,
|
MessageChannelMessageAssociationModule,
|
||||||
|
MessageChannelModule,
|
||||||
|
CreateCompaniesAndContactsModule,
|
||||||
],
|
],
|
||||||
providers: [MessageService],
|
providers: [MessageService],
|
||||||
exports: [MessageService],
|
exports: [MessageService],
|
||||||
|
|||||||
@ -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 { DataSourceEntity } from 'src/metadata/data-source/data-source.entity';
|
||||||
import { GmailMessage } from 'src/workspace/messaging/types/gmail-message';
|
import { GmailMessage } from 'src/workspace/messaging/types/gmail-message';
|
||||||
import { ConnectedAccountObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/connected-account.object-metadata';
|
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 { 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 { MessageParticipantService } from 'src/workspace/messaging/repositories/message-participant/message-participant.service';
|
||||||
import { MessageThreadService } from 'src/workspace/messaging/repositories/message-thread/message-thread.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 { 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()
|
@Injectable()
|
||||||
export class MessageService {
|
export class MessageService {
|
||||||
constructor(
|
constructor(
|
||||||
@ -20,6 +22,8 @@ export class MessageService {
|
|||||||
private readonly messageChannelMessageAssociationService: MessageChannelMessageAssociationService,
|
private readonly messageChannelMessageAssociationService: MessageChannelMessageAssociationService,
|
||||||
private readonly messageThreadService: MessageThreadService,
|
private readonly messageThreadService: MessageThreadService,
|
||||||
private readonly messageParticipantService: MessageParticipantService,
|
private readonly messageParticipantService: MessageParticipantService,
|
||||||
|
private readonly messageChannelService: MessageChannelService,
|
||||||
|
private readonly createCompaniesAndContactsService: CreateCompaniesAndContactsService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async getFirstByHeaderMessageId(
|
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(
|
await this.messageParticipantService.saveMessageParticipants(
|
||||||
message.participants,
|
message.participants,
|
||||||
newMessageId,
|
newMessageId,
|
||||||
dataSourceMetadata,
|
workspaceId,
|
||||||
manager,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return Promise.resolve(newMessageId);
|
return Promise.resolve(newMessageId);
|
||||||
|
|||||||
@ -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 {}
|
||||||
@ -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<void> {
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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 {}
|
||||||
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,9 +1,11 @@
|
|||||||
import { Module } from '@nestjs/common';
|
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 { CreateCompanyService } from 'src/workspace/messaging/services/create-company/create-company.service';
|
||||||
|
import { CompanyModule } from 'src/workspace/messaging/repositories/company/company.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [],
|
imports: [WorkspaceDataSourceModule, CompanyModule],
|
||||||
providers: [CreateCompanyService],
|
providers: [CreateCompanyService],
|
||||||
exports: [CreateCompanyService],
|
exports: [CreateCompanyService],
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,16 +1,16 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
import { EntityManager } from 'typeorm';
|
import { EntityManager } from 'typeorm';
|
||||||
import axios, { AxiosInstance } from 'axios';
|
|
||||||
import { v4 } from 'uuid';
|
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';
|
import { capitalize } from 'src/utils/capitalize';
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CreateCompanyService {
|
export class CreateCompanyService {
|
||||||
private readonly httpService: AxiosInstance;
|
private readonly httpService: AxiosInstance;
|
||||||
|
|
||||||
constructor() {
|
constructor(private readonly companyService: CompanyService) {
|
||||||
this.httpService = axios.create({
|
this.httpService = axios.create({
|
||||||
baseURL: 'https://companies.twenty.com',
|
baseURL: 'https://companies.twenty.com',
|
||||||
});
|
});
|
||||||
@ -18,17 +18,19 @@ export class CreateCompanyService {
|
|||||||
|
|
||||||
async createCompanies(
|
async createCompanies(
|
||||||
domainNames: string[],
|
domainNames: string[],
|
||||||
dataSourceMetadata: DataSourceEntity,
|
workspaceId: string,
|
||||||
manager: EntityManager,
|
transactionManager?: EntityManager,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
[domainName: string]: string;
|
[domainName: string]: string;
|
||||||
}> {
|
}> {
|
||||||
const uniqueDomainNames = [...new Set(domainNames)];
|
const uniqueDomainNames = [...new Set(domainNames)];
|
||||||
|
|
||||||
const existingCompanies = await manager.query(
|
const existingCompanies =
|
||||||
`SELECT id, "domainName" FROM ${dataSourceMetadata.schema}.company WHERE "domainName" = ANY($1)`,
|
await this.companyService.getExistingCompaniesByDomainNames(
|
||||||
[uniqueDomainNames],
|
uniqueDomainNames,
|
||||||
);
|
workspaceId,
|
||||||
|
transactionManager,
|
||||||
|
);
|
||||||
|
|
||||||
const companiesObject = existingCompanies.reduce(
|
const companiesObject = existingCompanies.reduce(
|
||||||
(
|
(
|
||||||
@ -57,8 +59,8 @@ export class CreateCompanyService {
|
|||||||
for (const domainName of filteredDomainNames) {
|
for (const domainName of filteredDomainNames) {
|
||||||
companiesObject[domainName] = await this.createCompany(
|
companiesObject[domainName] = await this.createCompany(
|
||||||
domainName,
|
domainName,
|
||||||
dataSourceMetadata,
|
workspaceId,
|
||||||
manager,
|
transactionManager,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,17 +69,20 @@ export class CreateCompanyService {
|
|||||||
|
|
||||||
async createCompany(
|
async createCompany(
|
||||||
domainName: string,
|
domainName: string,
|
||||||
dataSourceMetadata: DataSourceEntity,
|
workspaceId: string,
|
||||||
manager: EntityManager,
|
transactionManager?: EntityManager,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const companyId = v4();
|
const companyId = v4();
|
||||||
|
|
||||||
const { name, city } = await this.getCompanyInfoFromDomainName(domainName);
|
const { name, city } = await this.getCompanyInfoFromDomainName(domainName);
|
||||||
|
|
||||||
await manager.query(
|
this.companyService.createCompany(
|
||||||
`INSERT INTO ${dataSourceMetadata.schema}.company (id, name, "domainName", address)
|
companyId,
|
||||||
VALUES ($1, $2, $3, $4)`,
|
name,
|
||||||
[companyId, name, domainName, city],
|
domainName,
|
||||||
|
city,
|
||||||
|
workspaceId,
|
||||||
|
transactionManager,
|
||||||
);
|
);
|
||||||
|
|
||||||
return companyId;
|
return companyId;
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
import { Module } from '@nestjs/common';
|
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 { CreateContactService } from 'src/workspace/messaging/services/create-contact/create-contact.service';
|
||||||
|
import { PersonModule } from 'src/workspace/messaging/repositories/person/person.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [],
|
imports: [WorkspaceDataSourceModule, PersonModule],
|
||||||
providers: [CreateContactService],
|
providers: [CreateContactService],
|
||||||
exports: [CreateContactService],
|
exports: [CreateContactService],
|
||||||
})
|
})
|
||||||
|
|||||||
@ -3,8 +3,8 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import { EntityManager } from 'typeorm';
|
import { EntityManager } from 'typeorm';
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
import { DataSourceEntity } from 'src/metadata/data-source/data-source.entity';
|
import { PersonService } from 'src/workspace/messaging/repositories/person/person.service';
|
||||||
import { capitalize } from 'src/utils/capitalize';
|
import { getFirstNameAndLastNameFromHandleAndDisplayName } from 'src/workspace/messaging/utils/get-first-name-and-last-name-from-handle-and-display-name.util';
|
||||||
|
|
||||||
type ContactToCreate = {
|
type ContactToCreate = {
|
||||||
handle: string;
|
handle: string;
|
||||||
@ -22,67 +22,42 @@ type FormattedContactToCreate = {
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CreateContactService {
|
export class CreateContactService {
|
||||||
constructor() {}
|
constructor(private readonly personService: PersonService) {}
|
||||||
|
|
||||||
formatContacts(
|
public formatContacts(
|
||||||
contactsToCreate: ContactToCreate[],
|
contactsToCreate: ContactToCreate[],
|
||||||
): FormattedContactToCreate[] {
|
): FormattedContactToCreate[] {
|
||||||
return contactsToCreate.map((contact) => {
|
return contactsToCreate.map((contact) => {
|
||||||
|
const id = v4();
|
||||||
|
|
||||||
const { handle, displayName, companyId } = contact;
|
const { handle, displayName, companyId } = contact;
|
||||||
|
|
||||||
const contactFirstName = displayName.split(' ')[0];
|
const { firstName, lastName } =
|
||||||
const contactLastName = displayName.split(' ')[1];
|
getFirstNameAndLastNameFromHandleAndDisplayName(handle, displayName);
|
||||||
|
|
||||||
const contactFullNameFromHandle = handle.split('@')[0];
|
|
||||||
const contactFirstNameFromHandle =
|
|
||||||
contactFullNameFromHandle.split('.')[0];
|
|
||||||
const contactLastNameFromHandle = contactFullNameFromHandle.split('.')[1];
|
|
||||||
|
|
||||||
const id = v4();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
handle,
|
handle,
|
||||||
firstName: capitalize(
|
firstName,
|
||||||
contactFirstName || contactFirstNameFromHandle || '',
|
lastName,
|
||||||
),
|
|
||||||
lastName: capitalize(
|
|
||||||
contactLastName || contactLastNameFromHandle || '',
|
|
||||||
),
|
|
||||||
companyId,
|
companyId,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async createContacts(
|
public async createContacts(
|
||||||
contactsToCreate: ContactToCreate[],
|
contactsToCreate: ContactToCreate[],
|
||||||
dataSourceMetadata: DataSourceEntity,
|
workspaceId: string,
|
||||||
manager: EntityManager,
|
transactionManager?: EntityManager,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (contactsToCreate.length === 0) return;
|
if (contactsToCreate.length === 0) return;
|
||||||
|
|
||||||
const formattedContacts = this.formatContacts(contactsToCreate);
|
const formattedContacts = this.formatContacts(contactsToCreate);
|
||||||
|
|
||||||
const valuesString = formattedContacts
|
await this.personService.createPeople(
|
||||||
.map(
|
formattedContacts,
|
||||||
(_, index) =>
|
workspaceId,
|
||||||
`($${index * 5 + 1}, $${index * 5 + 2}, $${index * 5 + 3}, $${
|
transactionManager,
|
||||||
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(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,3 +20,7 @@ export type Participant = {
|
|||||||
handle: string;
|
handle: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ParticipantWithId = Participant & {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|||||||
@ -0,0 +1,3 @@
|
|||||||
|
export function getDomainNameFromHandle(handle: string): string {
|
||||||
|
return handle.split('@')?.[1].split('.').slice(-2).join('.').toLowerCase();
|
||||||
|
}
|
||||||
@ -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 || ''),
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -71,6 +71,14 @@ export class MessageChannelObjectMetadata extends BaseObjectMetadata {
|
|||||||
})
|
})
|
||||||
type: string;
|
type: string;
|
||||||
|
|
||||||
|
@FieldMetadata({
|
||||||
|
type: FieldMetadataType.BOOLEAN,
|
||||||
|
label: 'Is Contact Auto Creation Enabled',
|
||||||
|
description: 'Is Contact Auto Creation Enabled',
|
||||||
|
icon: 'IconUserCircle',
|
||||||
|
})
|
||||||
|
isContactAutoCreationEnabled: boolean;
|
||||||
|
|
||||||
@FieldMetadata({
|
@FieldMetadata({
|
||||||
type: FieldMetadataType.RELATION,
|
type: FieldMetadataType.RELATION,
|
||||||
label: 'Message Channel Association',
|
label: 'Message Channel Association',
|
||||||
|
|||||||
Reference in New Issue
Block a user