mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-23 12:02:10 +03:00
feat: created by email calendar (#6536)
This PR is a followup of #6324 to add support of EMAIL and CALENDAR source for the created by composite field.
This commit is contained in:
parent
dce5a64ec5
commit
11a41b3d97
@ -192,7 +192,7 @@
|
||||
"tslib": "^2.3.0",
|
||||
"tsup": "^8.0.1",
|
||||
"type-fest": "4.10.1",
|
||||
"typeorm": "^0.3.20",
|
||||
"typeorm": "patch:typeorm@0.3.20#./packages/twenty-server/patches/typeorm+0.3.20.patch",
|
||||
"typescript": "5.3.3",
|
||||
"use-context-selector": "^2.0.0",
|
||||
"use-debounce": "^10.0.0",
|
||||
|
14
packages/twenty-server/patches/typeorm+0.3.20.patch
Normal file
14
packages/twenty-server/patches/typeorm+0.3.20.patch
Normal file
@ -0,0 +1,14 @@
|
||||
diff --git a/node_modules/typeorm/common/PickKeysByType.d.ts b/node_modules/typeorm/common/PickKeysByType.d.ts
|
||||
index 55ad347..1a8a184 100644
|
||||
--- a/common/PickKeysByType.d.ts
|
||||
+++ b/common/PickKeysByType.d.ts
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Pick only the keys that match the Type `U`
|
||||
*/
|
||||
-export type PickKeysByType<T, U> = string & keyof {
|
||||
- [P in keyof T as T[P] extends U ? P : never]: T[P];
|
||||
-};
|
||||
+export type PickKeysByType<T, U> = string & {
|
||||
+ [P in keyof T]: Exclude<T[P], null> extends U ? P : never;
|
||||
+}[keyof T];
|
@ -1,12 +1,10 @@
|
||||
import { BlocklistRepository } from 'src/modules/blocklist/repositories/blocklist.repository';
|
||||
import { CompanyRepository } from 'src/modules/company/repositories/company.repository';
|
||||
import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository';
|
||||
import { MessageChannelMessageAssociationRepository } from 'src/modules/messaging/common/repositories/message-channel-message-association.repository';
|
||||
import { MessageChannelRepository } from 'src/modules/messaging/common/repositories/message-channel.repository';
|
||||
import { MessageParticipantRepository } from 'src/modules/messaging/common/repositories/message-participant.repository';
|
||||
import { MessageThreadRepository } from 'src/modules/messaging/common/repositories/message-thread.repository';
|
||||
import { MessageRepository } from 'src/modules/messaging/common/repositories/message.repository';
|
||||
import { PersonRepository } from 'src/modules/person/repositories/person.repository';
|
||||
import { AuditLogRepository } from 'src/modules/timeline/repositiories/audit-log.repository';
|
||||
import { TimelineActivityRepository } from 'src/modules/timeline/repositiories/timeline-activity.repository';
|
||||
import { WorkspaceMemberRepository } from 'src/modules/workspace-member/repositories/workspace-member.repository';
|
||||
@ -14,7 +12,6 @@ import { WorkspaceMemberRepository } from 'src/modules/workspace-member/reposito
|
||||
export const metadataToRepositoryMapping = {
|
||||
AuditLogWorkspaceEntity: AuditLogRepository,
|
||||
BlocklistWorkspaceEntity: BlocklistRepository,
|
||||
CompanyWorkspaceEntity: CompanyRepository,
|
||||
ConnectedAccountWorkspaceEntity: ConnectedAccountRepository,
|
||||
MessageChannelMessageAssociationWorkspaceEntity:
|
||||
MessageChannelMessageAssociationRepository,
|
||||
@ -22,7 +19,6 @@ export const metadataToRepositoryMapping = {
|
||||
MessageWorkspaceEntity: MessageRepository,
|
||||
MessageParticipantWorkspaceEntity: MessageParticipantRepository,
|
||||
MessageThreadWorkspaceEntity: MessageThreadRepository,
|
||||
PersonWorkspaceEntity: PersonRepository,
|
||||
TimelineActivityWorkspaceEntity: TimelineActivityRepository,
|
||||
WorkspaceMemberWorkspaceEntity: WorkspaceMemberRepository,
|
||||
};
|
||||
|
@ -18,9 +18,9 @@ import {
|
||||
SaveOptions,
|
||||
UpdateResult,
|
||||
} from 'typeorm';
|
||||
import { PickKeysByType } from 'typeorm/common/PickKeysByType';
|
||||
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
|
||||
import { UpsertOptions } from 'typeorm/repository/UpsertOptions';
|
||||
import { PickKeysByType } from 'typeorm/common/PickKeysByType';
|
||||
|
||||
import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface';
|
||||
|
||||
|
@ -27,7 +27,6 @@ import { CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standa
|
||||
import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module';
|
||||
import { RefreshAccessTokenManagerModule } from 'src/modules/connected-account/refresh-access-token-manager/refresh-access-token-manager.module';
|
||||
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
@Module({
|
||||
@ -41,7 +40,6 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
|
||||
ObjectMetadataRepositoryModule.forFeature([
|
||||
ConnectedAccountWorkspaceEntity,
|
||||
BlocklistWorkspaceEntity,
|
||||
PersonWorkspaceEntity,
|
||||
WorkspaceMemberWorkspaceEntity,
|
||||
]),
|
||||
CalendarEventParticipantManagerModule,
|
||||
|
@ -19,6 +19,7 @@ import {
|
||||
CreateCompanyAndContactJob,
|
||||
CreateCompanyAndContactJobData,
|
||||
} from 'src/modules/contact-creation-manager/jobs/create-company-and-contact.job';
|
||||
import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
|
||||
@Injectable()
|
||||
export class CalendarSaveEventsService {
|
||||
@ -153,6 +154,7 @@ export class CalendarSaveEventsService {
|
||||
workspaceId,
|
||||
connectedAccount,
|
||||
contactsToCreate: participantsToSave,
|
||||
source: FieldActorSource.CALENDAR,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
||||
import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
|
||||
import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity';
|
||||
import { CreateCompanyAndContactService } from 'src/modules/contact-creation-manager/services/create-company-and-contact.service';
|
||||
import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
|
||||
export type CalendarCreateCompanyAndContactAfterSyncJobData = {
|
||||
workspaceId: string;
|
||||
@ -96,6 +97,7 @@ export class CalendarCreateCompanyAndContactAfterSyncJob {
|
||||
connectedAccount,
|
||||
calendarEventParticipantsWithoutPersonIdAndWorkspaceMemberId,
|
||||
workspaceId,
|
||||
FieldActorSource.CALENDAR,
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
|
@ -1,85 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { EntityManager } from 'typeorm';
|
||||
|
||||
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
||||
|
||||
export type CompanyToCreate = {
|
||||
id: string;
|
||||
domainName: string;
|
||||
name?: string;
|
||||
city?: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class CompanyRepository {
|
||||
constructor(
|
||||
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
||||
) {}
|
||||
|
||||
public async getExistingCompaniesByDomainNames(
|
||||
domainNames: string[],
|
||||
workspaceId: string,
|
||||
companyDomainNameColumnName: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<{ id: string; domainName: string }[]> {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
const existingCompanies =
|
||||
await this.workspaceDataSourceService.executeRawQuery(
|
||||
`SELECT id, "${companyDomainNameColumnName}" AS "domainName" FROM ${dataSourceSchema}.company WHERE REGEXP_REPLACE("${companyDomainNameColumnName}", '^https?://', '') = ANY($1)`,
|
||||
[domainNames],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
return existingCompanies;
|
||||
}
|
||||
|
||||
public async getLastCompanyPosition(
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<number> {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
const result = await this.workspaceDataSourceService.executeRawQuery(
|
||||
`SELECT MAX(position) FROM ${dataSourceSchema}.company`,
|
||||
[],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
return result[0].max ?? 0;
|
||||
}
|
||||
|
||||
public async createCompany(
|
||||
workspaceId: string,
|
||||
companyToCreate: CompanyToCreate,
|
||||
companyDomainNameColumnName: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<void> {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
const lastCompanyPosition = await this.getLastCompanyPosition(
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
await this.workspaceDataSourceService.executeRawQuery(
|
||||
`INSERT INTO ${dataSourceSchema}.company (id, "${companyDomainNameColumnName}", name, "addressAddressCity", position)
|
||||
VALUES ($1, $2, $3, $4, $5)`,
|
||||
[
|
||||
companyToCreate.id,
|
||||
'https://' + companyToCreate.domainName,
|
||||
companyToCreate.name ?? '',
|
||||
companyToCreate.city ?? '',
|
||||
lastCompanyPosition + 1,
|
||||
],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
}
|
@ -1,5 +1,3 @@
|
||||
import { Address } from 'nodemailer/lib/mailer';
|
||||
|
||||
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
|
||||
|
||||
import {
|
||||
@ -32,6 +30,7 @@ import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/perso
|
||||
import { TaskTargetWorkspaceEntity } from 'src/modules/task/standard-objects/task-target.workspace-entity';
|
||||
import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
import { AddressMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/address.composite-type';
|
||||
|
||||
@WorkspaceEntity({
|
||||
standardId: STANDARD_OBJECT_IDS.company,
|
||||
@ -60,7 +59,7 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
'The company website URL. We use this url to fetch the company icon',
|
||||
icon: 'IconLink',
|
||||
})
|
||||
domainName?: string;
|
||||
domainName?: LinksMetadata;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: COMPANY_STANDARD_FIELD_IDS.employees,
|
||||
@ -111,7 +110,7 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
icon: 'IconMap',
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
address: Address;
|
||||
address: AddressMetadata;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: COMPANY_STANDARD_FIELD_IDS.idealCustomerProfile,
|
||||
|
@ -2,30 +2,22 @@ import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
import { CompanyWorkspaceEntity } from 'src/modules/company/standard-objects/company.workspace-entity';
|
||||
import { AutoCompaniesAndContactsCreationCalendarChannelListener } from 'src/modules/contact-creation-manager/listeners/auto-companies-and-contacts-creation-calendar-channel.listener';
|
||||
import { AutoCompaniesAndContactsCreationMessageChannelListener } from 'src/modules/contact-creation-manager/listeners/auto-companies-and-contacts-creation-message-channel.listener';
|
||||
import { CreateCompanyAndContactService } from 'src/modules/contact-creation-manager/services/create-company-and-contact.service';
|
||||
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 { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ObjectMetadataRepositoryModule.forFeature([
|
||||
PersonWorkspaceEntity,
|
||||
WorkspaceMemberWorkspaceEntity,
|
||||
CompanyWorkspaceEntity,
|
||||
]),
|
||||
ObjectMetadataRepositoryModule.forFeature([WorkspaceMemberWorkspaceEntity]),
|
||||
WorkspaceDataSourceModule,
|
||||
TypeOrmModule.forFeature([FeatureFlagEntity], 'core'),
|
||||
TypeOrmModule.forFeature([ObjectMetadataEntity], 'metadata'),
|
||||
TypeOrmModule.forFeature([FieldMetadataEntity], 'metadata'),
|
||||
],
|
||||
providers: [
|
||||
CreateCompanyService,
|
||||
|
@ -1,6 +1,7 @@
|
||||
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 { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
import { CreateCompanyAndContactService } from 'src/modules/contact-creation-manager/services/create-company-and-contact.service';
|
||||
|
||||
@ -11,6 +12,7 @@ export type CreateCompanyAndContactJobData = {
|
||||
displayName: string;
|
||||
handle: string;
|
||||
}[];
|
||||
source: FieldActorSource;
|
||||
};
|
||||
|
||||
@Processor(MessageQueue.contactCreationQueue)
|
||||
@ -21,7 +23,7 @@ export class CreateCompanyAndContactJob {
|
||||
|
||||
@Process(CreateCompanyAndContactJob.name)
|
||||
async handle(data: CreateCompanyAndContactJobData): Promise<void> {
|
||||
const { workspaceId, connectedAccount, contactsToCreate } = data;
|
||||
const { workspaceId, connectedAccount, contactsToCreate, source } = data;
|
||||
|
||||
await this.createCompanyAndContactService.createCompaniesAndContactsAndUpdateParticipants(
|
||||
connectedAccount,
|
||||
@ -30,6 +32,7 @@ export class CreateCompanyAndContactJob {
|
||||
displayName: contact.displayName,
|
||||
})),
|
||||
workspaceId,
|
||||
source,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -4,16 +4,11 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import chunk from 'lodash.chunk';
|
||||
import compact from 'lodash.compact';
|
||||
import { EntityManager, Repository } from 'typeorm';
|
||||
import { Any, EntityManager, Repository } from 'typeorm';
|
||||
|
||||
import { ObjectRecordCreateEvent } from 'src/engine/integrations/event-emitter/types/object-record-create.event';
|
||||
import {
|
||||
FieldMetadataEntity,
|
||||
FieldMetadataType,
|
||||
} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
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 { COMPANY_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 { 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';
|
||||
@ -23,39 +18,43 @@ 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 { 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';
|
||||
import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
|
||||
@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>,
|
||||
@InjectRepository(FieldMetadataEntity, 'metadata')
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {}
|
||||
|
||||
private async createCompaniesAndPeople(
|
||||
connectedAccount: ConnectedAccountWorkspaceEntity,
|
||||
contactsToCreate: Contact[],
|
||||
workspaceId: string,
|
||||
companyDomainNameColumnName: string,
|
||||
source: FieldActorSource,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<PersonWorkspaceEntity[]> {
|
||||
): Promise<DeepPartial<PersonWorkspaceEntity>[]> {
|
||||
if (!contactsToCreate || contactsToCreate.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const personRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
PersonWorkspaceEntity,
|
||||
);
|
||||
|
||||
const workspaceMembers =
|
||||
await this.workspaceMemberRepository.getAllByWorkspaceId(
|
||||
workspaceId,
|
||||
@ -77,11 +76,11 @@ export class CreateCompanyAndContactService {
|
||||
return [];
|
||||
}
|
||||
|
||||
const alreadyCreatedContacts = await this.personRepository.getByEmails(
|
||||
uniqueHandles,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
const alreadyCreatedContacts = await personRepository.find({
|
||||
where: {
|
||||
email: Any(uniqueHandles),
|
||||
},
|
||||
});
|
||||
|
||||
const alreadyCreatedContactEmails: string[] = alreadyCreatedContacts?.map(
|
||||
({ email }) => email,
|
||||
@ -103,15 +102,18 @@ export class CreateCompanyAndContactService {
|
||||
}));
|
||||
|
||||
const domainNamesToCreate = compact(
|
||||
filteredContactsToCreateWithCompanyDomainNames.map(
|
||||
(participant) => participant.companyDomainName,
|
||||
),
|
||||
filteredContactsToCreateWithCompanyDomainNames
|
||||
.filter((participant) => participant.companyDomainName)
|
||||
.map((participant) => ({
|
||||
domainName: participant.companyDomainName!,
|
||||
createdBySource: source,
|
||||
createdByWorkspaceMember: connectedAccount.accountOwner,
|
||||
})),
|
||||
);
|
||||
|
||||
const companiesObject = await this.createCompaniesService.createCompanies(
|
||||
domainNamesToCreate,
|
||||
workspaceId,
|
||||
companyDomainNameColumnName,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
@ -123,9 +125,11 @@ export class CreateCompanyAndContactService {
|
||||
contact.companyDomainName && contact.companyDomainName !== ''
|
||||
? companiesObject[contact.companyDomainName]
|
||||
: undefined,
|
||||
createdBySource: source,
|
||||
createdByWorkspaceMember: connectedAccount.accountOwner,
|
||||
}));
|
||||
|
||||
return await this.createContactService.createPeople(
|
||||
return this.createContactService.createPeople(
|
||||
formattedContactsToCreate,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
@ -136,6 +140,7 @@ export class CreateCompanyAndContactService {
|
||||
connectedAccount: ConnectedAccountWorkspaceEntity,
|
||||
contactsToCreate: Contact[],
|
||||
workspaceId: string,
|
||||
source: FieldActorSource,
|
||||
) {
|
||||
const contactsBatches = chunk(
|
||||
contactsToCreate,
|
||||
@ -155,31 +160,43 @@ export class CreateCompanyAndContactService {
|
||||
throw new Error('Object metadata not found');
|
||||
}
|
||||
|
||||
const domainNameFieldMetadata = await this.fieldMetadataRepository.findOne({
|
||||
where: {
|
||||
workspaceId: workspaceId,
|
||||
standardId: COMPANY_STANDARD_FIELD_IDS.domainName,
|
||||
},
|
||||
});
|
||||
// In some jobs the accountOwner is not populated
|
||||
if (!connectedAccount.accountOwner) {
|
||||
const workspaceMemberRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
WorkspaceMemberWorkspaceEntity,
|
||||
);
|
||||
|
||||
const companyDomainNameColumnName =
|
||||
domainNameFieldMetadata?.type === FieldMetadataType.LINKS
|
||||
? 'domainNamePrimaryLinkUrl'
|
||||
: 'domainName';
|
||||
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) {
|
||||
const createdPeople = await this.createCompaniesAndPeople(
|
||||
connectedAccount,
|
||||
contactsBatch,
|
||||
workspaceId,
|
||||
companyDomainNameColumnName,
|
||||
source,
|
||||
);
|
||||
|
||||
for (const createdPerson of createdPeople) {
|
||||
this.eventEmitter.emit('person.created', {
|
||||
name: 'person.created',
|
||||
workspaceId,
|
||||
recordId: createdPerson.id,
|
||||
// FixMe: TypeORM typing issue... id is always returned when using save
|
||||
recordId: createdPerson.id!,
|
||||
objectMetadata,
|
||||
properties: {
|
||||
after: createdPerson,
|
||||
|
@ -1,108 +1,189 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import { EntityManager } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
import { EntityManager, ILike } from 'typeorm';
|
||||
import uniqBy from 'lodash.uniqby';
|
||||
|
||||
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
|
||||
import { CompanyRepository } from 'src/modules/company/repositories/company.repository';
|
||||
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 { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { computeDisplayName } from 'src/utils/compute-display-name';
|
||||
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
|
||||
|
||||
type CompanyToCreate = {
|
||||
domainName: string;
|
||||
createdBySource: FieldActorSource;
|
||||
createdByWorkspaceMember?: WorkspaceMemberWorkspaceEntity | null;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class CreateCompanyService {
|
||||
private readonly httpService: AxiosInstance;
|
||||
|
||||
constructor(
|
||||
@InjectObjectMetadataRepository(CompanyWorkspaceEntity)
|
||||
private readonly companyRepository: CompanyRepository,
|
||||
) {
|
||||
constructor(private readonly twentyORMGlobalManager: TwentyORMGlobalManager) {
|
||||
this.httpService = axios.create({
|
||||
baseURL: 'https://companies.twenty.com',
|
||||
});
|
||||
}
|
||||
|
||||
async createCompanies(
|
||||
domainNames: string[],
|
||||
companies: CompanyToCreate[],
|
||||
workspaceId: string,
|
||||
companyDomainNameColumnName: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<{
|
||||
[domainName: string]: string;
|
||||
}> {
|
||||
if (domainNames.length === 0) {
|
||||
if (companies.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const uniqueDomainNames = [...new Set(domainNames)];
|
||||
|
||||
const existingCompanies =
|
||||
await this.companyRepository.getExistingCompaniesByDomainNames(
|
||||
uniqueDomainNames,
|
||||
const companyRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
companyDomainNameColumnName,
|
||||
transactionManager,
|
||||
CompanyWorkspaceEntity,
|
||||
);
|
||||
|
||||
const companiesObject = existingCompanies.reduce(
|
||||
(
|
||||
acc: {
|
||||
[domainName: string]: string;
|
||||
},
|
||||
company: {
|
||||
domainName: string;
|
||||
id: string;
|
||||
},
|
||||
) => ({
|
||||
...acc,
|
||||
[extractDomainFromLink(company.domainName)]: company.id,
|
||||
}),
|
||||
{},
|
||||
);
|
||||
// Avoid creating duplicate companies
|
||||
const uniqueCompanies = uniqBy(companies, 'domainName');
|
||||
const conditions = uniqueCompanies.map((companyToCreate) => ({
|
||||
domainName: {
|
||||
primaryLinkUrl: ILike(`%${companyToCreate.domainName}%`),
|
||||
},
|
||||
}));
|
||||
|
||||
const filteredDomainNames = uniqueDomainNames.filter(
|
||||
(domainName) =>
|
||||
// Find existing companies
|
||||
const existingCompanies = await companyRepository.find(
|
||||
{
|
||||
where: conditions,
|
||||
},
|
||||
transactionManager,
|
||||
);
|
||||
const existingCompanyIdsMap = this.createCompanyMap(existingCompanies);
|
||||
|
||||
// Filter out companies that already exist
|
||||
const newCompaniesToCreate = uniqueCompanies.filter(
|
||||
(company) =>
|
||||
!existingCompanies.some(
|
||||
(company: { domainName: string }) =>
|
||||
extractDomainFromLink(company.domainName) === domainName,
|
||||
(existingCompany) =>
|
||||
existingCompany.domainName &&
|
||||
extractDomainFromLink(existingCompany.domainName.primaryLinkUrl) ===
|
||||
company.domainName,
|
||||
),
|
||||
);
|
||||
|
||||
for (const domainName of filteredDomainNames) {
|
||||
companiesObject[domainName] = await this.createCompany(
|
||||
domainName,
|
||||
workspaceId,
|
||||
companyDomainNameColumnName,
|
||||
transactionManager,
|
||||
);
|
||||
if (newCompaniesToCreate.length === 0) {
|
||||
return existingCompanyIdsMap;
|
||||
}
|
||||
|
||||
return companiesObject;
|
||||
// Retrieve the last company position
|
||||
let lastCompanyPosition = await this.getLastCompanyPosition(
|
||||
companyRepository,
|
||||
transactionManager,
|
||||
);
|
||||
const newCompaniesData = await Promise.all(
|
||||
newCompaniesToCreate.map((company) =>
|
||||
this.prepareCompanyData(company, ++lastCompanyPosition),
|
||||
),
|
||||
);
|
||||
|
||||
// Create new companies
|
||||
const createdCompanies = await companyRepository.save(
|
||||
newCompaniesData,
|
||||
undefined,
|
||||
transactionManager,
|
||||
);
|
||||
const createdCompanyIdsMap = this.createCompanyMap(createdCompanies);
|
||||
|
||||
return {
|
||||
...existingCompanyIdsMap,
|
||||
...createdCompanyIdsMap,
|
||||
};
|
||||
}
|
||||
|
||||
private async createCompany(
|
||||
domainName: string,
|
||||
async createCompany(
|
||||
company: CompanyToCreate,
|
||||
workspaceId: string,
|
||||
companyDomainNameColumnName,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<string> {
|
||||
const companyId = v4();
|
||||
|
||||
const { name, city } = await this.getCompanyInfoFromDomainName(domainName);
|
||||
|
||||
await this.companyRepository.createCompany(
|
||||
workspaceId,
|
||||
{
|
||||
id: companyId,
|
||||
domainName,
|
||||
name,
|
||||
city,
|
||||
},
|
||||
companyDomainNameColumnName,
|
||||
const companyRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
CompanyWorkspaceEntity,
|
||||
);
|
||||
let lastCompanyPosition = await this.getLastCompanyPosition(
|
||||
companyRepository,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
return companyId;
|
||||
const data = await this.prepareCompanyData(company, ++lastCompanyPosition);
|
||||
|
||||
const createdCompany = await companyRepository.save(
|
||||
data,
|
||||
undefined,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
return createdCompany.id;
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
address: {
|
||||
addressCity: city,
|
||||
},
|
||||
position,
|
||||
};
|
||||
}
|
||||
|
||||
private async createCompanyMap(companies: CompanyWorkspaceEntity[]) {
|
||||
return companies.reduce(
|
||||
(acc, company) => {
|
||||
if (!company.domainName) {
|
||||
return acc;
|
||||
}
|
||||
const key = extractDomainFromLink(company.domainName.primaryLinkUrl);
|
||||
|
||||
acc[key] = company.id;
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as { [domainName: string]: string },
|
||||
);
|
||||
}
|
||||
|
||||
private async getLastCompanyPosition(
|
||||
companyRepository: WorkspaceRepository<CompanyWorkspaceEntity>,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<number> {
|
||||
const lastCompanyPosition = await companyRepository.maximum(
|
||||
'position',
|
||||
undefined,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
return lastCompanyPosition ?? 0;
|
||||
}
|
||||
|
||||
private async getCompanyInfoFromDomainName(domainName: string): Promise<{
|
||||
|
@ -3,49 +3,61 @@ import { Injectable } from '@nestjs/common';
|
||||
import { EntityManager } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
|
||||
import { getFirstNameAndLastNameFromHandleAndDisplayName } from 'src/modules/contact-creation-manager/utils/get-first-name-and-last-name-from-handle-and-display-name.util';
|
||||
import { PersonRepository } from 'src/modules/person/repositories/person.repository';
|
||||
import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
import { computeDisplayName } from 'src/utils/compute-display-name';
|
||||
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
|
||||
|
||||
type ContactToCreate = {
|
||||
handle: string;
|
||||
displayName: string;
|
||||
companyId?: string;
|
||||
};
|
||||
|
||||
type FormattedContactToCreate = {
|
||||
id: string;
|
||||
handle: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
companyId?: string;
|
||||
createdBySource: FieldActorSource;
|
||||
createdByWorkspaceMember?: WorkspaceMemberWorkspaceEntity | null;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class CreateContactService {
|
||||
constructor(
|
||||
@InjectObjectMetadataRepository(PersonWorkspaceEntity)
|
||||
private readonly personRepository: PersonRepository,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {}
|
||||
|
||||
private formatContacts(
|
||||
contactsToCreate: ContactToCreate[],
|
||||
): FormattedContactToCreate[] {
|
||||
lastPersonPosition: number,
|
||||
): DeepPartial<PersonWorkspaceEntity>[] {
|
||||
return contactsToCreate.map((contact) => {
|
||||
const id = v4();
|
||||
|
||||
const { handle, displayName, companyId } = contact;
|
||||
const {
|
||||
handle,
|
||||
displayName,
|
||||
companyId,
|
||||
createdBySource,
|
||||
createdByWorkspaceMember,
|
||||
} = contact;
|
||||
|
||||
const { firstName, lastName } =
|
||||
getFirstNameAndLastNameFromHandleAndDisplayName(handle, displayName);
|
||||
const createdByName = computeDisplayName(createdByWorkspaceMember?.name);
|
||||
|
||||
return {
|
||||
id,
|
||||
handle,
|
||||
firstName,
|
||||
lastName,
|
||||
email: handle,
|
||||
name: {
|
||||
firstName,
|
||||
lastName,
|
||||
},
|
||||
companyId,
|
||||
createdBy: {
|
||||
source: createdBySource,
|
||||
workspaceMemberId: contact.createdByWorkspaceMember?.id,
|
||||
name: createdByName,
|
||||
},
|
||||
position: ++lastPersonPosition,
|
||||
};
|
||||
});
|
||||
}
|
||||
@ -54,15 +66,42 @@ export class CreateContactService {
|
||||
contactsToCreate: ContactToCreate[],
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<PersonWorkspaceEntity[]> {
|
||||
): Promise<DeepPartial<PersonWorkspaceEntity>[]> {
|
||||
if (contactsToCreate.length === 0) return [];
|
||||
|
||||
const formattedContacts = this.formatContacts(contactsToCreate);
|
||||
const personRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
PersonWorkspaceEntity,
|
||||
);
|
||||
|
||||
return await this.personRepository.createPeople(
|
||||
const lastPersonPosition = await this.getLastPersonPosition(
|
||||
personRepository,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
const formattedContacts = this.formatContacts(
|
||||
contactsToCreate,
|
||||
lastPersonPosition,
|
||||
);
|
||||
|
||||
return personRepository.save(
|
||||
formattedContacts,
|
||||
workspaceId,
|
||||
undefined,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
|
||||
private async getLastPersonPosition(
|
||||
personRepository: WorkspaceRepository<PersonWorkspaceEntity>,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<number> {
|
||||
const lastPersonPosition = await personRepository.maximum(
|
||||
'position',
|
||||
undefined,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
return lastPersonPosition ?? 0;
|
||||
}
|
||||
}
|
||||
|
@ -9,13 +9,11 @@ import { MessagingChannelSyncStatusService } from 'src/modules/messaging/common/
|
||||
import { MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity';
|
||||
import { MessageThreadWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-thread.workspace-entity';
|
||||
import { MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message.workspace-entity';
|
||||
import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
WorkspaceDataSourceModule,
|
||||
ObjectMetadataRepositoryModule.forFeature([
|
||||
PersonWorkspaceEntity,
|
||||
MessageParticipantWorkspaceEntity,
|
||||
MessageWorkspaceEntity,
|
||||
MessageThreadWorkspaceEntity,
|
||||
|
@ -24,6 +24,7 @@ import { MessagingMessageService } from 'src/modules/messaging/message-import-ma
|
||||
import { MessagingMessageParticipantService } from 'src/modules/messaging/message-participant-manager/services/messaging-message-participant.service';
|
||||
import { isGroupEmail } from 'src/utils/is-group-email';
|
||||
import { isWorkEmail } from 'src/utils/is-work-email';
|
||||
import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
|
||||
@Injectable()
|
||||
export class MessagingSaveMessagesAndEnqueueContactCreationService {
|
||||
@ -121,6 +122,7 @@ export class MessagingSaveMessagesAndEnqueueContactCreationService {
|
||||
workspaceId,
|
||||
connectedAccount,
|
||||
contactsToCreate,
|
||||
source: FieldActorSource.EMAIL,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import { Logger } from '@nestjs/common';
|
||||
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 { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
|
||||
import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository';
|
||||
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
@ -84,6 +85,7 @@ export class MessagingCreateCompanyAndContactAfterSyncJob {
|
||||
connectedAccount,
|
||||
contactsToCreate,
|
||||
workspaceId,
|
||||
FieldActorSource.EMAIL,
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
|
@ -1,89 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { EntityManager } from 'typeorm';
|
||||
|
||||
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
||||
import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
|
||||
import { getFlattenedValuesAndValuesStringForBatchRawQuery } from 'src/modules/calendar/calendar-event-import-manager/utils/get-flattened-values-and-values-string-for-batch-raw-query.util';
|
||||
|
||||
@Injectable()
|
||||
export class PersonRepository {
|
||||
constructor(
|
||||
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
||||
) {}
|
||||
|
||||
async getByEmails(
|
||||
emails: string[],
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<PersonWorkspaceEntity[]> {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
return await this.workspaceDataSourceService.executeRawQuery(
|
||||
`SELECT * FROM ${dataSourceSchema}.person WHERE email = ANY($1)`,
|
||||
[emails],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
|
||||
async getLastPersonPosition(
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<number> {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
const result = await this.workspaceDataSourceService.executeRawQuery(
|
||||
`SELECT MAX(position) FROM ${dataSourceSchema}.person`,
|
||||
[],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
return result[0].max ?? 0;
|
||||
}
|
||||
|
||||
async createPeople(
|
||||
peopleToCreate: {
|
||||
id: string;
|
||||
handle: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
companyId?: string;
|
||||
}[],
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<PersonWorkspaceEntity[]> {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
const lastPersonPosition = await this.getLastPersonPosition(
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
peopleToCreate = peopleToCreate.map((contact, index) => ({
|
||||
...contact,
|
||||
position: lastPersonPosition + index + 1,
|
||||
}));
|
||||
|
||||
const { flattenedValues, valuesString } =
|
||||
getFlattenedValuesAndValuesStringForBatchRawQuery(peopleToCreate, {
|
||||
id: 'uuid',
|
||||
handle: 'text',
|
||||
firstName: 'text',
|
||||
lastName: 'text',
|
||||
companyId: 'uuid',
|
||||
position: 'double precision',
|
||||
});
|
||||
|
||||
return await this.workspaceDataSourceService.executeRawQuery(
|
||||
`INSERT INTO ${dataSourceSchema}.person (id, email, "nameFirstName", "nameLastName", "companyId", "position") VALUES ${valuesString} RETURNING *`,
|
||||
flattenedValues,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
}
|
12
packages/twenty-server/src/utils/compute-display-name.ts
Normal file
12
packages/twenty-server/src/utils/compute-display-name.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { FullNameMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type';
|
||||
import { isDefined } from 'src/utils/is-defined';
|
||||
|
||||
export const computeDisplayName = (
|
||||
name: FullNameMetadata | null | undefined,
|
||||
) => {
|
||||
if (!name) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return Object.values(name).filter(isDefined).join(' ');
|
||||
};
|
84
yarn.lock
84
yarn.lock
@ -49531,7 +49531,7 @@ __metadata:
|
||||
tsup: "npm:^8.0.1"
|
||||
tsx: "npm:^4.7.2"
|
||||
type-fest: "npm:4.10.1"
|
||||
typeorm: "npm:^0.3.20"
|
||||
typeorm: "patch:typeorm@0.3.20#./packages/twenty-server/patches/typeorm+0.3.20.patch"
|
||||
typescript: "npm:5.3.3"
|
||||
use-context-selector: "npm:^2.0.0"
|
||||
use-debounce: "npm:^10.0.0"
|
||||
@ -49657,7 +49657,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typeorm@npm:^0.3.20":
|
||||
"typeorm@npm:0.3.20":
|
||||
version: 0.3.20
|
||||
resolution: "typeorm@npm:0.3.20"
|
||||
dependencies:
|
||||
@ -49737,6 +49737,86 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typeorm@patch:typeorm@0.3.20#./packages/twenty-server/patches/typeorm+0.3.20.patch::locator=twenty%40workspace%3A.":
|
||||
version: 0.3.20
|
||||
resolution: "typeorm@patch:typeorm@npm%3A0.3.20#./packages/twenty-server/patches/typeorm+0.3.20.patch::version=0.3.20&hash=9584e4&locator=twenty%40workspace%3A."
|
||||
dependencies:
|
||||
"@sqltools/formatter": "npm:^1.2.5"
|
||||
app-root-path: "npm:^3.1.0"
|
||||
buffer: "npm:^6.0.3"
|
||||
chalk: "npm:^4.1.2"
|
||||
cli-highlight: "npm:^2.1.11"
|
||||
dayjs: "npm:^1.11.9"
|
||||
debug: "npm:^4.3.4"
|
||||
dotenv: "npm:^16.0.3"
|
||||
glob: "npm:^10.3.10"
|
||||
mkdirp: "npm:^2.1.3"
|
||||
reflect-metadata: "npm:^0.2.1"
|
||||
sha.js: "npm:^2.4.11"
|
||||
tslib: "npm:^2.5.0"
|
||||
uuid: "npm:^9.0.0"
|
||||
yargs: "npm:^17.6.2"
|
||||
peerDependencies:
|
||||
"@google-cloud/spanner": ^5.18.0
|
||||
"@sap/hana-client": ^2.12.25
|
||||
better-sqlite3: ^7.1.2 || ^8.0.0 || ^9.0.0
|
||||
hdb-pool: ^0.1.6
|
||||
ioredis: ^5.0.4
|
||||
mongodb: ^5.8.0
|
||||
mssql: ^9.1.1 || ^10.0.1
|
||||
mysql2: ^2.2.5 || ^3.0.1
|
||||
oracledb: ^6.3.0
|
||||
pg: ^8.5.1
|
||||
pg-native: ^3.0.0
|
||||
pg-query-stream: ^4.0.0
|
||||
redis: ^3.1.1 || ^4.0.0
|
||||
sql.js: ^1.4.0
|
||||
sqlite3: ^5.0.3
|
||||
ts-node: ^10.7.0
|
||||
typeorm-aurora-data-api-driver: ^2.0.0
|
||||
peerDependenciesMeta:
|
||||
"@google-cloud/spanner":
|
||||
optional: true
|
||||
"@sap/hana-client":
|
||||
optional: true
|
||||
better-sqlite3:
|
||||
optional: true
|
||||
hdb-pool:
|
||||
optional: true
|
||||
ioredis:
|
||||
optional: true
|
||||
mongodb:
|
||||
optional: true
|
||||
mssql:
|
||||
optional: true
|
||||
mysql2:
|
||||
optional: true
|
||||
oracledb:
|
||||
optional: true
|
||||
pg:
|
||||
optional: true
|
||||
pg-native:
|
||||
optional: true
|
||||
pg-query-stream:
|
||||
optional: true
|
||||
redis:
|
||||
optional: true
|
||||
sql.js:
|
||||
optional: true
|
||||
sqlite3:
|
||||
optional: true
|
||||
ts-node:
|
||||
optional: true
|
||||
typeorm-aurora-data-api-driver:
|
||||
optional: true
|
||||
bin:
|
||||
typeorm: cli.js
|
||||
typeorm-ts-node-commonjs: cli-ts-node-commonjs.js
|
||||
typeorm-ts-node-esm: cli-ts-node-esm.js
|
||||
checksum: 10c0/f817ca0a7a6c2d4d242fd64ffc1e5337b2db4b03bc87004daf307a9ad77cf79d7050d461a0103a2674df2fba09cc2e95275b6bdef9eb6287d2c779c6ec673676
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typescript@npm:5.3.3":
|
||||
version: 5.3.3
|
||||
resolution: "typescript@npm:5.3.3"
|
||||
|
Loading…
Reference in New Issue
Block a user