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
@ -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