4285 timebox create google calendar full sync (#4442)
* calendar module * wip * creating a folder for common files between calendar and messages * wip * wip * wip * wip * update calendar search filter * wip * working on full sync service * reorganizing folders * adding repositories * fix typo * working on full-sync service * Add calendarQueue to MessageQueue enum and update dependencies * start transaction * wip * add save and update functions for event * wip * save events * improving step by step * add calendar scope * fix nest modules imports * renaming * create calendar channel * create job for google calendar full-sync * call GoogleCalendarFullSyncJob after connected account creation * ask for scope conditionnally * fixes * create channels conditionnally * fix * fixes * fix FK bug * filter out canceled events * create save and update functions for calendarEventAttendee repository * saving messageParticipants is working * save calendarEventAttendees is working * add calendarEvent cleaner * calendar event cleaner is working * working on updating attendees * wip * reintroducing google-gmail endpoint to ensure smooth deploy * modify callbackURL * modify front url * changes to be able to merge * put back feature flag * fixes after PR comments * add feature flag check * remove unused modules * separate delete connected account associated job data in two jobs * fix error * rename calendar_v3 as calendarV3 * Update packages/twenty-server/src/workspace/calendar-and-messaging/utils/valueStringForBatchRawQuery.util.ts Co-authored-by: Jérémy M <jeremy.magrin@gmail.com> * improve readability * renaming to remove plural * renaming to remove plural * don't throw if no connected account is found * use calendar queue * modify usage of HttpService in fetch-by-batch * modify valuesStringForBatchRawQuery to improve api and return flattened values * fix auth module feature flag import * fix getFlattenedValuesAndValuesStringForBatchRawQuery --------- Co-authored-by: Jérémy M <jeremy.magrin@gmail.com>
This commit is contained in:
@ -0,0 +1,21 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { PersonModule } from 'src/workspace/repositories/person/person.module';
|
||||
import { WorkspaceMemberModule } from 'src/workspace/repositories/workspace-member/workspace-member.module';
|
||||
import { CreateCompanyAndContactService } from 'src/workspace/auto-companies-and-contacts-creation/create-company-and-contact/create-company-and-contact.service';
|
||||
import { CreateCompanyModule } from 'src/workspace/auto-companies-and-contacts-creation/create-company/create-company.module';
|
||||
import { CreateContactModule } from 'src/workspace/auto-companies-and-contacts-creation/create-contact/create-contact.module';
|
||||
import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
WorkspaceDataSourceModule,
|
||||
CreateContactModule,
|
||||
CreateCompanyModule,
|
||||
WorkspaceMemberModule,
|
||||
PersonModule,
|
||||
],
|
||||
providers: [CreateCompanyAndContactService],
|
||||
exports: [CreateCompanyAndContactService],
|
||||
})
|
||||
export class CreateCompaniesAndContactsModule {}
|
||||
@ -0,0 +1,112 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { EntityManager } from 'typeorm';
|
||||
import compact from 'lodash/compact';
|
||||
|
||||
import { Participant } from 'src/workspace/messaging/types/gmail-message';
|
||||
import { getDomainNameFromHandle } from 'src/workspace/messaging/utils/get-domain-name-from-handle.util';
|
||||
import { CreateCompanyService } from 'src/workspace/auto-companies-and-contacts-creation/create-company/create-company.service';
|
||||
import { CreateContactService } from 'src/workspace/auto-companies-and-contacts-creation/create-contact/create-contact.service';
|
||||
import { PersonService } from 'src/workspace/repositories/person/person.service';
|
||||
import { WorkspaceMemberService } from 'src/workspace/repositories/workspace-member/workspace-member.service';
|
||||
import { getUniqueParticipantsAndHandles } from 'src/workspace/messaging/utils/get-unique-participants-and-handles.util';
|
||||
import { filterOutParticipantsFromCompanyOrWorkspace } from 'src/workspace/messaging/utils/filter-out-participants-from-company-or-workspace.util';
|
||||
import { isWorkEmail } from 'src/utils/is-work-email';
|
||||
|
||||
@Injectable()
|
||||
export class CreateCompanyAndContactService {
|
||||
constructor(
|
||||
private readonly personService: PersonService,
|
||||
private readonly createContactService: CreateContactService,
|
||||
private readonly createCompaniesService: CreateCompanyService,
|
||||
private readonly workspaceMemberService: WorkspaceMemberService,
|
||||
) {}
|
||||
|
||||
async createCompaniesAndContacts(
|
||||
selfHandle: string,
|
||||
participants: Participant[],
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
) {
|
||||
if (!participants || participants.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: This is a feature that may be implemented in the future
|
||||
const isContactAutoCreationForNonWorkEmailsEnabled = false;
|
||||
|
||||
const workspaceMembers =
|
||||
await this.workspaceMemberService.getAllByWorkspaceId(
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
const participantsFromOtherCompanies =
|
||||
filterOutParticipantsFromCompanyOrWorkspace(
|
||||
participants,
|
||||
selfHandle,
|
||||
workspaceMembers,
|
||||
);
|
||||
|
||||
const { uniqueParticipants, uniqueHandles } =
|
||||
getUniqueParticipantsAndHandles(participantsFromOtherCompanies);
|
||||
|
||||
if (uniqueHandles.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const alreadyCreatedContacts = await this.personService.getByEmails(
|
||||
uniqueHandles,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const alreadyCreatedContactEmails: string[] = alreadyCreatedContacts?.map(
|
||||
({ email }) => email,
|
||||
);
|
||||
|
||||
const filteredParticipants = uniqueParticipants.filter(
|
||||
(participant) =>
|
||||
!alreadyCreatedContactEmails.includes(participant.handle) &&
|
||||
participant.handle.includes('@') &&
|
||||
(isContactAutoCreationForNonWorkEmailsEnabled ||
|
||||
isWorkEmail(participant.handle)),
|
||||
);
|
||||
|
||||
const filteredParticipantsWithCompanyDomainNames =
|
||||
filteredParticipants?.map((participant) => ({
|
||||
handle: participant.handle,
|
||||
displayName: participant.displayName,
|
||||
companyDomainName: isWorkEmail(participant.handle)
|
||||
? getDomainNameFromHandle(participant.handle)
|
||||
: undefined,
|
||||
}));
|
||||
|
||||
const domainNamesToCreate = compact(
|
||||
filteredParticipantsWithCompanyDomainNames.map(
|
||||
(participant) => participant.companyDomainName,
|
||||
),
|
||||
);
|
||||
|
||||
const companiesObject = await this.createCompaniesService.createCompanies(
|
||||
domainNamesToCreate,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
const contactsToCreate = filteredParticipantsWithCompanyDomainNames.map(
|
||||
(participant) => ({
|
||||
handle: participant.handle,
|
||||
displayName: participant.displayName,
|
||||
companyId:
|
||||
participant.companyDomainName &&
|
||||
companiesObject[participant.companyDomainName],
|
||||
}),
|
||||
);
|
||||
|
||||
await this.createContactService.createContacts(
|
||||
contactsToCreate,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module';
|
||||
import { CreateCompanyService } from 'src/workspace/auto-companies-and-contacts-creation/create-company/create-company.service';
|
||||
import { CompanyModule } from 'src/workspace/messaging/repositories/company/company.module';
|
||||
|
||||
@Module({
|
||||
imports: [WorkspaceDataSourceModule, CompanyModule],
|
||||
providers: [CreateCompanyService],
|
||||
exports: [CreateCompanyService],
|
||||
})
|
||||
export class CreateCompanyModule {}
|
||||
@ -0,0 +1,117 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { EntityManager } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
|
||||
import { CompanyService } from 'src/workspace/messaging/repositories/company/company.service';
|
||||
import { getCompanyNameFromDomainName } from 'src/workspace/messaging/utils/get-company-name-from-domain-name.util';
|
||||
@Injectable()
|
||||
export class CreateCompanyService {
|
||||
private readonly httpService: AxiosInstance;
|
||||
|
||||
constructor(private readonly companyService: CompanyService) {
|
||||
this.httpService = axios.create({
|
||||
baseURL: 'https://companies.twenty.com',
|
||||
});
|
||||
}
|
||||
|
||||
async createCompanies(
|
||||
domainNames: string[],
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<{
|
||||
[domainName: string]: string;
|
||||
}> {
|
||||
if (domainNames.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const uniqueDomainNames = [...new Set(domainNames)];
|
||||
|
||||
const existingCompanies =
|
||||
await this.companyService.getExistingCompaniesByDomainNames(
|
||||
uniqueDomainNames,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
const companiesObject = existingCompanies.reduce(
|
||||
(
|
||||
acc: {
|
||||
[domainName: string]: string;
|
||||
},
|
||||
company: {
|
||||
domainName: string;
|
||||
id: string;
|
||||
},
|
||||
) => ({
|
||||
...acc,
|
||||
[company.domainName]: company.id,
|
||||
}),
|
||||
{},
|
||||
);
|
||||
|
||||
const filteredDomainNames = uniqueDomainNames.filter(
|
||||
(domainName) =>
|
||||
!existingCompanies.some(
|
||||
(company: { domainName: string }) =>
|
||||
company.domainName === domainName,
|
||||
),
|
||||
);
|
||||
|
||||
for (const domainName of filteredDomainNames) {
|
||||
companiesObject[domainName] = await this.createCompany(
|
||||
domainName,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
|
||||
return companiesObject;
|
||||
}
|
||||
|
||||
async createCompany(
|
||||
domainName: string,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<string> {
|
||||
const companyId = v4();
|
||||
|
||||
const { name, city } = await this.getCompanyInfoFromDomainName(domainName);
|
||||
|
||||
this.companyService.createCompany(
|
||||
workspaceId,
|
||||
{
|
||||
id: companyId,
|
||||
domainName,
|
||||
name,
|
||||
city,
|
||||
},
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
return companyId;
|
||||
}
|
||||
|
||||
async getCompanyInfoFromDomainName(domainName: string): Promise<{
|
||||
name: string;
|
||||
city: string;
|
||||
}> {
|
||||
try {
|
||||
const response = await this.httpService.get(`/${domainName}`);
|
||||
|
||||
const data = response.data;
|
||||
|
||||
return {
|
||||
name: data.name ?? getCompanyNameFromDomainName(domainName),
|
||||
city: data.city,
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
name: getCompanyNameFromDomainName(domainName),
|
||||
city: '',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module';
|
||||
import { CreateContactService } from 'src/workspace/auto-companies-and-contacts-creation/create-contact/create-contact.service';
|
||||
import { PersonModule } from 'src/workspace/repositories/person/person.module';
|
||||
|
||||
@Module({
|
||||
imports: [WorkspaceDataSourceModule, PersonModule],
|
||||
providers: [CreateContactService],
|
||||
exports: [CreateContactService],
|
||||
})
|
||||
export class CreateContactModule {}
|
||||
@ -0,0 +1,63 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { EntityManager } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { PersonService } from 'src/workspace/repositories/person/person.service';
|
||||
import { getFirstNameAndLastNameFromHandleAndDisplayName } from 'src/workspace/messaging/utils/get-first-name-and-last-name-from-handle-and-display-name.util';
|
||||
|
||||
type ContactToCreate = {
|
||||
handle: string;
|
||||
displayName: string;
|
||||
companyId?: string;
|
||||
};
|
||||
|
||||
type FormattedContactToCreate = {
|
||||
id: string;
|
||||
handle: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
companyId?: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class CreateContactService {
|
||||
constructor(private readonly personService: PersonService) {}
|
||||
|
||||
public formatContacts(
|
||||
contactsToCreate: ContactToCreate[],
|
||||
): FormattedContactToCreate[] {
|
||||
return contactsToCreate.map((contact) => {
|
||||
const id = v4();
|
||||
|
||||
const { handle, displayName, companyId } = contact;
|
||||
|
||||
const { firstName, lastName } =
|
||||
getFirstNameAndLastNameFromHandleAndDisplayName(handle, displayName);
|
||||
|
||||
return {
|
||||
id,
|
||||
handle,
|
||||
firstName,
|
||||
lastName,
|
||||
companyId,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public async createContacts(
|
||||
contactsToCreate: ContactToCreate[],
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<void> {
|
||||
if (contactsToCreate.length === 0) return;
|
||||
|
||||
const formattedContacts = this.formatContacts(contactsToCreate);
|
||||
|
||||
await this.personService.createPeople(
|
||||
formattedContacts,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user