Files
twenty_crm/packages/twenty-server/src/modules/contact-creation-manager/services/create-company-and-contact.service.ts
Marie a189f15313 [permissions] fix workflows + remove shouldBypassPermissionChecks for system objects (#12559)
In this PR 

1. fix workflow step creation by adding forgotten
`shouldBypassPermissionChecks` in WorkflowVersionStepWorkspaceService
2. clarify the rule for twentyORMGlobalManager: do not add unnecessary
`shouldBypassPermissionChecks` for system objects (there are no
object-records permission checks on system objects, they are dealt with
at resolver level)
2025-06-12 13:56:41 +02:00

261 lines
9.5 KiB
TypeScript

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isNonEmptyString } from '@sniptt/guards';
import chunk from 'lodash.chunk';
import compact from 'lodash.compact';
import { DeepPartial, Repository } from 'typeorm';
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { CONTACTS_CREATION_BATCH_SIZE } from 'src/modules/contact-creation-manager/constants/contacts-creation-batch-size.constant';
import { CreateCompanyService } from 'src/modules/contact-creation-manager/services/create-company.service';
import { CreateContactService } from 'src/modules/contact-creation-manager/services/create-contact.service';
import { Contact } from 'src/modules/contact-creation-manager/types/contact.type';
import { filterOutSelfAndContactsFromCompanyOrWorkspace } from 'src/modules/contact-creation-manager/utils/filter-out-contacts-from-company-or-workspace.util';
import { getDomainNameFromHandle } from 'src/modules/contact-creation-manager/utils/get-domain-name-from-handle.util';
import { getUniqueContactsAndHandles } from 'src/modules/contact-creation-manager/utils/get-unique-contacts-and-handles.util';
import { addPersonEmailFiltersToQueryBuilder } from 'src/modules/match-participant/utils/add-person-email-filters-to-query-builder';
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 { isWorkDomain, isWorkEmail } from 'src/utils/is-work-email';
@Injectable()
export class CreateCompanyAndContactService {
constructor(
private readonly createContactService: CreateContactService,
private readonly createCompaniesService: CreateCompanyService,
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
@InjectRepository(ObjectMetadataEntity, 'core')
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly exceptionHandlerService: ExceptionHandlerService,
) {}
private async createCompaniesAndPeople(
connectedAccount: ConnectedAccountWorkspaceEntity,
contactsToCreate: Contact[],
workspaceId: string,
source: FieldActorSource,
): Promise<DeepPartial<PersonWorkspaceEntity>[]> {
if (!contactsToCreate || contactsToCreate.length === 0) {
return [];
}
const personRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
PersonWorkspaceEntity,
{
shouldBypassPermissionChecks: true,
},
);
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
WorkspaceMemberWorkspaceEntity,
);
const workspaceMembers = await workspaceMemberRepository.find();
const contactsToCreateFromOtherCompanies =
filterOutSelfAndContactsFromCompanyOrWorkspace(
contactsToCreate,
connectedAccount,
workspaceMembers,
);
const { uniqueContacts, uniqueHandles } = getUniqueContactsAndHandles(
contactsToCreateFromOtherCompanies,
);
if (uniqueHandles.length === 0) {
return [];
}
const queryBuilder = addPersonEmailFiltersToQueryBuilder({
queryBuilder: personRepository.createQueryBuilder('person'),
emails: uniqueHandles,
});
const rawAlreadyCreatedContacts = await queryBuilder
.orderBy('person.createdAt', 'ASC')
.getMany();
const alreadyCreatedContacts = await personRepository.formatResult(
rawAlreadyCreatedContacts,
);
const alreadyCreatedContactEmails: string[] =
alreadyCreatedContacts?.reduce<string[]>((acc, { emails }) => {
const currentContactEmails: string[] = [];
if (isNonEmptyString(emails?.primaryEmail)) {
currentContactEmails.push(emails.primaryEmail.toLowerCase());
}
if (Array.isArray(emails?.additionalEmails)) {
const additionalEmails = emails.additionalEmails
.filter(isNonEmptyString)
.map((email) => email.toLowerCase());
currentContactEmails.push(...additionalEmails);
}
return [...acc, ...currentContactEmails];
}, []);
const filteredContactsToCreate = uniqueContacts.filter(
(participant) =>
!alreadyCreatedContactEmails.includes(
participant.handle.toLowerCase(),
) && 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
.filter((participant) => participant.companyDomainName)
.map((participant) => ({
domainName: participant.companyDomainName,
createdBySource: source,
createdByWorkspaceMember: connectedAccount.accountOwner,
})),
);
const workDomainNamesToCreate = domainNamesToCreate.filter(
(domainName) =>
domainName?.domainName && isWorkDomain(domainName.domainName),
);
const workDomainNamesToCreateFormatted = workDomainNamesToCreate.map(
(domainName) => ({
...domainName,
createdBySource: source,
createdByWorkspaceMember: connectedAccount.accountOwner,
createdByContext: {
provider: connectedAccount.provider,
},
}),
);
const companiesObject = await this.createCompaniesService.createCompanies(
workDomainNamesToCreateFormatted,
workspaceId,
);
const formattedContactsToCreate =
filteredContactsToCreateWithCompanyDomainNames.map((contact) => ({
handle: contact.handle,
displayName: contact.displayName,
companyId:
contact.companyDomainName && contact.companyDomainName !== ''
? companiesObject[contact.companyDomainName]
: undefined,
createdBySource: source,
createdByWorkspaceMember: connectedAccount.accountOwner,
createdByContext: {
provider: connectedAccount.provider,
},
}));
return this.createContactService.createPeople(
formattedContactsToCreate,
workspaceId,
);
}
async createCompaniesAndContactsAndUpdateParticipants(
connectedAccount: ConnectedAccountWorkspaceEntity,
contactsToCreate: Contact[],
workspaceId: string,
source: FieldActorSource,
) {
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');
}
// In some jobs the accountOwner is not populated
if (!connectedAccount.accountOwner) {
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
WorkspaceMemberWorkspaceEntity,
);
const workspaceMember = await workspaceMemberRepository.findOne({
where: {
id: connectedAccount.accountOwnerId,
},
});
if (!workspaceMember) {
throw new Error(
`Workspace member with id ${connectedAccount.accountOwnerId} not found in workspace ${workspaceId}`,
);
}
connectedAccount.accountOwner = workspaceMember;
}
for (const contactsBatch of contactsBatches) {
try {
const createdPeople = await this.createCompaniesAndPeople(
connectedAccount,
contactsBatch,
workspaceId,
source,
);
this.workspaceEventEmitter.emitDatabaseBatchEvent({
objectMetadataNameSingular: 'person',
action: DatabaseEventAction.CREATED,
events: createdPeople.map((createdPerson) => ({
// Fix ' as string': TypeORM typing issue... id is always returned when using save
recordId: createdPerson.id as string,
objectMetadata,
properties: {
after: createdPerson,
},
})),
workspaceId,
});
} catch (error) {
this.exceptionHandlerService.captureExceptions([error], {
workspace: {
id: workspaceId,
},
});
}
}
}
}