Refactor connected account module (#6225)

- Refactor connected account module
- Move blocklist into it's own module
- Move contact-creation-manager into it's own module

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
bosiraphael
2024-07-12 20:15:33 +02:00
committed by GitHub
parent c8a889995f
commit 11da718482
53 changed files with 212 additions and 192 deletions

View File

@ -1,18 +0,0 @@
import { Module } from '@nestjs/common';
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
import { BlocklistValidationService } from 'src/modules/connected-account/services/blocklist/blocklist-validation.service';
import { BlocklistWorkspaceEntity } from 'src/modules/connected-account/standard-objects/blocklist.workspace-entity';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@Module({
imports: [
ObjectMetadataRepositoryModule.forFeature([
BlocklistWorkspaceEntity,
WorkspaceMemberWorkspaceEntity,
]),
],
providers: [BlocklistValidationService],
exports: [BlocklistValidationService],
})
export class BlocklistValidationModule {}

View File

@ -1,146 +0,0 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { z } from 'zod';
import {
CreateManyResolverArgs,
UpdateOneResolverArgs,
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
import { isDomain } from 'src/engine/utils/is-domain';
import { BlocklistRepository } from 'src/modules/connected-account/repositories/blocklist.repository';
import { BlocklistWorkspaceEntity } from 'src/modules/connected-account/standard-objects/blocklist.workspace-entity';
import { WorkspaceMemberRepository } from 'src/modules/workspace-member/repositories/workspace-member.repository';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
export type BlocklistItem = Omit<
BlocklistWorkspaceEntity,
'createdAt' | 'updatedAt' | 'workspaceMember'
> & {
createdAt: string;
updatedAt: string;
workspaceMemberId: string;
};
@Injectable()
export class BlocklistValidationService {
constructor(
@InjectObjectMetadataRepository(BlocklistWorkspaceEntity)
private readonly blocklistRepository: BlocklistRepository,
@InjectObjectMetadataRepository(WorkspaceMemberWorkspaceEntity)
private readonly workspaceMemberRepository: WorkspaceMemberRepository,
) {}
public async validateBlocklistForCreateMany(
payload: CreateManyResolverArgs<BlocklistItem>,
userId: string,
workspaceId: string,
) {
await this.validateSchema(payload.data);
await this.validateUniquenessForCreateMany(payload, userId, workspaceId);
}
public async validateBlocklistForUpdateOne(
payload: UpdateOneResolverArgs<BlocklistItem>,
userId: string,
workspaceId: string,
) {
if (payload.data.handle) {
await this.validateSchema([payload.data]);
}
await this.validateUniquenessForUpdateOne(payload, userId, workspaceId);
}
public async validateSchema(blocklist: BlocklistItem[]) {
const emailOrDomainSchema = z
.string()
.trim()
.email('Invalid email or domain')
.or(
z
.string()
.refine(
(value) => value.startsWith('@') && isDomain(value.slice(1)),
'Invalid email or domain',
),
);
for (const handle of blocklist.map((item) => item.handle)) {
if (!handle) {
throw new BadRequestException('Blocklist handle is required');
}
const result = emailOrDomainSchema.safeParse(handle);
if (!result.success) {
throw new BadRequestException(result.error.errors[0].message);
}
}
}
public async validateUniquenessForCreateMany(
payload: CreateManyResolverArgs<BlocklistItem>,
userId: string,
workspaceId: string,
) {
const currentWorkspaceMember =
await this.workspaceMemberRepository.getByIdOrFail(userId, workspaceId);
const currentBlocklist =
await this.blocklistRepository.getByWorkspaceMemberId(
currentWorkspaceMember.id,
workspaceId,
);
const currentBlocklistHandles = currentBlocklist.map(
(blocklist) => blocklist.handle,
);
if (
payload.data.some((item) => currentBlocklistHandles.includes(item.handle))
) {
throw new BadRequestException('Blocklist handle already exists');
}
}
public async validateUniquenessForUpdateOne(
payload: UpdateOneResolverArgs<BlocklistItem>,
userId: string,
workspaceId: string,
) {
const existingRecord = await this.blocklistRepository.getById(
payload.id,
workspaceId,
);
if (!existingRecord) {
throw new BadRequestException('Blocklist item not found');
}
if (existingRecord.workspaceMemberId !== payload.data.workspaceMemberId) {
throw new BadRequestException('Workspace member cannot be updated');
}
if (existingRecord.handle === payload.data.handle) {
return;
}
const currentWorkspaceMember =
await this.workspaceMemberRepository.getByIdOrFail(userId, workspaceId);
const currentBlocklist =
await this.blocklistRepository.getByWorkspaceMemberId(
currentWorkspaceMember.id,
workspaceId,
);
const currentBlocklistHandles = currentBlocklist
.filter((blocklist) => blocklist.id !== payload.id)
.map((blocklist) => blocklist.handle);
if (currentBlocklistHandles.includes(payload.data.handle)) {
throw new BadRequestException('Blocklist handle already exists');
}
}
}

View File

@ -1,18 +0,0 @@
import { Module } from '@nestjs/common';
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
import { GoogleAPIRefreshAccessTokenService } from 'src/modules/connected-account/services/google-api-refresh-access-token/google-api-refresh-access-token.service';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { MessagingCommonModule } from 'src/modules/messaging/common/messaging-common.module';
@Module({
imports: [
ObjectMetadataRepositoryModule.forFeature([
ConnectedAccountWorkspaceEntity,
]),
MessagingCommonModule,
],
providers: [GoogleAPIRefreshAccessTokenService],
exports: [GoogleAPIRefreshAccessTokenService],
})
export class GoogleAPIRefreshAccessTokenModule {}

View File

@ -1,64 +0,0 @@
import { Injectable } from '@nestjs/common';
import axios from 'axios';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
@Injectable()
export class GoogleAPIRefreshAccessTokenService {
constructor(
private readonly environmentService: EnvironmentService,
@InjectObjectMetadataRepository(ConnectedAccountWorkspaceEntity)
private readonly connectedAccountRepository: ConnectedAccountRepository,
) {}
async refreshAndSaveAccessToken(
connectedAccount: ConnectedAccountWorkspaceEntity,
workspaceId: string,
): Promise<string> {
const refreshToken = connectedAccount.refreshToken;
if (!refreshToken) {
throw new Error(
`No refresh token found for connected account ${connectedAccount.id} in workspace ${workspaceId}`,
);
}
const accessToken = await this.refreshAccessToken(refreshToken);
await this.connectedAccountRepository.updateAccessToken(
accessToken,
connectedAccount.id,
workspaceId,
);
await this.connectedAccountRepository.updateAccessToken(
accessToken,
connectedAccount.id,
workspaceId,
);
return accessToken;
}
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;
}
}