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

@ -0,0 +1,18 @@
import { Module } from '@nestjs/common';
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
import { BlocklistValidationService } from 'src/modules/blocklist/blocklist-validation-manager/services/blocklist-validation.service';
import { BlocklistWorkspaceEntity } from 'src/modules/blocklist/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 BlocklistValidationManagerModule {}

View File

@ -0,0 +1,146 @@
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/blocklist/repositories/blocklist.repository';
import { BlocklistWorkspaceEntity } from 'src/modules/blocklist/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

@ -0,0 +1,29 @@
import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { CreateManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
import {
BlocklistItem,
BlocklistValidationService,
} from 'src/modules/blocklist/blocklist-validation-manager/services/blocklist-validation.service';
@WorkspaceQueryHook(`blocklist.createMany`)
export class BlocklistCreateManyPreQueryHook
implements WorkspaceQueryHookInstance
{
constructor(
private readonly blocklistValidationService: BlocklistValidationService,
) {}
async execute(
userId: string,
workspaceId: string,
payload: CreateManyResolverArgs<BlocklistItem>,
): Promise<void> {
await this.blocklistValidationService.validateBlocklistForCreateMany(
payload,
userId,
workspaceId,
);
}
}

View File

@ -0,0 +1,16 @@
import { Module } from '@nestjs/common';
import { BlocklistValidationManagerModule } from 'src/modules/blocklist/blocklist-validation-manager/blocklist-validation-manager.module';
import { BlocklistCreateManyPreQueryHook } from 'src/modules/blocklist/query-hooks/blocklist-create-many.pre-query.hook';
import { BlocklistUpdateManyPreQueryHook } from 'src/modules/blocklist/query-hooks/blocklist-update-many.pre-query.hook';
import { BlocklistUpdateOnePreQueryHook } from 'src/modules/blocklist/query-hooks/blocklist-update-one.pre-query.hook';
@Module({
imports: [BlocklistValidationManagerModule],
providers: [
BlocklistCreateManyPreQueryHook,
BlocklistUpdateManyPreQueryHook,
BlocklistUpdateOnePreQueryHook,
],
})
export class BlocklistQueryHookModule {}

View File

@ -0,0 +1,16 @@
import { MethodNotAllowedException } from '@nestjs/common';
import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
@WorkspaceQueryHook(`blocklist.updateMany`)
export class BlocklistUpdateManyPreQueryHook
implements WorkspaceQueryHookInstance
{
constructor() {}
async execute(): Promise<void> {
throw new MethodNotAllowedException('Method not allowed.');
}
}

View File

@ -0,0 +1,29 @@
import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { UpdateOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
import {
BlocklistItem,
BlocklistValidationService,
} from 'src/modules/blocklist/blocklist-validation-manager/services/blocklist-validation.service';
@WorkspaceQueryHook(`blocklist.updateOne`)
export class BlocklistUpdateOnePreQueryHook
implements WorkspaceQueryHookInstance
{
constructor(
private readonly blocklistValidationService: BlocklistValidationService,
) {}
async execute(
userId: string,
workspaceId: string,
payload: UpdateOneResolverArgs<BlocklistItem>,
): Promise<void> {
await this.blocklistValidationService.validateBlocklistForUpdateOne(
payload,
userId,
workspaceId,
);
}
}

View File

@ -0,0 +1,69 @@
import { Injectable } from '@nestjs/common';
import { EntityManager } from 'typeorm';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
import { BlocklistWorkspaceEntity } from 'src/modules/blocklist/standard-objects/blocklist.workspace-entity';
@Injectable()
export class BlocklistRepository {
constructor(
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
) {}
public async getById(
id: string,
workspaceId: string,
transactionManager?: EntityManager,
): Promise<BlocklistWorkspaceEntity | null> {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
const blocklistItems =
await this.workspaceDataSourceService.executeRawQuery(
`SELECT * FROM ${dataSourceSchema}."blocklist" WHERE "id" = $1`,
[id],
workspaceId,
transactionManager,
);
if (!blocklistItems || blocklistItems.length === 0) {
return null;
}
return blocklistItems[0];
}
public async getByWorkspaceMemberId(
workspaceMemberId: string,
workspaceId: string,
transactionManager?: EntityManager,
): Promise<BlocklistWorkspaceEntity[]> {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
return await this.workspaceDataSourceService.executeRawQuery(
`SELECT * FROM ${dataSourceSchema}."blocklist" WHERE "workspaceMemberId" = $1`,
[workspaceMemberId],
workspaceId,
transactionManager,
);
}
public async getByWorkspaceMemberIdAndHandle(
workspaceMemberId: string,
handle: string,
workspaceId: string,
transactionManager?: EntityManager,
): Promise<BlocklistWorkspaceEntity[]> {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
return await this.workspaceDataSourceService.executeRawQuery(
`SELECT * FROM ${dataSourceSchema}."blocklist" WHERE "workspaceMemberId" = $1 AND "handle" = $2`,
[workspaceMemberId, handle],
workspaceId,
transactionManager,
);
}
}

View File

@ -0,0 +1,49 @@
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { BLOCKLIST_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 { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
@WorkspaceEntity({
standardId: STANDARD_OBJECT_IDS.blocklist,
namePlural: 'blocklists',
labelSingular: 'Blocklist',
labelPlural: 'Blocklists',
description: 'Blocklist',
icon: 'IconForbid2',
})
@WorkspaceIsSystem()
@WorkspaceIsNotAuditLogged()
export class BlocklistWorkspaceEntity extends BaseWorkspaceEntity {
@WorkspaceField({
standardId: BLOCKLIST_STANDARD_FIELD_IDS.handle,
type: FieldMetadataType.TEXT,
label: 'Handle',
description: 'Handle',
icon: 'IconAt',
})
handle: string;
@WorkspaceRelation({
standardId: BLOCKLIST_STANDARD_FIELD_IDS.workspaceMember,
type: RelationMetadataType.MANY_TO_ONE,
label: 'WorkspaceMember',
description: 'WorkspaceMember',
icon: 'IconCircleUser',
inverseSideTarget: () => WorkspaceMemberWorkspaceEntity,
inverseSideFieldKey: 'blocklist',
})
workspaceMember: Relation<WorkspaceMemberWorkspaceEntity>;
@WorkspaceJoinColumn('workspaceMember')
workspaceMemberId: string;
}