From 31c02202bd8e3a55ad480ca8a46b8263088025ee Mon Sep 17 00:00:00 2001 From: "gitstart-app[bot]" <57568882+gitstart-app[bot]@users.noreply.github.com> Date: Thu, 12 Sep 2024 18:31:11 +0200 Subject: [PATCH] Handle migration of Email to Emails fields (#6885) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is the second PR on TWNTY-6261 which handlesdata migration of Email field to Emails field.\ \ How to Test?\ Firstly make sure that you have completed the testing steps on first PR then follow the below steps: - Checkout to TWNTY-6261-emails-migrations branch - Rebuild typescript using "npx nx build twenty-server" - Run command "yarn command:prod upgrade-0.25" to do migration\ \ Loom Video:\ **Testing Messaging Sync functionality:** Please watch the below video to see that the synchronization of contacts is working fine after migrating Email field to Emails field:\ **Question to the client** should we rename email to emails here? in the DomainName PR, the name did not change. ```typescript @WorkspaceField({ standardId: PERSON_STANDARD_FIELD_IDS.email, type: FieldMetadataType.EMAILS, label: 'Email', description: 'Contact’s Email', icon: 'IconMail', }) email: EmailsMetadata; ``` **Test Messaging Sync** This pr will update messaging sync files so the changes shouldn't break existing functionality of importing people and companies in the app.\ To test messaging sync you should follow the below steps:\ 1. you need to connect a google account to see the importing functionality. For this purpose you have to create a project inside Google Cloud. But to make things easier you can use the below credentials of an already created project. Put them in .env of twenty-server package: ```properties MESSAGING_PROVIDER_GMAIL_ENABLED=true CALENDAR_PROVIDER_GOOGLE_ENABLED=true AUTH_GOOGLE_ENABLED=true AUTH_GOOGLE_CLIENT_ID=951231465939-h61tg6nkpkv1821qi899fjbj9looquto.apps.googleusercontent.com AUTH_GOOGLE_CLIENT_SECRET=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 AUTH_GOOGLE_CALLBACK_URL=http://localhost:3000/auth/google/redirect AUTH_GOOGLE_APIS_CALLBACK_URL=http://localhost:3000/auth/google-apis/get-access-token MESSAGE_QUEUE_TYPE=bull-mq ``` Alternative env ```properties MESSAGING_PROVIDER_GMAIL_ENABLED=true CALENDAR_PROVIDER_GOOGLE_ENABLED=true AUTH_GOOGLE_ENABLED=true AUTH_GOOGLE_CLIENT_ID=622006708006-dc4n3vrtf3cs2h6k7hgbborudme7ku9l.apps.googleusercontent.com AUTH_GOOGLE_CLIENT_SECRET=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 AUTH_GOOGLE_CALLBACK_URL=http://localhost:3000/auth/google/redirect AUTH_GOOGLE_APIS_CALLBACK_URL=http://localhost:3000/auth/google-apis/get-access-token MESSAGE_QUEUE_TYPE=bull-mq ``` 1. Launch your worker with `npx nx run twenty-server:worker` 2. npx nx run twenty-server:command cron:messaging:messages-import 3. npx nx run twenty-server:command cron:messaging:message-list-fetch 4. npx nx run twenty-server:command cron:calendar:calendar-event-list-fetch 5. Run the app and navigate to Settings/Accounts then connect your Google account --------- Co-authored-by: gitstart-twenty Co-authored-by: Marie Stoppa Co-authored-by: Weiko --- .../mapFieldMetadataToGraphQLQuery.test.tsx | 6 +- .../mapObjectMetadataToGraphQLQuery.test.tsx | 6 +- .../commands/database-command.module.ts | 2 + .../0-24/0-24-upgrade-version.command.ts | 3 - .../0-24/0-24-upgrade-version.module.ts | 2 +- ...-migrate-email-fields-to-emails.command.ts | 338 ++++++++++++++++++ ...ustom-object-is-soft-deletable.command.ts} | 2 +- .../0-30/0-30-upgrade-version.command.ts | 45 +++ .../0-30/0-30-upgrade-version.module.ts | 37 ++ .../typeorm-seeds/workspace/people.ts | 32 +- .../composite-types/emails.composite-type.ts | 2 +- .../dtos/default-value.input.ts | 2 +- .../demo-objects-prefill-data/person.ts | 4 +- .../standard-objects-prefill-data/person.ts | 12 +- .../views/people-all.view.ts | 2 +- .../constants/standard-field-ids.ts | 1 + ...endar-event-participant-person.listener.ts | 17 +- .../contact-creation-manager.module.ts | 6 +- .../create-company-and-contact.service.ts | 29 +- .../services/create-contact.service.ts | 7 +- .../match-participant.module.ts | 4 +- .../match-participant.service.ts | 45 ++- .../message-participant-person.listener.ts | 23 +- .../person.workspace-entity.ts | 12 + 24 files changed, 577 insertions(+), 62 deletions(-) create mode 100644 packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-migrate-email-fields-to-emails.command.ts rename packages/twenty-server/src/database/commands/upgrade-version/{0-24/0-24-set-custom-object-is-soft-deletable.command.ts => 0-30/0-30-set-custom-object-is-soft-deletable.command.ts} (96%) create mode 100644 packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-upgrade-version.command.ts create mode 100644 packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-upgrade-version.module.ts 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 38ab9211e..da578f897 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 d1838963d..5a0090964 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 d240657f5..6281a5e31 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 2862f566a..5ac771d77 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 556438046..2cebb1eb1 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 000000000..cf057f293 --- /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 a27a3cc14..eca303963 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 000000000..2191860af --- /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 000000000..3e266cebe --- /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 6063b4be6..6624fd503 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 3cee1f441..9ca5ceea5 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 45de1dfce..a617f8ad9 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 c12b0b201..13cc32d40 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 b30974e81..fb227b15c 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 4eb9a60db..6beb5cda8 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 06d5993a4..8f0d135fd 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 47f0d16e8..e2124ab2a 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 6f6a1a677..536f0c3a2 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 62ca1e4b4..c5b6e93c6 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 7aad43da1..cada68c10 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 7f0904d7d..1b01665f7 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 509bbfe6b..195a88ea4 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 86b3a0289..1d4a0c6c3 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 ac8aa9f62..a39a401ca 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,