Files
twenty_crm/packages/twenty-server/src/modules/contact-creation-manager/services/create-company.service.ts
Weiko 2deac9448e Add db event emitter in twenty orm (#13167)
## Context
Add an eventEmitter instance to twenty datasources so we can emit DB
events.
Add input and output formatting to twenty orm (formatData, formatResult)
Those 2 elements simplified existing logic when we interact with the
ORM, input will be formatted by the ORM so we can directly use
field-like structure instead of column-like. The output will be
formatted, for builder queries it will be in `result.generatedMaps`
where `result.raw` preserves the previous column-like structure.

Important change: We now have an authContext that we can pass when we
get a repository, this will be used for the different events emitted in
the ORM. We also removed the caching for repositories as it was not
scaling well and not necessary imho

Note: An upcoming PR should handle the onDelete: cascade behavior where
we send DESTROY events in cascade when there is an onDelete: CASCADE on
the FK.

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2025-07-17 18:07:28 +02:00

195 lines
5.9 KiB
TypeScript

import { Injectable } from '@nestjs/common';
import axios, { AxiosInstance } from 'axios';
import uniqBy from 'lodash.uniqby';
import { TWENTY_COMPANIES_BASE_URL } from 'twenty-shared/constants';
import { ConnectedAccountProvider } from 'twenty-shared/types';
import { lowercaseUrlOriginAndRemoveTrailingSlash } from 'twenty-shared/utils';
import { DeepPartial, ILike } from 'typeorm';
import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { CompanyWorkspaceEntity } from 'src/modules/company/standard-objects/company.workspace-entity';
import { extractDomainFromLink } from 'src/modules/contact-creation-manager/utils/extract-domain-from-link.util';
import { getCompanyNameFromDomainName } from 'src/modules/contact-creation-manager/utils/get-company-name-from-domain-name.util';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { computeDisplayName } from 'src/utils/compute-display-name';
export type CompanyToCreate = {
domainName: string | undefined;
createdBySource: FieldActorSource;
createdByWorkspaceMember?: WorkspaceMemberWorkspaceEntity | null;
createdByContext: {
provider: ConnectedAccountProvider;
};
};
@Injectable()
export class CreateCompanyService {
private readonly httpService: AxiosInstance;
constructor(private readonly twentyORMGlobalManager: TwentyORMGlobalManager) {
this.httpService = axios.create({
baseURL: TWENTY_COMPANIES_BASE_URL,
});
}
async createCompanies(
companies: CompanyToCreate[],
workspaceId: string,
): Promise<{
[domainName: string]: string;
}> {
if (companies.length === 0) {
return {};
}
const companyRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
CompanyWorkspaceEntity,
{
shouldBypassPermissionChecks: true,
},
);
// Remove trailing slash from domain names
const companiesWithoutTrailingSlash = companies.map((company) => ({
...company,
domainName: company.domainName
? lowercaseUrlOriginAndRemoveTrailingSlash(company.domainName)
: undefined,
}));
// Avoid creating duplicate companies, e.g. example.com and example.com/
const uniqueCompanies = uniqBy(companiesWithoutTrailingSlash, 'domainName');
const conditions = uniqueCompanies.map((companyToCreate) => ({
domainName: {
primaryLinkUrl: ILike(`%${companyToCreate.domainName}%`),
},
}));
// Find existing companies
const existingCompanies = await companyRepository.find({
where: conditions,
});
const existingCompanyIdsMap = this.createCompanyMap(existingCompanies);
// Filter out companies that already exist
const newCompaniesToCreate = uniqueCompanies.filter(
(company) =>
!existingCompanies.some(
(existingCompany) =>
existingCompany.domainName &&
extractDomainFromLink(existingCompany.domainName.primaryLinkUrl) ===
company.domainName,
),
);
if (newCompaniesToCreate.length === 0) {
return existingCompanyIdsMap;
}
// Retrieve the last company position
let lastCompanyPosition =
await this.getLastCompanyPosition(companyRepository);
const newCompaniesData = await Promise.all(
newCompaniesToCreate.map((company) =>
this.prepareCompanyData(company, ++lastCompanyPosition),
),
);
// Create new companies
const createdCompanies = await companyRepository.save(newCompaniesData);
const createdCompanyIdsMap = this.createCompanyMap(createdCompanies);
return {
...existingCompanyIdsMap,
...createdCompanyIdsMap,
};
}
private async prepareCompanyData(
company: CompanyToCreate,
position: number,
): Promise<DeepPartial<CompanyWorkspaceEntity>> {
const { name, city } = await this.getCompanyInfoFromDomainName(
company.domainName,
);
const createdByName = computeDisplayName(
company.createdByWorkspaceMember?.name,
);
return {
domainName: {
primaryLinkUrl: 'https://' + company.domainName,
},
name,
createdBy: {
source: company.createdBySource,
workspaceMemberId: company.createdByWorkspaceMember?.id,
name: createdByName,
context: {
provider: company.createdByContext.provider,
},
},
address: {
addressCity: city,
},
position,
};
}
private createCompanyMap(companies: DeepPartial<CompanyWorkspaceEntity>[]) {
return companies.reduce(
(acc, company) => {
if (!company.domainName?.primaryLinkUrl || !company.id) {
return acc;
}
const key = extractDomainFromLink(company.domainName.primaryLinkUrl);
acc[key] = company.id;
return acc;
},
{} as { [domainName: string]: string },
);
}
private async getLastCompanyPosition(
companyRepository: WorkspaceRepository<CompanyWorkspaceEntity>,
): Promise<number> {
const lastCompanyPosition = await companyRepository.maximum(
'position',
undefined,
);
return lastCompanyPosition ?? 0;
}
private async getCompanyInfoFromDomainName(
domainName: string | undefined,
): 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: '',
};
}
}
}