Refactor backend folder structure (#4505)

* Refactor backend folder structure

Co-authored-by: Charles Bochet <charles@twenty.com>

* fix tests

* fix

* move yoga hooks

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Weiko
2024-03-15 18:37:09 +01:00
committed by GitHub
parent afb9b3e375
commit 2c09096edd
523 changed files with 1386 additions and 1856 deletions

View File

@ -0,0 +1,21 @@
import { Module } from '@nestjs/common';
import { PersonModule } from 'src/modules/person/repositories/person/person.module';
import { WorkspaceMemberModule } from 'src/modules/workspace-member/repositories/workspace-member/workspace-member.module';
import { CreateCompanyAndContactService } from 'src/modules/connected-account/auto-companies-and-contacts-creation/create-company-and-contact/create-company-and-contact.service';
import { CreateCompanyModule } from 'src/modules/connected-account/auto-companies-and-contacts-creation/create-company/create-company.module';
import { CreateContactModule } from 'src/modules/connected-account/auto-companies-and-contacts-creation/create-contact/create-contact.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
@Module({
imports: [
WorkspaceDataSourceModule,
CreateContactModule,
CreateCompanyModule,
WorkspaceMemberModule,
PersonModule,
],
providers: [CreateCompanyAndContactService],
exports: [CreateCompanyAndContactService],
})
export class CreateCompaniesAndContactsModule {}

View File

@ -0,0 +1,112 @@
import { Injectable } from '@nestjs/common';
import { EntityManager } from 'typeorm';
import compact from 'lodash/compact';
import { Participant } from 'src/modules/messaging/types/gmail-message';
import { getDomainNameFromHandle } from 'src/modules/messaging/utils/get-domain-name-from-handle.util';
import { CreateCompanyService } from 'src/modules/connected-account/auto-companies-and-contacts-creation/create-company/create-company.service';
import { CreateContactService } from 'src/modules/connected-account/auto-companies-and-contacts-creation/create-contact/create-contact.service';
import { PersonService } from 'src/modules/person/repositories/person/person.service';
import { WorkspaceMemberService } from 'src/modules/workspace-member/repositories/workspace-member/workspace-member.service';
import { getUniqueParticipantsAndHandles } from 'src/modules/messaging/utils/get-unique-participants-and-handles.util';
import { filterOutParticipantsFromCompanyOrWorkspace } from 'src/modules/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,
);
}
}

View File

@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { CreateCompanyService } from 'src/modules/connected-account/auto-companies-and-contacts-creation/create-company/create-company.service';
import { CompanyModule } from 'src/modules/messaging/repositories/company/company.module';
@Module({
imports: [WorkspaceDataSourceModule, CompanyModule],
providers: [CreateCompanyService],
exports: [CreateCompanyService],
})
export class CreateCompanyModule {}

View File

@ -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/modules/messaging/repositories/company/company.service';
import { getCompanyNameFromDomainName } from 'src/modules/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: '',
};
}
}
}

View File

@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { CreateContactService } from 'src/modules/connected-account/auto-companies-and-contacts-creation/create-contact/create-contact.service';
import { PersonModule } from 'src/modules/person/repositories/person/person.module';
@Module({
imports: [WorkspaceDataSourceModule, PersonModule],
providers: [CreateContactService],
exports: [CreateContactService],
})
export class CreateContactModule {}

View File

@ -0,0 +1,63 @@
import { Injectable } from '@nestjs/common';
import { EntityManager } from 'typeorm';
import { v4 } from 'uuid';
import { PersonService } from 'src/modules/person/repositories/person/person.service';
import { getFirstNameAndLastNameFromHandleAndDisplayName } from 'src/modules/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,
);
}
}

View File

@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { BlocklistService } from 'src/modules/connected-account/repositories/blocklist/blocklist.service';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
@Module({
imports: [WorkspaceDataSourceModule],
providers: [BlocklistService],
exports: [BlocklistService],
})
export class BlocklistModule {}

View File

@ -0,0 +1,30 @@
import { Injectable } from '@nestjs/common';
import { EntityManager } from 'typeorm';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
import { ObjectRecord } from 'src/engine/workspace-manager/workspace-sync-metadata/types/object-record';
import { BlocklistObjectMetadata } from 'src/modules/connected-account/standard-objects/blocklist.object-metadata';
@Injectable()
export class BlocklistService {
constructor(
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
) {}
public async getByWorkspaceMemberId(
workspaceMemberId: string,
workspaceId: string,
transactionManager?: EntityManager,
): Promise<ObjectRecord<BlocklistObjectMetadata>[]> {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
return await this.workspaceDataSourceService.executeRawQuery(
`SELECT * FROM ${dataSourceSchema}."blocklist" WHERE "workspaceMemberId" = $1`,
[workspaceMemberId],
workspaceId,
transactionManager,
);
}
}

View File

@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { ConnectedAccountService } from 'src/modules/connected-account/repositories/connected-account/connected-account.service';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
@Module({
imports: [WorkspaceDataSourceModule],
providers: [ConnectedAccountService],
exports: [ConnectedAccountService],
})
export class ConnectedAccountModule {}

View File

@ -0,0 +1,153 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { EntityManager } from 'typeorm';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
import { ConnectedAccountObjectMetadata } from 'src/modules/connected-account/standard-objects/connected-account.object-metadata';
import { ObjectRecord } from 'src/engine/workspace-manager/workspace-sync-metadata/types/object-record';
@Injectable()
export class ConnectedAccountService {
constructor(
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
) {}
public async getAll(
workspaceId: string,
transactionManager?: EntityManager,
): Promise<ObjectRecord<ConnectedAccountObjectMetadata>[]> {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
return await this.workspaceDataSourceService.executeRawQuery(
`SELECT * FROM ${dataSourceSchema}."connectedAccount" WHERE "provider" = 'google'`,
[],
workspaceId,
transactionManager,
);
}
public async getByIds(
connectedAccountIds: string[],
workspaceId: string,
transactionManager?: EntityManager,
): Promise<ObjectRecord<ConnectedAccountObjectMetadata>[]> {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
return await this.workspaceDataSourceService.executeRawQuery(
`SELECT * FROM ${dataSourceSchema}."connectedAccount" WHERE "id" = ANY($1)`,
[connectedAccountIds],
workspaceId,
transactionManager,
);
}
public async getById(
connectedAccountId: string,
workspaceId: string,
transactionManager?: EntityManager,
): Promise<ObjectRecord<ConnectedAccountObjectMetadata> | undefined> {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
const connectedAccounts =
await this.workspaceDataSourceService.executeRawQuery(
`SELECT * FROM ${dataSourceSchema}."connectedAccount" WHERE "id" = $1 LIMIT 1`,
[connectedAccountId],
workspaceId,
transactionManager,
);
return connectedAccounts[0];
}
public async getByIdOrFail(
connectedAccountId: string,
workspaceId: string,
transactionManager?: EntityManager,
): Promise<ObjectRecord<ConnectedAccountObjectMetadata>> {
const connectedAccount = await this.getById(
connectedAccountId,
workspaceId,
transactionManager,
);
if (!connectedAccount) {
throw new NotFoundException(
`Connected account with id ${connectedAccountId} not found in workspace ${workspaceId}`,
);
}
return connectedAccount;
}
public async updateLastSyncHistoryId(
historyId: string,
connectedAccountId: string,
workspaceId: string,
transactionManager?: EntityManager,
) {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
await this.workspaceDataSourceService.executeRawQuery(
`UPDATE ${dataSourceSchema}."connectedAccount" SET "lastSyncHistoryId" = $1 WHERE "id" = $2`,
[historyId, connectedAccountId],
workspaceId,
transactionManager,
);
}
public async updateLastSyncHistoryIdIfHigher(
historyId: string,
connectedAccountId: string,
workspaceId: string,
transactionManager?: EntityManager,
) {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
await this.workspaceDataSourceService.executeRawQuery(
`UPDATE ${dataSourceSchema}."connectedAccount" SET "lastSyncHistoryId" = $1
WHERE "id" = $2
AND ("lastSyncHistoryId" < $1 OR "lastSyncHistoryId" = '')`,
[historyId, connectedAccountId],
workspaceId,
transactionManager,
);
}
public async deleteHistoryId(
connectedAccountId: string,
workspaceId: string,
transactionManager?: EntityManager,
) {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
await this.workspaceDataSourceService.executeRawQuery(
`UPDATE ${dataSourceSchema}."connectedAccount" SET "lastSyncHistoryId" = '' WHERE "id" = $1`,
[connectedAccountId],
workspaceId,
transactionManager,
);
}
public async updateAccessToken(
accessToken: string,
connectedAccountId: string,
workspaceId: string,
transactionManager?: EntityManager,
) {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
await this.workspaceDataSourceService.executeRawQuery(
`UPDATE ${dataSourceSchema}."connectedAccount" SET "accessToken" = $1 WHERE "id" = $2`,
[accessToken, connectedAccountId],
workspaceId,
transactionManager,
);
}
}

View File

@ -0,0 +1,65 @@
import { Injectable } from '@nestjs/common';
import axios from 'axios';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { ConnectedAccountService } from 'src/modules/connected-account/repositories/connected-account/connected-account.service';
@Injectable()
export class GoogleAPIsRefreshAccessTokenService {
constructor(
private readonly environmentService: EnvironmentService,
private readonly connectedAccountService: ConnectedAccountService,
) {}
async refreshAndSaveAccessToken(
workspaceId: string,
connectedAccountId: string,
): Promise<void> {
const connectedAccount = await this.connectedAccountService.getById(
connectedAccountId,
workspaceId,
);
if (!connectedAccount) {
throw new Error(
`No connected account found for ${connectedAccountId} in workspace ${workspaceId}`,
);
}
const refreshToken = connectedAccount.refreshToken;
if (!refreshToken) {
throw new Error(
`No refresh token found for connected account ${connectedAccountId} in workspace ${workspaceId}`,
);
}
const accessToken = await this.refreshAccessToken(refreshToken);
await this.connectedAccountService.updateAccessToken(
accessToken,
connectedAccountId,
workspaceId,
);
}
async refreshAccessToken(refreshToken: string): Promise<string> {
const response = await axios.post(
'https://oauth2.googleapis.com/token',
{
client_id: this.environmentService.get('AUTH_GOOGLE_CLIENT_ID'),
client_secret: this.environmentService.get('AUTH_GOOGLE_CLIENT_SECRET'),
refresh_token: refreshToken,
grant_type: 'refresh_token',
},
{
headers: {
'Content-Type': 'application/json',
},
},
);
return response.data.access_token;
}
}

View File

@ -0,0 +1,38 @@
import { FieldMetadataType } from 'src/engine-metadata/field-metadata/field-metadata.entity';
import { blocklistStandardFieldIds } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
import { standardObjectIds } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
import { FieldMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/field-metadata.decorator';
import { IsSystem } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/is-system.decorator';
import { ObjectMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/object-metadata.decorator';
import { BaseObjectMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects/base.object-metadata';
import { WorkspaceMemberObjectMetadata } from 'src/modules/workspace-member/standard-objects/workspace-member.object-metadata';
@ObjectMetadata({
standardId: standardObjectIds.blocklist,
namePlural: 'blocklists',
labelSingular: 'Blocklist',
labelPlural: 'Blocklists',
description: 'Blocklist',
icon: 'IconForbid2',
})
@IsSystem()
export class BlocklistObjectMetadata extends BaseObjectMetadata {
@FieldMetadata({
standardId: blocklistStandardFieldIds.handle,
type: FieldMetadataType.TEXT,
label: 'Handle',
description: 'Handle',
icon: 'IconAt',
})
handle: string;
@FieldMetadata({
standardId: blocklistStandardFieldIds.workspaceMember,
type: FieldMetadataType.RELATION,
label: 'WorkspaceMember',
description: 'WorkspaceMember',
icon: 'IconCircleUser',
joinColumn: 'workspaceMemberId',
})
workspaceMember: WorkspaceMemberObjectMetadata;
}

View File

@ -0,0 +1,114 @@
import { FeatureFlagKeys } from 'src/engine/modules/feature-flag/feature-flag.entity';
import { FieldMetadataType } from 'src/engine-metadata/field-metadata/field-metadata.entity';
import {
RelationMetadataType,
RelationOnDeleteAction,
} from 'src/engine-metadata/relation-metadata/relation-metadata.entity';
import { connectedAccountStandardFieldIds } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
import { standardObjectIds } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
import { FieldMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/field-metadata.decorator';
import { Gate } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/gate.decorator';
import { IsSystem } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/is-system.decorator';
import { ObjectMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/object-metadata.decorator';
import { RelationMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/decorators/relation-metadata.decorator';
import { BaseObjectMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects/base.object-metadata';
import { CalendarChannelObjectMetadata } from 'src/modules/calendar/standard-objects/calendar-channel.object-metadata';
import { MessageChannelObjectMetadata } from 'src/modules/messaging/standard-objects/message-channel.object-metadata';
import { WorkspaceMemberObjectMetadata } from 'src/modules/workspace-member/standard-objects/workspace-member.object-metadata';
@ObjectMetadata({
standardId: standardObjectIds.connectedAccount,
namePlural: 'connectedAccounts',
labelSingular: 'Connected Account',
labelPlural: 'Connected Accounts',
description: 'A connected account',
icon: 'IconAt',
})
@IsSystem()
export class ConnectedAccountObjectMetadata extends BaseObjectMetadata {
@FieldMetadata({
standardId: connectedAccountStandardFieldIds.handle,
type: FieldMetadataType.TEXT,
label: 'handle',
description: 'The account handle (email, username, phone number, etc.)',
icon: 'IconMail',
})
handle: string;
@FieldMetadata({
standardId: connectedAccountStandardFieldIds.provider,
type: FieldMetadataType.TEXT,
label: 'provider',
description: 'The account provider',
icon: 'IconSettings',
})
provider: string;
@FieldMetadata({
standardId: connectedAccountStandardFieldIds.accessToken,
type: FieldMetadataType.TEXT,
label: 'Access Token',
description: 'Messaging provider access token',
icon: 'IconKey',
})
accessToken: string;
@FieldMetadata({
standardId: connectedAccountStandardFieldIds.refreshToken,
type: FieldMetadataType.TEXT,
label: 'Refresh Token',
description: 'Messaging provider refresh token',
icon: 'IconKey',
})
refreshToken: string;
@FieldMetadata({
standardId: connectedAccountStandardFieldIds.accountOwner,
type: FieldMetadataType.RELATION,
label: 'Account Owner',
description: 'Account Owner',
icon: 'IconUserCircle',
joinColumn: 'accountOwnerId',
})
accountOwner: WorkspaceMemberObjectMetadata;
@FieldMetadata({
standardId: connectedAccountStandardFieldIds.lastSyncHistoryId,
type: FieldMetadataType.TEXT,
label: 'Last sync history ID',
description: 'Last sync history ID',
icon: 'IconHistory',
})
lastSyncHistoryId: string;
@FieldMetadata({
standardId: connectedAccountStandardFieldIds.messageChannels,
type: FieldMetadataType.RELATION,
label: 'Message Channel',
description: 'Message Channel',
icon: 'IconMessage',
})
@RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY,
inverseSideTarget: () => MessageChannelObjectMetadata,
onDelete: RelationOnDeleteAction.CASCADE,
})
messageChannels: MessageChannelObjectMetadata[];
@FieldMetadata({
standardId: connectedAccountStandardFieldIds.calendarChannels,
type: FieldMetadataType.RELATION,
label: 'Calendar Channel',
description: 'Calendar Channel',
icon: 'IconCalendar',
})
@RelationMetadata({
type: RelationMetadataType.ONE_TO_MANY,
inverseSideTarget: () => CalendarChannelObjectMetadata,
onDelete: RelationOnDeleteAction.CASCADE,
})
@Gate({
featureFlag: FeatureFlagKeys.IsCalendarEnabled,
})
calendarChannels: CalendarChannelObjectMetadata[];
}