diff --git a/package.json b/package.json index 44e90e7e33fd..8b64c22de530 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/twenty-server/patches/typeorm+0.3.20.patch b/packages/twenty-server/patches/typeorm+0.3.20.patch new file mode 100644 index 000000000000..f3730c15d746 --- /dev/null +++ b/packages/twenty-server/patches/typeorm+0.3.20.patch @@ -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 = string & keyof { +- [P in keyof T as T[P] extends U ? P : never]: T[P]; +-}; ++export type PickKeysByType = string & { ++ [P in keyof T]: Exclude extends U ? P : never; ++}[keyof T]; diff --git a/packages/twenty-server/src/engine/object-metadata-repository/metadata-to-repository.mapping.ts b/packages/twenty-server/src/engine/object-metadata-repository/metadata-to-repository.mapping.ts index 1ead430c1a88..99a2413d10e4 100644 --- a/packages/twenty-server/src/engine/object-metadata-repository/metadata-to-repository.mapping.ts +++ b/packages/twenty-server/src/engine/object-metadata-repository/metadata-to-repository.mapping.ts @@ -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, }; diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts index c98949d2af54..d0dc9a5cb072 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts @@ -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'; diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/calendar-event-import-manager.module.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/calendar-event-import-manager.module.ts index 8fa22e0a06e5..f23b6005ea5d 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/calendar-event-import-manager.module.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/calendar-event-import-manager.module.ts @@ -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, diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-save-events.service.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-save-events.service.ts index 4ea030dcfb86..e67d2c498394 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-save-events.service.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-save-events.service.ts @@ -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, }, ); } diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/jobs/calendar-create-company-and-contact-after-sync.job.ts b/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/jobs/calendar-create-company-and-contact-after-sync.job.ts index 61d12ab391bb..877fa0f457b7 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/jobs/calendar-create-company-and-contact-after-sync.job.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/jobs/calendar-create-company-and-contact-after-sync.job.ts @@ -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( diff --git a/packages/twenty-server/src/modules/company/repositories/company.repository.ts b/packages/twenty-server/src/modules/company/repositories/company.repository.ts deleted file mode 100644 index 0f99e8f9ca99..000000000000 --- a/packages/twenty-server/src/modules/company/repositories/company.repository.ts +++ /dev/null @@ -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 { - 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 { - 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, - ); - } -} diff --git a/packages/twenty-server/src/modules/company/standard-objects/company.workspace-entity.ts b/packages/twenty-server/src/modules/company/standard-objects/company.workspace-entity.ts index 9112e49843be..3947cd94b92b 100644 --- a/packages/twenty-server/src/modules/company/standard-objects/company.workspace-entity.ts +++ b/packages/twenty-server/src/modules/company/standard-objects/company.workspace-entity.ts @@ -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, diff --git a/packages/twenty-server/src/modules/contact-creation-manager/contact-creation-manager.module.ts b/packages/twenty-server/src/modules/contact-creation-manager/contact-creation-manager.module.ts index 40b0bb3e9659..6f6a1a677c45 100644 --- a/packages/twenty-server/src/modules/contact-creation-manager/contact-creation-manager.module.ts +++ b/packages/twenty-server/src/modules/contact-creation-manager/contact-creation-manager.module.ts @@ -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, diff --git a/packages/twenty-server/src/modules/contact-creation-manager/jobs/create-company-and-contact.job.ts b/packages/twenty-server/src/modules/contact-creation-manager/jobs/create-company-and-contact.job.ts index 822d14299795..0a00c1ec9af3 100644 --- a/packages/twenty-server/src/modules/contact-creation-manager/jobs/create-company-and-contact.job.ts +++ b/packages/twenty-server/src/modules/contact-creation-manager/jobs/create-company-and-contact.job.ts @@ -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 { - 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, ); } } diff --git a/packages/twenty-server/src/modules/contact-creation-manager/services/create-company-and-contact.service.ts b/packages/twenty-server/src/modules/contact-creation-manager/services/create-company-and-contact.service.ts index fc912fb5a130..57ede6f2dec8 100644 --- a/packages/twenty-server/src/modules/contact-creation-manager/services/create-company-and-contact.service.ts +++ b/packages/twenty-server/src/modules/contact-creation-manager/services/create-company-and-contact.service.ts @@ -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, - @InjectRepository(FieldMetadataEntity, 'metadata') - private readonly fieldMetadataRepository: Repository, + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, ) {} private async createCompaniesAndPeople( connectedAccount: ConnectedAccountWorkspaceEntity, contactsToCreate: Contact[], workspaceId: string, - companyDomainNameColumnName: string, + source: FieldActorSource, transactionManager?: EntityManager, - ): Promise { + ): Promise[]> { 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 workspaceMember = await workspaceMemberRepository.findOne({ + where: { + id: connectedAccount.accountOwnerId, + }, + }); + + if (!workspaceMember) { + throw new Error( + `Workspace member with id ${connectedAccount.accountOwnerId} not found in workspace ${workspaceId}`, + ); + } - const companyDomainNameColumnName = - domainNameFieldMetadata?.type === FieldMetadataType.LINKS - ? 'domainNamePrimaryLinkUrl' - : 'domainName'; + 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, diff --git a/packages/twenty-server/src/modules/contact-creation-manager/services/create-company.service.ts b/packages/twenty-server/src/modules/contact-creation-manager/services/create-company.service.ts index 829334b7bb25..6e2f89526251 100644 --- a/packages/twenty-server/src/modules/contact-creation-manager/services/create-company.service.ts +++ b/packages/twenty-server/src/modules/contact-creation-manager/services/create-company.service.ts @@ -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}%`), + }, + })); + + // Find existing companies + const existingCompanies = await companyRepository.find( + { + where: conditions, + }, + transactionManager, ); + const existingCompanyIdsMap = this.createCompanyMap(existingCompanies); - const filteredDomainNames = uniqueDomainNames.filter( - (domainName) => + // 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 { - const companyId = v4(); + const companyRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + CompanyWorkspaceEntity, + ); + let lastCompanyPosition = await this.getLastCompanyPosition( + companyRepository, + transactionManager, + ); - const { name, city } = await this.getCompanyInfoFromDomainName(domainName); + const data = await this.prepareCompanyData(company, ++lastCompanyPosition); - await this.companyRepository.createCompany( - workspaceId, - { - id: companyId, - domainName, - name, - city, + const createdCompany = await companyRepository.save( + data, + undefined, + transactionManager, + ); + + return createdCompany.id; + } + + private async prepareCompanyData( + company: CompanyToCreate, + position: number, + ): Promise> { + 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, }, - companyDomainNameColumnName, + 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, + transactionManager?: EntityManager, + ): Promise { + const lastCompanyPosition = await companyRepository.maximum( + 'position', + undefined, transactionManager, ); - return companyId; + return lastCompanyPosition ?? 0; } private async getCompanyInfoFromDomainName(domainName: string): Promise<{ diff --git a/packages/twenty-server/src/modules/contact-creation-manager/services/create-contact.service.ts b/packages/twenty-server/src/modules/contact-creation-manager/services/create-contact.service.ts index 775525c5af77..dd971cedaab9 100644 --- a/packages/twenty-server/src/modules/contact-creation-manager/services/create-contact.service.ts +++ b/packages/twenty-server/src/modules/contact-creation-manager/services/create-contact.service.ts @@ -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[] { 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 { + ): Promise[]> { if (contactsToCreate.length === 0) return []; - const formattedContacts = this.formatContacts(contactsToCreate); + const personRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + PersonWorkspaceEntity, + ); + + const lastPersonPosition = await this.getLastPersonPosition( + personRepository, + transactionManager, + ); + + const formattedContacts = this.formatContacts( + contactsToCreate, + lastPersonPosition, + ); - return await this.personRepository.createPeople( + return personRepository.save( formattedContacts, - workspaceId, + undefined, transactionManager, ); } + + private async getLastPersonPosition( + personRepository: WorkspaceRepository, + transactionManager?: EntityManager, + ): Promise { + const lastPersonPosition = await personRepository.maximum( + 'position', + undefined, + transactionManager, + ); + + return lastPersonPosition ?? 0; + } } diff --git a/packages/twenty-server/src/modules/messaging/common/messaging-common.module.ts b/packages/twenty-server/src/modules/messaging/common/messaging-common.module.ts index 29b8dd7c222c..d8836222bb99 100644 --- a/packages/twenty-server/src/modules/messaging/common/messaging-common.module.ts +++ b/packages/twenty-server/src/modules/messaging/common/messaging-common.module.ts @@ -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, diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-save-messages-and-enqueue-contact-creation.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-save-messages-and-enqueue-contact-creation.service.ts index fe8c8177cb3f..c1a8bf3b449d 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-save-messages-and-enqueue-contact-creation.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-save-messages-and-enqueue-contact-creation.service.ts @@ -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, }, ); } diff --git a/packages/twenty-server/src/modules/messaging/message-participant-manager/jobs/messaging-create-company-and-contact-after-sync.job.ts b/packages/twenty-server/src/modules/messaging/message-participant-manager/jobs/messaging-create-company-and-contact-after-sync.job.ts index 620432e06848..4db25adef73b 100644 --- a/packages/twenty-server/src/modules/messaging/message-participant-manager/jobs/messaging-create-company-and-contact-after-sync.job.ts +++ b/packages/twenty-server/src/modules/messaging/message-participant-manager/jobs/messaging-create-company-and-contact-after-sync.job.ts @@ -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( diff --git a/packages/twenty-server/src/modules/person/repositories/person.repository.ts b/packages/twenty-server/src/modules/person/repositories/person.repository.ts deleted file mode 100644 index adb40f1cbdb3..000000000000 --- a/packages/twenty-server/src/modules/person/repositories/person.repository.ts +++ /dev/null @@ -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 { - 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 { - 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 { - 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, - ); - } -} diff --git a/packages/twenty-server/src/utils/compute-display-name.ts b/packages/twenty-server/src/utils/compute-display-name.ts new file mode 100644 index 000000000000..728dcd3a174c --- /dev/null +++ b/packages/twenty-server/src/utils/compute-display-name.ts @@ -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(' '); +}; diff --git a/yarn.lock b/yarn.lock index 11878418e3a4..62da4fad2f46 100644 --- a/yarn.lock +++ b/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"