Refactor connected account module (#6225)
- Refactor connected account module - Move blocklist into it's own module - Move contact-creation-manager into it's own module --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -1,35 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { CreateCompanyAndContactService } from 'src/modules/connected-account/auto-companies-and-contacts-creation/services/create-company-and-contact.service';
|
||||
import { CreateCompanyModule } from 'src/modules/connected-account/auto-companies-and-contacts-creation/create-company/create-company.module';
|
||||
import { CreateContactModule } from 'src/modules/connected-account/auto-companies-and-contacts-creation/create-contact/create-contact.module';
|
||||
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
|
||||
import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { AutoCompaniesAndContactsCreationMessageChannelListener } from 'src/modules/connected-account/auto-companies-and-contacts-creation/listeners/auto-companies-and-contacts-creation-message-channel.listener';
|
||||
import { AutoCompaniesAndContactsCreationCalendarChannelListener } from 'src/modules/connected-account/auto-companies-and-contacts-creation/listeners/auto-companies-and-contacts-creation-calendar-channel.listener';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
CreateContactModule,
|
||||
CreateCompanyModule,
|
||||
ObjectMetadataRepositoryModule.forFeature([
|
||||
PersonWorkspaceEntity,
|
||||
WorkspaceMemberWorkspaceEntity,
|
||||
]),
|
||||
WorkspaceDataSourceModule,
|
||||
TypeOrmModule.forFeature([FeatureFlagEntity], 'core'),
|
||||
TypeOrmModule.forFeature([ObjectMetadataEntity], 'metadata'),
|
||||
],
|
||||
providers: [
|
||||
CreateCompanyAndContactService,
|
||||
AutoCompaniesAndContactsCreationMessageChannelListener,
|
||||
AutoCompaniesAndContactsCreationCalendarChannelListener,
|
||||
],
|
||||
exports: [CreateCompanyAndContactService],
|
||||
})
|
||||
export class AutoCompaniesAndContactsCreationModule {}
|
||||
@ -1 +0,0 @@
|
||||
export const CONTACTS_CREATION_BATCH_SIZE = 100;
|
||||
@ -1,14 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
|
||||
import { CompanyWorkspaceEntity } from 'src/modules/company/standard-objects/company.workspace-entity';
|
||||
import { CreateCompanyService } from 'src/modules/connected-account/auto-companies-and-contacts-creation/create-company/create-company.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ObjectMetadataRepositoryModule.forFeature([CompanyWorkspaceEntity]),
|
||||
],
|
||||
providers: [CreateCompanyService],
|
||||
exports: [CreateCompanyService],
|
||||
})
|
||||
export class CreateCompanyModule {}
|
||||
@ -1,122 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { EntityManager } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
|
||||
import { CompanyRepository } from 'src/modules/company/repositories/company.repository';
|
||||
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
|
||||
import { CompanyWorkspaceEntity } from 'src/modules/company/standard-objects/company.workspace-entity';
|
||||
import { getCompanyNameFromDomainName } from 'src/modules/connected-account/auto-companies-and-contacts-creation/utils/get-company-name-from-domain-name.util';
|
||||
@Injectable()
|
||||
export class CreateCompanyService {
|
||||
private readonly httpService: AxiosInstance;
|
||||
|
||||
constructor(
|
||||
@InjectObjectMetadataRepository(CompanyWorkspaceEntity)
|
||||
private readonly companyRepository: CompanyRepository,
|
||||
) {
|
||||
this.httpService = axios.create({
|
||||
baseURL: 'https://companies.twenty.com',
|
||||
});
|
||||
}
|
||||
|
||||
async createCompanies(
|
||||
domainNames: string[],
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<{
|
||||
[domainName: string]: string;
|
||||
}> {
|
||||
if (domainNames.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const uniqueDomainNames = [...new Set(domainNames)];
|
||||
|
||||
const existingCompanies =
|
||||
await this.companyRepository.getExistingCompaniesByDomainNames(
|
||||
uniqueDomainNames,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
const companiesObject = existingCompanies.reduce(
|
||||
(
|
||||
acc: {
|
||||
[domainName: string]: string;
|
||||
},
|
||||
company: {
|
||||
domainName: string;
|
||||
id: string;
|
||||
},
|
||||
) => ({
|
||||
...acc,
|
||||
[company.domainName]: company.id,
|
||||
}),
|
||||
{},
|
||||
);
|
||||
|
||||
const filteredDomainNames = uniqueDomainNames.filter(
|
||||
(domainName) =>
|
||||
!existingCompanies.some(
|
||||
(company: { domainName: string }) =>
|
||||
company.domainName === domainName,
|
||||
),
|
||||
);
|
||||
|
||||
for (const domainName of filteredDomainNames) {
|
||||
companiesObject[domainName] = await this.createCompany(
|
||||
domainName,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
|
||||
return companiesObject;
|
||||
}
|
||||
|
||||
async createCompany(
|
||||
domainName: string,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<string> {
|
||||
const companyId = v4();
|
||||
|
||||
const { name, city } = await this.getCompanyInfoFromDomainName(domainName);
|
||||
|
||||
await this.companyRepository.createCompany(
|
||||
workspaceId,
|
||||
{
|
||||
id: companyId,
|
||||
domainName,
|
||||
name,
|
||||
city,
|
||||
},
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
return companyId;
|
||||
}
|
||||
|
||||
async getCompanyInfoFromDomainName(domainName: string): Promise<{
|
||||
name: string;
|
||||
city: string;
|
||||
}> {
|
||||
try {
|
||||
const response = await this.httpService.get(`/${domainName}`);
|
||||
|
||||
const data = response.data;
|
||||
|
||||
return {
|
||||
name: data.name ?? getCompanyNameFromDomainName(domainName),
|
||||
city: data.city,
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
name: getCompanyNameFromDomainName(domainName),
|
||||
city: '',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
|
||||
import { CreateContactService } from 'src/modules/connected-account/auto-companies-and-contacts-creation/create-contact/create-contact.service';
|
||||
import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
|
||||
|
||||
@Module({
|
||||
imports: [ObjectMetadataRepositoryModule.forFeature([PersonWorkspaceEntity])],
|
||||
providers: [CreateContactService],
|
||||
exports: [CreateContactService],
|
||||
})
|
||||
export class CreateContactModule {}
|
||||
@ -1,68 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { EntityManager } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { PersonRepository } from 'src/modules/person/repositories/person.repository';
|
||||
import { getFirstNameAndLastNameFromHandleAndDisplayName } from 'src/modules/connected-account/auto-companies-and-contacts-creation/utils/get-first-name-and-last-name-from-handle-and-display-name.util';
|
||||
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
|
||||
import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
|
||||
|
||||
type ContactToCreate = {
|
||||
handle: string;
|
||||
displayName: string;
|
||||
companyId?: string;
|
||||
};
|
||||
|
||||
type FormattedContactToCreate = {
|
||||
id: string;
|
||||
handle: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
companyId?: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class CreateContactService {
|
||||
constructor(
|
||||
@InjectObjectMetadataRepository(PersonWorkspaceEntity)
|
||||
private readonly personRepository: PersonRepository,
|
||||
) {}
|
||||
|
||||
public formatContacts(
|
||||
contactsToCreate: ContactToCreate[],
|
||||
): FormattedContactToCreate[] {
|
||||
return contactsToCreate.map((contact) => {
|
||||
const id = v4();
|
||||
|
||||
const { handle, displayName, companyId } = contact;
|
||||
|
||||
const { firstName, lastName } =
|
||||
getFirstNameAndLastNameFromHandleAndDisplayName(handle, displayName);
|
||||
|
||||
return {
|
||||
id,
|
||||
handle,
|
||||
firstName,
|
||||
lastName,
|
||||
companyId,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public async createPeople(
|
||||
contactsToCreate: ContactToCreate[],
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<PersonWorkspaceEntity[]> {
|
||||
if (contactsToCreate.length === 0) return [];
|
||||
|
||||
const formattedContacts = this.formatContacts(contactsToCreate);
|
||||
|
||||
return await this.personRepository.createPeople(
|
||||
formattedContacts,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { AutoCompaniesAndContactsCreationModule } from 'src/modules/connected-account/auto-companies-and-contacts-creation/auto-companies-and-contacts-creation.module';
|
||||
import { CreateCompanyAndContactJob } from 'src/modules/connected-account/auto-companies-and-contacts-creation/jobs/create-company-and-contact.job';
|
||||
|
||||
@Module({
|
||||
imports: [AutoCompaniesAndContactsCreationModule],
|
||||
providers: [CreateCompanyAndContactJob],
|
||||
})
|
||||
export class AutoCompaniesAndContactsCreationJobModule {}
|
||||
@ -1,35 +0,0 @@
|
||||
import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator';
|
||||
import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator';
|
||||
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
|
||||
import { CreateCompanyAndContactService } from 'src/modules/connected-account/auto-companies-and-contacts-creation/services/create-company-and-contact.service';
|
||||
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
|
||||
export type CreateCompanyAndContactJobData = {
|
||||
workspaceId: string;
|
||||
connectedAccount: ConnectedAccountWorkspaceEntity;
|
||||
contactsToCreate: {
|
||||
displayName: string;
|
||||
handle: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
@Processor(MessageQueue.contactCreationQueue)
|
||||
export class CreateCompanyAndContactJob {
|
||||
constructor(
|
||||
private readonly createCompanyAndContactService: CreateCompanyAndContactService,
|
||||
) {}
|
||||
|
||||
@Process(CreateCompanyAndContactJob.name)
|
||||
async handle(data: CreateCompanyAndContactJobData): Promise<void> {
|
||||
const { workspaceId, connectedAccount, contactsToCreate } = data;
|
||||
|
||||
await this.createCompanyAndContactService.createCompaniesAndContactsAndUpdateParticipants(
|
||||
connectedAccount,
|
||||
contactsToCreate.map((contact) => ({
|
||||
handle: contact.handle,
|
||||
displayName: contact.displayName,
|
||||
})),
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,42 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
|
||||
import { ObjectRecordUpdateEvent } from 'src/engine/integrations/event-emitter/types/object-record-update.event';
|
||||
import { objectRecordChangedProperties } from 'src/engine/integrations/event-emitter/utils/object-record-changed-properties.util';
|
||||
import { InjectMessageQueue } from 'src/engine/integrations/message-queue/decorators/message-queue.decorator';
|
||||
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
|
||||
import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service';
|
||||
import {
|
||||
CalendarCreateCompanyAndContactAfterSyncJobData,
|
||||
CalendarCreateCompanyAndContactAfterSyncJob,
|
||||
} from 'src/modules/calendar/calendar-event-participant-manager/jobs/calendar-create-company-and-contact-after-sync.job';
|
||||
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
|
||||
@Injectable()
|
||||
export class AutoCompaniesAndContactsCreationCalendarChannelListener {
|
||||
constructor(
|
||||
@InjectMessageQueue(MessageQueue.calendarQueue)
|
||||
private readonly messageQueueService: MessageQueueService,
|
||||
) {}
|
||||
|
||||
@OnEvent('calendarChannel.updated')
|
||||
async handleUpdatedEvent(
|
||||
payload: ObjectRecordUpdateEvent<MessageChannelWorkspaceEntity>,
|
||||
) {
|
||||
if (
|
||||
objectRecordChangedProperties(
|
||||
payload.properties.before,
|
||||
payload.properties.after,
|
||||
).includes('isContactAutoCreationEnabled') &&
|
||||
payload.properties.after.isContactAutoCreationEnabled
|
||||
) {
|
||||
await this.messageQueueService.add<CalendarCreateCompanyAndContactAfterSyncJobData>(
|
||||
CalendarCreateCompanyAndContactAfterSyncJob.name,
|
||||
{
|
||||
workspaceId: payload.workspaceId,
|
||||
calendarChannelId: payload.recordId,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,42 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
|
||||
import { ObjectRecordUpdateEvent } from 'src/engine/integrations/event-emitter/types/object-record-update.event';
|
||||
import { objectRecordChangedProperties } from 'src/engine/integrations/event-emitter/utils/object-record-changed-properties.util';
|
||||
import { InjectMessageQueue } from 'src/engine/integrations/message-queue/decorators/message-queue.decorator';
|
||||
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
|
||||
import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service';
|
||||
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
import {
|
||||
MessagingCreateCompanyAndContactAfterSyncJobData,
|
||||
MessagingCreateCompanyAndContactAfterSyncJob,
|
||||
} from 'src/modules/messaging/message-participant-manager/jobs/messaging-create-company-and-contact-after-sync.job';
|
||||
|
||||
@Injectable()
|
||||
export class AutoCompaniesAndContactsCreationMessageChannelListener {
|
||||
constructor(
|
||||
@InjectMessageQueue(MessageQueue.contactCreationQueue)
|
||||
private readonly messageQueueService: MessageQueueService,
|
||||
) {}
|
||||
|
||||
@OnEvent('messageChannel.updated')
|
||||
async handleUpdatedEvent(
|
||||
payload: ObjectRecordUpdateEvent<MessageChannelWorkspaceEntity>,
|
||||
) {
|
||||
if (
|
||||
objectRecordChangedProperties(
|
||||
payload.properties.before,
|
||||
payload.properties.after,
|
||||
).includes('isContactAutoCreationEnabled') &&
|
||||
payload.properties.after.isContactAutoCreationEnabled
|
||||
) {
|
||||
await this.messageQueueService.add<MessagingCreateCompanyAndContactAfterSyncJobData>(
|
||||
MessagingCreateCompanyAndContactAfterSyncJob.name,
|
||||
{
|
||||
workspaceId: payload.workspaceId,
|
||||
messageChannelId: payload.recordId,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,169 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import chunk from 'lodash.chunk';
|
||||
import compact from 'lodash.compact';
|
||||
import { EntityManager, Repository } from 'typeorm';
|
||||
|
||||
import { ObjectRecordCreateEvent } from 'src/engine/integrations/event-emitter/types/object-record-create.event';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
|
||||
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||
import { CONTACTS_CREATION_BATCH_SIZE } from 'src/modules/connected-account/auto-companies-and-contacts-creation/constants/contacts-creation-batch-size.constant';
|
||||
import { CreateCompanyService } from 'src/modules/connected-account/auto-companies-and-contacts-creation/create-company/create-company.service';
|
||||
import { CreateContactService } from 'src/modules/connected-account/auto-companies-and-contacts-creation/create-contact/create-contact.service';
|
||||
import { Contact } from 'src/modules/connected-account/auto-companies-and-contacts-creation/types/contact.type';
|
||||
import { filterOutSelfAndContactsFromCompanyOrWorkspace } from 'src/modules/connected-account/auto-companies-and-contacts-creation/utils/filter-out-contacts-from-company-or-workspace.util';
|
||||
import { getDomainNameFromHandle } from 'src/modules/connected-account/auto-companies-and-contacts-creation/utils/get-domain-name-from-handle.util';
|
||||
import { getUniqueContactsAndHandles } from 'src/modules/connected-account/auto-companies-and-contacts-creation/utils/get-unique-contacts-and-handles.util';
|
||||
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
import { PersonRepository } from 'src/modules/person/repositories/person.repository';
|
||||
import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
|
||||
import { WorkspaceMemberRepository } from 'src/modules/workspace-member/repositories/workspace-member.repository';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
import { isWorkEmail } from 'src/utils/is-work-email';
|
||||
|
||||
@Injectable()
|
||||
export class CreateCompanyAndContactService {
|
||||
constructor(
|
||||
private readonly createContactService: CreateContactService,
|
||||
private readonly createCompaniesService: CreateCompanyService,
|
||||
@InjectObjectMetadataRepository(PersonWorkspaceEntity)
|
||||
private readonly personRepository: PersonRepository,
|
||||
@InjectObjectMetadataRepository(WorkspaceMemberWorkspaceEntity)
|
||||
private readonly workspaceMemberRepository: WorkspaceMemberRepository,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
@InjectRepository(ObjectMetadataEntity, 'metadata')
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||
) {}
|
||||
|
||||
private async createCompaniesAndPeople(
|
||||
connectedAccount: ConnectedAccountWorkspaceEntity,
|
||||
contactsToCreate: Contact[],
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<PersonWorkspaceEntity[]> {
|
||||
if (!contactsToCreate || contactsToCreate.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const workspaceMembers =
|
||||
await this.workspaceMemberRepository.getAllByWorkspaceId(
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
const contactsToCreateFromOtherCompanies =
|
||||
filterOutSelfAndContactsFromCompanyOrWorkspace(
|
||||
contactsToCreate,
|
||||
connectedAccount,
|
||||
workspaceMembers,
|
||||
);
|
||||
|
||||
const { uniqueContacts, uniqueHandles } = getUniqueContactsAndHandles(
|
||||
contactsToCreateFromOtherCompanies,
|
||||
);
|
||||
|
||||
if (uniqueHandles.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const alreadyCreatedContacts = await this.personRepository.getByEmails(
|
||||
uniqueHandles,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
const alreadyCreatedContactEmails: string[] = alreadyCreatedContacts?.map(
|
||||
({ email }) => email,
|
||||
);
|
||||
|
||||
const filteredContactsToCreate = uniqueContacts.filter(
|
||||
(participant) =>
|
||||
!alreadyCreatedContactEmails.includes(participant.handle) &&
|
||||
participant.handle.includes('@'),
|
||||
);
|
||||
|
||||
const filteredContactsToCreateWithCompanyDomainNames =
|
||||
filteredContactsToCreate?.map((participant) => ({
|
||||
handle: participant.handle,
|
||||
displayName: participant.displayName,
|
||||
companyDomainName: isWorkEmail(participant.handle)
|
||||
? getDomainNameFromHandle(participant.handle)
|
||||
: undefined,
|
||||
}));
|
||||
|
||||
const domainNamesToCreate = compact(
|
||||
filteredContactsToCreateWithCompanyDomainNames.map(
|
||||
(participant) => participant.companyDomainName,
|
||||
),
|
||||
);
|
||||
|
||||
const companiesObject = await this.createCompaniesService.createCompanies(
|
||||
domainNamesToCreate,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
const formattedContactsToCreate =
|
||||
filteredContactsToCreateWithCompanyDomainNames.map((contact) => ({
|
||||
handle: contact.handle,
|
||||
displayName: contact.displayName,
|
||||
companyId:
|
||||
contact.companyDomainName && contact.companyDomainName !== ''
|
||||
? companiesObject[contact.companyDomainName]
|
||||
: undefined,
|
||||
}));
|
||||
|
||||
return await this.createContactService.createPeople(
|
||||
formattedContactsToCreate,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
|
||||
async createCompaniesAndContactsAndUpdateParticipants(
|
||||
connectedAccount: ConnectedAccountWorkspaceEntity,
|
||||
contactsToCreate: Contact[],
|
||||
workspaceId: string,
|
||||
) {
|
||||
const contactsBatches = chunk(
|
||||
contactsToCreate,
|
||||
CONTACTS_CREATION_BATCH_SIZE,
|
||||
);
|
||||
|
||||
// TODO: Remove this when events are emitted directly inside TwentyORM
|
||||
|
||||
const objectMetadata = await this.objectMetadataRepository.findOne({
|
||||
where: {
|
||||
standardId: STANDARD_OBJECT_IDS.person,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!objectMetadata) {
|
||||
throw new Error('Object metadata not found');
|
||||
}
|
||||
|
||||
for (const contactsBatch of contactsBatches) {
|
||||
const createdPeople = await this.createCompaniesAndPeople(
|
||||
connectedAccount,
|
||||
contactsBatch,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
for (const createdPerson of createdPeople) {
|
||||
this.eventEmitter.emit('person.created', {
|
||||
name: 'person.created',
|
||||
workspaceId,
|
||||
recordId: createdPerson.id,
|
||||
objectMetadata,
|
||||
properties: {
|
||||
after: createdPerson,
|
||||
},
|
||||
} satisfies ObjectRecordCreateEvent<any>);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,4 +0,0 @@
|
||||
export type Contact = {
|
||||
handle: string;
|
||||
displayName: string;
|
||||
};
|
||||
@ -1,32 +0,0 @@
|
||||
import { Contact } from 'src/modules/connected-account/auto-companies-and-contacts-creation/types/contact.type';
|
||||
import { getUniqueContactsAndHandles } from 'src/modules/connected-account/auto-companies-and-contacts-creation/utils/get-unique-contacts-and-handles.util';
|
||||
|
||||
describe('getUniqueContactsAndHandles', () => {
|
||||
it('should return empty arrays when contacts is empty', () => {
|
||||
const contacts: Contact[] = [];
|
||||
const result = getUniqueContactsAndHandles(contacts);
|
||||
|
||||
expect(result.uniqueContacts).toEqual([]);
|
||||
expect(result.uniqueHandles).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return unique contacts and handles', () => {
|
||||
const contacts: Contact[] = [
|
||||
{ handle: 'john@twenty.com', displayName: 'John Doe' },
|
||||
{ handle: 'john@twenty.com', displayName: 'John Doe' },
|
||||
{ handle: 'jane@twenty.com', displayName: 'Jane Smith' },
|
||||
{ handle: 'jane@twenty.com', displayName: 'Jane Smith' },
|
||||
{ handle: 'jane@twenty.com', displayName: 'Jane Smith' },
|
||||
];
|
||||
const result = getUniqueContactsAndHandles(contacts);
|
||||
|
||||
expect(result.uniqueContacts).toEqual([
|
||||
{ handle: 'john@twenty.com', displayName: 'John Doe' },
|
||||
{ handle: 'jane@twenty.com', displayName: 'Jane Smith' },
|
||||
]);
|
||||
expect(result.uniqueHandles).toEqual([
|
||||
'john@twenty.com',
|
||||
'jane@twenty.com',
|
||||
]);
|
||||
});
|
||||
});
|
||||
@ -1,30 +0,0 @@
|
||||
import { Contact } from 'src/modules/connected-account/auto-companies-and-contacts-creation/types/contact.type';
|
||||
import { getDomainNameFromHandle } from 'src/modules/connected-account/auto-companies-and-contacts-creation/utils/get-domain-name-from-handle.util';
|
||||
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
export function filterOutSelfAndContactsFromCompanyOrWorkspace(
|
||||
contacts: Contact[],
|
||||
connectedAccount: ConnectedAccountWorkspaceEntity,
|
||||
workspaceMembers: WorkspaceMemberWorkspaceEntity[],
|
||||
): Contact[] {
|
||||
const selfDomainName = getDomainNameFromHandle(connectedAccount.handle);
|
||||
|
||||
const handleAliases = connectedAccount.handleAliases?.split(',') || [];
|
||||
|
||||
const workspaceMembersMap = workspaceMembers.reduce(
|
||||
(map, workspaceMember) => {
|
||||
map[workspaceMember.userEmail] = true;
|
||||
|
||||
return map;
|
||||
},
|
||||
new Map<string, boolean>(),
|
||||
);
|
||||
|
||||
return contacts.filter(
|
||||
(contact) =>
|
||||
getDomainNameFromHandle(contact.handle) !== selfDomainName &&
|
||||
!workspaceMembersMap[contact.handle] &&
|
||||
!handleAliases.includes(contact.handle),
|
||||
);
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
import psl from 'psl';
|
||||
|
||||
import { capitalize } from 'src/utils/capitalize';
|
||||
|
||||
export const getCompanyNameFromDomainName = (domainName: string) => {
|
||||
const { sld } = psl.parse(domainName);
|
||||
|
||||
return sld ? capitalize(sld) : '';
|
||||
};
|
||||
@ -1,9 +0,0 @@
|
||||
import psl from 'psl';
|
||||
|
||||
export const getDomainNameFromHandle = (handle: string): string => {
|
||||
const wholeDomain = handle?.split('@')?.[1] || '';
|
||||
|
||||
const { domain } = psl.parse(wholeDomain);
|
||||
|
||||
return domain || '';
|
||||
};
|
||||
@ -1,18 +0,0 @@
|
||||
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 || ''),
|
||||
};
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
import uniq from 'lodash.uniq';
|
||||
import uniqBy from 'lodash.uniqby';
|
||||
|
||||
import { Contact } from 'src/modules/connected-account/auto-companies-and-contacts-creation/types/contact.type';
|
||||
|
||||
export function getUniqueContactsAndHandles(contacts: Contact[]): {
|
||||
uniqueContacts: Contact[];
|
||||
uniqueHandles: string[];
|
||||
} {
|
||||
if (contacts.length === 0) {
|
||||
return { uniqueContacts: [], uniqueHandles: [] };
|
||||
}
|
||||
|
||||
const uniqueHandles = uniq(contacts.map((participant) => participant.handle));
|
||||
|
||||
const uniqueContacts = uniqBy(contacts, 'handle');
|
||||
|
||||
return { uniqueContacts, uniqueHandles };
|
||||
}
|
||||
@ -1,29 +0,0 @@
|
||||
import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
|
||||
import { CreateManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
|
||||
import {
|
||||
BlocklistItem,
|
||||
BlocklistValidationService,
|
||||
} from 'src/modules/connected-account/services/blocklist/blocklist-validation.service';
|
||||
|
||||
@WorkspaceQueryHook(`blocklist.createMany`)
|
||||
export class BlocklistCreateManyPreQueryHook
|
||||
implements WorkspaceQueryHookInstance
|
||||
{
|
||||
constructor(
|
||||
private readonly blocklistValidationService: BlocklistValidationService,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
payload: CreateManyResolverArgs<BlocklistItem>,
|
||||
): Promise<void> {
|
||||
await this.blocklistValidationService.validateBlocklistForCreateMany(
|
||||
payload,
|
||||
userId,
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
import { MethodNotAllowedException } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
|
||||
|
||||
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
|
||||
|
||||
@WorkspaceQueryHook(`blocklist.updateMany`)
|
||||
export class BlocklistUpdateManyPreQueryHook
|
||||
implements WorkspaceQueryHookInstance
|
||||
{
|
||||
constructor() {}
|
||||
|
||||
async execute(): Promise<void> {
|
||||
throw new MethodNotAllowedException('Method not allowed.');
|
||||
}
|
||||
}
|
||||
@ -1,29 +0,0 @@
|
||||
import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
|
||||
import { UpdateOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
|
||||
import {
|
||||
BlocklistItem,
|
||||
BlocklistValidationService,
|
||||
} from 'src/modules/connected-account/services/blocklist/blocklist-validation.service';
|
||||
|
||||
@WorkspaceQueryHook(`blocklist.updateOne`)
|
||||
export class BlocklistUpdateOnePreQueryHook
|
||||
implements WorkspaceQueryHookInstance
|
||||
{
|
||||
constructor(
|
||||
private readonly blocklistValidationService: BlocklistValidationService,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
payload: UpdateOneResolverArgs<BlocklistItem>,
|
||||
): Promise<void> {
|
||||
await this.blocklistValidationService.validateBlocklistForUpdateOne(
|
||||
payload,
|
||||
userId,
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,23 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
||||
import { BlocklistCreateManyPreQueryHook } from 'src/modules/connected-account/query-hooks/blocklist/blocklist-create-many.pre-query.hook';
|
||||
import { BlocklistUpdateManyPreQueryHook } from 'src/modules/connected-account/query-hooks/blocklist/blocklist-update-many.pre-query.hook';
|
||||
import { BlocklistUpdateOnePreQueryHook } from 'src/modules/connected-account/query-hooks/blocklist/blocklist-update-one.pre-query.hook';
|
||||
import { ConnectedAccountDeleteOnePreQueryHook } from 'src/modules/connected-account/query-hooks/connected-account/connected-account-delete-one.pre-query.hook';
|
||||
import { BlocklistValidationModule } from 'src/modules/connected-account/services/blocklist/blocklist-validation.module';
|
||||
import { ConnectedAccountDeleteOnePreQueryHook } from 'src/modules/connected-account/query-hooks/connected-account-delete-one.pre-query.hook';
|
||||
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
BlocklistValidationModule,
|
||||
TwentyORMModule.forFeature([MessageChannelWorkspaceEntity]),
|
||||
],
|
||||
providers: [
|
||||
BlocklistCreateManyPreQueryHook,
|
||||
BlocklistUpdateManyPreQueryHook,
|
||||
BlocklistUpdateOnePreQueryHook,
|
||||
ConnectedAccountDeleteOnePreQueryHook,
|
||||
],
|
||||
imports: [TwentyORMModule.forFeature([MessageChannelWorkspaceEntity])],
|
||||
providers: [ConnectedAccountDeleteOnePreQueryHook],
|
||||
})
|
||||
export class ConnectedAccountQueryHookModule {}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
|
||||
import { GoogleAPIRefreshAccessTokenService } from 'src/modules/connected-account/services/google-api-refresh-access-token/google-api-refresh-access-token.service';
|
||||
import { GoogleAPIRefreshAccessTokenService } from 'src/modules/connected-account/refresh-access-token-manager/drivers/google/services/google-api-refresh-access-token.service';
|
||||
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
import { MessagingCommonModule } from 'src/modules/messaging/common/messaging-common.module';
|
||||
|
||||
@ -0,0 +1,29 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import axios from 'axios';
|
||||
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
|
||||
@Injectable()
|
||||
export class GoogleAPIRefreshAccessTokenService {
|
||||
constructor(private readonly environmentService: EnvironmentService) {}
|
||||
|
||||
async refreshAccessToken(refreshToken: string): Promise<string> {
|
||||
const response = await axios.post(
|
||||
'https://oauth2.googleapis.com/token',
|
||||
{
|
||||
client_id: this.environmentService.get('AUTH_GOOGLE_CLIENT_ID'),
|
||||
client_secret: this.environmentService.get('AUTH_GOOGLE_CLIENT_SECRET'),
|
||||
refresh_token: refreshToken,
|
||||
grant_type: 'refresh_token',
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return response.data.access_token;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { GoogleAPIRefreshAccessTokenModule } from 'src/modules/connected-account/refresh-access-token-manager/drivers/google/google-api-refresh-access-token.module';
|
||||
import { RefreshAccessTokenService } from 'src/modules/connected-account/refresh-access-token-manager/services/refresh-access-token.service';
|
||||
|
||||
@Module({
|
||||
imports: [GoogleAPIRefreshAccessTokenModule],
|
||||
providers: [RefreshAccessTokenService],
|
||||
exports: [RefreshAccessTokenService],
|
||||
})
|
||||
export class RefreshAccessTokenManagerModule {}
|
||||
@ -1,16 +1,14 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import axios from 'axios';
|
||||
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
|
||||
import { GoogleAPIRefreshAccessTokenService } from 'src/modules/connected-account/refresh-access-token-manager/drivers/google/services/google-api-refresh-access-token.service';
|
||||
import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository';
|
||||
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
|
||||
@Injectable()
|
||||
export class GoogleAPIRefreshAccessTokenService {
|
||||
export class RefreshAccessTokenService {
|
||||
constructor(
|
||||
private readonly environmentService: EnvironmentService,
|
||||
private readonly googleAPIRefreshAccessTokenService: GoogleAPIRefreshAccessTokenService,
|
||||
@InjectObjectMetadataRepository(ConnectedAccountWorkspaceEntity)
|
||||
private readonly connectedAccountRepository: ConnectedAccountRepository,
|
||||
) {}
|
||||
@ -26,7 +24,10 @@ export class GoogleAPIRefreshAccessTokenService {
|
||||
`No refresh token found for connected account ${connectedAccount.id} in workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
const accessToken = await this.refreshAccessToken(refreshToken);
|
||||
const accessToken = await this.refreshAccessToken(
|
||||
connectedAccount,
|
||||
refreshToken,
|
||||
);
|
||||
|
||||
await this.connectedAccountRepository.updateAccessToken(
|
||||
accessToken,
|
||||
@ -43,22 +44,19 @@ export class GoogleAPIRefreshAccessTokenService {
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
async refreshAccessToken(refreshToken: string): Promise<string> {
|
||||
const response = await axios.post(
|
||||
'https://oauth2.googleapis.com/token',
|
||||
{
|
||||
client_id: this.environmentService.get('AUTH_GOOGLE_CLIENT_ID'),
|
||||
client_secret: this.environmentService.get('AUTH_GOOGLE_CLIENT_SECRET'),
|
||||
refresh_token: refreshToken,
|
||||
grant_type: 'refresh_token',
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return response.data.access_token;
|
||||
async refreshAccessToken(
|
||||
connectedAccount: ConnectedAccountWorkspaceEntity,
|
||||
refreshToken: string,
|
||||
): Promise<string> {
|
||||
switch (connectedAccount.provider) {
|
||||
case 'google':
|
||||
return this.googleAPIRefreshAccessTokenService.refreshAccessToken(
|
||||
refreshToken,
|
||||
);
|
||||
default:
|
||||
throw new Error(
|
||||
`Provider ${connectedAccount.provider} is not supported.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,69 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { EntityManager } from 'typeorm';
|
||||
|
||||
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
||||
import { BlocklistWorkspaceEntity } from 'src/modules/connected-account/standard-objects/blocklist.workspace-entity';
|
||||
|
||||
@Injectable()
|
||||
export class BlocklistRepository {
|
||||
constructor(
|
||||
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
||||
) {}
|
||||
|
||||
public async getById(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<BlocklistWorkspaceEntity | null> {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
const blocklistItems =
|
||||
await this.workspaceDataSourceService.executeRawQuery(
|
||||
`SELECT * FROM ${dataSourceSchema}."blocklist" WHERE "id" = $1`,
|
||||
[id],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
if (!blocklistItems || blocklistItems.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return blocklistItems[0];
|
||||
}
|
||||
|
||||
public async getByWorkspaceMemberId(
|
||||
workspaceMemberId: string,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<BlocklistWorkspaceEntity[]> {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
return await this.workspaceDataSourceService.executeRawQuery(
|
||||
`SELECT * FROM ${dataSourceSchema}."blocklist" WHERE "workspaceMemberId" = $1`,
|
||||
[workspaceMemberId],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
|
||||
public async getByWorkspaceMemberIdAndHandle(
|
||||
workspaceMemberId: string,
|
||||
handle: string,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<BlocklistWorkspaceEntity[]> {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
return await this.workspaceDataSourceService.executeRawQuery(
|
||||
`SELECT * FROM ${dataSourceSchema}."blocklist" WHERE "workspaceMemberId" = $1 AND "handle" = $2`,
|
||||
[workspaceMemberId, handle],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,18 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
|
||||
import { BlocklistValidationService } from 'src/modules/connected-account/services/blocklist/blocklist-validation.service';
|
||||
import { BlocklistWorkspaceEntity } from 'src/modules/connected-account/standard-objects/blocklist.workspace-entity';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ObjectMetadataRepositoryModule.forFeature([
|
||||
BlocklistWorkspaceEntity,
|
||||
WorkspaceMemberWorkspaceEntity,
|
||||
]),
|
||||
],
|
||||
providers: [BlocklistValidationService],
|
||||
exports: [BlocklistValidationService],
|
||||
})
|
||||
export class BlocklistValidationModule {}
|
||||
@ -1,146 +0,0 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
CreateManyResolverArgs,
|
||||
UpdateOneResolverArgs,
|
||||
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
|
||||
import { isDomain } from 'src/engine/utils/is-domain';
|
||||
import { BlocklistRepository } from 'src/modules/connected-account/repositories/blocklist.repository';
|
||||
import { BlocklistWorkspaceEntity } from 'src/modules/connected-account/standard-objects/blocklist.workspace-entity';
|
||||
import { WorkspaceMemberRepository } from 'src/modules/workspace-member/repositories/workspace-member.repository';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
export type BlocklistItem = Omit<
|
||||
BlocklistWorkspaceEntity,
|
||||
'createdAt' | 'updatedAt' | 'workspaceMember'
|
||||
> & {
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workspaceMemberId: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class BlocklistValidationService {
|
||||
constructor(
|
||||
@InjectObjectMetadataRepository(BlocklistWorkspaceEntity)
|
||||
private readonly blocklistRepository: BlocklistRepository,
|
||||
@InjectObjectMetadataRepository(WorkspaceMemberWorkspaceEntity)
|
||||
private readonly workspaceMemberRepository: WorkspaceMemberRepository,
|
||||
) {}
|
||||
|
||||
public async validateBlocklistForCreateMany(
|
||||
payload: CreateManyResolverArgs<BlocklistItem>,
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
) {
|
||||
await this.validateSchema(payload.data);
|
||||
await this.validateUniquenessForCreateMany(payload, userId, workspaceId);
|
||||
}
|
||||
|
||||
public async validateBlocklistForUpdateOne(
|
||||
payload: UpdateOneResolverArgs<BlocklistItem>,
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
) {
|
||||
if (payload.data.handle) {
|
||||
await this.validateSchema([payload.data]);
|
||||
}
|
||||
await this.validateUniquenessForUpdateOne(payload, userId, workspaceId);
|
||||
}
|
||||
|
||||
public async validateSchema(blocklist: BlocklistItem[]) {
|
||||
const emailOrDomainSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.email('Invalid email or domain')
|
||||
.or(
|
||||
z
|
||||
.string()
|
||||
.refine(
|
||||
(value) => value.startsWith('@') && isDomain(value.slice(1)),
|
||||
'Invalid email or domain',
|
||||
),
|
||||
);
|
||||
|
||||
for (const handle of blocklist.map((item) => item.handle)) {
|
||||
if (!handle) {
|
||||
throw new BadRequestException('Blocklist handle is required');
|
||||
}
|
||||
|
||||
const result = emailOrDomainSchema.safeParse(handle);
|
||||
|
||||
if (!result.success) {
|
||||
throw new BadRequestException(result.error.errors[0].message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async validateUniquenessForCreateMany(
|
||||
payload: CreateManyResolverArgs<BlocklistItem>,
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
) {
|
||||
const currentWorkspaceMember =
|
||||
await this.workspaceMemberRepository.getByIdOrFail(userId, workspaceId);
|
||||
|
||||
const currentBlocklist =
|
||||
await this.blocklistRepository.getByWorkspaceMemberId(
|
||||
currentWorkspaceMember.id,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const currentBlocklistHandles = currentBlocklist.map(
|
||||
(blocklist) => blocklist.handle,
|
||||
);
|
||||
|
||||
if (
|
||||
payload.data.some((item) => currentBlocklistHandles.includes(item.handle))
|
||||
) {
|
||||
throw new BadRequestException('Blocklist handle already exists');
|
||||
}
|
||||
}
|
||||
|
||||
public async validateUniquenessForUpdateOne(
|
||||
payload: UpdateOneResolverArgs<BlocklistItem>,
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
) {
|
||||
const existingRecord = await this.blocklistRepository.getById(
|
||||
payload.id,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (!existingRecord) {
|
||||
throw new BadRequestException('Blocklist item not found');
|
||||
}
|
||||
|
||||
if (existingRecord.workspaceMemberId !== payload.data.workspaceMemberId) {
|
||||
throw new BadRequestException('Workspace member cannot be updated');
|
||||
}
|
||||
|
||||
if (existingRecord.handle === payload.data.handle) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentWorkspaceMember =
|
||||
await this.workspaceMemberRepository.getByIdOrFail(userId, workspaceId);
|
||||
|
||||
const currentBlocklist =
|
||||
await this.blocklistRepository.getByWorkspaceMemberId(
|
||||
currentWorkspaceMember.id,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const currentBlocklistHandles = currentBlocklist
|
||||
.filter((blocklist) => blocklist.id !== payload.id)
|
||||
.map((blocklist) => blocklist.handle);
|
||||
|
||||
if (currentBlocklistHandles.includes(payload.data.handle)) {
|
||||
throw new BadRequestException('Blocklist handle already exists');
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,49 +0,0 @@
|
||||
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
|
||||
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { BLOCKLIST_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
|
||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
||||
import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
|
||||
|
||||
@WorkspaceEntity({
|
||||
standardId: STANDARD_OBJECT_IDS.blocklist,
|
||||
namePlural: 'blocklists',
|
||||
labelSingular: 'Blocklist',
|
||||
labelPlural: 'Blocklists',
|
||||
description: 'Blocklist',
|
||||
icon: 'IconForbid2',
|
||||
})
|
||||
@WorkspaceIsSystem()
|
||||
@WorkspaceIsNotAuditLogged()
|
||||
export class BlocklistWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
@WorkspaceField({
|
||||
standardId: BLOCKLIST_STANDARD_FIELD_IDS.handle,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Handle',
|
||||
description: 'Handle',
|
||||
icon: 'IconAt',
|
||||
})
|
||||
handle: string;
|
||||
|
||||
@WorkspaceRelation({
|
||||
standardId: BLOCKLIST_STANDARD_FIELD_IDS.workspaceMember,
|
||||
type: RelationMetadataType.MANY_TO_ONE,
|
||||
label: 'WorkspaceMember',
|
||||
description: 'WorkspaceMember',
|
||||
icon: 'IconCircleUser',
|
||||
inverseSideTarget: () => WorkspaceMemberWorkspaceEntity,
|
||||
inverseSideFieldKey: 'blocklist',
|
||||
})
|
||||
workspaceMember: Relation<WorkspaceMemberWorkspaceEntity>;
|
||||
|
||||
@WorkspaceJoinColumn('workspaceMember')
|
||||
workspaceMemberId: string;
|
||||
}
|
||||
Reference in New Issue
Block a user