Skip to content

Commit a054521

Browse files
committed
add restoration logic for contacts and companies - to do test
1 parent 8121f48 commit a054521

File tree

5 files changed

+200
-16
lines changed

5 files changed

+200
-16
lines changed

packages/twenty-server/src/modules/contact-creation-manager/services/__tests__/create-company.service.spec.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ describe('CreateCompanyService', () => {
138138
});
139139

140140
it('should successfully create a company', async () => {
141-
await service.createCompanies([companyToCreate1], workspaceId);
141+
await service.createOrRestoreCompanies([companyToCreate1], workspaceId);
142142

143143
expect(mockCompanyRepository.find).toHaveBeenCalled();
144144
expect(mockCompanyRepository.save).toHaveBeenCalledWith([
@@ -147,7 +147,7 @@ describe('CreateCompanyService', () => {
147147
});
148148

149149
it('should successfully two companies', async () => {
150-
await service.createCompanies(
150+
await service.createOrRestoreCompanies(
151151
[companyToCreate1, companyToCreate2],
152152
workspaceId,
153153
);
@@ -160,7 +160,7 @@ describe('CreateCompanyService', () => {
160160
});
161161

162162
it('should create only one of example.com & example.com/ ', async () => {
163-
await service.createCompanies(
163+
await service.createOrRestoreCompanies(
164164
[companyToCreate1, companyToCreate1withSlash],
165165
workspaceId,
166166
);
@@ -190,7 +190,10 @@ describe('CreateCompanyService', () => {
190190
});
191191

192192
it('should not create a company if it already exists', async () => {
193-
await service.createCompanies([companyToCreateExisting], workspaceId);
193+
await service.createOrRestoreCompanies(
194+
[companyToCreateExisting],
195+
workspaceId,
196+
);
194197

195198
expect(mockCompanyRepository.find).toHaveBeenCalled();
196199
expect(mockCompanyRepository.save).not.toHaveBeenCalled();

packages/twenty-server/src/modules/contact-creation-manager/services/create-company-and-contact.service.ts

Lines changed: 86 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common';
33
import { isNonEmptyString } from '@sniptt/guards';
44
import chunk from 'lodash.chunk';
55
import compact from 'lodash.compact';
6+
import { isDefined } from 'twenty-shared/utils';
67
import { type DeepPartial } from 'typeorm';
78

89
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
@@ -141,10 +142,11 @@ export class CreateCompanyAndContactService {
141142
}),
142143
);
143144

144-
const companiesObject = await this.createCompaniesService.createCompanies(
145-
workDomainNamesToCreateFormatted,
146-
workspaceId,
147-
);
145+
const companiesObject =
146+
await this.createCompaniesService.createOrRestoreCompanies(
147+
workDomainNamesToCreateFormatted,
148+
workspaceId,
149+
);
148150

149151
const formattedContactsToCreate =
150152
filteredContactsToCreateWithCompanyDomainNames.map((contact) => ({
@@ -160,10 +162,89 @@ export class CreateCompanyAndContactService {
160162
},
161163
}));
162164

163-
return this.createContactService.createPeople(
165+
const restoredContacts =
166+
await this.restorePeopleAndRestoreOrCreateCompanies(
167+
alreadyCreatedContacts,
168+
uniqueContacts,
169+
workspaceId,
170+
source,
171+
connectedAccount,
172+
);
173+
174+
const createdContacts = await this.createContactService.createPeople(
164175
formattedContactsToCreate,
165176
workspaceId,
166177
);
178+
179+
return [...restoredContacts, ...createdContacts];
180+
}
181+
182+
async restorePeopleAndRestoreOrCreateCompanies(
183+
alreadyCreatedContacts: PersonWorkspaceEntity[],
184+
uniqueContacts: Contact[],
185+
workspaceId: string,
186+
source: FieldActorSource,
187+
connectedAccount: ConnectedAccountWorkspaceEntity,
188+
) {
189+
const filteredContactsToRestore = uniqueContacts
190+
.map((contact) => {
191+
const softDeletedContact = alreadyCreatedContacts.find(
192+
(person) =>
193+
isDefined(person.deletedAt) &&
194+
(person.emails.primaryEmail === contact.handle.toLowerCase() ||
195+
(Array.isArray(person.emails?.additionalEmails) &&
196+
person.emails.additionalEmails.includes(
197+
contact.handle.toLowerCase(),
198+
))),
199+
);
200+
201+
return isDefined(softDeletedContact)
202+
? {
203+
id: softDeletedContact.id,
204+
companyDomainName: isWorkEmail(contact.handle)
205+
? getDomainNameFromHandle(contact.handle)
206+
: undefined,
207+
}
208+
: undefined;
209+
})
210+
.filter(isDefined);
211+
212+
const workDomainNamesToCreate = filteredContactsToRestore
213+
.filter(
214+
(participant) =>
215+
isDefined(participant.companyDomainName) &&
216+
isWorkDomain(participant.companyDomainName),
217+
)
218+
.map((participant) => ({
219+
domainName: participant.companyDomainName,
220+
createdBySource: source,
221+
createdByWorkspaceMember: connectedAccount.accountOwner,
222+
createdByContext: {
223+
provider: connectedAccount.provider,
224+
},
225+
}));
226+
227+
const companiesObject =
228+
await this.createCompaniesService.createOrRestoreCompanies(
229+
workDomainNamesToCreate,
230+
workspaceId,
231+
);
232+
233+
const formattedContactsToRestore = filteredContactsToRestore.map(
234+
(contact) => {
235+
return {
236+
id: contact.id,
237+
companyId: isNonEmptyString(contact.companyDomainName)
238+
? companiesObject[contact.companyDomainName]
239+
: undefined,
240+
};
241+
},
242+
);
243+
244+
return this.createContactService.restorePeople(
245+
formattedContactsToRestore,
246+
workspaceId,
247+
);
167248
}
168249

169250
async createCompaniesAndContactsAndUpdateParticipants(

packages/twenty-server/src/modules/contact-creation-manager/services/create-company.service.ts

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import axios, { type AxiosInstance } from 'axios';
44
import uniqBy from 'lodash.uniqby';
55
import { TWENTY_COMPANIES_BASE_URL } from 'twenty-shared/constants';
66
import { type ConnectedAccountProvider } from 'twenty-shared/types';
7-
import { lowercaseUrlOriginAndRemoveTrailingSlash } from 'twenty-shared/utils';
7+
import {
8+
isDefined,
9+
lowercaseUrlOriginAndRemoveTrailingSlash,
10+
} from 'twenty-shared/utils';
811
import { type DeepPartial, ILike } from 'typeorm';
912

1013
import { type FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
@@ -35,7 +38,7 @@ export class CreateCompanyService {
3538
});
3639
}
3740

38-
async createCompanies(
41+
async createOrRestoreCompanies(
3942
companies: CompanyToCreate[],
4043
workspaceId: string,
4144
): Promise<{
@@ -73,6 +76,7 @@ export class CreateCompanyService {
7376
// Find existing companies
7477
const existingCompanies = await companyRepository.find({
7578
where: conditions,
79+
withDeleted: true,
7680
});
7781
const existingCompanyIdsMap = this.createCompanyMap(existingCompanies);
7882

@@ -87,7 +91,12 @@ export class CreateCompanyService {
8791
),
8892
);
8993

90-
if (newCompaniesToCreate.length === 0) {
94+
const companiesToRestore = this.filterCompaniesToRestore(
95+
uniqueCompanies,
96+
existingCompanies,
97+
);
98+
99+
if (newCompaniesToCreate.length === 0 && companiesToRestore.length === 0) {
91100
return existingCompanyIdsMap;
92101
}
93102

@@ -103,14 +112,66 @@ export class CreateCompanyService {
103112
// Create new companies
104113
const createdCompanies = await companyRepository.save(newCompaniesData);
105114

106-
const createdCompanyIdsMap = this.createCompanyMap(createdCompanies);
115+
const restoredCompanies = await companyRepository.updateMany(
116+
companiesToRestore.map((company) => {
117+
return {
118+
criteria: company.id,
119+
partialEntity: {
120+
deletedAt: null,
121+
},
122+
};
123+
}),
124+
undefined,
125+
['domainNamePrimaryLinkUrl', 'id'],
126+
);
127+
128+
//TODO : Fix updateMany method to return formatted records
129+
const formattedRestoredCompanies = restoredCompanies.raw.map(
130+
(row: { id: string; domainNamePrimaryLinkUrl: string }) => {
131+
return {
132+
id: row.id,
133+
domainName: {
134+
primaryLinkUrl: row.domainNamePrimaryLinkUrl,
135+
},
136+
};
137+
},
138+
);
107139

108140
return {
109141
...existingCompanyIdsMap,
110-
...createdCompanyIdsMap,
142+
...(createdCompanies.length > 0
143+
? this.createCompanyMap(createdCompanies)
144+
: {}),
145+
...(formattedRestoredCompanies.length > 0
146+
? this.createCompanyMap(formattedRestoredCompanies)
147+
: {}),
111148
};
112149
}
113150

151+
private filterCompaniesToRestore(
152+
uniqueCompanies: CompanyToCreate[],
153+
existingCompanies: CompanyWorkspaceEntity[],
154+
) {
155+
return uniqueCompanies
156+
.map((company) => {
157+
const existingCompany = existingCompanies.find(
158+
(existingCompany) =>
159+
existingCompany.domainName &&
160+
extractDomainFromLink(existingCompany.domainName.primaryLinkUrl) ===
161+
company.domainName,
162+
);
163+
164+
return isDefined(existingCompany)
165+
? {
166+
domainName: company.domainName,
167+
id: existingCompany.id,
168+
deletedAt: null,
169+
}
170+
: undefined;
171+
})
172+
.filter(isDefined);
173+
}
174+
114175
private async prepareCompanyData(
115176
company: CompanyToCreate,
116177
position: number,
@@ -142,7 +203,9 @@ export class CreateCompanyService {
142203
};
143204
}
144205

145-
private createCompanyMap(companies: DeepPartial<CompanyWorkspaceEntity>[]) {
206+
private createCompanyMap(
207+
companies: Pick<CompanyWorkspaceEntity, 'id' | 'domainName'>[],
208+
) {
146209
return companies.reduce(
147210
(acc, company) => {
148211
if (!company.domainName?.primaryLinkUrl || !company.id) {

packages/twenty-server/src/modules/contact-creation-manager/services/create-contact.service.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ type ContactToCreate = {
2323
};
2424
};
2525

26+
type ContactToRestore = {
27+
id: string;
28+
companyId?: string;
29+
};
30+
2631
@Injectable()
2732
export class CreateContactService {
2833
constructor(
@@ -97,6 +102,36 @@ export class CreateContactService {
97102
return personRepository.save(formattedContacts, undefined);
98103
}
99104

105+
public async restorePeople(
106+
contactsToRestore: ContactToRestore[],
107+
workspaceId: string,
108+
): Promise<DeepPartial<PersonWorkspaceEntity>[]> {
109+
if (contactsToRestore.length === 0) return [];
110+
111+
const personRepository =
112+
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
113+
workspaceId,
114+
PersonWorkspaceEntity,
115+
{
116+
shouldBypassPermissionChecks: true,
117+
},
118+
);
119+
120+
const restoredContacts = await personRepository.updateMany(
121+
contactsToRestore.map((contact) => ({
122+
criteria: contact.id,
123+
partialEntity: {
124+
companyId: contact.companyId,
125+
deletedAt: null,
126+
},
127+
})),
128+
undefined,
129+
['companyId', 'id'],
130+
);
131+
132+
return restoredContacts.raw;
133+
}
134+
100135
private async getLastPersonPosition(
101136
personRepository: WorkspaceRepository<PersonWorkspaceEntity>,
102137
): Promise<number> {

packages/twenty-server/src/modules/match-participant/utils/add-person-email-filters-to-query-builder.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,12 @@ export function addPersonEmailFiltersToQueryBuilder({
3030
'person.id',
3131
'person.emailsPrimaryEmail',
3232
'person.emailsAdditionalEmails',
33+
'person.deletedAt',
3334
])
3435
.where('LOWER(person.emailsPrimaryEmail) IN (:...emails)', {
3536
emails: normalizedEmails,
36-
});
37+
})
38+
.withDeleted();
3739

3840
if (excludePersonIds.length > 0) {
3941
queryBuilder = queryBuilder.andWhere(

0 commit comments

Comments
 (0)