mirror of
https://github.com/lingble/twenty.git
synced 2025-10-29 20:02:29 +00:00
Handle migration of Email to Emails fields (#6885)
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:\ <https://www.loom.com/share/f82b8d29f8f64f92abe3c59c01147b45?sid=9f8ccc05-aa38-4c49-b139-fd0823066273> **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:\ <https://www.loom.com/share/400949464b244272b78c25e338cc6ab2?sid=103f6625-5933-4b99-9825-0fed33782f36> **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-event-list-fetch 5. Run the app and navigate to Settings/Accounts then connect your Google account --------- Co-authored-by: gitstart-twenty <gitstart-twenty@users.noreply.github.com> Co-authored-by: Marie Stoppa <marie.stoppa@essec.edu> Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
committed by
GitHub
parent
3548751be2
commit
31c02202bd
@@ -185,7 +185,11 @@ xLink
|
||||
id
|
||||
createdAt
|
||||
city
|
||||
email
|
||||
emails
|
||||
{
|
||||
primaryEmail
|
||||
additionalEmails
|
||||
}
|
||||
jobTitle
|
||||
name
|
||||
{
|
||||
|
||||
@@ -41,7 +41,11 @@ describe('mapObjectMetadataToGraphQLQuery', () => {
|
||||
firstName
|
||||
lastName
|
||||
}
|
||||
email
|
||||
emails
|
||||
{
|
||||
primaryEmail
|
||||
additionalEmails
|
||||
}
|
||||
phone
|
||||
createdAt
|
||||
avatarUrl
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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<Workspace>,
|
||||
@InjectRepository(FieldMetadataEntity, 'metadata')
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
@InjectRepository(ObjectMetadataEntity, 'metadata')
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||
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<void> {
|
||||
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<ViewFieldWorkspaceEntity>(
|
||||
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}"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -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<void> {
|
||||
await this.syncWorkspaceMetadataCommand.run(passedParam, {
|
||||
...options,
|
||||
force: true,
|
||||
});
|
||||
await this.setCustomObjectIsSoftDeletableCommand.run(passedParam, options);
|
||||
await this.migrateEmailFieldsToEmails.run(passedParam, options);
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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',
|
||||
|
||||
@@ -22,5 +22,5 @@ export const emailsCompositeType: CompositeType = {
|
||||
|
||||
export type EmailsMetadata = {
|
||||
primaryEmail: string;
|
||||
additionalEmails: string[] | null;
|
||||
additionalEmails: object | null;
|
||||
};
|
||||
|
||||
@@ -183,7 +183,7 @@ export class FieldMetadataDefaultValueEmails {
|
||||
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsObject()
|
||||
additionalEmails: string[] | null;
|
||||
additionalEmails: object | null;
|
||||
}
|
||||
|
||||
export class FieldMetadataDefaultValuePhones {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<ObjectMetadataEntity>,
|
||||
@InjectRepository(FieldMetadataEntity, 'metadata')
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
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,
|
||||
);
|
||||
|
||||
@@ -28,6 +28,7 @@ export class CreateContactService {
|
||||
private formatContacts(
|
||||
contactsToCreate: ContactToCreate[],
|
||||
lastPersonPosition: number,
|
||||
shouldUseEmailsField: boolean,
|
||||
): DeepPartial<PersonWorkspaceEntity>[] {
|
||||
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<DeepPartial<PersonWorkspaceEntity>[]> {
|
||||
@@ -83,6 +87,7 @@ export class CreateContactService {
|
||||
const formattedContacts = this.formatContacts(
|
||||
contactsToCreate,
|
||||
lastPersonPosition,
|
||||
shouldUseEmailsField,
|
||||
);
|
||||
|
||||
return personRepository.save(
|
||||
|
||||
@@ -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],
|
||||
})
|
||||
|
||||
@@ -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<FieldMetadataEntity>,
|
||||
) {}
|
||||
|
||||
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<PersonWorkspaceEntity>(
|
||||
'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<WorkspaceMemberWorkspaceEntity>(
|
||||
@@ -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,
|
||||
|
||||
@@ -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<MessageParticipantUnmatchParticipantJobData>(
|
||||
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,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user