diff --git a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapFieldMetadataToGraphQLQuery.test.tsx b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapFieldMetadataToGraphQLQuery.test.tsx index 38ab9211e457..da578f8973f8 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapFieldMetadataToGraphQLQuery.test.tsx +++ b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapFieldMetadataToGraphQLQuery.test.tsx @@ -185,7 +185,11 @@ xLink id createdAt city -email +emails +{ + primaryEmail + additionalEmails +} jobTitle name { diff --git a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapObjectMetadataToGraphQLQuery.test.tsx b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapObjectMetadataToGraphQLQuery.test.tsx index d1838963d118..5a00909648f2 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapObjectMetadataToGraphQLQuery.test.tsx +++ b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapObjectMetadataToGraphQLQuery.test.tsx @@ -41,7 +41,11 @@ describe('mapObjectMetadataToGraphQLQuery', () => { firstName lastName } - email + emails + { + primaryEmail + additionalEmails + } phone createdAt avatarUrl diff --git a/packages/twenty-server/src/database/commands/database-command.module.ts b/packages/twenty-server/src/database/commands/database-command.module.ts index d240657f5f0f..6281a5e31eb1 100644 --- a/packages/twenty-server/src/database/commands/database-command.module.ts +++ b/packages/twenty-server/src/database/commands/database-command.module.ts @@ -8,6 +8,7 @@ import { DataSeedDemoWorkspaceModule } from 'src/database/commands/data-seed-dem import { DataSeedWorkspaceCommand } from 'src/database/commands/data-seed-dev-workspace.command'; import { ConfirmationQuestion } from 'src/database/commands/questions/confirmation.question'; import { UpgradeTo0_24CommandModule } from 'src/database/commands/upgrade-version/0-24/0-24-upgrade-version.module'; +import { UpgradeTo0_30CommandModule } from 'src/database/commands/upgrade-version/0-30/0-30-upgrade-version.module'; import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; @@ -46,6 +47,7 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp DataSeedDemoWorkspaceModule, WorkspaceMetadataVersionModule, UpgradeTo0_24CommandModule, + UpgradeTo0_30CommandModule, ], providers: [ DataSeedWorkspaceCommand, diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-24/0-24-upgrade-version.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-24/0-24-upgrade-version.command.ts index 2862f566a34e..5ac771d77e27 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-24/0-24-upgrade-version.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-24/0-24-upgrade-version.command.ts @@ -1,6 +1,5 @@ import { Command, CommandRunner, Option } from 'nest-commander'; -import { SetCustomObjectIsSoftDeletableCommand } from 'src/database/commands/upgrade-version/0-24/0-24-set-custom-object-is-soft-deletable.command'; import { SetMessageDirectionCommand } from 'src/database/commands/upgrade-version/0-24/0-24-set-message-direction.command'; import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command'; @@ -16,7 +15,6 @@ export class UpgradeTo0_24Command extends CommandRunner { constructor( private readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand, private readonly setMessagesDirectionCommand: SetMessageDirectionCommand, - private readonly setCustomObjectIsSoftDeletableCommand: SetCustomObjectIsSoftDeletableCommand, ) { super(); } @@ -40,6 +38,5 @@ export class UpgradeTo0_24Command extends CommandRunner { force: true, }); await this.setMessagesDirectionCommand.run(passedParam, options); - await this.setCustomObjectIsSoftDeletableCommand.run(passedParam, options); } } diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-24/0-24-upgrade-version.module.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-24/0-24-upgrade-version.module.ts index 55643804686e..2cebb1eb1b7a 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-24/0-24-upgrade-version.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-24/0-24-upgrade-version.module.ts @@ -1,9 +1,9 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { SetCustomObjectIsSoftDeletableCommand } from 'src/database/commands/upgrade-version/0-24/0-24-set-custom-object-is-soft-deletable.command'; import { SetMessageDirectionCommand } from 'src/database/commands/upgrade-version/0-24/0-24-set-message-direction.command'; import { UpgradeTo0_24Command } from 'src/database/commands/upgrade-version/0-24/0-24-upgrade-version.command'; +import { SetCustomObjectIsSoftDeletableCommand } from 'src/database/commands/upgrade-version/0-30/0-30-set-custom-object-is-soft-deletable.command'; import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity'; import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module'; diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-migrate-email-fields-to-emails.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-migrate-email-fields-to-emails.command.ts new file mode 100644 index 000000000000..cf057f293ed5 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-migrate-email-fields-to-emails.command.ts @@ -0,0 +1,338 @@ +import { InjectRepository } from '@nestjs/typeorm'; + +import chalk from 'chalk'; +import { Command } from 'nest-commander'; +import { QueryRunner, Repository } from 'typeorm'; + +import { + ActiveWorkspacesCommandOptions, + ActiveWorkspacesCommandRunner, +} from 'src/database/commands/active-workspaces.command'; +import { TypeORMService } from 'src/database/typeorm/typeorm.service'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; +import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; +import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input'; +import { + FieldMetadataEntity, + FieldMetadataType, +} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/field-metadata.service'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { computeTableName } from 'src/engine/utils/compute-table-name.util'; +import { PERSON_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; +import { ViewService } from 'src/modules/view/services/view.service'; +import { ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity'; +@Command({ + name: 'upgrade-0.30:migrate-email-fields-to-emails', + description: 'Migrating fields of deprecated type EMAIL to type EMAILS', +}) +export class MigrateEmailFieldsToEmailsCommand extends ActiveWorkspacesCommandRunner { + constructor( + @InjectRepository(Workspace, 'core') + protected readonly workspaceRepository: Repository, + @InjectRepository(FieldMetadataEntity, 'metadata') + private readonly fieldMetadataRepository: Repository, + @InjectRepository(ObjectMetadataEntity, 'metadata') + private readonly objectMetadataRepository: Repository, + private readonly fieldMetadataService: FieldMetadataService, + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly typeORMService: TypeORMService, + private readonly dataSourceService: DataSourceService, + private readonly viewService: ViewService, + ) { + super(workspaceRepository); + } + + async executeActiveWorkspacesCommand( + _passedParam: string[], + _options: ActiveWorkspacesCommandOptions, + workspaceIds: string[], + ): Promise { + this.logger.log( + 'Running command to migrate email type fields to emails type', + ); + + for (const workspaceId of workspaceIds) { + this.logger.log(`Running command for workspace ${workspaceId}`); + try { + const dataSourceMetadata = + await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceId( + workspaceId, + ); + + if (!dataSourceMetadata) { + throw new Error( + `Could not find dataSourceMetadata for workspace ${workspaceId}`, + ); + } + + const workspaceDataSource = + await this.typeORMService.connectToDataSource(dataSourceMetadata); + + if (!workspaceDataSource) { + throw new Error( + `Could not connect to dataSource for workspace ${workspaceId}`, + ); + } + + const workspaceQueryRunner = workspaceDataSource.createQueryRunner(); + + await workspaceQueryRunner.connect(); + + const customFieldsWithEmailType = + await this.fieldMetadataRepository.find({ + where: { + workspaceId, + type: FieldMetadataType.EMAIL, + isCustom: true, + }, + }); + + await this.migratePersonEmailFieldToEmailsField( + workspaceId, + workspaceQueryRunner, + dataSourceMetadata, + ); + + for (const customFieldWithEmailType of customFieldsWithEmailType) { + const objectMetadata = await this.objectMetadataRepository.findOne({ + where: { id: customFieldWithEmailType.objectMetadataId }, + }); + + if (!objectMetadata) { + throw new Error( + `Could not find objectMetadata for field ${customFieldWithEmailType.name}`, + ); + } + + this.logger.log( + `Attempting to migrate custom field ${customFieldWithEmailType.name} on ${objectMetadata.nameSingular}.`, + ); + + const fieldName = customFieldWithEmailType.name; + const { id: _id, ...fieldWithEmailTypeWithoutId } = + customFieldWithEmailType; + + const emailDefaultValue = fieldWithEmailTypeWithoutId.defaultValue; + + const defaultValueForEmailsField = { + primaryEmail: emailDefaultValue, + additionalEmails: null, + }; + + try { + const tmpNewEmailsField = await this.fieldMetadataService.createOne( + { + ...fieldWithEmailTypeWithoutId, + type: FieldMetadataType.EMAILS, + defaultValue: defaultValueForEmailsField, + name: `${fieldName}Tmp`, + } satisfies CreateFieldInput, + ); + + const tableName = computeTableName( + objectMetadata.nameSingular, + objectMetadata.isCustom, + ); + + // Migrate data from email to emails.primaryEmail + await this.migrateDataWithinTable({ + sourceColumnName: `${customFieldWithEmailType.name}`, + targetColumnName: `${tmpNewEmailsField.name}PrimaryEmail`, + tableName, + workspaceQueryRunner, + dataSourceMetadata, + }); + + // Duplicate email field's views behaviour for new emails field + await this.viewService.removeFieldFromViews({ + workspaceId: workspaceId, + fieldId: tmpNewEmailsField.id, + }); + + const viewFieldRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + 'viewField', + ); + const viewFieldsWithDeprecatedField = + await viewFieldRepository.find({ + where: { + fieldMetadataId: customFieldWithEmailType.id, + isVisible: true, + }, + }); + + await this.viewService.addFieldToViews({ + workspaceId: workspaceId, + fieldId: tmpNewEmailsField.id, + viewsIds: viewFieldsWithDeprecatedField + .filter((viewField) => viewField.viewId !== null) + .map((viewField) => viewField.viewId as string), + positions: viewFieldsWithDeprecatedField.reduce( + (acc, viewField) => { + if (!viewField.viewId) { + return acc; + } + acc[viewField.viewId] = viewField.position; + + return acc; + }, + [], + ), + }); + + // Delete email field + await this.fieldMetadataService.deleteOneField( + { id: customFieldWithEmailType.id }, + workspaceId, + ); + + // Rename temporary emails field + await this.fieldMetadataService.updateOne(tmpNewEmailsField.id, { + id: tmpNewEmailsField.id, + workspaceId: tmpNewEmailsField.workspaceId, + name: `${fieldName}`, + isCustom: false, + }); + + this.logger.log( + `Migration of ${customFieldWithEmailType.name} on ${objectMetadata.nameSingular} done!`, + ); + } catch (error) { + this.logger.log( + `Failed to migrate field ${customFieldWithEmailType.name} on ${objectMetadata.nameSingular}, rolling back.`, + ); + + // Re-create initial field if it was deleted + const initialField = + await this.fieldMetadataService.findOneWithinWorkspace( + workspaceId, + { + where: { + name: `${customFieldWithEmailType.name}`, + objectMetadataId: customFieldWithEmailType.objectMetadataId, + }, + }, + ); + + const tmpNewEmailsField = + await this.fieldMetadataService.findOneWithinWorkspace( + workspaceId, + { + where: { + name: `${customFieldWithEmailType.name}Tmp`, + objectMetadataId: customFieldWithEmailType.objectMetadataId, + }, + }, + ); + + if (!initialField) { + this.logger.log( + `Re-creating initial Email field ${customFieldWithEmailType.name} but of type emails`, // Cannot create email fields anymore + ); + const restoredField = await this.fieldMetadataService.createOne({ + ...customFieldWithEmailType, + defaultValue: defaultValueForEmailsField, + type: FieldMetadataType.EMAILS, + }); + const tableName = computeTableName( + objectMetadata.nameSingular, + objectMetadata.isCustom, + ); + + if (tmpNewEmailsField) { + this.logger.log( + `Restoring data in field ${customFieldWithEmailType.name}`, + ); + await this.migrateDataWithinTable({ + sourceColumnName: `${tmpNewEmailsField.name}PrimaryEmail`, + targetColumnName: `${restoredField.name}PrimaryEmail`, + tableName, + workspaceQueryRunner, + dataSourceMetadata, + }); + } else { + this.logger.log( + `Failed to restore data in link field ${customFieldWithEmailType.name}`, + ); + } + } + + if (tmpNewEmailsField) { + await this.fieldMetadataService.deleteOneField( + { id: tmpNewEmailsField.id }, + workspaceId, + ); + } + } finally { + await workspaceQueryRunner.release(); + } + } + } catch (error) { + this.logger.log( + chalk.red( + `Running command on workspace ${workspaceId} failed with error: ${error}`, + ), + ); + continue; + } + + this.logger.log(chalk.green(`Command completed!`)); + } + } + + private async migratePersonEmailFieldToEmailsField( + workspaceId: string, + workspaceQueryRunner: any, + dataSourceMetadata: any, + ) { + this.logger.log(`Migrating person email field of type EMAIL to EMAILS`); + + await this.migrateDataWithinTable({ + sourceColumnName: 'email', + targetColumnName: 'emailsPrimaryEmail', + tableName: 'person', + workspaceQueryRunner, + dataSourceMetadata, + }); + + const personEmailFieldMetadata = await this.fieldMetadataRepository.findOne( + { + where: { + workspaceId, + standardId: PERSON_STANDARD_FIELD_IDS.email, + }, + }, + ); + + if (personEmailFieldMetadata) { + await this.fieldMetadataService.deleteOneField( + { + id: personEmailFieldMetadata.id, + }, + workspaceId, + ); + } + } + + private async migrateDataWithinTable({ + sourceColumnName, + targetColumnName, + tableName, + workspaceQueryRunner, + dataSourceMetadata, + }: { + sourceColumnName: string; + targetColumnName: string; + tableName: string; + workspaceQueryRunner: QueryRunner; + dataSourceMetadata: DataSourceEntity; + }) { + await workspaceQueryRunner.query( + `UPDATE "${dataSourceMetadata.schema}"."${tableName}" SET "${targetColumnName}" = "${sourceColumnName}"`, + ); + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-24/0-24-set-custom-object-is-soft-deletable.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-set-custom-object-is-soft-deletable.command.ts similarity index 96% rename from packages/twenty-server/src/database/commands/upgrade-version/0-24/0-24-set-custom-object-is-soft-deletable.command.ts rename to packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-set-custom-object-is-soft-deletable.command.ts index a27a3cc144eb..eca303963df4 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-24/0-24-set-custom-object-is-soft-deletable.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-set-custom-object-is-soft-deletable.command.ts @@ -14,7 +14,7 @@ type SetCustomObjectIsSoftDeletableCommandOptions = ActiveWorkspacesCommandOptions; @Command({ - name: 'upgrade-0.24:set-custom-object-is-soft-deletable', + name: 'upgrade-0.30:set-custom-object-is-soft-deletable', description: 'Set custom object is soft deletable', }) export class SetCustomObjectIsSoftDeletableCommand extends ActiveWorkspacesCommandRunner { diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-upgrade-version.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-upgrade-version.command.ts new file mode 100644 index 000000000000..2191860af087 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-upgrade-version.command.ts @@ -0,0 +1,45 @@ +import { Command, CommandRunner, Option } from 'nest-commander'; + +import { MigrateEmailFieldsToEmailsCommand } from 'src/database/commands/upgrade-version/0-30/0-30-migrate-email-fields-to-emails.command'; +import { SetCustomObjectIsSoftDeletableCommand } from 'src/database/commands/upgrade-version/0-30/0-30-set-custom-object-is-soft-deletable.command'; +import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command'; + +interface UpdateTo0_30CommandOptions { + workspaceId?: string; +} + +@Command({ + name: 'upgrade-0.30', + description: 'Upgrade to 0.30', +}) +export class UpgradeTo0_30Command extends CommandRunner { + constructor( + private readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand, + private readonly migrateEmailFieldsToEmails: MigrateEmailFieldsToEmailsCommand, + private readonly setCustomObjectIsSoftDeletableCommand: SetCustomObjectIsSoftDeletableCommand, + ) { + super(); + } + + @Option({ + flags: '-w, --workspace-id [workspace_id]', + description: + 'workspace id. Command runs on all active workspaces if not provided', + required: false, + }) + parseWorkspaceId(value: string): string { + return value; + } + + async run( + passedParam: string[], + options: UpdateTo0_30CommandOptions, + ): Promise { + await this.syncWorkspaceMetadataCommand.run(passedParam, { + ...options, + force: true, + }); + await this.setCustomObjectIsSoftDeletableCommand.run(passedParam, options); + await this.migrateEmailFieldsToEmails.run(passedParam, options); + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-upgrade-version.module.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-upgrade-version.module.ts new file mode 100644 index 000000000000..3e266cebec17 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-upgrade-version.module.ts @@ -0,0 +1,37 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { MigrateEmailFieldsToEmailsCommand } from 'src/database/commands/upgrade-version/0-30/0-30-migrate-email-fields-to-emails.command'; +import { SetCustomObjectIsSoftDeletableCommand } from 'src/database/commands/upgrade-version/0-30/0-30-set-custom-object-is-soft-deletable.command'; +import { UpgradeTo0_30Command } from 'src/database/commands/upgrade-version/0-30/0-30-upgrade-version.command'; +import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module'; +import { WorkspaceSyncMetadataCommandsModule } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/workspace-sync-metadata-commands.module'; +import { ViewModule } from 'src/modules/view/view.module'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([Workspace], 'core'), + WorkspaceSyncMetadataCommandsModule, + DataSourceModule, + WorkspaceMetadataVersionModule, + FieldMetadataModule, + TypeOrmModule.forFeature( + [FieldMetadataEntity, ObjectMetadataEntity], + 'metadata', + ), + TypeORMModule, + ViewModule, + ], + providers: [ + UpgradeTo0_30Command, + MigrateEmailFieldsToEmailsCommand, + SetCustomObjectIsSoftDeletableCommand, + ], +}) +export class UpgradeTo0_30CommandModule {} diff --git a/packages/twenty-server/src/database/typeorm-seeds/workspace/people.ts b/packages/twenty-server/src/database/typeorm-seeds/workspace/people.ts index 6063b4be6237..6624fd50331e 100644 --- a/packages/twenty-server/src/database/typeorm-seeds/workspace/people.ts +++ b/packages/twenty-server/src/database/typeorm-seeds/workspace/people.ts @@ -37,7 +37,7 @@ export const seedPeople = async ( 'phone', 'city', 'companyId', - 'email', + 'emailsPrimaryEmail', 'position', 'whatsapp', 'createdBySource', @@ -53,7 +53,7 @@ export const seedPeople = async ( phone: '+33789012345', city: 'Seattle', companyId: DEV_SEED_COMPANY_IDS.LINKEDIN, - email: 'christoph.calisto@linkedin.com', + emailsPrimaryEmail: 'christoph.calisto@linkedin.com', position: 1, whatsapp: '+33789012345', createdBySource: 'MANUAL', @@ -67,7 +67,7 @@ export const seedPeople = async ( phone: '+33780123456', city: 'Los Angeles', companyId: DEV_SEED_COMPANY_IDS.LINKEDIN, - email: 'sylvie.palmer@linkedin.com', + emailsPrimaryEmail: 'sylvie.palmer@linkedin.com', position: 2, whatsapp: '+33780123456', createdBySource: 'MANUAL', @@ -81,7 +81,7 @@ export const seedPeople = async ( phone: '+33789012345', city: 'Seattle', companyId: DEV_SEED_COMPANY_IDS.QONTO, - email: 'christopher.gonzalez@qonto.com', + emailsPrimaryEmail: 'christopher.gonzalez@qonto.com', position: 3, whatsapp: '+33789012345', createdBySource: 'MANUAL', @@ -95,7 +95,7 @@ export const seedPeople = async ( phone: '+33780123456', city: 'Los Angeles', companyId: DEV_SEED_COMPANY_IDS.QONTO, - email: 'ashley.parker@qonto.com', + emailsPrimaryEmail: 'ashley.parker@qonto.com', position: 4, whatsapp: '+33780123456', createdBySource: 'MANUAL', @@ -109,7 +109,7 @@ export const seedPeople = async ( phone: '+33781234567', city: 'Seattle', companyId: DEV_SEED_COMPANY_IDS.MICROSOFT, - email: 'nicholas.wright@microsoft.com', + emailsPrimaryEmail: 'nicholas.wright@microsoft.com', position: 5, whatsapp: '+33781234567', createdBySource: 'MANUAL', @@ -123,7 +123,7 @@ export const seedPeople = async ( phone: '+33782345678', city: 'New York', companyId: DEV_SEED_COMPANY_IDS.MICROSOFT, - email: 'isabella.scott@microsoft.com', + emailsPrimaryEmail: 'isabella.scott@microsoft.com', position: 6, whatsapp: '+33782345678', createdBySource: 'MANUAL', @@ -137,7 +137,7 @@ export const seedPeople = async ( phone: '+33783456789', city: 'Seattle', companyId: DEV_SEED_COMPANY_IDS.MICROSOFT, - email: 'matthew.green@microsoft.com', + emailsPrimaryEmail: 'matthew.green@microsoft.com', position: 7, whatsapp: '+33783456789', createdBySource: 'MANUAL', @@ -151,7 +151,7 @@ export const seedPeople = async ( phone: '+33784567890', city: 'New York', companyId: DEV_SEED_COMPANY_IDS.AIRBNB, - email: 'elizabeth.baker@airbnb.com', + emailsPrimaryEmail: 'elizabeth.baker@airbnb.com', position: 8, whatsapp: '+33784567890', createdBySource: 'MANUAL', @@ -165,7 +165,7 @@ export const seedPeople = async ( phone: '+33785678901', city: 'San Francisco', companyId: DEV_SEED_COMPANY_IDS.AIRBNB, - email: 'christopher.nelson@airbnb.com', + emailsPrimaryEmail: 'christopher.nelson@airbnb.com', position: 9, whatsapp: '+33785678901', createdBySource: 'MANUAL', @@ -179,7 +179,7 @@ export const seedPeople = async ( phone: '+33786789012', city: 'New York', companyId: DEV_SEED_COMPANY_IDS.AIRBNB, - email: 'avery.carter@airbnb.com', + emailsPrimaryEmail: 'avery.carter@airbnb.com', position: 10, whatsapp: '+33786789012', createdBySource: 'MANUAL', @@ -193,7 +193,7 @@ export const seedPeople = async ( phone: '+33787890123', city: 'Los Angeles', companyId: DEV_SEED_COMPANY_IDS.GOOGLE, - email: 'ethan.mitchell@google.com', + emailsPrimaryEmail: 'ethan.mitchell@google.com', position: 11, whatsapp: '+33787890123', createdBySource: 'MANUAL', @@ -207,7 +207,7 @@ export const seedPeople = async ( phone: '+33788901234', city: 'Seattle', companyId: DEV_SEED_COMPANY_IDS.GOOGLE, - email: 'madison.perez@google.com', + emailsPrimaryEmail: 'madison.perez@google.com', position: 12, whatsapp: '+33788901234', createdBySource: 'MANUAL', @@ -221,7 +221,7 @@ export const seedPeople = async ( phone: '+33788901234', city: 'Seattle', companyId: DEV_SEED_COMPANY_IDS.GOOGLE, - email: 'bertrand.voulzy@google.com', + emailsPrimaryEmail: 'bertrand.voulzy@google.com', position: 13, whatsapp: '+33788901234', createdBySource: 'MANUAL', @@ -235,7 +235,7 @@ export const seedPeople = async ( phone: '+33788901234', city: 'Seattle', companyId: DEV_SEED_COMPANY_IDS.GOOGLE, - email: 'louis.duss@google.com', + emailsPrimaryEmail: 'louis.duss@google.com', position: 14, whatsapp: '+33788901234', createdBySource: 'MANUAL', @@ -249,7 +249,7 @@ export const seedPeople = async ( phone: '+33788901235', city: 'Seattle', companyId: DEV_SEED_COMPANY_IDS.GOOGLE, - email: 'lorie.vladim@google.com', + emailsPrimaryEmail: 'lorie.vladim@google.com', position: 15, whatsapp: '+33788901235', createdBySource: 'MANUAL', diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/emails.composite-type.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/emails.composite-type.ts index 3cee1f44127a..9ca5ceea55a6 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/emails.composite-type.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/emails.composite-type.ts @@ -22,5 +22,5 @@ export const emailsCompositeType: CompositeType = { export type EmailsMetadata = { primaryEmail: string; - additionalEmails: string[] | null; + additionalEmails: object | null; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/default-value.input.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/default-value.input.ts index 45de1dfceaf7..a617f8ad971e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/default-value.input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/default-value.input.ts @@ -183,7 +183,7 @@ export class FieldMetadataDefaultValueEmails { @ValidateIf((_object, value) => value !== null) @IsObject() - additionalEmails: string[] | null; + additionalEmails: object | null; } export class FieldMetadataDefaultValuePhones { diff --git a/packages/twenty-server/src/engine/workspace-manager/demo-objects-prefill-data/person.ts b/packages/twenty-server/src/engine/workspace-manager/demo-objects-prefill-data/person.ts index c12b0b201d02..13cc32d400ce 100644 --- a/packages/twenty-server/src/engine/workspace-manager/demo-objects-prefill-data/person.ts +++ b/packages/twenty-server/src/engine/workspace-manager/demo-objects-prefill-data/person.ts @@ -14,7 +14,7 @@ export const personPrefillDemoData = async ( const people = peopleDemo.map((person, index) => ({ nameFirstName: person.firstName, nameLastName: person.lastName, - email: person.email, + emailsPrimaryEmail: person.email, linkedinLinkPrimaryLinkUrl: person.linkedinUrl, jobTitle: person.jobTitle, city: person.city, @@ -32,7 +32,7 @@ export const personPrefillDemoData = async ( .into(`${schemaName}.person`, [ 'nameFirstName', 'nameLastName', - 'email', + 'emailsPrimaryEmail', 'linkedinLinkPrimaryLinkUrl', 'jobTitle', 'city', diff --git a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/person.ts b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/person.ts index b30974e81e9a..fb227b15ca56 100644 --- a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/person.ts +++ b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/person.ts @@ -12,7 +12,7 @@ export const personPrefillData = async ( 'nameFirstName', 'nameLastName', 'city', - 'email', + 'emailsPrimaryEmail', 'avatarUrl', 'position', 'createdBySource', @@ -25,7 +25,7 @@ export const personPrefillData = async ( nameFirstName: 'Brian', nameLastName: 'Chesky', city: 'San Francisco', - email: 'chesky@airbnb.com', + emailsPrimaryEmail: 'chesky@airbnb.com', avatarUrl: 'https://twentyhq.github.io/placeholder-images/people/image-3.png', position: 1, @@ -37,7 +37,7 @@ export const personPrefillData = async ( nameFirstName: 'Alexandre', nameLastName: 'Prot', city: 'Paris', - email: 'prot@qonto.com', + emailsPrimaryEmail: 'prot@qonto.com', avatarUrl: 'https://twentyhq.github.io/placeholder-images/people/image-89.png', position: 2, @@ -49,7 +49,7 @@ export const personPrefillData = async ( nameFirstName: 'Patrick', nameLastName: 'Collison', city: 'San Francisco', - email: 'collison@stripe.com', + emailsPrimaryEmail: 'collison@stripe.com', avatarUrl: 'https://twentyhq.github.io/placeholder-images/people/image-47.png', position: 3, @@ -61,7 +61,7 @@ export const personPrefillData = async ( nameFirstName: 'Dylan', nameLastName: 'Field', city: 'San Francisco', - email: 'field@figma.com', + emailsPrimaryEmail: 'field@figma.com', avatarUrl: 'https://twentyhq.github.io/placeholder-images/people/image-40.png', position: 4, @@ -73,7 +73,7 @@ export const personPrefillData = async ( nameFirstName: 'Ivan', nameLastName: 'Zhao', city: 'San Francisco', - email: 'zhao@notion.com', + emailsPrimaryEmail: 'zhao@notion.com', avatarUrl: 'https://twentyhq.github.io/placeholder-images/people/image-68.png', position: 5, diff --git a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/people-all.view.ts b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/people-all.view.ts index 4eb9a60dbf16..6beb5cda8c4d 100644 --- a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/people-all.view.ts +++ b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/people-all.view.ts @@ -30,7 +30,7 @@ export const peopleAllView = async ( { fieldMetadataId: objectMetadataMap[STANDARD_OBJECT_IDS.person].fields[ - PERSON_STANDARD_FIELD_IDS.email + PERSON_STANDARD_FIELD_IDS.emails ], position: 1, isVisible: true, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts index 06d5993a4653..8f0d135fdbb3 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts @@ -305,6 +305,7 @@ export const OPPORTUNITY_STANDARD_FIELD_IDS = { export const PERSON_STANDARD_FIELD_IDS = { name: '20202020-3875-44d5-8c33-a6239011cab8', email: '20202020-a740-42bb-8849-8980fb3f12e1', + emails: '20202020-3c51-43fa-8b6e-af39e29368ab', linkedinLink: '20202020-f1af-48f7-893b-2007a73dd508', xLink: '20202020-8fc2-487c-b84a-55a99b145cfd', jobTitle: '20202020-b0d0-415a-bef9-640a26dacd9b', diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/listeners/calendar-event-participant-person.listener.ts b/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/listeners/calendar-event-participant-person.listener.ts index 47f0d16e8a9a..e2124ab2a9e6 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/listeners/calendar-event-participant-person.listener.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/listeners/calendar-event-participant-person.listener.ts @@ -32,7 +32,10 @@ export class CalendarEventParticipantPersonListener { >, ) { for (const eventPayload of payload.events) { - if (!eventPayload.properties.after.email) { + if ( + eventPayload.properties.after.emails?.primaryEmail === null && + eventPayload.properties.after.email === null + ) { continue; } @@ -41,7 +44,9 @@ export class CalendarEventParticipantPersonListener { CalendarEventParticipantMatchParticipantJob.name, { workspaceId: payload.workspaceId, - email: eventPayload.properties.after.email, + email: + eventPayload.properties.after.emails?.primaryEmail ?? + eventPayload.properties.after.email, // TODO personId: eventPayload.recordId, }, ); @@ -66,7 +71,9 @@ export class CalendarEventParticipantPersonListener { CalendarEventParticipantUnmatchParticipantJob.name, { workspaceId: payload.workspaceId, - email: eventPayload.properties.before.email, + email: + eventPayload.properties.before.emails?.primaryEmail ?? + eventPayload.properties.before.email, personId: eventPayload.recordId, }, ); @@ -75,7 +82,9 @@ export class CalendarEventParticipantPersonListener { CalendarEventParticipantMatchParticipantJob.name, { workspaceId: payload.workspaceId, - email: eventPayload.properties.after.email, + email: + eventPayload.properties.after.emails?.primaryEmail ?? + eventPayload.properties.after.email, personId: eventPayload.recordId, }, ); 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 6f6a1a677c45..536f0c3a2328 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,6 +2,7 @@ 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'; @@ -17,7 +18,10 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta ObjectMetadataRepositoryModule.forFeature([WorkspaceMemberWorkspaceEntity]), WorkspaceDataSourceModule, TypeOrmModule.forFeature([FeatureFlagEntity], 'core'), - TypeOrmModule.forFeature([ObjectMetadataEntity], 'metadata'), + TypeOrmModule.forFeature( + [ObjectMetadataEntity, FieldMetadataEntity], + 'metadata', + ), ], providers: [ CreateCompanyService, 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 62ca1e4b4a47..c5b6e93c6d22 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 @@ -1,16 +1,19 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { isDefined } from 'class-validator'; import chunk from 'lodash.chunk'; import compact from 'lodash.compact'; import { Any, EntityManager, Repository } from 'typeorm'; import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event'; import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; +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 { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; +import { PERSON_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'; @@ -35,6 +38,8 @@ export class CreateCompanyAndContactService { private readonly workspaceEventEmitter: WorkspaceEventEmitter, @InjectRepository(ObjectMetadataEntity, 'metadata') private readonly objectMetadataRepository: Repository, + @InjectRepository(FieldMetadataEntity, 'metadata') + private readonly fieldMetadataRepository: Repository, private readonly twentyORMGlobalManager: TwentyORMGlobalManager, ) {} @@ -49,6 +54,13 @@ export class CreateCompanyAndContactService { return []; } + const emailsFieldMetadata = await this.fieldMetadataRepository.findOne({ + where: { + workspaceId: workspaceId, + standardId: PERSON_STANDARD_FIELD_IDS.emails, + }, + }); + const personRepository = await this.twentyORMGlobalManager.getRepositoryForWorkspace( workspaceId, @@ -77,14 +89,16 @@ export class CreateCompanyAndContactService { } const alreadyCreatedContacts = await personRepository.find({ - where: { - email: Any(uniqueHandles), - }, + where: isDefined(emailsFieldMetadata) + ? { + emails: { primaryEmail: Any(uniqueHandles) }, + } + : { email: Any(uniqueHandles) }, }); - const alreadyCreatedContactEmails: string[] = alreadyCreatedContacts?.map( - ({ email }) => email, - ); + const alreadyCreatedContactEmails: string[] = isDefined(emailsFieldMetadata) + ? alreadyCreatedContacts?.map(({ emails }) => emails?.primaryEmail) + : alreadyCreatedContacts?.map(({ email }) => email); const filteredContactsToCreate = uniqueContacts.filter( (participant) => @@ -129,8 +143,11 @@ export class CreateCompanyAndContactService { createdByWorkspaceMember: connectedAccount.accountOwner, })); + const shouldUseEmailsField = isDefined(emailsFieldMetadata); + return this.createContactService.createPeople( formattedContactsToCreate, + shouldUseEmailsField, workspaceId, transactionManager, ); 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 7aad43da10eb..cada68c10acc 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 @@ -28,6 +28,7 @@ export class CreateContactService { private formatContacts( contactsToCreate: ContactToCreate[], lastPersonPosition: number, + shouldUseEmailsField: boolean, ): DeepPartial[] { return contactsToCreate.map((contact) => { const id = v4(); @@ -46,7 +47,9 @@ export class CreateContactService { return { id, - email: handle, + ...(shouldUseEmailsField + ? { emails: { primaryEmail: handle, additionalEmails: null } } + : { email: handle }), name: { firstName, lastName, @@ -64,6 +67,7 @@ export class CreateContactService { public async createPeople( contactsToCreate: ContactToCreate[], + shouldUseEmailsField: boolean, workspaceId: string, transactionManager?: EntityManager, ): Promise[]> { @@ -83,6 +87,7 @@ export class CreateContactService { const formattedContacts = this.formatContacts( contactsToCreate, lastPersonPosition, + shouldUseEmailsField, ); return personRepository.save( diff --git a/packages/twenty-server/src/modules/match-participant/match-participant.module.ts b/packages/twenty-server/src/modules/match-participant/match-participant.module.ts index 7f0904d7d3a3..1b01665f7a10 100644 --- a/packages/twenty-server/src/modules/match-participant/match-participant.module.ts +++ b/packages/twenty-server/src/modules/match-participant/match-participant.module.ts @@ -1,10 +1,12 @@ import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory'; import { MatchParticipantService } from 'src/modules/match-participant/match-participant.service'; @Module({ - imports: [], + imports: [TypeOrmModule.forFeature([FieldMetadataEntity], 'metadata')], providers: [ScopedWorkspaceContextFactory, MatchParticipantService], exports: [MatchParticipantService], }) diff --git a/packages/twenty-server/src/modules/match-participant/match-participant.service.ts b/packages/twenty-server/src/modules/match-participant/match-participant.service.ts index 509bbfe6b6ab..195a88ea4e3c 100644 --- a/packages/twenty-server/src/modules/match-participant/match-participant.service.ts +++ b/packages/twenty-server/src/modules/match-participant/match-participant.service.ts @@ -1,10 +1,13 @@ import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; -import { Any, EntityManager } from 'typeorm'; +import { Any, EntityManager, Repository } from 'typeorm'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; +import { PERSON_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity'; import { MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity'; import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity'; @@ -20,6 +23,8 @@ export class MatchParticipantService< private readonly workspaceEventEmitter: WorkspaceEventEmitter, private readonly twentyORMManager: TwentyORMManager, private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory, + @InjectRepository(FieldMetadataEntity, 'metadata') + private readonly fieldMetadataRepository: Repository, ) {} private async getParticipantRepository( @@ -55,19 +60,35 @@ export class MatchParticipantService< ...new Set(participants.map((participant) => participant.handle)), ]; + const emailsFieldMetadata = await this.fieldMetadataRepository.findOne({ + where: { + workspaceId: workspaceId, + standardId: PERSON_STANDARD_FIELD_IDS.emails, + }, + }); + const personRepository = await this.twentyORMManager.getRepository( 'person', ); - const people = await personRepository.find( - { - where: { - email: Any(uniqueParticipantsHandles), - }, - }, - transactionManager, - ); + const people = emailsFieldMetadata + ? await personRepository.find( + { + where: { + emails: Any(uniqueParticipantsHandles), + }, + }, + transactionManager, + ) + : await personRepository.find( + { + where: { + email: Any(uniqueParticipantsHandles), + }, + }, + transactionManager, + ); const workspaceMemberRepository = await this.twentyORMManager.getRepository( @@ -84,7 +105,11 @@ export class MatchParticipantService< ); for (const handle of uniqueParticipantsHandles) { - const person = people.find((person) => person.email === handle); + const person = people.find((person) => + emailsFieldMetadata + ? person.emails?.primaryEmail === handle + : person.email === handle, + ); const workspaceMember = workspaceMembers.find( (workspaceMember) => workspaceMember.userEmail === handle, diff --git a/packages/twenty-server/src/modules/messaging/message-participant-manager/listeners/message-participant-person.listener.ts b/packages/twenty-server/src/modules/messaging/message-participant-manager/listeners/message-participant-person.listener.ts index 86b3a0289ca9..1d4a0c6c38aa 100644 --- a/packages/twenty-server/src/modules/messaging/message-participant-manager/listeners/message-participant-person.listener.ts +++ b/packages/twenty-server/src/modules/messaging/message-participant-manager/listeners/message-participant-person.listener.ts @@ -32,7 +32,10 @@ export class MessageParticipantPersonListener { >, ) { for (const eventPayload of payload.events) { - if (!eventPayload.properties.after.email) { + if ( + !eventPayload.properties.after.emails?.primaryEmail && + !eventPayload.properties.after.email + ) { continue; } @@ -40,7 +43,9 @@ export class MessageParticipantPersonListener { MessageParticipantMatchParticipantJob.name, { workspaceId: payload.workspaceId, - email: eventPayload.properties.after.email, + email: + eventPayload.properties.after.emails?.primaryEmail ?? + eventPayload.properties.after.email, personId: eventPayload.recordId, }, ); @@ -58,13 +63,19 @@ export class MessageParticipantPersonListener { objectRecordUpdateEventChangedProperties( eventPayload.properties.before, eventPayload.properties.after, - ).includes('email') + ).includes('email') || + objectRecordUpdateEventChangedProperties( + eventPayload.properties.before, + eventPayload.properties.after, + ).includes('emails') ) { await this.messageQueueService.add( MessageParticipantUnmatchParticipantJob.name, { workspaceId: payload.workspaceId, - email: eventPayload.properties.before.email, + email: + eventPayload.properties.before.emails?.primaryEmail ?? + eventPayload.properties.before.email, personId: eventPayload.recordId, }, ); @@ -73,7 +84,9 @@ export class MessageParticipantPersonListener { MessageParticipantMatchParticipantJob.name, { workspaceId: payload.workspaceId, - email: eventPayload.properties.after.email, + email: + eventPayload.properties.after.emails?.primaryEmail ?? + eventPayload.properties.after.email, personId: eventPayload.recordId, }, ); diff --git a/packages/twenty-server/src/modules/person/standard-objects/person.workspace-entity.ts b/packages/twenty-server/src/modules/person/standard-objects/person.workspace-entity.ts index ac8aa9f628a5..a39a401ca02d 100644 --- a/packages/twenty-server/src/modules/person/standard-objects/person.workspace-entity.ts +++ b/packages/twenty-server/src/modules/person/standard-objects/person.workspace-entity.ts @@ -4,6 +4,7 @@ import { ActorMetadata, FieldActorSource, } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; +import { EmailsMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/emails.composite-type'; import { FullNameMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type'; import { LinksMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/links.composite-type'; import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; @@ -14,6 +15,7 @@ import { import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator'; +import { WorkspaceIsDeprecated } from 'src/engine/twenty-orm/decorators/workspace-is-deprecated.decorator'; import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator'; import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator'; import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator'; @@ -59,8 +61,18 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity { description: 'Contact’s Email', icon: 'IconMail', }) + @WorkspaceIsDeprecated() email: string; + @WorkspaceField({ + standardId: PERSON_STANDARD_FIELD_IDS.emails, + type: FieldMetadataType.EMAILS, + label: 'Emails', + description: 'Contact’s Emails', + icon: 'IconMail', + }) + emails: EmailsMetadata; + @WorkspaceField({ standardId: PERSON_STANDARD_FIELD_IDS.linkedinLink, type: FieldMetadataType.LINKS,