diff --git a/packages/twenty-server/src/app.module.ts b/packages/twenty-server/src/app.module.ts index 849d8da49e6..94f49fa7ea6 100644 --- a/packages/twenty-server/src/app.module.ts +++ b/packages/twenty-server/src/app.module.ts @@ -27,6 +27,7 @@ import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/work import { GraphQLHydrateRequestFromTokenMiddleware } from 'src/engine/middlewares/graphql-hydrate-request-from-token.middleware'; import { MiddlewareModule } from 'src/engine/middlewares/middleware.module'; import { RestCoreMiddleware } from 'src/engine/middlewares/rest-core.middleware'; +import { GlobalWorkspaceDataSourceModule } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource.module'; import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; import { ModulesModule } from 'src/modules/modules.module'; @@ -57,6 +58,7 @@ const MIGRATED_REST_METHODS = [ useClass: GraphQLConfigService, }), TwentyORMModule, + GlobalWorkspaceDataSourceModule, ClickHouseModule, // Core engine module, contains all the core modules CoreEngineModule, diff --git a/packages/twenty-server/src/database/commands/command-runners/__tests__/upgrade.command-runner.spec.ts b/packages/twenty-server/src/database/commands/command-runners/__tests__/upgrade.command-runner.spec.ts index cdd068e9c71..0381ad9334d 100644 --- a/packages/twenty-server/src/database/commands/command-runners/__tests__/upgrade.command-runner.spec.ts +++ b/packages/twenty-server/src/database/commands/command-runners/__tests__/upgrade.command-runner.spec.ts @@ -12,7 +12,7 @@ import { type ConfigVariables } from 'src/engine/core-modules/twenty-config/conf import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command'; class BasicUpgradeCommandRunner extends UpgradeCommandRunner { @@ -84,14 +84,14 @@ const buildUpgradeCommandModule = async ({ useFactory: ( workspaceRepository: Repository, twentyConfigService: TwentyConfigService, - twentyORMGlobalManager: TwentyORMGlobalManager, + globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, dataSourceService: DataSourceService, syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand, ) => { return new commandRunner( workspaceRepository, twentyConfigService, - twentyORMGlobalManager, + globalWorkspaceOrmManager, dataSourceService, syncWorkspaceMetadataCommand, ); @@ -99,7 +99,7 @@ const buildUpgradeCommandModule = async ({ inject: [ getRepositoryToken(WorkspaceEntity), TwentyConfigService, - TwentyORMGlobalManager, + GlobalWorkspaceOrmManager, DataSourceService, SyncWorkspaceMetadataCommand, ], @@ -132,11 +132,14 @@ const buildUpgradeCommandModule = async ({ }, }, { - provide: TwentyORMGlobalManager, + provide: GlobalWorkspaceOrmManager, useValue: { connect: jest.fn(), destroyDataSourceForWorkspace: jest.fn(), getDataSourceForWorkspace: jest.fn(), + executeInWorkspaceContext: jest + .fn() + .mockImplementation((_authContext: any, fn: () => any) => fn()), }, }, { @@ -162,7 +165,7 @@ describe('UpgradeCommandRunner', () => { let runAfterSyncMetadataSpy: jest.SpyInstance; let runBeforeSyncMetadataSpy: jest.SpyInstance; let runCoreMigrationsSpy: jest.SpyInstance; - let twentyORMGlobalManagerSpy: TwentyORMGlobalManager; + let globalWorkspaceOrmManagerSpy: GlobalWorkspaceOrmManager; type BuildModuleAndSetupSpiesArgs = { numberOfWorkspace?: number; @@ -215,8 +218,8 @@ describe('UpgradeCommandRunner', () => { getRepositoryToken(WorkspaceEntity), ); syncWorkspaceMetadataCommand = module.get(SyncWorkspaceMetadataCommand); - twentyORMGlobalManagerSpy = module.get( - TwentyORMGlobalManager, + globalWorkspaceOrmManagerSpy = module.get( + GlobalWorkspaceOrmManager, ); }; @@ -246,7 +249,7 @@ describe('UpgradeCommandRunner', () => { expect(failReport.length).toBe(0); [ - twentyORMGlobalManagerSpy.destroyDataSourceForWorkspace, + globalWorkspaceOrmManagerSpy.destroyDataSourceForWorkspace, upgradeCommandRunner.runOnWorkspace, ].forEach((fn) => expect(fn).toHaveBeenCalledTimes(1)); @@ -278,7 +281,7 @@ describe('UpgradeCommandRunner', () => { upgradeCommandRunner.runBeforeSyncMetadata, upgradeCommandRunner.runAfterSyncMetadata, syncWorkspaceMetadataCommand.runOnWorkspace, - twentyORMGlobalManagerSpy.destroyDataSourceForWorkspace, + globalWorkspaceOrmManagerSpy.destroyDataSourceForWorkspace, ].forEach((fn) => expect(fn).toHaveBeenCalledTimes(numberOfWorkspace)); expect(workspaceRepository.update).toHaveBeenNthCalledWith( numberOfWorkspace, @@ -303,7 +306,7 @@ describe('UpgradeCommandRunner', () => { upgradeCommandRunner.runBeforeSyncMetadata, upgradeCommandRunner.runAfterSyncMetadata, syncWorkspaceMetadataCommand.runOnWorkspace, - twentyORMGlobalManagerSpy.destroyDataSourceForWorkspace, + globalWorkspaceOrmManagerSpy.destroyDataSourceForWorkspace, ].forEach((fn) => expect(fn).toHaveBeenCalledTimes(1)); // Verify order of execution diff --git a/packages/twenty-server/src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner.ts b/packages/twenty-server/src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner.ts index c50cde3bb92..9056c5959cc 100644 --- a/packages/twenty-server/src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner.ts +++ b/packages/twenty-server/src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner.ts @@ -7,7 +7,7 @@ import { } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { type DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { type TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { type GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; export type ActiveOrSuspendedWorkspacesMigrationCommandOptions = WorkspacesMigrationCommandOptions; @@ -18,10 +18,10 @@ export abstract class ActiveOrSuspendedWorkspacesMigrationCommandRunner< > extends WorkspacesMigrationCommandRunner { constructor( protected readonly workspaceRepository: Repository, - protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, protected readonly dataSourceService: DataSourceService, ) { - super(workspaceRepository, twentyORMGlobalManager, dataSourceService, [ + super(workspaceRepository, globalWorkspaceOrmManager, dataSourceService, [ WorkspaceActivationStatus.ACTIVE, WorkspaceActivationStatus.SUSPENDED, ]); diff --git a/packages/twenty-server/src/database/commands/command-runners/upgrade.command-runner.ts b/packages/twenty-server/src/database/commands/command-runners/upgrade.command-runner.ts index d7560c786ff..11c4fcc90a7 100644 --- a/packages/twenty-server/src/database/commands/command-runners/upgrade.command-runner.ts +++ b/packages/twenty-server/src/database/commands/command-runners/upgrade.command-runner.ts @@ -20,7 +20,7 @@ import { import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { type DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command'; import { type CompareVersionMajorAndMinorReturnType, @@ -52,11 +52,11 @@ export abstract class UpgradeCommandRunner extends ActiveOrSuspendedWorkspacesMi @InjectRepository(WorkspaceEntity) protected readonly workspaceRepository: Repository, protected readonly twentyConfigService: TwentyConfigService, - protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, protected readonly dataSourceService: DataSourceService, protected readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand, ) { - super(workspaceRepository, twentyORMGlobalManager, dataSourceService); + super(workspaceRepository, globalWorkspaceOrmManager, dataSourceService); } private async loadActiveOrSuspendedWorkspace() { diff --git a/packages/twenty-server/src/database/commands/command-runners/workspaces-migration.command-runner.ts b/packages/twenty-server/src/database/commands/command-runners/workspaces-migration.command-runner.ts index f84a1d38ee7..2749619e1a2 100644 --- a/packages/twenty-server/src/database/commands/command-runners/workspaces-migration.command-runner.ts +++ b/packages/twenty-server/src/database/commands/command-runners/workspaces-migration.command-runner.ts @@ -1,14 +1,16 @@ import chalk from 'chalk'; import { Option } from 'nest-commander'; +import { isDefined } from 'twenty-shared/utils'; import { WorkspaceActivationStatus } from 'twenty-shared/workspace'; import { In, MoreThanOrEqual, type Repository } from 'typeorm'; -import { isDefined } from 'twenty-shared/utils'; + +import { type WorkspaceDataSourceInterface } from 'src/engine/twenty-orm/interfaces/workspace-datasource.interface'; import { MigrationCommandRunner } from 'src/database/commands/command-runners/migration.command-runner'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { type WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; -import { type TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { type GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; export type WorkspacesMigrationCommandOptions = { workspaceIds: string[]; @@ -21,7 +23,7 @@ export type WorkspacesMigrationCommandOptions = { export type RunOnWorkspaceArgs = { options: WorkspacesMigrationCommandOptions; workspaceId: string; - dataSource?: WorkspaceDataSource; + dataSource?: WorkspaceDataSourceInterface; index: number; total: number; }; @@ -50,7 +52,7 @@ export abstract class WorkspacesMigrationCommandRunner< constructor( protected readonly workspaceRepository: Repository, - protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, protected readonly dataSourceService: DataSourceService, protected readonly activationStatuses: WorkspaceActivationStatus[], ) { @@ -138,24 +140,32 @@ export abstract class WorkspacesMigrationCommandRunner< ); try { - const workspaceHasDataSource = - await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceId( - workspaceId, - ); + const authContext = buildSystemAuthContext(workspaceId); - const dataSource = isDefined(workspaceHasDataSource) - ? await this.twentyORMGlobalManager.getDataSourceForWorkspace({ + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workspaceHasDataSource = + await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceId( + workspaceId, + ); + + const dataSource = isDefined(workspaceHasDataSource) + ? await this.globalWorkspaceOrmManager.getDataSourceForWorkspace( + workspaceId, + ) + : undefined; + + await this.runOnWorkspace({ + options, workspaceId, - }) - : undefined; + dataSource, + index: index, + total: workspaceIdsToProcess.length, + }); + }, + ); - await this.runOnWorkspace({ - options, - workspaceId, - dataSource, - index: index, - total: workspaceIdsToProcess.length, - }); this.migrationReport.success.push({ workspaceId, }); @@ -170,7 +180,7 @@ export abstract class WorkspacesMigrationCommandRunner< } try { - await this.twentyORMGlobalManager.destroyDataSourceForWorkspace( + await this.globalWorkspaceOrmManager.destroyDataSourceForWorkspace( workspaceId, ); } catch (error) { diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-add-workflow-run-stop-statuses.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-add-workflow-run-stop-statuses.command.ts index c273c6760e1..e9958e8d921 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-add-workflow-run-stop-statuses.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-add-workflow-run-stop-statuses.command.ts @@ -10,7 +10,7 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { FieldMetadataComplexOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util'; import { WORKFLOW_RUN_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; import { WorkflowRunStatus } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity'; @@ -23,14 +23,14 @@ export class AddWorkflowRunStopStatusesCommand extends ActiveOrSuspendedWorkspac constructor( @InjectRepository(WorkspaceEntity) protected readonly workspaceRepository: Repository, - protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, protected readonly dataSourceService: DataSourceService, @InjectRepository(FieldMetadataEntity) private readonly fieldMetadataRepository: Repository, @InjectDataSource() private readonly coreDataSource: DataSource, ) { - super(workspaceRepository, twentyORMGlobalManager, dataSourceService); + super(workspaceRepository, globalWorkspaceOrmManager, dataSourceService); } override async runOnWorkspace({ diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-clean-orphaned-kanban-aggregate-operation-field-metadata-id.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-clean-orphaned-kanban-aggregate-operation-field-metadata-id.command.ts index b5d82bf2ada..e51afb6b998 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-clean-orphaned-kanban-aggregate-operation-field-metadata-id.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-clean-orphaned-kanban-aggregate-operation-field-metadata-id.command.ts @@ -11,7 +11,7 @@ import { DataSourceService } from 'src/engine/metadata-modules/data-source/data- import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; import { ViewType } from 'src/engine/metadata-modules/view/enums/view-type.enum'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; @Command({ name: 'upgrade:1-10:clean-orphaned-kanban-aggregate-operation-field-metadata-id', @@ -22,7 +22,7 @@ export class CleanOrphanedKanbanAggregateOperationFieldMetadataIdCommand extends constructor( @InjectRepository(WorkspaceEntity) protected readonly workspaceRepository: Repository, - protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, protected readonly dataSourceService: DataSourceService, @InjectRepository(ViewEntity) private readonly viewRepository: Repository, diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-create-view-kanban-field-metadata-id-foreign-key-migration.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-create-view-kanban-field-metadata-id-foreign-key-migration.command.ts index 5b38b1ec74b..0a996c1a58a 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-create-view-kanban-field-metadata-id-foreign-key-migration.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-create-view-kanban-field-metadata-id-foreign-key-migration.command.ts @@ -7,7 +7,7 @@ import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/ import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; @Command({ name: '1-10-create-view-kanban-field-metadata-id-foreign-key-migration', @@ -20,7 +20,7 @@ export class CreateViewKanbanFieldMetadataIdForeignKeyMigrationCommand extends A constructor( @InjectRepository(WorkspaceEntity) protected readonly workspaceRepository: Repository, - protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, protected readonly dataSourceService: DataSourceService, @InjectDataSource() private readonly coreDataSource: DataSource, diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-flush-cache.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-flush-cache.command.ts index 4479ec67e95..f9c80afb074 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-flush-cache.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-flush-cache.command.ts @@ -7,7 +7,7 @@ import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/ import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; @Command({ @@ -18,7 +18,7 @@ export class FlushCacheCommand extends ActiveOrSuspendedWorkspacesMigrationComma constructor( @InjectRepository(WorkspaceEntity) protected readonly workspaceRepository: Repository, - protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, protected readonly dataSourceService: DataSourceService, protected readonly workspaceCacheStorageService: WorkspaceCacheStorageService, ) { diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-make-sure-dashboard-naming-available.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-make-sure-dashboard-naming-available.command.ts index e328f1318c1..9ac9b7c8195 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-make-sure-dashboard-naming-available.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-make-sure-dashboard-naming-available.command.ts @@ -10,7 +10,7 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; @Command({ name: 'upgrade:1-10:make-sure-dashboard-naming-available', @@ -20,7 +20,7 @@ export class MakeSureDashboardNamingAvailableCommand extends ActiveOrSuspendedWo constructor( @InjectRepository(WorkspaceEntity) protected readonly workspaceRepository: Repository, - protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, protected readonly dataSourceService: DataSourceService, @InjectRepository(ObjectMetadataEntity) private readonly objectMetadataRepository: Repository, diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-author-to-created-by.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-author-to-created-by.command.ts index e53191f4f01..d406a65bcd0 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-author-to-created-by.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-author-to-created-by.command.ts @@ -9,7 +9,7 @@ import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/ import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { type AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; @@ -22,7 +22,7 @@ export class MigrateAttachmentAuthorToCreatedByCommand extends ActiveOrSuspended constructor( @InjectRepository(WorkspaceEntity) protected readonly workspaceRepository: Repository, - protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, protected readonly dataSourceService: DataSourceService, ) { super(workspaceRepository, twentyORMGlobalManager, dataSourceService); @@ -36,13 +36,13 @@ export class MigrateAttachmentAuthorToCreatedByCommand extends ActiveOrSuspended ); const attachmentRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( + await this.twentyORMGlobalManager.getRepository( workspaceId, 'attachment', ); const workspaceMemberRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( + await this.twentyORMGlobalManager.getRepository( workspaceId, 'workspaceMember', ); diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-type-to-file-category.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-type-to-file-category.command.ts index c95658a3281..1274775c356 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-type-to-file-category.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-type-to-file-category.command.ts @@ -8,7 +8,7 @@ import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/ import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { type AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity'; const TYPE_TO_FILE_CATEGORY_MAPPING: Record = { @@ -31,7 +31,7 @@ export class MigrateAttachmentTypeToFileCategoryCommand extends ActiveOrSuspende constructor( @InjectRepository(WorkspaceEntity) protected readonly workspaceRepository: Repository, - protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, protected readonly dataSourceService: DataSourceService, ) { super(workspaceRepository, twentyORMGlobalManager, dataSourceService); @@ -45,7 +45,7 @@ export class MigrateAttachmentTypeToFileCategoryCommand extends ActiveOrSuspende ); const attachmentRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( + await this.twentyORMGlobalManager.getRepository( workspaceId, 'attachment', ); diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-migrate-channel-partial-full-sync-stages.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-migrate-channel-partial-full-sync-stages.command.ts index 44d8d33f0e6..2525360bae0 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-migrate-channel-partial-full-sync-stages.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-migrate-channel-partial-full-sync-stages.command.ts @@ -7,7 +7,7 @@ import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/ import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util'; @Command({ @@ -19,7 +19,7 @@ export class MigrateChannelPartialFullSyncStagesCommand extends ActiveOrSuspende constructor( @InjectRepository(WorkspaceEntity) protected readonly workspaceRepository: Repository, - protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, protected readonly dataSourceService: DataSourceService, @InjectDataSource() private readonly coreDataSource: DataSource, diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-regenerate-search-vectors.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-regenerate-search-vectors.command.ts index bd4b8e88dc2..d698b6adba9 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-regenerate-search-vectors.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-regenerate-search-vectors.command.ts @@ -12,7 +12,7 @@ import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants'; import { SEARCH_FIELDS_FOR_CUSTOM_OBJECT } from 'src/engine/twenty-orm/custom.workspace-entity'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { WorkspaceSchemaManagerService } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.service'; import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util'; @@ -40,7 +40,7 @@ export class RegenerateSearchVectorsCommand extends ActiveOrSuspendedWorkspacesM constructor( @InjectRepository(WorkspaceEntity) protected readonly workspaceRepository: Repository, - protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, protected readonly dataSourceService: DataSourceService, private readonly workspaceSchemaManager: WorkspaceSchemaManagerService, @InjectRepository(ObjectMetadataEntity) diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-seed-dashboard-view.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-seed-dashboard-view.command.ts index 359e9c30872..55fe7951ae8 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-seed-dashboard-view.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-seed-dashboard-view.command.ts @@ -15,7 +15,7 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; import { ViewKey } from 'src/engine/metadata-modules/view/enums/view-key.enum'; import { WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { createCoreViews } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-core-views'; import { prefillWorkspaceFavorites } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-workspace-favorites'; import { dashboardsAllView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/dashboards-all.view'; @@ -30,7 +30,7 @@ export class SeedDashboardViewCommand extends ActiveOrSuspendedWorkspacesMigrati private readonly coreDataSource: DataSource, @InjectRepository(WorkspaceEntity) protected readonly workspaceRepository: Repository, - protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, protected readonly dataSourceService: DataSourceService, @InjectRepository(ObjectMetadataEntity) private readonly objectMetadataRepository: Repository, diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-11/1-11-clean-orphaned-role-targets.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-11/1-11-clean-orphaned-role-targets.command.ts index 7e74a7e686e..7e9cb524593 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-11/1-11-clean-orphaned-role-targets.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-11/1-11-clean-orphaned-role-targets.command.ts @@ -9,7 +9,7 @@ import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; @Command({ name: 'upgrade:1-11:clean-orphaned-role-targets', @@ -23,7 +23,7 @@ export class CleanOrphanedRoleTargetsCommand extends ActiveOrSuspendedWorkspaces private readonly roleTargetRepository: Repository, @InjectRepository(WorkspaceEntity) protected readonly workspaceRepository: Repository, - protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, protected readonly dataSourceService: DataSourceService, ) { super(workspaceRepository, twentyORMGlobalManager, dataSourceService); diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-11/1-11-clean-orphaned-user-workspaces.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-11/1-11-clean-orphaned-user-workspaces.command.ts index 176d36702ba..d634c1e95de 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-11/1-11-clean-orphaned-user-workspaces.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-11/1-11-clean-orphaned-user-workspaces.command.ts @@ -8,7 +8,7 @@ import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/worksp import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; @Command({ @@ -20,7 +20,7 @@ export class CleanOrphanedUserWorkspacesCommand extends ActiveOrSuspendedWorkspa constructor( @InjectRepository(WorkspaceEntity) protected readonly workspaceRepository: Repository, - protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, protected readonly dataSourceService: DataSourceService, @InjectRepository(UserWorkspaceEntity) private readonly userWorkspaceRepository: Repository, @@ -49,7 +49,7 @@ export class CleanOrphanedUserWorkspacesCommand extends ActiveOrSuspendedWorkspa } const workspaceMemberRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( + await this.twentyORMGlobalManager.getRepository( workspaceId, 'workspaceMember', { shouldBypassPermissionChecks: true }, diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-11/1-11-create-twenty-standard-application.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-11/1-11-create-twenty-standard-application.command.ts index 2c464eb6131..d131e0d225c 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-11/1-11-create-twenty-standard-application.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-11/1-11-create-twenty-standard-application.command.ts @@ -9,7 +9,7 @@ import { ApplicationEntity } from 'src/engine/core-modules/application/applicati import { ApplicationService } from 'src/engine/core-modules/application/application.service'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications'; @Command({ @@ -21,7 +21,7 @@ export class CreateTwentyStandardApplicationCommand extends ActiveOrSuspendedWor constructor( @InjectRepository(WorkspaceEntity) protected readonly workspaceRepository: Repository, - protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, protected readonly dataSourceService: DataSourceService, @InjectRepository(ApplicationEntity) private readonly applicationRepository: Repository, diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-12/1-12-add-calendar-events-import-scheduled-sync-stage.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-12/1-12-add-calendar-events-import-scheduled-sync-stage.command.ts index 77ace80d295..eab276ca752 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-12/1-12-add-calendar-events-import-scheduled-sync-stage.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-12/1-12-add-calendar-events-import-scheduled-sync-stage.command.ts @@ -10,7 +10,7 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util'; import { CALENDAR_CHANNEL_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; import { CalendarChannelSyncStage } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; @@ -24,7 +24,7 @@ export class AddCalendarEventsImportScheduledSyncStageCommand extends ActiveOrSu constructor( @InjectRepository(WorkspaceEntity) protected readonly workspaceRepository: Repository, - protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, protected readonly dataSourceService: DataSourceService, @InjectRepository(ObjectMetadataEntity) private readonly objectMetadataRepository: Repository, diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-12/1-12-add-messages-import-scheduled-sync-stage.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-12/1-12-add-messages-import-scheduled-sync-stage.command.ts index a87827d701e..88fad8a1263 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-12/1-12-add-messages-import-scheduled-sync-stage.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-12/1-12-add-messages-import-scheduled-sync-stage.command.ts @@ -10,7 +10,7 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util'; import { MESSAGE_CHANNEL_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; import { MessageChannelSyncStage } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; @@ -24,7 +24,7 @@ export class AddMessagesImportScheduledSyncStageCommand extends ActiveOrSuspende constructor( @InjectRepository(WorkspaceEntity) protected readonly workspaceRepository: Repository, - protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, protected readonly dataSourceService: DataSourceService, @InjectRepository(ObjectMetadataEntity) private readonly objectMetadataRepository: Repository, diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-12/1-12-clean-null-equivalent-values.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-12/1-12-clean-null-equivalent-values.ts index ad80618a8be..a060d7b2f4c 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-12/1-12-clean-null-equivalent-values.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-12/1-12-clean-null-equivalent-values.ts @@ -31,7 +31,7 @@ import { WorkspaceMigrationTableActionType, } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util'; import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service'; @@ -46,7 +46,7 @@ export class CleanNullEquivalentValuesCommand extends ActiveOrSuspendedWorkspace constructor( @InjectRepository(WorkspaceEntity) protected readonly workspaceRepository: Repository, - protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, @InjectRepository(ObjectMetadataEntity) protected readonly objectMetadataRepository: Repository, @InjectRepository(FieldMetadataEntity) diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-12/1-12-create-workspace-custom-application.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-12/1-12-create-workspace-custom-application.command.ts index e1afb936d2f..55ed9a4a500 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-12/1-12-create-workspace-custom-application.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-12/1-12-create-workspace-custom-application.command.ts @@ -12,7 +12,7 @@ import { import { ApplicationService } from 'src/engine/core-modules/application/application.service'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; @Command({ name: 'upgrade:1-12:create-workspace-custom-application', @@ -23,7 +23,7 @@ export class CreateWorkspaceCustomApplicationCommand extends WorkspacesMigration constructor( @InjectRepository(WorkspaceEntity) protected readonly workspaceRepository: Repository, - protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, protected readonly dataSourceService: DataSourceService, private readonly applicationService: ApplicationService, ) { diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-12/1-12-set-standard-application-not-uninstallable.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-12/1-12-set-standard-application-not-uninstallable.command.ts index 04efd248f72..917633fcc7e 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-12/1-12-set-standard-application-not-uninstallable.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-12/1-12-set-standard-application-not-uninstallable.command.ts @@ -8,7 +8,7 @@ import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/worksp import { ApplicationService } from 'src/engine/core-modules/application/application.service'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; // Note: this command needs to be run after CreateWorkspaceCustomApplicationCommand @Command({ @@ -19,7 +19,7 @@ export class SetStandardApplicationNotUninstallableCommand extends ActiveOrSuspe constructor( @InjectRepository(WorkspaceEntity) protected readonly workspaceRepository: Repository, - protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, protected readonly dataSourceService: DataSourceService, private readonly applicationService: ApplicationService, ) { diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-12/1-12-workspace-custom-application-id-non-nullable-migration.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-12/1-12-workspace-custom-application-id-non-nullable-migration.command.ts index fce70e80f80..c2374919843 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-12/1-12-workspace-custom-application-id-non-nullable-migration.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-12/1-12-workspace-custom-application-id-non-nullable-migration.command.ts @@ -7,7 +7,7 @@ import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/ import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; @Command({ name: '1-12-workspace-custom-application-id-non-nullable-migration', @@ -19,7 +19,7 @@ export class WorkspaceCustomApplicationIdNonNullableCommand extends ActiveOrSusp constructor( @InjectRepository(WorkspaceEntity) protected readonly workspaceRepository: Repository, - protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, protected readonly dataSourceService: DataSourceService, @InjectDataSource() private readonly coreDataSource: DataSource, diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-backfill-page-layout-universal-identifiers.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-backfill-page-layout-universal-identifiers.command.ts index 1b5b865d189..2e51c96bcc5 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-backfill-page-layout-universal-identifiers.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-backfill-page-layout-universal-identifiers.command.ts @@ -12,7 +12,7 @@ import { DataSourceService } from 'src/engine/metadata-modules/data-source/data- import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout-tab.entity'; import { PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout-widget.entity'; import { PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; @Command({ @@ -30,7 +30,7 @@ export class BackfillPageLayoutUniversalIdentifiersCommand extends ActiveOrSuspe private readonly pageLayoutTabRepository: Repository, @InjectRepository(PageLayoutEntity) private readonly pageLayoutRepository: Repository, - protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, protected readonly dataSourceService: DataSourceService, private readonly applicationService: ApplicationService, private readonly workspaceCacheService: WorkspaceCacheService, diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-backfill-view-main-group-by-field-metadata-id.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-backfill-view-main-group-by-field-metadata-id.command.ts index ee3c7a6ade6..dd5caa28fcb 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-backfill-view-main-group-by-field-metadata-id.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-backfill-view-main-group-by-field-metadata-id.command.ts @@ -11,7 +11,7 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity'; import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; @Command({ name: 'upgrade:1-13:backfill:view-main-group-by-field-metadata-id', @@ -26,7 +26,7 @@ export class BackfillViewMainGroupByFieldMetadataIdCommand extends ActiveOrSuspe private readonly viewGroupRepository: Repository, @InjectRepository(WorkspaceEntity) protected readonly workspaceRepository: Repository, - protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, protected readonly dataSourceService: DataSourceService, ) { super(workspaceRepository, twentyORMGlobalManager, dataSourceService); diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-clean-empty-string-null-in-text-fields.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-clean-empty-string-null-in-text-fields.command.ts index 4455926a695..5f153365b91 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-clean-empty-string-null-in-text-fields.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-clean-empty-string-null-in-text-fields.command.ts @@ -6,14 +6,15 @@ import { FieldMetadataType } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { Repository } from 'typeorm'; +import { type WorkspaceDataSourceInterface } from 'src/engine/twenty-orm/interfaces/workspace-datasource.interface'; + import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util'; @@ -30,14 +31,14 @@ export class CleanEmptyStringNullInTextFieldsCommand extends ActiveOrSuspendedWo constructor( @InjectRepository(WorkspaceEntity) protected readonly workspaceRepository: Repository, - protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, @InjectRepository(ObjectMetadataEntity) protected readonly objectMetadataRepository: Repository, @InjectRepository(FieldMetadataEntity) protected readonly fieldMetadataRepository: Repository, protected readonly dataSourceService: DataSourceService, ) { - super(workspaceRepository, twentyORMGlobalManager, dataSourceService); + super(workspaceRepository, globalWorkspaceOrmManager, dataSourceService); } override async runOnWorkspace({ @@ -113,7 +114,7 @@ export class CleanEmptyStringNullInTextFieldsCommand extends ActiveOrSuspendedWo objectMetadataItem: ObjectMetadataEntity, tableName: string, schemaName: string, - dataSource: WorkspaceDataSource, + dataSource: WorkspaceDataSourceInterface, isDryRun: boolean, ): Promise { const textFields = objectMetadataItem.fields.filter( @@ -154,7 +155,7 @@ export class CleanEmptyStringNullInTextFieldsCommand extends ActiveOrSuspendedWo objectMetadataItem: ObjectMetadataEntity, tableName: string, schemaName: string, - dataSource: WorkspaceDataSource, + dataSource: WorkspaceDataSourceInterface, isDryRun: boolean, ): Promise { const nameField = objectMetadataItem.fields.find( diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-deduplicate-role-targets.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-deduplicate-role-targets.command.ts index dbed5ae3a34..cbea2855d33 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-deduplicate-role-targets.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-deduplicate-role-targets.command.ts @@ -8,7 +8,7 @@ import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/worksp import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; type DuplicateKey = 'apiKeyId' | 'agentId' | 'userWorkspaceId'; @@ -28,7 +28,7 @@ export class DeduplicateRoleTargetsCommand extends ActiveOrSuspendedWorkspacesMi private readonly roleTargetRepository: Repository, @InjectRepository(WorkspaceEntity) protected readonly workspaceRepository: Repository, - protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, protected readonly dataSourceService: DataSourceService, ) { super(workspaceRepository, twentyORMGlobalManager, dataSourceService); diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-migrate-standard-invalid-entities.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-migrate-standard-invalid-entities.command.ts index cf0d10b1c62..286405c6839 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-migrate-standard-invalid-entities.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-migrate-standard-invalid-entities.command.ts @@ -20,7 +20,7 @@ import { MetadataFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/ import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util'; import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; import { AllStandardObjectFieldName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-object-field-name.type'; import { AllStandardObjectName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-object-name.type'; @@ -263,7 +263,7 @@ export class MigrateStandardInvalidEntitiesCommand extends ActiveOrSuspendedWork constructor( @InjectRepository(WorkspaceEntity) protected readonly workspaceRepository: Repository, - protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, protected readonly dataSourceService: DataSourceService, private readonly workspaceCacheService: WorkspaceCacheService, private readonly fieldMetadataService: FieldMetadataService, diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-rename-unique-index.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-rename-unique-index.command.ts index c0079447a0d..d9204539b95 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-rename-unique-index.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-rename-unique-index.command.ts @@ -13,7 +13,7 @@ import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-m import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; import { generateFlatIndexMetadataWithNameOrThrow } from 'src/engine/metadata-modules/index-metadata/utils/generate-flat-index.util'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util'; @@ -27,7 +27,7 @@ export class RenameIndexNameCommand extends ActiveOrSuspendedWorkspacesMigration constructor( @InjectRepository(WorkspaceEntity) protected readonly workspaceRepository: Repository, - protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, protected readonly workspaceCacheService: WorkspaceCacheService, protected readonly dataSourceService: DataSourceService, @InjectDataSource() diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-update-role-targets-unique-constraint-migration.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-update-role-targets-unique-constraint-migration.command.ts index 4d6da9a8244..3a118ef1516 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-update-role-targets-unique-constraint-migration.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-update-role-targets-unique-constraint-migration.command.ts @@ -7,7 +7,7 @@ import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/ import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; @Command({ name: 'upgrade:1-13:update-role-targets-unique-constraint-migration', @@ -20,7 +20,7 @@ export class UpdateRoleTargetsUniqueConstraintMigrationCommand extends ActiveOrS constructor( @InjectRepository(WorkspaceEntity) protected readonly workspaceRepository: Repository, - protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, protected readonly dataSourceService: DataSourceService, @InjectDataSource() private readonly coreDataSource: DataSource, diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-6/1-6-fix-label-identifier-position-and-visibility.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-6/1-6-fix-label-identifier-position-and-visibility.command.ts index 3807d587d6e..cec31e0fb0e 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-6/1-6-fix-label-identifier-position-and-visibility.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-6/1-6-fix-label-identifier-position-and-visibility.command.ts @@ -9,7 +9,7 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity'; import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; @Command({ name: 'upgrade:1-6:fix-label-identifier-position-and-visibility', @@ -20,7 +20,7 @@ export class FixLabelIdentifierPositionAndVisibilityCommand extends ActiveOrSusp constructor( @InjectRepository(WorkspaceEntity) protected readonly workspaceRepository: Repository, - protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, protected readonly dataSourceService: DataSourceService, @InjectRepository(ViewEntity) private readonly viewRepository: Repository, diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-7/1-7-backfill-workflow-manual-trigger-availability.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-7/1-7-backfill-workflow-manual-trigger-availability.command.ts index 36af4cf137a..9568c26aa63 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-7/1-7-backfill-workflow-manual-trigger-availability.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-7/1-7-backfill-workflow-manual-trigger-availability.command.ts @@ -8,7 +8,7 @@ import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/ import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util'; import { WorkflowTriggerType } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type'; @@ -21,7 +21,7 @@ export class BackfillWorkflowManualTriggerAvailabilityCommand extends ActiveOrSu constructor( @InjectRepository(WorkspaceEntity) protected readonly workspaceRepository: Repository, - protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, protected readonly dataSourceService: DataSourceService, @InjectDataSource() private readonly coreDataSource: DataSource, diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-8/1-8-deduplicate-unique-fields.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-8/1-8-deduplicate-unique-fields.command.ts index fe8e60dab83..9379ac5fe37 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-8/1-8-deduplicate-unique-fields.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-8/1-8-deduplicate-unique-fields.command.ts @@ -5,6 +5,8 @@ import { Command } from 'nest-commander'; import { isDefined } from 'twenty-shared/utils'; import { IsNull, Not, Repository } from 'typeorm'; +import { type WorkspaceDataSourceInterface } from 'src/engine/twenty-orm/interfaces/workspace-datasource.interface'; + import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -22,8 +24,7 @@ import { WorkspaceMigrationTableActionType, } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service'; -import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service'; @@ -37,7 +38,7 @@ export class DeduplicateUniqueFieldsCommand extends ActiveOrSuspendedWorkspacesM constructor( @InjectRepository(WorkspaceEntity) protected readonly workspaceRepository: Repository, - protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, protected readonly dataSourceService: DataSourceService, protected readonly indexMetadataService: IndexMetadataService, protected readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService, @@ -188,7 +189,7 @@ export class DeduplicateUniqueFieldsCommand extends ActiveOrSuspendedWorkspacesM dataSource, dryRun, }: { - dataSource: WorkspaceDataSource; + dataSource: WorkspaceDataSourceInterface; dryRun: boolean; }) { const workspaceMemberRepository = dataSource.getRepository( @@ -248,7 +249,7 @@ export class DeduplicateUniqueFieldsCommand extends ActiveOrSuspendedWorkspacesM dataSource, dryRun, }: { - dataSource: WorkspaceDataSource; + dataSource: WorkspaceDataSourceInterface; dryRun: boolean; }) { const companyRepository = dataSource.getRepository('company', { @@ -308,7 +309,7 @@ export class DeduplicateUniqueFieldsCommand extends ActiveOrSuspendedWorkspacesM dataSource, dryRun, }: { - dataSource: WorkspaceDataSource; + dataSource: WorkspaceDataSourceInterface; dryRun: boolean; }) { const personRepository = dataSource.getRepository('person', { diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-8/1-8-fill-null-serverless-function-layer-id.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-8/1-8-fill-null-serverless-function-layer-id.command.ts index 68c22bbc0aa..06a58b87b8a 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-8/1-8-fill-null-serverless-function-layer-id.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-8/1-8-fill-null-serverless-function-layer-id.command.ts @@ -10,7 +10,7 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { ServerlessFunctionLayerEntity } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.entity'; import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; @Command({ name: 'upgrade:1-8:fill-null-serverless-function-layer-id', @@ -24,7 +24,7 @@ export class FillNullServerlessFunctionLayerIdCommand extends ActiveOrSuspendedW constructor( @InjectRepository(WorkspaceEntity) protected readonly workspaceRepository: Repository, - protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, protected readonly dataSourceService: DataSourceService, @InjectRepository(ServerlessFunctionEntity) protected readonly serverlessFunctionRepository: Repository, diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-8/1-8-migrate-channel-sync-stages.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-8/1-8-migrate-channel-sync-stages.command.ts index fec1b717fdf..d61e2c8e6dd 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-8/1-8-migrate-channel-sync-stages.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-8/1-8-migrate-channel-sync-stages.command.ts @@ -11,7 +11,7 @@ import { DataSourceService } from 'src/engine/metadata-modules/data-source/data- import { type FieldMetadataComplexOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util'; import { @@ -30,7 +30,7 @@ export class MigrateChannelSyncStagesCommand extends ActiveOrSuspendedWorkspaces constructor( @InjectRepository(WorkspaceEntity) protected readonly workspaceRepository: Repository, - protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, protected readonly dataSourceService: DataSourceService, @InjectRepository(ObjectMetadataEntity) private readonly objectMetadataRepository: Repository, diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-8/1-8-migrate-workflow-step-filter-operand-value.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-8/1-8-migrate-workflow-step-filter-operand-value.ts index 75c48095083..8ee0d5537bd 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-8/1-8-migrate-workflow-step-filter-operand-value.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-8/1-8-migrate-workflow-step-filter-operand-value.ts @@ -12,7 +12,7 @@ import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/ import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity'; import { isWorkflowFilterAction } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/guards/is-workflow-filter-action.guard'; import { isWorkflowFindRecordsAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/guards/is-workflow-find-records-action.guard'; @@ -26,7 +26,7 @@ export class MigrateWorkflowStepFilterOperandValueCommand extends ActiveOrSuspen constructor( @InjectRepository(WorkspaceEntity) protected readonly workspaceRepository: Repository, - protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, protected readonly dataSourceService: DataSourceService, ) { super(workspaceRepository, twentyORMGlobalManager, dataSourceService); @@ -43,7 +43,7 @@ export class MigrateWorkflowStepFilterOperandValueCommand extends ActiveOrSuspen ); const workflowVersionRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( + await this.twentyORMGlobalManager.getRepository( workspaceId, 'workflowVersion', { shouldBypassPermissionChecks: true }, diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-8/1-8-regenerate-person-search-vector-with-phones.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-8/1-8-regenerate-person-search-vector-with-phones.command.ts index 89e6c5ae88c..de658862d32 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-8/1-8-regenerate-person-search-vector-with-phones.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-8/1-8-regenerate-person-search-vector-with-phones.command.ts @@ -7,7 +7,7 @@ import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/ import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { WorkspaceSchemaManagerService } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.service'; import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util'; import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util'; @@ -24,7 +24,7 @@ export class RegeneratePersonSearchVectorWithPhonesCommand extends ActiveOrSuspe protected readonly workspaceRepository: Repository, @InjectDataSource() private readonly coreDataSource: DataSource, - protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, protected readonly dataSourceService: DataSourceService, private readonly workspaceSchemaManager: WorkspaceSchemaManagerService, ) { diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts index b8d4f1a85f3..d2c848d3da8 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts @@ -41,7 +41,7 @@ import { RegeneratePersonSearchVectorWithPhonesCommand } from 'src/database/comm import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command'; @Command({ @@ -55,7 +55,7 @@ export class UpgradeCommand extends UpgradeCommandRunner { @InjectRepository(WorkspaceEntity) protected readonly workspaceRepository: Repository, protected readonly twentyConfigService: TwentyConfigService, - protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, protected readonly dataSourceService: DataSourceService, protected readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand, @@ -106,7 +106,7 @@ export class UpgradeCommand extends UpgradeCommandRunner { super( workspaceRepository, twentyConfigService, - twentyORMGlobalManager, + globalWorkspaceOrmManager, dataSourceService, syncWorkspaceMetadataCommand, ); diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts index cbf413fd518..5b2b15a0666 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts @@ -1,9 +1,9 @@ import { Inject, Injectable } from '@nestjs/common'; import { msg } from '@lingui/core/macro'; +import { type PermissionFlagType } from 'twenty-shared/constants'; import { isDefined } from 'twenty-shared/utils'; import { Omit } from 'zod/v4/core/util.cjs'; -import { type PermissionFlagType } from 'twenty-shared/constants'; import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface'; import { QueryResultFieldValue } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-field-value'; @@ -50,7 +50,6 @@ import { PermissionsService } from 'src/engine/metadata-modules/permissions/perm import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; @Injectable() @@ -65,8 +64,6 @@ export abstract class CommonBaseQueryRunnerService< @Inject() protected readonly dataArgProcessor: DataArgProcessor; @Inject() - protected readonly twentyORMGlobalManager: TwentyORMGlobalManager; - @Inject() protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager; @Inject() protected readonly processNestedRelationsHelper: ProcessNestedRelationsHelper; diff --git a/packages/twenty-server/src/engine/api/common/core-common-api.module.ts b/packages/twenty-server/src/engine/api/common/core-common-api.module.ts index e2c7ce20191..74838bc3080 100644 --- a/packages/twenty-server/src/engine/api/common/core-common-api.module.ts +++ b/packages/twenty-server/src/engine/api/common/core-common-api.module.ts @@ -23,7 +23,6 @@ import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role. import { ViewFilterGroupModule } from 'src/engine/metadata-modules/view-filter-group/view-filter-group.module'; import { ViewFilterModule } from 'src/engine/metadata-modules/view-filter/view-filter.module'; import { ViewModule } from 'src/engine/metadata-modules/view/view.module'; -import { GlobalWorkspaceDataSourceModule } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource.module'; import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; @Module({ @@ -42,7 +41,6 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache MetricsModule, RecordPositionModule, RecordTransformerModule, - GlobalWorkspaceDataSourceModule, FeatureFlagModule, WorkspaceCacheModule, ], diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-base.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-base.handler.ts index 47acc705997..33dc411f4d9 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-base.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-base.handler.ts @@ -33,7 +33,6 @@ import { PermissionsExceptionMessage, } from 'src/engine/metadata-modules/permissions/permissions.exception'; import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; import { standardObjectMetadataDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects'; @@ -55,8 +54,6 @@ export interface FormatResult { } export abstract class RestApiBaseHandler { - @Inject() - protected readonly twentyORMGlobalManager: TwentyORMGlobalManager; @Inject() protected readonly workspaceCacheService: WorkspaceCacheService; @Inject() diff --git a/packages/twenty-server/src/engine/core-modules/actor/services/__tests__/created-by-from-auth-context.service.spec.ts b/packages/twenty-server/src/engine/core-modules/actor/services/__tests__/created-by-from-auth-context.service.spec.ts index b532f78729a..9d8be929279 100644 --- a/packages/twenty-server/src/engine/core-modules/actor/services/__tests__/created-by-from-auth-context.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/actor/services/__tests__/created-by-from-auth-context.service.spec.ts @@ -14,7 +14,7 @@ import { type UserEntity } from 'src/engine/core-modules/user/user.entity'; import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; type TestingAuthContext = Omit & { @@ -36,12 +36,16 @@ describe('CreatedByFromAuthContextService', () => { const mockWorkspaceMemberRepository = { findOneOrFail: jest.fn(), }; - const twentyORMGlobalManager: jest.Mocked< - Pick + const globalWorkspaceOrmManager: jest.Mocked< + Pick< + GlobalWorkspaceOrmManager, + 'getRepository' | 'executeInWorkspaceContext' + > > = { - getRepositoryForWorkspace: jest + getRepository: jest.fn().mockResolvedValue(mockWorkspaceMemberRepository), + executeInWorkspaceContext: jest .fn() - .mockResolvedValue(mockWorkspaceMemberRepository), + .mockImplementation((_authContext: any, fn: () => any) => fn()), }; beforeEach(async () => { @@ -49,8 +53,8 @@ describe('CreatedByFromAuthContextService', () => { providers: [ CreatedByFromAuthContextService, { - provide: TwentyORMGlobalManager, - useValue: twentyORMGlobalManager, + provide: GlobalWorkspaceOrmManager, + useValue: globalWorkspaceOrmManager, }, { provide: getRepositoryToken(FieldMetadataEntity), diff --git a/packages/twenty-server/src/engine/core-modules/actor/services/created-by-from-auth-context.service.ts b/packages/twenty-server/src/engine/core-modules/actor/services/created-by-from-auth-context.service.ts index fd7b66031b6..2e9e23dd685 100644 --- a/packages/twenty-server/src/engine/core-modules/actor/services/created-by-from-auth-context.service.ts +++ b/packages/twenty-server/src/engine/core-modules/actor/services/created-by-from-auth-context.service.ts @@ -3,6 +3,8 @@ import { Injectable, Logger } from '@nestjs/common'; import { type ActorMetadata } from 'twenty-shared/types'; import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils'; +import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface'; + import { buildCreatedByFromApiKey } from 'src/engine/core-modules/actor/utils/build-created-by-from-api-key.util'; import { buildCreatedByFromFullNameMetadata } from 'src/engine/core-modules/actor/utils/build-created-by-from-full-name-metadata.util'; import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; @@ -10,7 +12,7 @@ import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -21,7 +23,7 @@ export class CreatedByFromAuthContextService { private readonly logger = new Logger(CreatedByFromAuthContextService.name); constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, ) {} @@ -109,23 +111,30 @@ export class CreatedByFromAuthContextService { assertIsDefinedOrThrow(workspace, WorkspaceNotFoundDefaultError); if (isDefined(user)) { - const workspaceMemberRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspace.id, - 'workspaceMember', - { shouldBypassPermissionChecks: true }, - ); + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext as WorkspaceAuthContext, + async () => { + const workspaceMemberRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspace.id, + 'workspaceMember', + { shouldBypassPermissionChecks: true }, + ); - const workspaceMember = await workspaceMemberRepository.findOneOrFail({ - where: { - userId: user.id, + const workspaceMember = await workspaceMemberRepository.findOneOrFail( + { + where: { + userId: user.id, + }, + }, + ); + + return buildCreatedByFromFullNameMetadata({ + fullNameMetadata: workspaceMember.name, + workspaceMemberId: workspaceMember.id, + }); }, - }); - - return buildCreatedByFromFullNameMetadata({ - fullNameMetadata: workspaceMember.name, - workspaceMemberId: workspaceMember.id, - }); + ); } if (isDefined(apiKey)) { diff --git a/packages/twenty-server/src/engine/core-modules/approved-access-domain/approved-access-domain.resolver.ts b/packages/twenty-server/src/engine/core-modules/approved-access-domain/approved-access-domain.resolver.ts index ce6fcee3c7f..debd49f3a16 100644 --- a/packages/twenty-server/src/engine/core-modules/approved-access-domain/approved-access-domain.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/approved-access-domain/approved-access-domain.resolver.ts @@ -17,7 +17,8 @@ import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; @UseGuards( @@ -32,7 +33,7 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta @Resolver() export class ApprovedAccessDomainResolver { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly approvedAccessDomainService: ApprovedAccessDomainService, ) {} @@ -42,18 +43,26 @@ export class ApprovedAccessDomainResolver { @AuthWorkspace() currentWorkspace: WorkspaceEntity, @AuthUser() currentUser: UserEntity, ): Promise { - const workspaceMemberRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - currentWorkspace.id, - 'workspaceMember', - { shouldBypassPermissionChecks: true }, - ); + const authContext = buildSystemAuthContext(currentWorkspace.id); - const workspaceMember = await workspaceMemberRepository.findOneOrFail({ - where: { - userId: currentUser.id, - }, - }); + const workspaceMember = + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workspaceMemberRepository = + await this.globalWorkspaceOrmManager.getRepository( + currentWorkspace.id, + 'workspaceMember', + { shouldBypassPermissionChecks: true }, + ); + + return workspaceMemberRepository.findOneOrFail({ + where: { + userId: currentUser.id, + }, + }); + }, + ); return this.approvedAccessDomainService.createApprovedAccessDomain( domain, diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/create-calendar-channel.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/create-calendar-channel.service.ts index 20a7b0ea2c7..09945ca913a 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/create-calendar-channel.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/create-calendar-channel.service.ts @@ -3,7 +3,8 @@ import { Injectable } from '@nestjs/common'; import { v4 } from 'uuid'; import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { CalendarChannelSyncStage, CalendarChannelSyncStatus, @@ -22,7 +23,7 @@ export type CreateCalendarChannelInput = { @Injectable() export class CreateCalendarChannelService { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, ) {} async createCalendarChannel( @@ -36,26 +37,33 @@ export class CreateCalendarChannelService { manager, } = input; - const calendarChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'calendarChannel', - ); + const authContext = buildSystemAuthContext(workspaceId); - const newCalendarChannel = await calendarChannelRepository.save( - { - id: v4(), - connectedAccountId, - handle, - visibility: - calendarVisibility || CalendarChannelVisibility.SHARE_EVERYTHING, - syncStatus: CalendarChannelSyncStatus.NOT_SYNCED, - syncStage: CalendarChannelSyncStage.PENDING_CONFIGURATION, + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const calendarChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'calendarChannel', + ); + + const newCalendarChannel = await calendarChannelRepository.save( + { + id: v4(), + connectedAccountId, + handle, + visibility: + calendarVisibility || CalendarChannelVisibility.SHARE_EVERYTHING, + syncStatus: CalendarChannelSyncStatus.NOT_SYNCED, + syncStage: CalendarChannelSyncStage.PENDING_CONFIGURATION, + }, + {}, + manager, + ); + + return newCalendarChannel.id; }, - {}, - manager, ); - - return newCalendarChannel.id; } } diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/create-connected-account.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/create-connected-account.service.ts index 4626c96419e..7c9af5d358a 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/create-connected-account.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/create-connected-account.service.ts @@ -3,7 +3,8 @@ import { Injectable } from '@nestjs/common'; import { type ConnectedAccountProvider } from 'twenty-shared/types'; import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; export type CreateConnectedAccountInput = { @@ -21,7 +22,7 @@ export type CreateConnectedAccountInput = { @Injectable() export class CreateConnectedAccountService { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, ) {} async createConnectedAccount( @@ -39,24 +40,31 @@ export class CreateConnectedAccountService { manager, } = input; - const connectedAccountRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'connectedAccount', - ); + const authContext = buildSystemAuthContext(workspaceId); - await connectedAccountRepository.save( - { - id: connectedAccountId, - handle, - provider, - accessToken, - refreshToken, - accountOwnerId, - scopes, + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const connectedAccountRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'connectedAccount', + ); + + await connectedAccountRepository.save( + { + id: connectedAccountId, + handle, + provider, + accessToken, + refreshToken, + accountOwnerId, + scopes, + }, + {}, + manager, + ); }, - {}, - manager, ); } } diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/create-message-channel.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/create-message-channel.service.ts index 05b6d880ba8..fba5a1fb978 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/create-message-channel.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/create-message-channel.service.ts @@ -4,7 +4,8 @@ import { isDefined } from 'twenty-shared/utils'; import { v4 } from 'uuid'; import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { MessageChannelPendingGroupEmailsAction, @@ -27,7 +28,7 @@ export type CreateMessageChannelInput = { @Injectable() export class CreateMessageChannelService { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly syncMessageFoldersService: SyncMessageFoldersService, ) {} @@ -42,49 +43,57 @@ export class CreateMessageChannelService { manager, } = input; - const connectedAccountRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'connectedAccount', - ); + const authContext = buildSystemAuthContext(workspaceId); - const connectedAccount = await connectedAccountRepository.findOne({ - where: { id: connectedAccountId }, - }); + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const connectedAccountRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'connectedAccount', + ); - const messageChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'messageChannel', - ); + const connectedAccount = await connectedAccountRepository.findOne({ + where: { id: connectedAccountId }, + }); - const newMessageChannel = await messageChannelRepository.save( - { - id: v4(), - connectedAccountId, - type: MessageChannelType.EMAIL, - handle, - visibility: - messageVisibility || MessageChannelVisibility.SHARE_EVERYTHING, - syncStatus: MessageChannelSyncStatus.NOT_SYNCED, - syncStage: MessageChannelSyncStage.PENDING_CONFIGURATION, - pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction.NONE, + const messageChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'messageChannel', + ); + + const newMessageChannel = await messageChannelRepository.save( + { + id: v4(), + connectedAccountId, + type: MessageChannelType.EMAIL, + handle, + visibility: + messageVisibility || MessageChannelVisibility.SHARE_EVERYTHING, + syncStatus: MessageChannelSyncStatus.NOT_SYNCED, + syncStage: MessageChannelSyncStage.PENDING_CONFIGURATION, + pendingGroupEmailsAction: + MessageChannelPendingGroupEmailsAction.NONE, + }, + {}, + manager, + ); + + if (isDefined(connectedAccount)) { + await this.syncMessageFoldersService.syncMessageFolders({ + workspaceId, + messageChannel: { + ...newMessageChannel, + connectedAccount, + }, + manager, + }); + } + + return newMessageChannel.id; }, - {}, - manager, ); - - if (isDefined(connectedAccount)) { - await this.syncMessageFoldersService.syncMessageFolders({ - workspaceId, - messageChannel: { - ...newMessageChannel, - connectedAccount, - }, - manager, - }); - } - - return newMessageChannel.id; } } diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.spec.ts b/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.spec.ts index 10f3fa58b74..c0152d1fb26 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.spec.ts @@ -13,7 +13,7 @@ import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queu import { getQueueToken } from 'src/engine/core-modules/message-queue/utils/get-queue-token.util'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { CalendarChannelSyncStatusService } from 'src/modules/calendar/common/services/calendar-channel-sync-status.service'; import { CalendarChannelSyncStage, @@ -77,9 +77,9 @@ describe('GoogleAPIsService', () => { providers: [ GoogleAPIsService, { - provide: TwentyORMGlobalManager, + provide: GlobalWorkspaceOrmManager, useValue: { - getRepositoryForWorkspace: jest + getRepository: jest .fn() .mockImplementation((_workspaceId, entity) => { if (entity === 'connectedAccount') @@ -96,6 +96,9 @@ describe('GoogleAPIsService', () => { getDataSourceForWorkspace: jest .fn() .mockImplementation(() => mockWorkspaceDataSource), + executeInWorkspaceContext: jest + .fn() + .mockImplementation((_authContext: any, fn: () => any) => fn()), }, }, { diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.ts index b2a0d972960..8ca046e6568 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.ts @@ -17,7 +17,8 @@ import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queu import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { CalendarEventListFetchJob, type CalendarEventListFetchJobData, @@ -45,7 +46,7 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta @Injectable() export class GoogleAPIsService { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, @InjectMessageQueue(MessageQueue.messagingQueue) private readonly messageQueueService: MessageQueueService, @InjectMessageQueue(MessageQueue.calendarQueue) @@ -82,36 +83,6 @@ export class GoogleAPIsService { 'CALENDAR_PROVIDER_GOOGLE_ENABLED', ); - const connectedAccountRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'connectedAccount', - ); - - const connectedAccount = await connectedAccountRepository.findOne({ - where: { handle, accountOwnerId: workspaceMemberId }, - }); - - const existingAccountId = connectedAccount?.id; - const newOrExistingConnectedAccountId = existingAccountId ?? v4(); - - const calendarChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'calendarChannel', - ); - - const messageChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'messageChannel', - ); - - const workspaceDataSource = - await this.twentyORMGlobalManager.getDataSourceForWorkspace({ - workspaceId, - }); - const { scopes, isValid } = await this.googleAPIScopesService.getScopesFromGoogleAccessTokenAndCheckIfExpectedScopesArePresent( input.accessToken, @@ -124,131 +95,167 @@ export class GoogleAPIsService { ); } - await workspaceDataSource.transaction( - async (manager: WorkspaceEntityManager) => { - if (!existingAccountId) { - await this.createConnectedAccountService.createConnectedAccount({ - workspaceId, - connectedAccountId: newOrExistingConnectedAccountId, - handle, - provider: ConnectedAccountProvider.GOOGLE, - accessToken: input.accessToken, - refreshToken: input.refreshToken, - accountOwnerId: workspaceMemberId, - scopes, - manager, - }); + const authContext = buildSystemAuthContext(workspaceId); - await this.createMessageChannelService.createMessageChannel({ + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const connectedAccountRepository = + await this.globalWorkspaceOrmManager.getRepository( workspaceId, - connectedAccountId: newOrExistingConnectedAccountId, - handle, - messageVisibility, - manager, - }); + 'connectedAccount', + ); - if (isCalendarEnabled) { - await this.createCalendarChannelService.createCalendarChannel({ - workspaceId, + const connectedAccount = await connectedAccountRepository.findOne({ + where: { handle, accountOwnerId: workspaceMemberId }, + }); + + const existingAccountId = connectedAccount?.id; + const newOrExistingConnectedAccountId = existingAccountId ?? v4(); + + const calendarChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'calendarChannel', + ); + + const messageChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'messageChannel', + ); + + const workspaceDataSource = + await this.globalWorkspaceOrmManager.getDataSourceForWorkspace( + workspaceId, + ); + + await workspaceDataSource.transaction( + async (manager: WorkspaceEntityManager) => { + if (!existingAccountId) { + await this.createConnectedAccountService.createConnectedAccount({ + workspaceId, + connectedAccountId: newOrExistingConnectedAccountId, + handle, + provider: ConnectedAccountProvider.GOOGLE, + accessToken: input.accessToken, + refreshToken: input.refreshToken, + accountOwnerId: workspaceMemberId, + scopes, + manager, + }); + + await this.createMessageChannelService.createMessageChannel({ + workspaceId, + connectedAccountId: newOrExistingConnectedAccountId, + handle, + messageVisibility, + manager, + }); + + if (isCalendarEnabled) { + await this.createCalendarChannelService.createCalendarChannel({ + workspaceId, + connectedAccountId: newOrExistingConnectedAccountId, + handle, + calendarVisibility, + manager, + }); + } + } else { + await this.updateConnectedAccountOnReconnectService.updateConnectedAccountOnReconnect( + { + workspaceId, + connectedAccountId: newOrExistingConnectedAccountId, + accessToken: input.accessToken, + refreshToken: input.refreshToken, + scopes, + connectedAccount, + manager, + }, + ); + + const workspaceMemberRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'workspaceMember', + { shouldBypassPermissionChecks: true }, + ); + + const workspaceMember = + await workspaceMemberRepository.findOneOrFail({ + where: { id: workspaceMemberId }, + }); + + const userId = workspaceMember.userId; + + await this.accountsToReconnectService.removeAccountToReconnect( + userId, + workspaceId, + newOrExistingConnectedAccountId, + ); + + await this.messagingChannelSyncStatusService.resetAndMarkAsMessagesListFetchPending( + [newOrExistingConnectedAccountId], + workspaceId, + ); + + await this.calendarChannelSyncStatusService.resetAndMarkAsCalendarEventListFetchPending( + [newOrExistingConnectedAccountId], + workspaceId, + ); + } + }, + ); + + if (this.twentyConfigService.get('MESSAGING_PROVIDER_GMAIL_ENABLED')) { + const messageChannels = await messageChannelRepository.find({ + where: { connectedAccountId: newOrExistingConnectedAccountId, - handle, - calendarVisibility, - manager, - }); + }, + }); + + for (const messageChannel of messageChannels) { + if ( + messageChannel.syncStage !== + MessageChannelSyncStage.PENDING_CONFIGURATION + ) { + await this.messageQueueService.add( + MessagingMessageListFetchJob.name, + { + workspaceId, + messageChannelId: messageChannel.id, + }, + ); + } } - } else { - await this.updateConnectedAccountOnReconnectService.updateConnectedAccountOnReconnect( - { - workspaceId, - connectedAccountId: newOrExistingConnectedAccountId, - accessToken: input.accessToken, - refreshToken: input.refreshToken, - scopes, - connectedAccount, - manager, - }, - ); - - const workspaceMemberRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'workspaceMember', - { shouldBypassPermissionChecks: true }, - ); - - const workspaceMember = await workspaceMemberRepository.findOneOrFail( - { - where: { id: workspaceMemberId }, - }, - ); - - const userId = workspaceMember.userId; - - await this.accountsToReconnectService.removeAccountToReconnect( - userId, - workspaceId, - newOrExistingConnectedAccountId, - ); - - await this.messagingChannelSyncStatusService.resetAndMarkAsMessagesListFetchPending( - [newOrExistingConnectedAccountId], - workspaceId, - ); - - await this.calendarChannelSyncStatusService.resetAndMarkAsCalendarEventListFetchPending( - [newOrExistingConnectedAccountId], - workspaceId, - ); } + + if (isCalendarEnabled) { + const calendarChannels = await calendarChannelRepository.find({ + where: { + connectedAccountId: newOrExistingConnectedAccountId, + }, + }); + + for (const calendarChannel of calendarChannels) { + if ( + calendarChannel.syncStage !== + CalendarChannelSyncStage.PENDING_CONFIGURATION + ) { + await this.calendarQueueService.add( + CalendarEventListFetchJob.name, + { + calendarChannelId: calendarChannel.id, + workspaceId, + }, + ); + } + } + } + + return newOrExistingConnectedAccountId; }, ); - - if (this.twentyConfigService.get('MESSAGING_PROVIDER_GMAIL_ENABLED')) { - const messageChannels = await messageChannelRepository.find({ - where: { - connectedAccountId: newOrExistingConnectedAccountId, - }, - }); - - for (const messageChannel of messageChannels) { - if ( - messageChannel.syncStage !== - MessageChannelSyncStage.PENDING_CONFIGURATION - ) { - await this.messageQueueService.add( - MessagingMessageListFetchJob.name, - { - workspaceId, - messageChannelId: messageChannel.id, - }, - ); - } - } - } - - if (isCalendarEnabled) { - const calendarChannels = await calendarChannelRepository.find({ - where: { - connectedAccountId: newOrExistingConnectedAccountId, - }, - }); - - for (const calendarChannel of calendarChannels) { - if ( - calendarChannel.syncStage !== - CalendarChannelSyncStage.PENDING_CONFIGURATION - ) { - await this.calendarQueueService.add( - CalendarEventListFetchJob.name, - { - calendarChannelId: calendarChannel.id, - workspaceId, - }, - ); - } - } - } - - return newOrExistingConnectedAccountId; } } diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/microsoft-apis.service.spec.ts b/packages/twenty-server/src/engine/core-modules/auth/services/microsoft-apis.service.spec.ts index beeb4b08fdc..ab2c1295983 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/microsoft-apis.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/microsoft-apis.service.spec.ts @@ -12,7 +12,7 @@ import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queu import { getQueueToken } from 'src/engine/core-modules/message-queue/utils/get-queue-token.util'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { CalendarChannelSyncStatusService } from 'src/modules/calendar/common/services/calendar-channel-sync-status.service'; import { CalendarChannelSyncStage, @@ -76,9 +76,9 @@ describe('MicrosoftAPIsService', () => { providers: [ MicrosoftAPIsService, { - provide: TwentyORMGlobalManager, + provide: GlobalWorkspaceOrmManager, useValue: { - getRepositoryForWorkspace: jest + getRepository: jest .fn() .mockImplementation((_workspaceId, entity) => { if (entity === 'connectedAccount') @@ -95,6 +95,9 @@ describe('MicrosoftAPIsService', () => { getDataSourceForWorkspace: jest .fn() .mockImplementation(() => mockWorkspaceDataSource), + executeInWorkspaceContext: jest + .fn() + .mockImplementation((_authContext: any, fn: () => any) => fn()), }, }, { diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/microsoft-apis.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/microsoft-apis.service.ts index 38940e109e9..b1214972c50 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/microsoft-apis.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/microsoft-apis.service.ts @@ -13,7 +13,8 @@ import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queu import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { CalendarEventListFetchJob, type CalendarEventListFetchJobData, @@ -41,7 +42,7 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta @Injectable() export class MicrosoftAPIsService { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, @InjectMessageQueue(MessageQueue.messagingQueue) private readonly messageQueueService: MessageQueueService, @InjectMessageQueue(MessageQueue.calendarQueue) @@ -73,170 +74,177 @@ export class MicrosoftAPIsService { messageVisibility, } = input; - const connectedAccountRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'connectedAccount', - ); - - const connectedAccount = await connectedAccountRepository.findOne({ - where: { handle, accountOwnerId: workspaceMemberId }, - }); - - const existingAccountId = connectedAccount?.id; - const newOrExistingConnectedAccountId = existingAccountId ?? v4(); - - const calendarChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'calendarChannel', - ); - - const messageChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'messageChannel', - ); - - const workspaceDataSource = - await this.twentyORMGlobalManager.getDataSourceForWorkspace({ - workspaceId, - }); - const scopes = getMicrosoftApisOauthScopes(); - await workspaceDataSource.transaction( - async (manager: WorkspaceEntityManager) => { - if (!existingAccountId) { - await this.createConnectedAccountService.createConnectedAccount({ - workspaceId, - connectedAccountId: newOrExistingConnectedAccountId, - handle, - provider: ConnectedAccountProvider.MICROSOFT, - accessToken: input.accessToken, - refreshToken: input.refreshToken, - accountOwnerId: workspaceMemberId, - scopes, - manager, - }); + const authContext = buildSystemAuthContext(workspaceId); - await this.createMessageChannelService.createMessageChannel({ + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const connectedAccountRepository = + await this.globalWorkspaceOrmManager.getRepository( workspaceId, - connectedAccountId: newOrExistingConnectedAccountId, - handle, - messageVisibility, - manager, - }); + 'connectedAccount', + ); - if ( - this.twentyConfigService.get('CALENDAR_PROVIDER_MICROSOFT_ENABLED') - ) { - await this.createCalendarChannelService.createCalendarChannel({ - workspaceId, + const connectedAccount = await connectedAccountRepository.findOne({ + where: { handle, accountOwnerId: workspaceMemberId }, + }); + + const existingAccountId = connectedAccount?.id; + const newOrExistingConnectedAccountId = existingAccountId ?? v4(); + + const calendarChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'calendarChannel', + ); + + const messageChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'messageChannel', + ); + + const workspaceDataSource = + await this.globalWorkspaceOrmManager.getDataSourceForWorkspace( + workspaceId, + ); + + await workspaceDataSource.transaction( + async (manager: WorkspaceEntityManager) => { + if (!existingAccountId) { + await this.createConnectedAccountService.createConnectedAccount({ + workspaceId, + connectedAccountId: newOrExistingConnectedAccountId, + handle, + provider: ConnectedAccountProvider.MICROSOFT, + accessToken: input.accessToken, + refreshToken: input.refreshToken, + accountOwnerId: workspaceMemberId, + scopes, + manager, + }); + + await this.createMessageChannelService.createMessageChannel({ + workspaceId, + connectedAccountId: newOrExistingConnectedAccountId, + handle, + messageVisibility, + manager, + }); + + if ( + this.twentyConfigService.get( + 'CALENDAR_PROVIDER_MICROSOFT_ENABLED', + ) + ) { + await this.createCalendarChannelService.createCalendarChannel({ + workspaceId, + connectedAccountId: newOrExistingConnectedAccountId, + handle, + calendarVisibility, + manager, + }); + } + } else { + await this.updateConnectedAccountOnReconnectService.updateConnectedAccountOnReconnect( + { + workspaceId, + connectedAccountId: newOrExistingConnectedAccountId, + accessToken: input.accessToken, + refreshToken: input.refreshToken, + scopes, + connectedAccount, + manager, + }, + ); + + const workspaceMemberRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'workspaceMember', + { shouldBypassPermissionChecks: true }, + ); + + const workspaceMember = + await workspaceMemberRepository.findOneOrFail({ + where: { id: workspaceMemberId }, + }); + + const userId = workspaceMember.userId; + + await this.accountsToReconnectService.removeAccountToReconnect( + userId, + workspaceId, + newOrExistingConnectedAccountId, + ); + + await this.messagingChannelSyncStatusService.resetAndMarkAsMessagesListFetchPending( + [newOrExistingConnectedAccountId], + workspaceId, + ); + + await this.calendarChannelSyncStatusService.resetAndMarkAsCalendarEventListFetchPending( + [newOrExistingConnectedAccountId], + workspaceId, + ); + } + }, + ); + + if ( + this.twentyConfigService.get('MESSAGING_PROVIDER_MICROSOFT_ENABLED') + ) { + const messageChannels = await messageChannelRepository.find({ + where: { connectedAccountId: newOrExistingConnectedAccountId, - handle, - calendarVisibility, - manager, - }); + }, + }); + + for (const messageChannel of messageChannels) { + if ( + messageChannel.syncStage !== + MessageChannelSyncStage.PENDING_CONFIGURATION + ) { + await this.messageQueueService.add( + MessagingMessageListFetchJob.name, + { + workspaceId, + messageChannelId: messageChannel.id, + }, + ); + } } - } else { - await this.updateConnectedAccountOnReconnectService.updateConnectedAccountOnReconnect( - { - workspaceId, - connectedAccountId: newOrExistingConnectedAccountId, - accessToken: input.accessToken, - refreshToken: input.refreshToken, - scopes, - connectedAccount, - manager, - }, - ); - - const workspaceMemberRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'workspaceMember', - { shouldBypassPermissionChecks: true }, - ); - - const workspaceMember = await workspaceMemberRepository.findOneOrFail( - { - where: { id: workspaceMemberId }, - }, - ); - - const userId = workspaceMember.userId; - - await this.accountsToReconnectService.removeAccountToReconnect( - userId, - workspaceId, - newOrExistingConnectedAccountId, - ); - - await this.messagingChannelSyncStatusService.resetAndMarkAsMessagesListFetchPending( - [newOrExistingConnectedAccountId], - workspaceId, - ); - - await this.calendarChannelSyncStatusService.resetAndMarkAsCalendarEventListFetchPending( - [newOrExistingConnectedAccountId], - workspaceId, - ); - - await this.calendarChannelSyncStatusService.resetAndMarkAsCalendarEventListFetchPending( - [newOrExistingConnectedAccountId], - workspaceId, - ); } + + if ( + this.twentyConfigService.get('CALENDAR_PROVIDER_MICROSOFT_ENABLED') + ) { + const calendarChannels = await calendarChannelRepository.find({ + where: { + connectedAccountId: newOrExistingConnectedAccountId, + }, + }); + + for (const calendarChannel of calendarChannels) { + if ( + calendarChannel.syncStage !== + CalendarChannelSyncStage.PENDING_CONFIGURATION + ) { + await this.calendarQueueService.add( + CalendarEventListFetchJob.name, + { + calendarChannelId: calendarChannel.id, + workspaceId, + }, + ); + } + } + } + + return newOrExistingConnectedAccountId; }, ); - - if (this.twentyConfigService.get('MESSAGING_PROVIDER_MICROSOFT_ENABLED')) { - const messageChannels = await messageChannelRepository.find({ - where: { - connectedAccountId: newOrExistingConnectedAccountId, - }, - }); - - for (const messageChannel of messageChannels) { - if ( - messageChannel.syncStage !== - MessageChannelSyncStage.PENDING_CONFIGURATION - ) { - await this.messageQueueService.add( - MessagingMessageListFetchJob.name, - { - workspaceId, - messageChannelId: messageChannel.id, - }, - ); - } - } - } - - if (this.twentyConfigService.get('CALENDAR_PROVIDER_MICROSOFT_ENABLED')) { - const calendarChannels = await calendarChannelRepository.find({ - where: { - connectedAccountId: newOrExistingConnectedAccountId, - }, - }); - - for (const calendarChannel of calendarChannels) { - if ( - calendarChannel.syncStage !== - CalendarChannelSyncStage.PENDING_CONFIGURATION - ) { - await this.calendarQueueService.add( - CalendarEventListFetchJob.name, - { - calendarChannelId: calendarChannel.id, - workspaceId, - }, - ); - } - } - } - - return newOrExistingConnectedAccountId; } } diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/update-connected-account-on-reconnect.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/update-connected-account-on-reconnect.service.ts index 48f089c79c4..022dc9b3f4f 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/update-connected-account-on-reconnect.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/update-connected-account-on-reconnect.service.ts @@ -1,7 +1,8 @@ import { Injectable } from '@nestjs/common'; import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; export type UpdateConnectedAccountOnReconnectInput = { @@ -17,7 +18,7 @@ export type UpdateConnectedAccountOnReconnectInput = { @Injectable() export class UpdateConnectedAccountOnReconnectService { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, ) {} async updateConnectedAccountOnReconnect( @@ -32,23 +33,30 @@ export class UpdateConnectedAccountOnReconnectService { manager, } = input; - const connectedAccountRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'connectedAccount', - ); + const authContext = buildSystemAuthContext(workspaceId); - await connectedAccountRepository.update( - { - id: connectedAccountId, + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const connectedAccountRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'connectedAccount', + ); + + await connectedAccountRepository.update( + { + id: connectedAccountId, + }, + { + accessToken, + refreshToken, + scopes, + authFailedAt: null, + }, + manager, + ); }, - { - accessToken, - refreshToken, - scopes, - authFailedAt: null, - }, - manager, ); } } diff --git a/packages/twenty-server/src/engine/core-modules/auth/token/services/access-token.service.spec.ts b/packages/twenty-server/src/engine/core-modules/auth/token/services/access-token.service.spec.ts index ee7eb8f2d7b..5126094091d 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/token/services/access-token.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/token/services/access-token.service.spec.ts @@ -17,7 +17,7 @@ import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user import { UserEntity } from 'src/engine/core-modules/user/user.entity'; import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { AccessTokenService } from './access-token.service'; @@ -27,7 +27,7 @@ describe('AccessTokenService', () => { let twentyConfigService: TwentyConfigService; let userRepository: Repository; let workspaceRepository: Repository; - let twentyORMGlobalManager: TwentyORMGlobalManager; + let globalWorkspaceOrmManager: GlobalWorkspaceOrmManager; let userWorkspaceRepository: Repository; beforeEach(async () => { @@ -77,9 +77,12 @@ describe('AccessTokenService', () => { useValue: {}, }, { - provide: TwentyORMGlobalManager, + provide: GlobalWorkspaceOrmManager, useValue: { - getRepositoryForWorkspace: jest.fn(), + getRepository: jest.fn(), + executeInWorkspaceContext: jest + .fn() + .mockImplementation((_authContext: any, fn: () => any) => fn()), }, }, ], @@ -94,8 +97,8 @@ describe('AccessTokenService', () => { workspaceRepository = module.get>( getRepositoryToken(WorkspaceEntity), ); - twentyORMGlobalManager = module.get( - TwentyORMGlobalManager, + globalWorkspaceOrmManager = module.get( + GlobalWorkspaceOrmManager, ); userWorkspaceRepository = module.get>( getRepositoryToken(UserWorkspaceEntity), @@ -131,11 +134,9 @@ describe('AccessTokenService', () => { jest .spyOn(userWorkspaceRepository, 'findOne') .mockResolvedValue(mockUserWorkspace as UserWorkspaceEntity); - jest - .spyOn(twentyORMGlobalManager, 'getRepositoryForWorkspace') - .mockResolvedValue({ - findOne: jest.fn().mockResolvedValue(mockWorkspaceMember), - } as any); + jest.spyOn(globalWorkspaceOrmManager, 'getRepository').mockResolvedValue({ + findOne: jest.fn().mockResolvedValue(mockWorkspaceMember), + } as any); jest.spyOn(jwtWrapperService, 'sign').mockReturnValue(mockToken); const result = await service.generateAccessToken({ @@ -192,11 +193,9 @@ describe('AccessTokenService', () => { id: impersonatedUserWorkspaceId, workspaceId, } as UserWorkspaceEntity); - jest - .spyOn(twentyORMGlobalManager, 'getRepositoryForWorkspace') - .mockResolvedValue({ - findOne: jest.fn().mockResolvedValue(mockWorkspaceMember), - } as any); + jest.spyOn(globalWorkspaceOrmManager, 'getRepository').mockResolvedValue({ + findOne: jest.fn().mockResolvedValue(mockWorkspaceMember), + } as any); const signSpy = jest .spyOn(jwtWrapperService, 'sign') .mockReturnValue(mockToken); diff --git a/packages/twenty-server/src/engine/core-modules/auth/token/services/access-token.service.ts b/packages/twenty-server/src/engine/core-modules/auth/token/services/access-token.service.ts index 76149bbab2f..374a736b5ca 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/token/services/access-token.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/token/services/access-token.service.ts @@ -28,7 +28,8 @@ import { UserEntity } from 'src/engine/core-modules/user/user.entity'; import { userValidator } from 'src/engine/core-modules/user/user.validate'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; @Injectable() @@ -41,7 +42,7 @@ export class AccessTokenService { private readonly userRepository: Repository, @InjectRepository(WorkspaceEntity) private readonly workspaceRepository: Repository, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, @InjectRepository(UserWorkspaceEntity) private readonly userWorkspaceRepository: Repository, ) {} @@ -79,31 +80,39 @@ export class AccessTokenService { assertIsDefinedOrThrow(workspace, WorkspaceNotFoundDefaultError); if (isWorkspaceActiveOrSuspended(workspace)) { - const workspaceMemberRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'workspaceMember', - { shouldBypassPermissionChecks: true }, - ); + const authContext = buildSystemAuthContext(workspaceId); - const workspaceMember = await workspaceMemberRepository.findOne({ - where: { - userId: user.id, - }, - }); + tokenWorkspaceMemberId = + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workspaceMemberRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'workspaceMember', + { shouldBypassPermissionChecks: true }, + ); - assertIsDefinedOrThrow( - workspaceMember, - new AuthException( - 'User is not a member of the workspace', - AuthExceptionCode.FORBIDDEN_EXCEPTION, - { - userFriendlyMessage: msg`User is not a member of the workspace.`, + const workspaceMember = await workspaceMemberRepository.findOne({ + where: { + userId: user.id, + }, + }); + + assertIsDefinedOrThrow( + workspaceMember, + new AuthException( + 'User is not a member of the workspace', + AuthExceptionCode.FORBIDDEN_EXCEPTION, + { + userFriendlyMessage: msg`User is not a member of the workspace.`, + }, + ), + ); + + return workspaceMember.id; }, - ), - ); - - tokenWorkspaceMemberId = workspaceMember.id; + ); } const userWorkspace = await this.userWorkspaceRepository.findOne({ where: { diff --git a/packages/twenty-server/src/engine/core-modules/billing/commands/billing-sync-customer-data.command.ts b/packages/twenty-server/src/engine/core-modules/billing/commands/billing-sync-customer-data.command.ts index a1dcb91e7ad..4b6bc0341d8 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/commands/billing-sync-customer-data.command.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/commands/billing-sync-customer-data.command.ts @@ -12,7 +12,7 @@ import { BillingCustomerEntity } from 'src/engine/core-modules/billing/entities/ import { StripeSubscriptionService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription.service'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; @Command({ name: 'billing:sync-customer-data', @@ -25,10 +25,10 @@ export class BillingSyncCustomerDataCommand extends ActiveOrSuspendedWorkspacesM private readonly stripeSubscriptionService: StripeSubscriptionService, @InjectRepository(BillingCustomerEntity) protected readonly billingCustomerRepository: Repository, - protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, protected readonly dataSourceService: DataSourceService, ) { - super(workspaceRepository, twentyORMGlobalManager, dataSourceService); + super(workspaceRepository, globalWorkspaceOrmManager, dataSourceService); } override async runOnWorkspace({ diff --git a/packages/twenty-server/src/engine/core-modules/billing/commands/billing-update-subscription-price.command.ts b/packages/twenty-server/src/engine/core-modules/billing/commands/billing-update-subscription-price.command.ts index d9c45a4b750..82e03993b6d 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/commands/billing-update-subscription-price.command.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/commands/billing-update-subscription-price.command.ts @@ -13,7 +13,7 @@ import { BillingSubscriptionService } from 'src/engine/core-modules/billing/serv import { StripeSubscriptionItemService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription-item.service'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; @Command({ name: 'billing:update-subscription-price', @@ -27,14 +27,14 @@ export class BillingUpdateSubscriptionPriceCommand extends ActiveOrSuspendedWork constructor( @InjectRepository(WorkspaceEntity) protected readonly workspaceRepository: Repository, - protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, @InjectRepository(BillingSubscriptionEntity) protected readonly billingSubscriptionRepository: Repository, private readonly billingSubscriptionService: BillingSubscriptionService, private readonly stripeSubscriptionItemService: StripeSubscriptionItemService, protected readonly dataSourceService: DataSourceService, ) { - super(workspaceRepository, twentyORMGlobalManager, dataSourceService); + super(workspaceRepository, globalWorkspaceOrmManager, dataSourceService); } @Option({ diff --git a/packages/twenty-server/src/engine/core-modules/billing/jobs/update-subscription-quantity.job.ts b/packages/twenty-server/src/engine/core-modules/billing/jobs/update-subscription-quantity.job.ts index 6956a9836ba..9ecf62c5800 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/jobs/update-subscription-quantity.job.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/jobs/update-subscription-quantity.job.ts @@ -7,7 +7,8 @@ import { StripeSubscriptionItemService } from 'src/engine/core-modules/billing/s import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator'; import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; export type UpdateSubscriptionQuantityJobData = { workspaceId: string }; @@ -21,41 +22,48 @@ export class UpdateSubscriptionQuantityJob { constructor( private readonly billingSubscriptionService: BillingSubscriptionService, private readonly stripeSubscriptionItemService: StripeSubscriptionItemService, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, ) {} @Process(UpdateSubscriptionQuantityJob.name) async handle(data: UpdateSubscriptionQuantityJobData): Promise { - const workspaceMemberRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - data.workspaceId, - 'workspaceMember', - ); + const authContext = buildSystemAuthContext(data.workspaceId); - const workspaceMembersCount = await workspaceMemberRepository.count(); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workspaceMemberRepository = + await this.globalWorkspaceOrmManager.getRepository( + data.workspaceId, + 'workspaceMember', + ); - if (!workspaceMembersCount || workspaceMembersCount <= 0) { - return; - } + const workspaceMembersCount = await workspaceMemberRepository.count(); - try { - const billingBaseProductSubscriptionItem = - await this.billingSubscriptionService.getBaseProductCurrentBillingSubscriptionItemOrThrow( - data.workspaceId, - ); + if (!workspaceMembersCount || workspaceMembersCount <= 0) { + return; + } - await this.stripeSubscriptionItemService.updateSubscriptionItem( - billingBaseProductSubscriptionItem.stripeSubscriptionItemId, - { quantity: workspaceMembersCount }, - ); + try { + const billingBaseProductSubscriptionItem = + await this.billingSubscriptionService.getBaseProductCurrentBillingSubscriptionItemOrThrow( + data.workspaceId, + ); - this.logger.log( - `Updating workspace ${data.workspaceId} subscription quantity to ${workspaceMembersCount} members`, - ); - } catch (e) { - this.logger.warn( - `Failed to update workspace ${data.workspaceId} subscription quantity to ${workspaceMembersCount} members. Error: ${e}`, - ); - } + await this.stripeSubscriptionItemService.updateSubscriptionItem( + billingBaseProductSubscriptionItem.stripeSubscriptionItemId, + { quantity: workspaceMembersCount }, + ); + + this.logger.log( + `Updating workspace ${data.workspaceId} subscription quantity to ${workspaceMembersCount} members`, + ); + } catch (e) { + this.logger.warn( + `Failed to update workspace ${data.workspaceId} subscription quantity to ${workspaceMembersCount} members. Error: ${e}`, + ); + } + }, + ); } } diff --git a/packages/twenty-server/src/engine/core-modules/calendar/timeline-calendar-event.service.spec.ts b/packages/twenty-server/src/engine/core-modules/calendar/timeline-calendar-event.service.spec.ts index 44620718929..f1aa453f543 100644 --- a/packages/twenty-server/src/engine/core-modules/calendar/timeline-calendar-event.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/calendar/timeline-calendar-event.service.spec.ts @@ -2,8 +2,8 @@ import { Test, type TestingModule } from '@nestjs/testing'; import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { CalendarChannelVisibility } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; import { type CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity'; @@ -36,18 +36,19 @@ describe('TimelineCalendarEventService', () => { findAndCount: jest.fn(), }; - const mockTwentyORMGlobalManager = { - getRepositoryForWorkspace: jest + const mockGlobalWorkspaceOrmManager = { + getRepository: jest.fn().mockResolvedValue(mockCalendarEventRepository), + executeInWorkspaceContext: jest .fn() - .mockResolvedValue(mockCalendarEventRepository), + .mockImplementation((_authContext: any, fn: () => any) => fn()), }; const module: TestingModule = await Test.createTestingModule({ providers: [ TimelineCalendarEventService, { - provide: TwentyORMGlobalManager, - useValue: mockTwentyORMGlobalManager, + provide: GlobalWorkspaceOrmManager, + useValue: mockGlobalWorkspaceOrmManager, }, ], }).compile(); diff --git a/packages/twenty-server/src/engine/core-modules/calendar/timeline-calendar-event.service.ts b/packages/twenty-server/src/engine/core-modules/calendar/timeline-calendar-event.service.ts index 839ce524d88..905792f798e 100644 --- a/packages/twenty-server/src/engine/core-modules/calendar/timeline-calendar-event.service.ts +++ b/packages/twenty-server/src/engine/core-modules/calendar/timeline-calendar-event.service.ts @@ -6,7 +6,8 @@ import { Any } from 'typeorm'; import { TIMELINE_CALENDAR_EVENTS_DEFAULT_PAGE_SIZE } from 'src/engine/core-modules/calendar/constants/calendar.constants'; import { type TimelineCalendarEventsWithTotalDTO } from 'src/engine/core-modules/calendar/dtos/timeline-calendar-events-with-total.dto'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { CalendarChannelVisibility } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; import { type CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity'; import { type OpportunityWorkspaceEntity } from 'src/modules/opportunity/standard-objects/opportunity.workspace-entity'; @@ -15,10 +16,9 @@ import { type PersonWorkspaceEntity } from 'src/modules/person/standard-objects/ @Injectable() export class TimelineCalendarEventService { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, ) {} - // TODO: Align return type with the entities to avoid mapping async getCalendarEventsFromPersonIds({ currentWorkspaceMemberId, personIds, @@ -32,136 +32,141 @@ export class TimelineCalendarEventService { page: number; pageSize: number; }): Promise { - const offset = (page - 1) * pageSize; + const authContext = buildSystemAuthContext(workspaceId); - const calendarEventRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'calendarEvent', - ); + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const offset = (page - 1) * pageSize; - const calendarEventIds = await calendarEventRepository.find({ - where: { - calendarEventParticipants: { - personId: Any(personIds), - }, - }, - select: { - id: true, - startsAt: true, - }, - skip: offset, - take: pageSize, - order: { - startsAt: 'DESC', - }, - }); + const calendarEventRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'calendarEvent', + ); - const ids = calendarEventIds.map(({ id }) => id); - - if (ids.length <= 0) { - return { - totalNumberOfCalendarEvents: 0, - timelineCalendarEvents: [], - }; - } - - // We've split the query into two parts, because we want to fetch all the participants without any filtering - const [events, total] = await calendarEventRepository.findAndCount({ - where: { - id: Any(ids), - }, - relations: { - calendarEventParticipants: { - person: true, - workspaceMember: true, - }, - calendarChannelEventAssociations: { - calendarChannel: { - connectedAccount: { - accountOwner: true, + const calendarEventIds = await calendarEventRepository.find({ + where: { + calendarEventParticipants: { + personId: Any(personIds), }, }, - }, - }, - }); + select: { + id: true, + startsAt: true, + }, + skip: offset, + take: pageSize, + order: { + startsAt: 'DESC', + }, + }); - // Keep events in the same order as they ids were returned - const orderedEvents = events.sort( - (a, b) => ids.indexOf(a.id) - ids.indexOf(b.id), - ); + const ids = calendarEventIds.map(({ id }) => id); - const timelineCalendarEvents = orderedEvents.map((event) => { - const participants = event.calendarEventParticipants.map( - (participant) => ({ - calendarEventId: event.id, - personId: participant.personId ?? null, - workspaceMemberId: participant.workspaceMemberId ?? null, - firstName: - participant.person?.name?.firstName || - participant.workspaceMember?.name.firstName || - '', - lastName: - participant.person?.name?.lastName || - participant.workspaceMember?.name.lastName || - '', - displayName: - participant.person?.name?.firstName || - participant.person?.name?.lastName || - participant.workspaceMember?.name.firstName || - participant.workspaceMember?.name.lastName || - participant.displayName || - participant.handle || - '', - avatarUrl: - participant.person?.avatarUrl || - participant.workspaceMember?.avatarUrl || - '', - handle: participant.handle ?? '', - }), - ); + if (ids.length <= 0) { + return { + totalNumberOfCalendarEvents: 0, + timelineCalendarEvents: [], + }; + } - const isCalendarEventImportedByCurrentWorkspaceMember = - event.calendarChannelEventAssociations.some( - (association) => - association.calendarChannel.connectedAccount.accountOwnerId === - currentWorkspaceMemberId, + const [events, total] = await calendarEventRepository.findAndCount({ + where: { + id: Any(ids), + }, + relations: { + calendarEventParticipants: { + person: true, + workspaceMember: true, + }, + calendarChannelEventAssociations: { + calendarChannel: { + connectedAccount: { + accountOwner: true, + }, + }, + }, + }, + }); + + const orderedEvents = events.sort( + (a, b) => ids.indexOf(a.id) - ids.indexOf(b.id), ); - const visibility = - event.calendarChannelEventAssociations.some( - (association) => - association.calendarChannel.visibility === 'SHARE_EVERYTHING', - ) || isCalendarEventImportedByCurrentWorkspaceMember - ? CalendarChannelVisibility.SHARE_EVERYTHING - : CalendarChannelVisibility.METADATA; + const timelineCalendarEvents = orderedEvents.map((event) => { + const participants = event.calendarEventParticipants.map( + (participant) => ({ + calendarEventId: event.id, + personId: participant.personId ?? null, + workspaceMemberId: participant.workspaceMemberId ?? null, + firstName: + participant.person?.name?.firstName || + participant.workspaceMember?.name.firstName || + '', + lastName: + participant.person?.name?.lastName || + participant.workspaceMember?.name.lastName || + '', + displayName: + participant.person?.name?.firstName || + participant.person?.name?.lastName || + participant.workspaceMember?.name.firstName || + participant.workspaceMember?.name.lastName || + participant.displayName || + participant.handle || + '', + avatarUrl: + participant.person?.avatarUrl || + participant.workspaceMember?.avatarUrl || + '', + handle: participant.handle ?? '', + }), + ); - return { - ...omit(event, [ - 'calendarEventParticipants', - 'calendarChannelEventAssociations', - ]), - title: - visibility === CalendarChannelVisibility.METADATA - ? FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED - : (event.title ?? ''), - description: - visibility === CalendarChannelVisibility.METADATA - ? FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED - : (event.description ?? ''), - startsAt: event.startsAt as unknown as Date, - endsAt: event.endsAt as unknown as Date, - participants, - visibility, - location: event.location ?? '', - conferenceSolution: event.conferenceSolution ?? '', - }; - }); + const isCalendarEventImportedByCurrentWorkspaceMember = + event.calendarChannelEventAssociations.some( + (association) => + association.calendarChannel.connectedAccount.accountOwnerId === + currentWorkspaceMemberId, + ); - return { - totalNumberOfCalendarEvents: total, - timelineCalendarEvents, - }; + const visibility = + event.calendarChannelEventAssociations.some( + (association) => + association.calendarChannel.visibility === 'SHARE_EVERYTHING', + ) || isCalendarEventImportedByCurrentWorkspaceMember + ? CalendarChannelVisibility.SHARE_EVERYTHING + : CalendarChannelVisibility.METADATA; + + return { + ...omit(event, [ + 'calendarEventParticipants', + 'calendarChannelEventAssociations', + ]), + title: + visibility === CalendarChannelVisibility.METADATA + ? FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED + : (event.title ?? ''), + description: + visibility === CalendarChannelVisibility.METADATA + ? FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED + : (event.description ?? ''), + startsAt: event.startsAt as unknown as Date, + endsAt: event.endsAt as unknown as Date, + participants, + visibility, + location: event.location ?? '', + conferenceSolution: event.conferenceSolution ?? '', + }; + }); + + return { + totalNumberOfCalendarEvents: total, + timelineCalendarEvents, + }; + }, + ); } async getCalendarEventsFromCompanyId({ @@ -177,39 +182,46 @@ export class TimelineCalendarEventService { page: number; pageSize: number; }): Promise { - const personRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'person', - ); + const authContext = buildSystemAuthContext(workspaceId); - const personIds = await personRepository.find({ - where: { - companyId, + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const personRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'person', + ); + + const personIds = await personRepository.find({ + where: { + companyId, + }, + select: { + id: true, + }, + }); + + if (personIds.length <= 0) { + return { + totalNumberOfCalendarEvents: 0, + timelineCalendarEvents: [], + }; + } + + const formattedPersonIds = personIds.map(({ id }) => id); + + const calendarEvents = await this.getCalendarEventsFromPersonIds({ + currentWorkspaceMemberId, + personIds: formattedPersonIds, + workspaceId, + page, + pageSize, + }); + + return calendarEvents; }, - select: { - id: true, - }, - }); - - if (personIds.length <= 0) { - return { - totalNumberOfCalendarEvents: 0, - timelineCalendarEvents: [], - }; - } - - const formattedPersonIds = personIds.map(({ id }) => id); - - const calendarEvents = await this.getCalendarEventsFromPersonIds({ - currentWorkspaceMemberId, - personIds: formattedPersonIds, - workspaceId, - page, - pageSize, - }); - - return calendarEvents; + ); } async getCalendarEventsFromOpportunityId({ @@ -225,36 +237,43 @@ export class TimelineCalendarEventService { page: number; pageSize: number; }): Promise { - const opportunityRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'opportunity', - ); + const authContext = buildSystemAuthContext(workspaceId); - const opportunity = await opportunityRepository.findOne({ - where: { - id: opportunityId, + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const opportunityRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'opportunity', + ); + + const opportunity = await opportunityRepository.findOne({ + where: { + id: opportunityId, + }, + select: { + companyId: true, + }, + }); + + if (!opportunity?.companyId) { + return { + totalNumberOfCalendarEvents: 0, + timelineCalendarEvents: [], + }; + } + + const calendarEvents = await this.getCalendarEventsFromCompanyId({ + currentWorkspaceMemberId, + companyId: opportunity.companyId, + workspaceId, + page, + pageSize, + }); + + return calendarEvents; }, - select: { - companyId: true, - }, - }); - - if (!opportunity?.companyId) { - return { - totalNumberOfCalendarEvents: 0, - timelineCalendarEvents: [], - }; - } - - const calendarEvents = await this.getCalendarEventsFromCompanyId({ - currentWorkspaceMemberId, - companyId: opportunity.companyId, - workspaceId, - page, - pageSize, - }); - - return calendarEvents; + ); } } diff --git a/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/imap-smtp-caldav-connection.resolver.ts b/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/imap-smtp-caldav-connection.resolver.ts index 25119dac6ba..b12b219d2ac 100644 --- a/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/imap-smtp-caldav-connection.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/imap-smtp-caldav-connection.resolver.ts @@ -7,9 +7,8 @@ import { } from '@nestjs/common'; import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; -import { ConnectedAccountProvider } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; import { PermissionFlagType } from 'twenty-shared/constants'; +import { isDefined } from 'twenty-shared/utils'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter'; @@ -27,9 +26,7 @@ import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorat import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { ImapSmtpCalDavAPIService } from 'src/modules/connected-account/services/imap-smtp-caldav-apis.service'; -import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; @Resolver() @UsePipes(ResolverValidationPipe) @@ -37,7 +34,6 @@ import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-acco @UseGuards(SettingsPermissionGuard(PermissionFlagType.WORKSPACE)) export class ImapSmtpCaldavResolver { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, private readonly ImapSmtpCaldavConnectionService: ImapSmtpCaldavService, private readonly imapSmtpCaldavApisService: ImapSmtpCalDavAPIService, private readonly featureFlagService: FeatureFlagService, @@ -50,16 +46,12 @@ export class ImapSmtpCaldavResolver { @Args('id', { type: () => UUIDScalarType }) id: string, @AuthWorkspace() workspace: WorkspaceEntity, ): Promise { - const connectedAccountRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( + const connectedAccount = + await this.imapSmtpCaldavApisService.getImapSmtpCaldavConnectedAccount( workspace.id, - 'connectedAccount', + id, ); - const connectedAccount = await connectedAccountRepository.findOne({ - where: { id, provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV }, - }); - if (!isDefined(connectedAccount) || !isDefined(connectedAccount?.handle)) { throw new UserInputError( `Connected mail account with ID ${id} not found`, diff --git a/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/services/imap-smtp-caldav-connection.service.ts b/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/services/imap-smtp-caldav-connection.service.ts index a71b9d68a8e..8d09a4374b0 100644 --- a/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/services/imap-smtp-caldav-connection.service.ts +++ b/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/services/imap-smtp-caldav-connection.service.ts @@ -10,7 +10,8 @@ import { type AccountType, type ConnectionParameters, } from 'src/engine/core-modules/imap-smtp-caldav-connection/types/imap-smtp-caldav-connection.type'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { CalDAVClient } from 'src/modules/calendar/calendar-event-import-manager/drivers/caldav/lib/caldav.client'; import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; @@ -19,7 +20,7 @@ export class ImapSmtpCaldavService { private readonly logger = new Logger(ImapSmtpCaldavService.name); constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, ) {} async testImapConnection( @@ -175,19 +176,26 @@ export class ImapSmtpCaldavService { workspaceId: string, connectionId: string, ): Promise { - const connectedAccountRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'connectedAccount', - ); + const authContext = buildSystemAuthContext(workspaceId); - const connectedAccount = await connectedAccountRepository.findOne({ - where: { - id: connectionId, - provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV, + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const connectedAccountRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'connectedAccount', + ); + + const connectedAccount = await connectedAccountRepository.findOne({ + where: { + id: connectionId, + provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV, + }, + }); + + return connectedAccount; }, - }); - - return connectedAccount; + ); } } diff --git a/packages/twenty-server/src/engine/core-modules/messaging/services/get-messages.service.ts b/packages/twenty-server/src/engine/core-modules/messaging/services/get-messages.service.ts index 0203313569f..3f0bbca6a86 100644 --- a/packages/twenty-server/src/engine/core-modules/messaging/services/get-messages.service.ts +++ b/packages/twenty-server/src/engine/core-modules/messaging/services/get-messages.service.ts @@ -4,14 +4,15 @@ import { TIMELINE_THREADS_DEFAULT_PAGE_SIZE } from 'src/engine/core-modules/mess import { type TimelineThreadsWithTotalDTO } from 'src/engine/core-modules/messaging/dtos/timeline-threads-with-total.dto'; import { TimelineMessagingService } from 'src/engine/core-modules/messaging/services/timeline-messaging.service'; import { formatThreads } from 'src/engine/core-modules/messaging/utils/format-threads.util'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { type OpportunityWorkspaceEntity } from 'src/modules/opportunity/standard-objects/opportunity.workspace-entity'; import { type PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity'; @Injectable() export class GetMessagesService { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly timelineMessagingService: TimelineMessagingService, ) {} @@ -73,38 +74,45 @@ export class GetMessagesService { page = 1, pageSize: number = TIMELINE_THREADS_DEFAULT_PAGE_SIZE, ): Promise { - const personRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'person', - ); - const personIds = ( - await personRepository.find({ - where: { - companyId, - }, - select: { - id: true, - }, - }) - ).map((person) => person.id); + const authContext = buildSystemAuthContext(workspaceId); - if (personIds.length === 0) { - return { - totalNumberOfThreads: 0, - timelineThreads: [], - }; - } + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const personRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'person', + ); + const personIds = ( + await personRepository.find({ + where: { + companyId, + }, + select: { + id: true, + }, + }) + ).map((person) => person.id); - const messageThreads = await this.getMessagesFromPersonIds( - workspaceMemberId, - personIds, - workspaceId, - page, - pageSize, + if (personIds.length === 0) { + return { + totalNumberOfThreads: 0, + timelineThreads: [], + }; + } + + const messageThreads = await this.getMessagesFromPersonIds( + workspaceMemberId, + personIds, + workspaceId, + page, + pageSize, + ); + + return messageThreads; + }, ); - - return messageThreads; } async getMessagesFromOpportunityId( @@ -114,36 +122,43 @@ export class GetMessagesService { page = 1, pageSize: number = TIMELINE_THREADS_DEFAULT_PAGE_SIZE, ): Promise { - const opportunityRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'opportunity', - ); + const authContext = buildSystemAuthContext(workspaceId); - const opportunity = await opportunityRepository.findOne({ - where: { - id: opportunityId, + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const opportunityRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'opportunity', + ); + + const opportunity = await opportunityRepository.findOne({ + where: { + id: opportunityId, + }, + select: { + companyId: true, + }, + }); + + if (!opportunity?.companyId) { + return { + totalNumberOfThreads: 0, + timelineThreads: [], + }; + } + + const messageThreads = await this.getMessagesFromCompanyId( + workspaceMemberId, + opportunity.companyId, + workspaceId, + page, + pageSize, + ); + + return messageThreads; }, - select: { - companyId: true, - }, - }); - - if (!opportunity?.companyId) { - return { - totalNumberOfThreads: 0, - timelineThreads: [], - }; - } - - const messageThreads = await this.getMessagesFromCompanyId( - workspaceMemberId, - opportunity.companyId, - workspaceId, - page, - pageSize, ); - - return messageThreads; } } diff --git a/packages/twenty-server/src/engine/core-modules/messaging/services/timeline-messaging.service.ts b/packages/twenty-server/src/engine/core-modules/messaging/services/timeline-messaging.service.ts index adbd2bfa2bd..203868a9ea4 100644 --- a/packages/twenty-server/src/engine/core-modules/messaging/services/timeline-messaging.service.ts +++ b/packages/twenty-server/src/engine/core-modules/messaging/services/timeline-messaging.service.ts @@ -1,10 +1,11 @@ import { Injectable } from '@nestjs/common'; -import { In } from 'typeorm'; import { MessageParticipantRole } from 'twenty-shared/types'; +import { In } from 'typeorm'; import { type TimelineThreadDTO } from 'src/engine/core-modules/messaging/dtos/timeline-thread.dto'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; import { type MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity'; import { type MessageThreadWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-thread.workspace-entity'; @@ -12,7 +13,7 @@ import { type MessageThreadWorkspaceEntity } from 'src/modules/messaging/common/ @Injectable() export class TimelineMessagingService { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, ) {} public async getAndCountMessageThreads( @@ -31,63 +32,74 @@ export class TimelineMessagingService { >[]; totalNumberOfThreads: number; }> { - const messageThreadRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'messageThread', - ); + const authContext = buildSystemAuthContext(workspaceId); - const totalNumberOfThreads = await messageThreadRepository - .createQueryBuilder('messageThread') - .innerJoin('messageThread.messages', 'messages') - .innerJoin('messages.messageParticipants', 'messageParticipants') - .where('messageParticipants.personId IN(:...personIds)', { personIds }) - .groupBy('messageThread.id') - .getCount(); + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const messageThreadRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'messageThread', + ); - const threadIdsQuery = await messageThreadRepository - .createQueryBuilder('messageThread') - .select('messageThread.id', 'id') - .addSelect('MAX(messages.receivedAt)', 'max_received_at') - .innerJoin('messageThread.messages', 'messages') - .innerJoin('messages.messageParticipants', 'messageParticipants') - .where('messageParticipants.personId IN (:...personIds)', { personIds }) - .groupBy('messageThread.id') - .orderBy('max_received_at', 'DESC') - .offset(offset) - .limit(pageSize) - .getRawMany(); + const totalNumberOfThreads = await messageThreadRepository + .createQueryBuilder('messageThread') + .innerJoin('messageThread.messages', 'messages') + .innerJoin('messages.messageParticipants', 'messageParticipants') + .where('messageParticipants.personId IN(:...personIds)', { + personIds, + }) + .groupBy('messageThread.id') + .getCount(); - const messageThreadIds = threadIdsQuery.map((thread) => thread.id); + const threadIdsQuery = await messageThreadRepository + .createQueryBuilder('messageThread') + .select('messageThread.id', 'id') + .addSelect('MAX(messages.receivedAt)', 'max_received_at') + .innerJoin('messageThread.messages', 'messages') + .innerJoin('messages.messageParticipants', 'messageParticipants') + .where('messageParticipants.personId IN (:...personIds)', { + personIds, + }) + .groupBy('messageThread.id') + .orderBy('max_received_at', 'DESC') + .offset(offset) + .limit(pageSize) + .getRawMany(); - const messageThreads = await messageThreadRepository.find({ - where: { - id: In(messageThreadIds), - }, - order: { - messages: { - receivedAt: 'DESC', - }, - }, - relations: ['messages'], - }); + const messageThreadIds = threadIdsQuery.map((thread) => thread.id); - return { - messageThreads: messageThreads.map((messageThread) => { - const lastMessage = messageThread.messages[0]; - const firstMessage = - messageThread.messages[messageThread.messages.length - 1]; + const messageThreads = await messageThreadRepository.find({ + where: { + id: In(messageThreadIds), + }, + order: { + messages: { + receivedAt: 'DESC', + }, + }, + relations: ['messages'], + }); return { - id: messageThread.id, - subject: firstMessage.subject ?? '', - lastMessageBody: lastMessage.text ?? '', - lastMessageReceivedAt: lastMessage.receivedAt ?? new Date(), - numberOfMessagesInThread: messageThread.messages.length, + messageThreads: messageThreads.map((messageThread) => { + const lastMessage = messageThread.messages[0]; + const firstMessage = + messageThread.messages[messageThread.messages.length - 1]; + + return { + id: messageThread.id, + subject: firstMessage.subject ?? '', + lastMessageBody: lastMessage.text ?? '', + lastMessageReceivedAt: lastMessage.receivedAt ?? new Date(), + numberOfMessagesInThread: messageThread.messages.length, + }; + }), + totalNumberOfThreads, }; - }), - totalNumberOfThreads, - }; + }, + ); } public async getThreadParticipantsByThreadId( @@ -96,89 +108,96 @@ export class TimelineMessagingService { ): Promise<{ [key: string]: MessageParticipantWorkspaceEntity[]; }> { - const messageParticipantRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'messageParticipant', - ); + const authContext = buildSystemAuthContext(workspaceId); - const threadParticipants = await messageParticipantRepository - .createQueryBuilder() - .select('messageParticipant') - .addSelect('message.messageThreadId') - .addSelect('message.receivedAt') - .leftJoinAndSelect('messageParticipant.person', 'person') - .leftJoinAndSelect( - 'messageParticipant.workspaceMember', - 'workspaceMember', - ) - .leftJoin('messageParticipant.message', 'message') - .where('message.messageThreadId = ANY(:messageThreadIds)', { - messageThreadIds, - }) - .andWhere('messageParticipant.role = :role', { - role: MessageParticipantRole.FROM, - }) - .orderBy('message.messageThreadId') - .distinctOn(['message.messageThreadId', 'messageParticipant.handle']) - .getMany(); + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const messageParticipantRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'messageParticipant', + ); - // This is because subqueries are not handled by twentyORM - const orderedThreadParticipants = threadParticipants.sort( - (a, b) => - (a.message.receivedAt ?? new Date()).getTime() - - (b.message.receivedAt ?? new Date()).getTime(), - ); + const threadParticipants = await messageParticipantRepository + .createQueryBuilder() + .select('messageParticipant') + .addSelect('message.messageThreadId') + .addSelect('message.receivedAt') + .leftJoinAndSelect('messageParticipant.person', 'person') + .leftJoinAndSelect( + 'messageParticipant.workspaceMember', + 'workspaceMember', + ) + .leftJoin('messageParticipant.message', 'message') + .where('message.messageThreadId = ANY(:messageThreadIds)', { + messageThreadIds, + }) + .andWhere('messageParticipant.role = :role', { + role: MessageParticipantRole.FROM, + }) + .orderBy('message.messageThreadId') + .distinctOn(['message.messageThreadId', 'messageParticipant.handle']) + .getMany(); - // This is because composite fields are not handled correctly by the ORM - const threadParticipantsWithCompositeFields = orderedThreadParticipants.map( - (threadParticipant) => ({ - ...threadParticipant, - person: { - id: threadParticipant.person?.id, - name: { - //eslint-disable-next-line - //@ts-ignore - firstName: threadParticipant.person?.nameFirstName, - //eslint-disable-next-line - //@ts-ignore - lastName: threadParticipant.person?.nameLastName, - }, - avatarUrl: threadParticipant.person?.avatarUrl, - }, - workspaceMember: { - id: threadParticipant.workspaceMember?.id, - name: { - //eslint-disable-next-line - //@ts-ignore - firstName: threadParticipant.workspaceMember?.nameFirstName, - //eslint-disable-next-line - //@ts-ignore - lastName: threadParticipant.workspaceMember?.nameLastName, - }, - avatarUrl: threadParticipant.workspaceMember?.avatarUrl, - }, - }), - ); - - return threadParticipantsWithCompositeFields.reduce( - (threadParticipantsAcc, threadParticipant) => { - if (!threadParticipant.message.messageThreadId) - return threadParticipantsAcc; - - // @ts-expect-error legacy noImplicitAny - if (!threadParticipantsAcc[threadParticipant.message.messageThreadId]) - // @ts-expect-error legacy noImplicitAny - threadParticipantsAcc[threadParticipant.message.messageThreadId] = []; - - // @ts-expect-error legacy noImplicitAny - threadParticipantsAcc[threadParticipant.message.messageThreadId].push( - threadParticipant, + const orderedThreadParticipants = threadParticipants.sort( + (a, b) => + (a.message.receivedAt ?? new Date()).getTime() - + (b.message.receivedAt ?? new Date()).getTime(), ); - return threadParticipantsAcc; + const threadParticipantsWithCompositeFields = + orderedThreadParticipants.map((threadParticipant) => ({ + ...threadParticipant, + person: { + id: threadParticipant.person?.id, + name: { + //eslint-disable-next-line + //@ts-ignore + firstName: threadParticipant.person?.nameFirstName, + //eslint-disable-next-line + //@ts-ignore + lastName: threadParticipant.person?.nameLastName, + }, + avatarUrl: threadParticipant.person?.avatarUrl, + }, + workspaceMember: { + id: threadParticipant.workspaceMember?.id, + name: { + //eslint-disable-next-line + //@ts-ignore + firstName: threadParticipant.workspaceMember?.nameFirstName, + //eslint-disable-next-line + //@ts-ignore + lastName: threadParticipant.workspaceMember?.nameLastName, + }, + avatarUrl: threadParticipant.workspaceMember?.avatarUrl, + }, + })); + + return threadParticipantsWithCompositeFields.reduce( + (threadParticipantsAcc, threadParticipant) => { + if (!threadParticipant.message.messageThreadId) + return threadParticipantsAcc; + + if ( + // @ts-expect-error legacy noImplicitAny + !threadParticipantsAcc[threadParticipant.message.messageThreadId] + ) + // @ts-expect-error legacy noImplicitAny + threadParticipantsAcc[threadParticipant.message.messageThreadId] = + []; + + // @ts-expect-error legacy noImplicitAny + threadParticipantsAcc[ + threadParticipant.message.messageThreadId + ].push(threadParticipant); + + return threadParticipantsAcc; + }, + {}, + ); }, - {}, ); } @@ -189,58 +208,65 @@ export class TimelineMessagingService { ): Promise<{ [key: string]: MessageChannelVisibility; }> { - const messageThreadRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'messageThread', - ); + const authContext = buildSystemAuthContext(workspaceId); - const threadVisibility = await messageThreadRepository - .createQueryBuilder() - .select('messageThread.id', 'id') - .addSelect('messageChannel.visibility', 'visibility') - .addSelect('connectedAccount.accountOwnerId', 'accountOwnerId') - .leftJoin('messageThread.messages', 'message') - .leftJoin( - 'message.messageChannelMessageAssociations', - 'messageChannelMessageAssociation', - ) - .leftJoin( - 'messageChannelMessageAssociation.messageChannel', - 'messageChannel', - ) - .leftJoin('messageChannel.connectedAccount', 'connectedAccount') - .where('messageThread.id = ANY(:messageThreadIds)', { - messageThreadIds: messageThreadIds, - }) - .getRawMany(); + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const messageThreadRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'messageThread', + ); - const visibilityValues = Object.values(MessageChannelVisibility); - - const threadVisibilityByThreadId: { - [key: string]: MessageChannelVisibility; - } = threadVisibility.reduce((threadVisibilityAcc, threadVisibility) => { - if (threadVisibility.accountOwnerId === workspaceMemberId) { - threadVisibilityAcc[threadVisibility.id] = - MessageChannelVisibility.SHARE_EVERYTHING; - - return threadVisibilityAcc; - } - - threadVisibilityAcc[threadVisibility.id] = - visibilityValues[ - Math.max( - visibilityValues.indexOf(threadVisibility.visibility), - visibilityValues.indexOf( - threadVisibilityAcc[threadVisibility.id] ?? - MessageChannelVisibility.METADATA, - ), + const threadVisibility = await messageThreadRepository + .createQueryBuilder() + .select('messageThread.id', 'id') + .addSelect('messageChannel.visibility', 'visibility') + .addSelect('connectedAccount.accountOwnerId', 'accountOwnerId') + .leftJoin('messageThread.messages', 'message') + .leftJoin( + 'message.messageChannelMessageAssociations', + 'messageChannelMessageAssociation', ) - ]; + .leftJoin( + 'messageChannelMessageAssociation.messageChannel', + 'messageChannel', + ) + .leftJoin('messageChannel.connectedAccount', 'connectedAccount') + .where('messageThread.id = ANY(:messageThreadIds)', { + messageThreadIds: messageThreadIds, + }) + .getRawMany(); - return threadVisibilityAcc; - }, {}); + const visibilityValues = Object.values(MessageChannelVisibility); - return threadVisibilityByThreadId; + const threadVisibilityByThreadId: { + [key: string]: MessageChannelVisibility; + } = threadVisibility.reduce((threadVisibilityAcc, threadVisibility) => { + if (threadVisibility.accountOwnerId === workspaceMemberId) { + threadVisibilityAcc[threadVisibility.id] = + MessageChannelVisibility.SHARE_EVERYTHING; + + return threadVisibilityAcc; + } + + threadVisibilityAcc[threadVisibility.id] = + visibilityValues[ + Math.max( + visibilityValues.indexOf(threadVisibility.visibility), + visibilityValues.indexOf( + threadVisibilityAcc[threadVisibility.id] ?? + MessageChannelVisibility.METADATA, + ), + ) + ]; + + return threadVisibilityAcc; + }, {}); + + return threadVisibilityByThreadId; + }, + ); } } diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/services/create-record.service.ts b/packages/twenty-server/src/engine/core-modules/record-crud/services/create-record.service.ts index 18844afe897..3081de8d9d5 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/services/create-record.service.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/services/create-record.service.ts @@ -16,14 +16,15 @@ import { RecordInputTransformerService } from 'src/engine/core-modules/record-tr import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type'; import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; @Injectable() export class CreateRecordService { private readonly logger = new Logger(CreateRecordService.name); constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly recordPositionService: RecordPositionService, private readonly recordInputTransformerService: RecordInputTransformerService, ) {} @@ -40,116 +41,124 @@ export class CreateRecordService { }; } + const authContext = buildSystemAuthContext(workspaceId); + try { - const repository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - objectName, - rolePermissionConfig, - ); + return await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const repository = await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + objectName, + rolePermissionConfig, + ); - const { - flatObjectMetadataMaps, - flatFieldMetadataMaps, - objectIdByNameSingular, - } = repository.internalContext; + const { + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, + } = repository.internalContext; - const objectId = objectIdByNameSingular[objectName]; + const objectId = objectIdByNameSingular[objectName]; - if (!isDefined(objectId)) { - throw new RecordCrudException( - `Object ${objectName} not found`, - RecordCrudExceptionCode.INVALID_REQUEST, - ); - } + if (!isDefined(objectId)) { + throw new RecordCrudException( + `Object ${objectName} not found`, + RecordCrudExceptionCode.INVALID_REQUEST, + ); + } - const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({ - flatEntityMaps: flatObjectMetadataMaps, - flatEntityId: objectId, - }); + const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityMaps: flatObjectMetadataMaps, + flatEntityId: objectId, + }); - if ( - !canObjectBeManagedByWorkflow({ - nameSingular: flatObjectMetadata.nameSingular, - isSystem: flatObjectMetadata.isSystem, - }) - ) { - throw new RecordCrudException( - 'Failed to create: Object cannot be created by workflow', - RecordCrudExceptionCode.INVALID_REQUEST, - ); - } + if ( + !canObjectBeManagedByWorkflow({ + nameSingular: flatObjectMetadata.nameSingular, + isSystem: flatObjectMetadata.isSystem, + }) + ) { + throw new RecordCrudException( + 'Failed to create: Object cannot be created by workflow', + RecordCrudExceptionCode.INVALID_REQUEST, + ); + } - const position = await this.recordPositionService.buildRecordPosition({ - value: 'first', - objectMetadata: flatObjectMetadata, - workspaceId, - }); + const position = await this.recordPositionService.buildRecordPosition( + { + value: 'first', + objectMetadata: flatObjectMetadata, + workspaceId, + }, + ); - const { fieldIdByName, fieldIdByJoinColumnName } = - buildFieldMapsFromFlatObjectMetadata( - flatFieldMetadataMaps, - flatObjectMetadata, - ); + const { fieldIdByName, fieldIdByJoinColumnName } = + buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + flatObjectMetadata, + ); - const validObjectRecord = Object.fromEntries( - Object.entries(objectRecord).filter( - ([key]) => - isDefined(fieldIdByName[key]) || - isDefined(fieldIdByJoinColumnName[key]), - ), - ); + const validObjectRecord = Object.fromEntries( + Object.entries(objectRecord).filter( + ([key]) => + isDefined(fieldIdByName[key]) || + isDefined(fieldIdByJoinColumnName[key]), + ), + ); - const transformedObjectRecord = - await this.recordInputTransformerService.process({ - recordInput: validObjectRecord, - flatObjectMetadata, - flatFieldMetadataMaps, - }); - - const restrictedFields = - repository.objectRecordsPermissions?.[flatObjectMetadata.id] - ?.restrictedFields; - - const selectedColumns = getSelectedColumnsFromRestrictedFields( - restrictedFields, - flatObjectMetadata, - flatFieldMetadataMaps, - ); - - const insertResult = await repository.insert( - { - ...transformedObjectRecord, - position, - createdBy: params.createdBy ?? { - source: FieldActorSource.WORKFLOW, - name: 'Workflow', - }, - }, - undefined, - selectedColumns, - ); - - const [createdRecord] = insertResult.generatedMaps; - - this.logger.log(`Record created successfully in ${objectName}`); - - return { - success: true, - message: `Record created successfully in ${objectName}`, - result: createdRecord, - recordReferences: [ - { - objectNameSingular: objectName, - recordId: createdRecord.id, - displayName: getRecordDisplayName( - { ...transformedObjectRecord, ...createdRecord }, + const transformedObjectRecord = + await this.recordInputTransformerService.process({ + recordInput: validObjectRecord, flatObjectMetadata, flatFieldMetadataMaps, - ), - }, - ], - }; + }); + + const restrictedFields = + repository.objectRecordsPermissions?.[flatObjectMetadata.id] + ?.restrictedFields; + + const selectedColumns = getSelectedColumnsFromRestrictedFields( + restrictedFields, + flatObjectMetadata, + flatFieldMetadataMaps, + ); + + const insertResult = await repository.insert( + { + ...transformedObjectRecord, + position, + createdBy: params.createdBy ?? { + source: FieldActorSource.WORKFLOW, + name: 'Workflow', + }, + }, + undefined, + selectedColumns, + ); + + const [createdRecord] = insertResult.generatedMaps; + + this.logger.log(`Record created successfully in ${objectName}`); + + return { + success: true, + message: `Record created successfully in ${objectName}`, + result: createdRecord, + recordReferences: [ + { + objectNameSingular: objectName, + recordId: createdRecord.id, + displayName: getRecordDisplayName( + { ...transformedObjectRecord, ...createdRecord }, + flatObjectMetadata, + flatFieldMetadataMaps, + ), + }, + ], + }; + }, + ); } catch (error) { if (error instanceof RecordCrudException) { return { diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/services/delete-record.service.ts b/packages/twenty-server/src/engine/core-modules/record-crud/services/delete-record.service.ts index 417350b3468..ccbd8e62844 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/services/delete-record.service.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/services/delete-record.service.ts @@ -10,14 +10,15 @@ import { import { type DeleteRecordParams } from 'src/engine/core-modules/record-crud/types/delete-record-params.type'; import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type'; import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; @Injectable() export class DeleteRecordService { private readonly logger = new Logger(DeleteRecordService.name); constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, ) {} async execute(params: DeleteRecordParams): Promise { @@ -45,85 +46,93 @@ export class DeleteRecordService { }; } + const authContext = buildSystemAuthContext(workspaceId); + try { - const repository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - objectName, - rolePermissionConfig, - ); + return await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const repository = await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + objectName, + rolePermissionConfig, + ); - const { flatObjectMetadataMaps, objectIdByNameSingular } = - repository.internalContext; + const { flatObjectMetadataMaps, objectIdByNameSingular } = + repository.internalContext; - const objectId = objectIdByNameSingular[objectName]; + const objectId = objectIdByNameSingular[objectName]; - if (!isDefined(objectId)) { - throw new RecordCrudException( - `Object ${objectName} not found`, - RecordCrudExceptionCode.INVALID_REQUEST, - ); - } + if (!isDefined(objectId)) { + throw new RecordCrudException( + `Object ${objectName} not found`, + RecordCrudExceptionCode.INVALID_REQUEST, + ); + } - const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({ - flatEntityMaps: flatObjectMetadataMaps, - flatEntityId: objectId, - }); + const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityMaps: flatObjectMetadataMaps, + flatEntityId: objectId, + }); - if ( - !canObjectBeManagedByWorkflow({ - nameSingular: flatObjectMetadata.nameSingular, - isSystem: flatObjectMetadata.isSystem, - }) - ) { - throw new RecordCrudException( - 'Failed to delete: Object cannot be deleted by workflow', - RecordCrudExceptionCode.INVALID_REQUEST, - ); - } + if ( + !canObjectBeManagedByWorkflow({ + nameSingular: flatObjectMetadata.nameSingular, + isSystem: flatObjectMetadata.isSystem, + }) + ) { + throw new RecordCrudException( + 'Failed to delete: Object cannot be deleted by workflow', + RecordCrudExceptionCode.INVALID_REQUEST, + ); + } - const objectRecord = await repository.findOne({ - where: { - id: objectRecordId, + const objectRecord = await repository.findOne({ + where: { + id: objectRecordId, + }, + }); + + if (!objectRecord) { + throw new RecordCrudException( + `Failed to delete: Record ${objectName} with id ${objectRecordId} not found`, + RecordCrudExceptionCode.RECORD_NOT_FOUND, + ); + } + + if (soft) { + const columnsToReturnForSoftDelete: string[] = []; + + await repository.softDelete( + objectRecordId, + undefined, + columnsToReturnForSoftDelete, + ); + + this.logger.log( + `Record soft deleted successfully from ${objectName}`, + ); + + return { + success: true, + message: `Record soft deleted successfully from ${objectName}`, + result: objectRecord, + }; + } else { + await repository.remove(objectRecord); + + this.logger.log( + `Record permanently deleted successfully from ${objectName}`, + ); + + return { + success: true, + message: `Record permanently deleted successfully from ${objectName}`, + result: { id: objectRecordId }, + }; + } }, - }); - - if (!objectRecord) { - throw new RecordCrudException( - `Failed to delete: Record ${objectName} with id ${objectRecordId} not found`, - RecordCrudExceptionCode.RECORD_NOT_FOUND, - ); - } - - if (soft) { - const columnsToReturnForSoftDelete: string[] = []; - - await repository.softDelete( - objectRecordId, - undefined, - columnsToReturnForSoftDelete, - ); - - this.logger.log(`Record soft deleted successfully from ${objectName}`); - - return { - success: true, - message: `Record soft deleted successfully from ${objectName}`, - result: objectRecord, - }; - } else { - await repository.remove(objectRecord); - - this.logger.log( - `Record permanently deleted successfully from ${objectName}`, - ); - - return { - success: true, - message: `Record permanently deleted successfully from ${objectName}`, - result: { id: objectRecordId }, - }; - } + ); } catch (error) { if (error instanceof RecordCrudException) { return { diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/services/find-records.service.ts b/packages/twenty-server/src/engine/core-modules/record-crud/services/find-records.service.ts index 85803d172e5..c85777a2cfe 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/services/find-records.service.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/services/find-records.service.ts @@ -25,16 +25,17 @@ import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/typ import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { type WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder'; import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; @Injectable() export class FindRecordsService { private readonly logger = new Logger(FindRecordsService.name); constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, ) {} async execute( @@ -58,82 +59,88 @@ export class FindRecordsService { }; } + const authContext = buildSystemAuthContext(workspaceId); + try { - const repository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - objectName, - rolePermissionConfig, - ); + return await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const repository = await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + objectName, + rolePermissionConfig, + ); - const { - flatObjectMetadataMaps, - flatFieldMetadataMaps, - objectIdByNameSingular, - } = repository.internalContext; + const { + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, + } = repository.internalContext; - const objectId = objectIdByNameSingular[objectName]; + const objectId = objectIdByNameSingular[objectName]; - if (!isDefined(objectId)) { - throw new RecordCrudException( - `Object ${objectName} not found`, - RecordCrudExceptionCode.INVALID_REQUEST, - ); - } + if (!isDefined(objectId)) { + throw new RecordCrudException( + `Object ${objectName} not found`, + RecordCrudExceptionCode.INVALID_REQUEST, + ); + } - const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({ - flatEntityMaps: flatObjectMetadataMaps, - flatEntityId: objectId, - }); + const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityMaps: flatObjectMetadataMaps, + flatEntityId: objectId, + }); - const graphqlQueryParser = new GraphqlQueryParser( - flatObjectMetadata, - flatObjectMetadataMaps, - flatFieldMetadataMaps, - ); + const graphqlQueryParser = new GraphqlQueryParser( + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + ); - const records = await this.getObjectRecords({ - objectName, - filter, - orderBy, - limit, - offset, - repository, - graphqlQueryParser, - flatObjectMetadata, - flatFieldMetadataMaps, - }); + const records = await this.getObjectRecords({ + objectName, + filter, + orderBy, + limit, + offset, + repository, + graphqlQueryParser, + flatObjectMetadata, + flatFieldMetadataMaps, + }); - const totalCount = await this.getTotalCount({ - objectName, - filter, - repository, - graphqlQueryParser, - flatObjectMetadata, - flatFieldMetadataMaps, - }); + const totalCount = await this.getTotalCount({ + objectName, + filter, + repository, + graphqlQueryParser, + flatObjectMetadata, + flatFieldMetadataMaps, + }); - this.logger.log(`Found ${records.length} records in ${objectName}`); + this.logger.log(`Found ${records.length} records in ${objectName}`); - const recordReferences = records.map((record) => ({ - objectNameSingular: objectName, - recordId: record.id as string, - displayName: getRecordDisplayName( - record, - flatObjectMetadata, - flatFieldMetadataMaps, - ), - })); + const recordReferences = records.map((record) => ({ + objectNameSingular: objectName, + recordId: record.id as string, + displayName: getRecordDisplayName( + record, + flatObjectMetadata, + flatFieldMetadataMaps, + ), + })); - return { - success: true, - message: `Found ${records.length} ${objectName} records`, - result: { - records, - count: totalCount, + return { + success: true, + message: `Found ${records.length} ${objectName} records`, + result: { + records, + count: totalCount, + }, + recordReferences, + }; }, - recordReferences, - }; + ); } catch (error) { this.logger.error(`Failed to find records: ${error}`); diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/services/update-record.service.ts b/packages/twenty-server/src/engine/core-modules/record-crud/services/update-record.service.ts index f04222b0c7d..dd9506ad8f3 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/services/update-record.service.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/services/update-record.service.ts @@ -14,14 +14,15 @@ import { getSelectedColumnsFromRestrictedFields } from 'src/engine/core-modules/ import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service'; import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type'; import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; @Injectable() export class UpdateRecordService { private readonly logger = new Logger(UpdateRecordService.name); constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly recordInputTransformerService: RecordInputTransformerService, ) {} @@ -51,135 +52,141 @@ export class UpdateRecordService { }; } + const authContext = buildSystemAuthContext(workspaceId); + try { - const repository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - objectName, - rolePermissionConfig, - ); + return await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const repository = await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + objectName, + rolePermissionConfig, + ); - const { - flatObjectMetadataMaps, - flatFieldMetadataMaps, - objectIdByNameSingular, - } = repository.internalContext; + const { + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, + } = repository.internalContext; - const objectId = objectIdByNameSingular[objectName]; + const objectId = objectIdByNameSingular[objectName]; - if (!isDefined(objectId)) { - throw new RecordCrudException( - `Object ${objectName} not found`, - RecordCrudExceptionCode.INVALID_REQUEST, - ); - } + if (!isDefined(objectId)) { + throw new RecordCrudException( + `Object ${objectName} not found`, + RecordCrudExceptionCode.INVALID_REQUEST, + ); + } - const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({ - flatEntityMaps: flatObjectMetadataMaps, - flatEntityId: objectId, - }); + const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityMaps: flatObjectMetadataMaps, + flatEntityId: objectId, + }); - const restrictedFields = - repository.objectRecordsPermissions?.[flatObjectMetadata.id] - ?.restrictedFields; + const restrictedFields = + repository.objectRecordsPermissions?.[flatObjectMetadata.id] + ?.restrictedFields; - const selectedColumns = getSelectedColumnsFromRestrictedFields( - restrictedFields, - flatObjectMetadata, - flatFieldMetadataMaps, - ); + const selectedColumns = getSelectedColumnsFromRestrictedFields( + restrictedFields, + flatObjectMetadata, + flatFieldMetadataMaps, + ); - const previousObjectRecord = await repository.findOne({ - where: { - id: objectRecordId, - }, - select: selectedColumns, - }); + const previousObjectRecord = await repository.findOne({ + where: { + id: objectRecordId, + }, + select: selectedColumns, + }); - if (!previousObjectRecord) { - throw new RecordCrudException( - `Failed to update: Record ${objectName} with id ${objectRecordId} not found`, - RecordCrudExceptionCode.RECORD_NOT_FOUND, - ); - } + if (!previousObjectRecord) { + throw new RecordCrudException( + `Failed to update: Record ${objectName} with id ${objectRecordId} not found`, + RecordCrudExceptionCode.RECORD_NOT_FOUND, + ); + } - const fieldsToUpdateArray = fieldsToUpdate || Object.keys(objectRecord); + const fieldsToUpdateArray = + fieldsToUpdate || Object.keys(objectRecord); - if (fieldsToUpdateArray.length === 0) { - return { - success: true, - message: 'No fields to update', - result: previousObjectRecord, - }; - } - - if ( - !canObjectBeManagedByWorkflow({ - nameSingular: flatObjectMetadata.nameSingular, - isSystem: flatObjectMetadata.isSystem, - }) - ) { - throw new RecordCrudException( - 'Failed to update: Object cannot be updated by workflow', - RecordCrudExceptionCode.INVALID_REQUEST, - ); - } - - const objectRecordWithFilteredFields = Object.keys(objectRecord).reduce( - (acc, key) => { - if (fieldsToUpdateArray.includes(key)) { + if (fieldsToUpdateArray.length === 0) { return { - ...acc, - [key]: objectRecord[key], + success: true, + message: 'No fields to update', + result: previousObjectRecord, }; } - return acc; - }, - {}, - ); + if ( + !canObjectBeManagedByWorkflow({ + nameSingular: flatObjectMetadata.nameSingular, + isSystem: flatObjectMetadata.isSystem, + }) + ) { + throw new RecordCrudException( + 'Failed to update: Object cannot be updated by workflow', + RecordCrudExceptionCode.INVALID_REQUEST, + ); + } - const transformedObjectRecord = - await this.recordInputTransformerService.process({ - recordInput: objectRecordWithFilteredFields, - flatObjectMetadata, - flatFieldMetadataMaps, - }); + const objectRecordWithFilteredFields = Object.keys( + objectRecord, + ).reduce((acc, key) => { + if (fieldsToUpdateArray.includes(key)) { + return { + ...acc, + [key]: objectRecord[key], + }; + } - const updatedObjectRecord = { - ...previousObjectRecord, - ...objectRecordWithFilteredFields, - }; + return acc; + }, {}); - if (!deepEqual(updatedObjectRecord, previousObjectRecord)) { - await repository.update( - objectRecordId, - { - ...transformedObjectRecord, - }, - undefined, - selectedColumns, - ); - } - - this.logger.log(`Record updated successfully in ${objectName}`); - - return { - success: true, - message: `Record updated successfully in ${objectName}`, - result: updatedObjectRecord, - recordReferences: [ - { - objectNameSingular: objectName, - recordId: objectRecordId, - displayName: getRecordDisplayName( - updatedObjectRecord, + const transformedObjectRecord = + await this.recordInputTransformerService.process({ + recordInput: objectRecordWithFilteredFields, flatObjectMetadata, flatFieldMetadataMaps, - ), - }, - ], - }; + }); + + const updatedObjectRecord = { + ...previousObjectRecord, + ...objectRecordWithFilteredFields, + }; + + if (!deepEqual(updatedObjectRecord, previousObjectRecord)) { + await repository.update( + objectRecordId, + { + ...transformedObjectRecord, + }, + undefined, + selectedColumns, + ); + } + + this.logger.log(`Record updated successfully in ${objectName}`); + + return { + success: true, + message: `Record updated successfully in ${objectName}`, + result: updatedObjectRecord, + recordReferences: [ + { + objectNameSingular: objectName, + recordId: objectRecordId, + displayName: getRecordDisplayName( + updatedObjectRecord, + flatObjectMetadata, + flatFieldMetadataMaps, + ), + }, + ], + }; + }, + ); } catch (error) { if (error instanceof RecordCrudException) { return { diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/services/upsert-record.service.ts b/packages/twenty-server/src/engine/core-modules/record-crud/services/upsert-record.service.ts index f7705b9d9c8..035a134eb25 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/services/upsert-record.service.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/services/upsert-record.service.ts @@ -16,14 +16,15 @@ import { getCompositeTypeOrThrow } from 'src/engine/metadata-modules/field-metad import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; @Injectable() export class UpsertRecordService { private readonly logger = new Logger(UpsertRecordService.name); constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly recordInputTransformerService: RecordInputTransformerService, ) {} @@ -39,163 +40,167 @@ export class UpsertRecordService { }; } + const authContext = buildSystemAuthContext(workspaceId); + try { - const repository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - objectName, - rolePermissionConfig, - ); + return await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const repository = await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + objectName, + rolePermissionConfig, + ); - const fieldsToUpdateArray = Object.keys(objectRecord).filter((field) => - isDefined(objectRecord[field]), - ); + const fieldsToUpdateArray = Object.keys(objectRecord).filter( + (field) => isDefined(objectRecord[field]), + ); - const { - flatObjectMetadataMaps, - flatFieldMetadataMaps, - objectIdByNameSingular, - } = repository.internalContext; + const { + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, + } = repository.internalContext; - const objectId = objectIdByNameSingular[objectName]; + const objectId = objectIdByNameSingular[objectName]; - if (!isDefined(objectId)) { - throw new RecordCrudException( - `Object ${objectName} not found`, - RecordCrudExceptionCode.INVALID_REQUEST, - ); - } - - const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({ - flatEntityMaps: flatObjectMetadataMaps, - flatEntityId: objectId, - }); - - if ( - !canObjectBeManagedByWorkflow({ - nameSingular: flatObjectMetadata.nameSingular, - isSystem: flatObjectMetadata.isSystem, - }) - ) { - throw new RecordCrudException( - 'Failed to update: Object cannot be updated by workflow', - RecordCrudExceptionCode.INVALID_REQUEST, - ); - } - - const objectRecordWithFilteredFields = Object.keys(objectRecord).reduce( - (acc, key) => { - if (fieldsToUpdateArray.includes(key)) { - return { - ...acc, - [key]: objectRecord[key], - }; - } - - return acc; - }, - {}, - ); - - const transformedObjectRecord = - await this.recordInputTransformerService.process({ - recordInput: objectRecordWithFilteredFields, - flatObjectMetadata, - flatFieldMetadataMaps, - }); - - const { fieldIdByName, fieldIdByJoinColumnName } = - buildFieldMapsFromFlatObjectMetadata( - flatFieldMetadataMaps, - flatObjectMetadata, - ); - - const uniqueFieldsToUpdate = fieldsToUpdateArray - .map( - (fieldName) => - fieldIdByName[fieldName] || fieldIdByJoinColumnName[fieldName], - ) - .map((fieldId) => flatFieldMetadataMaps.byId[fieldId]) - .filter(isDefined) - .filter((field) => field.isUnique || field.name === 'id'); - - const conflictPathsUniqueFieldsToUpdate = uniqueFieldsToUpdate.flatMap( - (field) => { - if (isCompositeFieldMetadataType(field.type)) { - const compositeType = getCompositeTypeOrThrow(field.type); - - const uniqueProperties = compositeType.properties.filter( - (prop) => prop.isIncludedInUniqueConstraint, - ); - - const propertiesToUse = - uniqueProperties.length > 0 - ? uniqueProperties - : [compositeType.properties[0]]; - - return propertiesToUse.map((prop) => - computeCompositeColumnName(field, prop), + if (!isDefined(objectId)) { + throw new RecordCrudException( + `Object ${objectName} not found`, + RecordCrudExceptionCode.INVALID_REQUEST, ); } - return [field.name]; + const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityMaps: flatObjectMetadataMaps, + flatEntityId: objectId, + }); + + if ( + !canObjectBeManagedByWorkflow({ + nameSingular: flatObjectMetadata.nameSingular, + isSystem: flatObjectMetadata.isSystem, + }) + ) { + throw new RecordCrudException( + 'Failed to update: Object cannot be updated by workflow', + RecordCrudExceptionCode.INVALID_REQUEST, + ); + } + + const objectRecordWithFilteredFields = Object.keys( + objectRecord, + ).reduce((acc, key) => { + if (fieldsToUpdateArray.includes(key)) { + return { + ...acc, + [key]: objectRecord[key], + }; + } + + return acc; + }, {}); + + const transformedObjectRecord = + await this.recordInputTransformerService.process({ + recordInput: objectRecordWithFilteredFields, + flatObjectMetadata, + flatFieldMetadataMaps, + }); + + const { fieldIdByName, fieldIdByJoinColumnName } = + buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + flatObjectMetadata, + ); + + const uniqueFieldsToUpdate = fieldsToUpdateArray + .map( + (fieldName) => + fieldIdByName[fieldName] || fieldIdByJoinColumnName[fieldName], + ) + .map((fieldId) => flatFieldMetadataMaps.byId[fieldId]) + .filter(isDefined) + .filter((field) => field.isUnique || field.name === 'id'); + + const conflictPathsUniqueFieldsToUpdate = + uniqueFieldsToUpdate.flatMap((field) => { + if (isCompositeFieldMetadataType(field.type)) { + const compositeType = getCompositeTypeOrThrow(field.type); + + const uniqueProperties = compositeType.properties.filter( + (prop) => prop.isIncludedInUniqueConstraint, + ); + + const propertiesToUse = + uniqueProperties.length > 0 + ? uniqueProperties + : [compositeType.properties[0]]; + + return propertiesToUse.map((prop) => + computeCompositeColumnName(field, prop), + ); + } + + return [field.name]; + }); + + const conflictPaths = + conflictPathsUniqueFieldsToUpdate.length > 0 + ? conflictPathsUniqueFieldsToUpdate + : ['id']; + + const restrictedFields = + repository.objectRecordsPermissions?.[flatObjectMetadata.id] + ?.restrictedFields; + + const selectedColumns = getSelectedColumnsFromRestrictedFields( + restrictedFields, + flatObjectMetadata, + flatFieldMetadataMaps, + ); + + const upsertResult = await repository.upsert( + transformedObjectRecord, + { + conflictPaths: conflictPaths, + }, + undefined, + selectedColumns, + ); + + const upsertedRecordId = upsertResult.identifiers?.[0].id; + + if (!isDefined(upsertedRecordId)) { + throw new RecordCrudException( + `Failed to upsert record in ${objectName}`, + RecordCrudExceptionCode.RECORD_UPSERT_FAILED, + ); + } + + const upsertedRecord = await repository.findOne({ + where: { + id: upsertedRecordId, + }, + select: selectedColumns, + }); + + if (!upsertedRecord) { + throw new RecordCrudException( + `Record not found after upsert with id ${upsertedRecordId} in ${objectName}`, + RecordCrudExceptionCode.RECORD_UPSERT_FAILED, + ); + } + + this.logger.log(`Record upserted successfully in ${objectName}`); + + return { + success: true, + message: `Record upserted successfully in ${objectName}`, + result: upsertedRecord, + }; }, ); - - const conflictPaths = - conflictPathsUniqueFieldsToUpdate.length > 0 - ? conflictPathsUniqueFieldsToUpdate - : ['id']; - - const restrictedFields = - repository.objectRecordsPermissions?.[flatObjectMetadata.id] - ?.restrictedFields; - - const selectedColumns = getSelectedColumnsFromRestrictedFields( - restrictedFields, - flatObjectMetadata, - flatFieldMetadataMaps, - ); - - const upsertResult = await repository.upsert( - transformedObjectRecord, - { - conflictPaths: conflictPaths, - }, - undefined, - selectedColumns, - ); - - const upsertedRecordId = upsertResult.identifiers?.[0].id; - - if (!isDefined(upsertedRecordId)) { - throw new RecordCrudException( - `Failed to upsert record in ${objectName}`, - RecordCrudExceptionCode.RECORD_UPSERT_FAILED, - ); - } - - const upsertedRecord = await repository.findOne({ - where: { - id: upsertedRecordId, - }, - select: selectedColumns, - }); - - if (!upsertedRecord) { - throw new RecordCrudException( - `Record not found after upsert with id ${upsertedRecordId} in ${objectName}`, - RecordCrudExceptionCode.RECORD_UPSERT_FAILED, - ); - } - - this.logger.log(`Record upserted successfully in ${objectName}`); - - return { - success: true, - message: `Record upserted successfully in ${objectName}`, - result: upsertedRecord, - }; } catch (error) { if (error instanceof RecordCrudException) { return { diff --git a/packages/twenty-server/src/engine/core-modules/record-position/services/__tests__/record-position.service.spec.ts b/packages/twenty-server/src/engine/core-modules/record-position/services/__tests__/record-position.service.spec.ts index 9a65021ebfb..996d0ed4de1 100644 --- a/packages/twenty-server/src/engine/core-modules/record-position/services/__tests__/record-position.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/record-position/services/__tests__/record-position.service.spec.ts @@ -1,10 +1,10 @@ import { Test, type TestingModule } from '@nestjs/testing'; import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; describe('RecordPositionService', () => { - let twentyORMGlobalManager: jest.Mocked; + let globalWorkspaceOrmManager: jest.Mocked; let mockRepository: any; let service: RecordPositionService; @@ -16,16 +16,19 @@ describe('RecordPositionService', () => { maximum: jest.fn().mockResolvedValue(1), }; - twentyORMGlobalManager = { - getRepositoryForWorkspace: jest.fn().mockResolvedValue(mockRepository), - } as unknown as jest.Mocked; + globalWorkspaceOrmManager = { + getRepository: jest.fn().mockResolvedValue(mockRepository), + executeInWorkspaceContext: jest + .fn() + .mockImplementation((_authContext: any, fn: () => any) => fn()), + } as unknown as jest.Mocked; const module: TestingModule = await Test.createTestingModule({ providers: [ RecordPositionService, { - provide: TwentyORMGlobalManager, - useValue: twentyORMGlobalManager, + provide: GlobalWorkspaceOrmManager, + useValue: globalWorkspaceOrmManager, }, ], }).compile(); diff --git a/packages/twenty-server/src/engine/core-modules/record-position/services/record-position.service.ts b/packages/twenty-server/src/engine/core-modules/record-position/services/record-position.service.ts index b994f235a6b..d1b8774088e 100644 --- a/packages/twenty-server/src/engine/core-modules/record-position/services/record-position.service.ts +++ b/packages/twenty-server/src/engine/core-modules/record-position/services/record-position.service.ts @@ -4,7 +4,8 @@ import { isNumber } from '@sniptt/guards'; import { type ObjectRecord } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; export type RecordPositionServiceCreateArgs = { value: number | 'first' | 'last'; @@ -16,7 +17,7 @@ export type RecordPositionServiceCreateArgs = { @Injectable() export class RecordPositionService { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, ) {} async buildRecordPosition({ @@ -153,20 +154,26 @@ export class RecordPositionService { objectMetadata: { isCustom: boolean; nameSingular: string }, workspaceId: string, ): Promise<{ id: string; position: number } | null> { - const repository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - objectMetadata.nameSingular, - { - shouldBypassPermissionChecks: true, - }, - ); + const authContext = buildSystemAuthContext(workspaceId); - const record = await repository.findOneBy({ - position: positionValue, - }); + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const repository = await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + objectMetadata.nameSingular, + { + shouldBypassPermissionChecks: true, + }, + ); - return record ? { id: record.id, position: record.position } : null; + const record = await repository.findOneBy({ + position: positionValue, + }); + + return record ? { id: record.id, position: record.position } : null; + }, + ); } async updatePosition( @@ -175,49 +182,67 @@ export class RecordPositionService { objectMetadata: { isCustom: boolean; nameSingular: string }, workspaceId: string, ): Promise { - const repository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - objectMetadata.nameSingular, - { - shouldBypassPermissionChecks: true, - }, - ); + const authContext = buildSystemAuthContext(workspaceId); - await repository.update(recordId, { - position: positionValue, - }); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const repository = await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + objectMetadata.nameSingular, + { + shouldBypassPermissionChecks: true, + }, + ); + + await repository.update(recordId, { + position: positionValue, + }); + }, + ); } private async findMinPosition( objectMetadata: { isCustom: boolean; nameSingular: string }, workspaceId: string, ): Promise { - const repository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - objectMetadata.nameSingular, - { - shouldBypassPermissionChecks: true, - }, - ); + const authContext = buildSystemAuthContext(workspaceId); - return await repository.minimum('position'); + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const repository = await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + objectMetadata.nameSingular, + { + shouldBypassPermissionChecks: true, + }, + ); + + return await repository.minimum('position'); + }, + ); } private async findMaxPosition( objectMetadata: { isCustom: boolean; nameSingular: string }, workspaceId: string, ): Promise { - const repository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - objectMetadata.nameSingular, - { - shouldBypassPermissionChecks: true, - }, - ); + const authContext = buildSystemAuthContext(workspaceId); - return await repository.maximum('position'); + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const repository = await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + objectMetadata.nameSingular, + { + shouldBypassPermissionChecks: true, + }, + ); + + return await repository.maximum('position'); + }, + ); } } diff --git a/packages/twenty-server/src/engine/core-modules/search/__tests__/search.service.spec.ts b/packages/twenty-server/src/engine/core-modules/search/__tests__/search.service.spec.ts index 169920f9d2f..9d4caf63112 100644 --- a/packages/twenty-server/src/engine/core-modules/search/__tests__/search.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/search/__tests__/search.service.spec.ts @@ -7,7 +7,7 @@ import { } from 'src/engine/core-modules/__mocks__/mockFlatObjectMetadatas'; import { FileService } from 'src/engine/core-modules/file/services/file.service'; import { SearchService } from 'src/engine/core-modules/search/services/search.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; describe('SearchService', () => { @@ -17,7 +17,7 @@ describe('SearchService', () => { const module: TestingModule = await Test.createTestingModule({ providers: [ SearchService, - { provide: TwentyORMGlobalManager, useValue: {} }, + { provide: GlobalWorkspaceOrmManager, useValue: {} }, { provide: WorkspaceCacheStorageService, useValue: {} }, { provide: FileService, useValue: {} }, ], diff --git a/packages/twenty-server/src/engine/core-modules/search/services/search.service.ts b/packages/twenty-server/src/engine/core-modules/search/services/search.service.ts index 43c09cdc47f..d472e88c753 100644 --- a/packages/twenty-server/src/engine/core-modules/search/services/search.service.ts +++ b/packages/twenty-server/src/engine/core-modules/search/services/search.service.ts @@ -30,9 +30,10 @@ import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/typ import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; type LastRanks = { tsRankCD: number; tsRank: number }; @@ -46,7 +47,7 @@ const OBJECT_METADATA_ITEMS_CHUNK_SIZE = 5; @Injectable() export class SearchService { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly fileService: FileService, ) {} @@ -81,29 +82,36 @@ export class SearchService { OBJECT_METADATA_ITEMS_CHUNK_SIZE, ); + const authContext = buildSystemAuthContext(workspaceId); + for (const objectMetadataItemChunk of filteredObjectMetadataItemsChunks) { const recordsWithObjectMetadataItems = await Promise.all( objectMetadataItemChunk.map(async (flatObjectMetadata) => { - const repository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - flatObjectMetadata.nameSingular, - rolePermissionConfig, - ); + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const repository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + flatObjectMetadata.nameSingular, + rolePermissionConfig, + ); - return { - objectMetadataItem: flatObjectMetadata, - records: await this.buildSearchQueryAndGetRecords({ - entityManager: repository, - flatObjectMetadata, - flatFieldMetadataMaps, - searchTerms: formatSearchTerms(searchInput, 'and'), - searchTermsOr: formatSearchTerms(searchInput, 'or'), - limit: limit as number, - filter: filter ?? ({} as ObjectRecordFilter), - after, - }), - }; + return { + objectMetadataItem: flatObjectMetadata, + records: await this.buildSearchQueryAndGetRecords({ + entityManager: repository, + flatObjectMetadata, + flatFieldMetadataMaps, + searchTerms: formatSearchTerms(searchInput, 'and'), + searchTermsOr: formatSearchTerms(searchInput, 'or'), + limit: limit as number, + filter: filter ?? ({} as ObjectRecordFilter), + after, + }), + }; + }, + ); }), ); diff --git a/packages/twenty-server/src/engine/core-modules/tool/tools/send-email-tool/send-email-tool.ts b/packages/twenty-server/src/engine/core-modules/tool/tools/send-email-tool/send-email-tool.ts index 5aca19cbe77..92993d73c92 100644 --- a/packages/twenty-server/src/engine/core-modules/tool/tools/send-email-tool/send-email-tool.ts +++ b/packages/twenty-server/src/engine/core-modules/tool/tools/send-email-tool/send-email-tool.ts @@ -9,6 +9,7 @@ import { In, Repository } from 'typeorm'; import { z } from 'zod'; import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity'; +import { FileService } from 'src/engine/core-modules/file/services/file.service'; import { extractFolderPathAndFilename } from 'src/engine/core-modules/file/utils/extract-folderpath-and-filename.utils'; import { SendEmailToolException, @@ -18,13 +19,13 @@ import { SendEmailToolParametersZodSchema } from 'src/engine/core-modules/tool/t import { type SendEmailInput } from 'src/engine/core-modules/tool/tools/send-email-tool/types/send-email-input.type'; import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type'; import { type Tool } from 'src/engine/core-modules/tool/types/tool.type'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { MessagingSendMessageService } from 'src/modules/messaging/message-import-manager/services/messaging-send-message.service'; +import { type MessageAttachment } from 'src/modules/messaging/message-import-manager/types/message'; import { parseEmailBody } from 'src/utils/parse-email-body'; import { streamToBuffer } from 'src/utils/stream-to-buffer'; -import { type MessageAttachment } from 'src/modules/messaging/message-import-manager/types/message'; -import { FileService } from 'src/engine/core-modules/file/services/file.service'; @Injectable() export class SendEmailTool implements Tool { @@ -35,7 +36,7 @@ export class SendEmailTool implements Tool { inputSchema = SendEmailToolParametersZodSchema; constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly sendMessageService: MessagingSendMessageService, @InjectRepository(FileEntity) private readonly fileRepository: Repository, @@ -53,49 +54,63 @@ export class SendEmailTool implements Tool { ); } - const connectedAccountRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'connectedAccount', - ); + const authContext = buildSystemAuthContext(workspaceId); - const connectedAccount = await connectedAccountRepository.findOne({ - where: { id: connectedAccountId }, - relations: { - messageChannels: { - messageFolders: true, - }, + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const connectedAccountRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'connectedAccount', + ); + + const connectedAccount = await connectedAccountRepository.findOne({ + where: { id: connectedAccountId }, + relations: { + messageChannels: { + messageFolders: true, + }, + }, + }); + + if (!isDefined(connectedAccount)) { + throw new SendEmailToolException( + `Connected Account '${connectedAccountId}' not found`, + SendEmailToolExceptionCode.CONNECTED_ACCOUNT_NOT_FOUND, + ); + } + + return connectedAccount; }, - }); - - if (!isDefined(connectedAccount)) { - throw new SendEmailToolException( - `Connected Account '${connectedAccountId}' not found`, - SendEmailToolExceptionCode.CONNECTED_ACCOUNT_NOT_FOUND, - ); - } - - return connectedAccount; + ); } private async getOrThrowFirstConnectedAccountId( workspaceId: string, ): Promise { - const connectedAccountRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'connectedAccount', - ); - const allAccounts = await connectedAccountRepository.find(); + const authContext = buildSystemAuthContext(workspaceId); - if (!allAccounts || allAccounts.length === 0) { - throw new SendEmailToolException( - 'No connected accounts found for this workspace', - SendEmailToolExceptionCode.CONNECTED_ACCOUNT_NOT_FOUND, - ); - } + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const connectedAccountRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'connectedAccount', + ); + const allAccounts = await connectedAccountRepository.find(); - return allAccounts[0].id; + if (!allAccounts || allAccounts.length === 0) { + throw new SendEmailToolException( + 'No connected accounts found for this workspace', + SendEmailToolExceptionCode.CONNECTED_ACCOUNT_NOT_FOUND, + ); + } + + return allAccounts[0].id; + }, + ); } private async getAttachments( diff --git a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.spec.ts b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.spec.ts index 517f1643024..83d5f4e8244 100644 --- a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.spec.ts @@ -27,7 +27,8 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat import { PermissionsException } from 'src/engine/metadata-modules/permissions/permissions.exception'; import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity'; import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; describe('UserWorkspaceService', () => { let service: UserWorkspaceService; @@ -35,7 +36,7 @@ describe('UserWorkspaceService', () => { let userRepository: Repository; let workspaceInvitationService: WorkspaceInvitationService; let approvedAccessDomainService: ApprovedAccessDomainService; - let twentyORMGlobalManager: TwentyORMGlobalManager; + let globalWorkspaceOrmManager: GlobalWorkspaceOrmManager; let userRoleService: UserRoleService; let fileService: FileService; let fileUploadService: FileUploadService; @@ -102,9 +103,12 @@ describe('UserWorkspaceService', () => { }, }, { - provide: TwentyORMGlobalManager, + provide: GlobalWorkspaceOrmManager, useValue: { - getRepositoryForWorkspace: jest.fn(), + executeInWorkspaceContext: jest + .fn() + .mockImplementation(async (_authContext, callback) => callback()), + getRepository: jest.fn(), }, }, { @@ -156,9 +160,14 @@ describe('UserWorkspaceService', () => { approvedAccessDomainService = module.get( ApprovedAccessDomainService, ); - twentyORMGlobalManager = module.get( - TwentyORMGlobalManager, + globalWorkspaceOrmManager = module.get( + GlobalWorkspaceOrmManager, ); + (globalWorkspaceOrmManager.getRepository as jest.Mock).mockResolvedValue({ + findOne: jest.fn(), + update: jest.fn(), + } as unknown as WorkspaceRepository); + userRoleService = module.get(UserRoleService); fileUploadService = module.get(FileUploadService); onboardingService = module.get(OnboardingService); @@ -374,7 +383,7 @@ describe('UserWorkspaceService', () => { .mockResolvedValueOnce(undefined) .mockResolvedValueOnce(workspaceMember); jest - .spyOn(twentyORMGlobalManager, 'getRepositoryForWorkspace') + .spyOn(globalWorkspaceOrmManager, 'getRepository') .mockResolvedValue(workspaceMemberRepository as any); jest.spyOn(userWorkspaceRepository, 'findOneOrFail').mockResolvedValue({ @@ -902,7 +911,7 @@ describe('UserWorkspaceService', () => { }; jest - .spyOn(twentyORMGlobalManager, 'getRepositoryForWorkspace') + .spyOn(globalWorkspaceOrmManager, 'getRepository') .mockResolvedValue(workspaceMemberRepository as any); const result = await service.getWorkspaceMemberOrThrow({ @@ -910,11 +919,13 @@ describe('UserWorkspaceService', () => { workspaceId, }); - expect( - twentyORMGlobalManager.getRepositoryForWorkspace, - ).toHaveBeenCalledWith(workspaceId, 'workspaceMember', { - shouldBypassPermissionChecks: true, - }); + expect(globalWorkspaceOrmManager.getRepository).toHaveBeenCalledWith( + workspaceId, + 'workspaceMember', + { + shouldBypassPermissionChecks: true, + }, + ); expect(workspaceMemberRepository.findOne).toHaveBeenCalledWith({ where: { id: workspaceMemberId, @@ -931,7 +942,7 @@ describe('UserWorkspaceService', () => { }; jest - .spyOn(twentyORMGlobalManager, 'getRepositoryForWorkspace') + .spyOn(globalWorkspaceOrmManager, 'getRepository') .mockResolvedValue(workspaceMemberRepository as any); await expect( diff --git a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts index 5a407ec56ae..3b1ce396731 100644 --- a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts +++ b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts @@ -33,7 +33,8 @@ import { } from 'src/engine/metadata-modules/permissions/permissions.exception'; import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity'; import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; import { assert } from 'src/utils/assert'; import { getDomainNameByEmail } from 'src/utils/get-domain-name-by-email'; @@ -50,7 +51,7 @@ export class UserWorkspaceService extends TypeOrmQueryService( - workspaceId, - 'workspaceMember', - { shouldBypassPermissionChecks: true }, - ); + const authContext = buildSystemAuthContext(workspaceId); - const userWorkspace = await this.userWorkspaceRepository.findOneOrFail({ - where: { - userId: user.id, - workspaceId, + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workspaceMemberRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'workspaceMember', + { shouldBypassPermissionChecks: true }, + ); + + const userWorkspace = await this.userWorkspaceRepository.findOneOrFail({ + where: { + userId: user.id, + workspaceId, + }, + }); + + await workspaceMemberRepository.insert({ + name: { + firstName: user.firstName, + lastName: user.lastName, + }, + colorScheme: 'System', + userId: user.id, + userEmail: user.email, + avatarUrl: userWorkspace.defaultAvatarUrl ?? '', + locale: (user.locale ?? SOURCE_LOCALE) as keyof typeof APP_LOCALES, + }); + + const workspaceMember = await workspaceMemberRepository.find({ + where: { + userId: user.id, + }, + }); + + assert( + workspaceMember?.length === 1, + `Error while creating workspace member ${user.email} on workspace ${workspaceId}`, + ); }, - }); - - await workspaceMemberRepository.insert({ - name: { - firstName: user.firstName, - lastName: user.lastName, - }, - colorScheme: 'System', - userId: user.id, - userEmail: user.email, - avatarUrl: userWorkspace.defaultAvatarUrl ?? '', - locale: (user.locale ?? SOURCE_LOCALE) as keyof typeof APP_LOCALES, - }); - - const workspaceMember = await workspaceMemberRepository.find({ - where: { - userId: user.id, - }, - }); - - assert( - workspaceMember?.length === 1, - `Error while creating workspace member ${user.email} on workspace ${workspaceId}`, ); } @@ -343,24 +351,31 @@ export class UserWorkspaceService extends TypeOrmQueryService { - const workspaceMemberRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'workspaceMember', - { shouldBypassPermissionChecks: true }, - ); + const authContext = buildSystemAuthContext(workspaceId); - const workspaceMember = await workspaceMemberRepository.findOne({ - where: { - id: workspaceMemberId, + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workspaceMemberRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'workspaceMember', + { shouldBypassPermissionChecks: true }, + ); + + const workspaceMember = await workspaceMemberRepository.findOne({ + where: { + id: workspaceMemberId, + }, + }); + + if (!isDefined(workspaceMember)) { + throw new Error('Workspace member not found'); + } + + return workspaceMember; }, - }); - - if (!isDefined(workspaceMember)) { - throw new Error('Workspace member not found'); - } - - return workspaceMember; + ); } private async computeDefaultAvatarUrl( diff --git a/packages/twenty-server/src/engine/core-modules/user/jobs/update-workspace-member-email.job.ts b/packages/twenty-server/src/engine/core-modules/user/jobs/update-workspace-member-email.job.ts index 04dccfddbd0..ab7b302f924 100644 --- a/packages/twenty-server/src/engine/core-modules/user/jobs/update-workspace-member-email.job.ts +++ b/packages/twenty-server/src/engine/core-modules/user/jobs/update-workspace-member-email.job.ts @@ -4,7 +4,8 @@ import { Process } from 'src/engine/core-modules/message-queue/decorators/proces import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; export type UpdateWorkspaceMemberEmailJobData = { @@ -21,7 +22,7 @@ export class UpdateWorkspaceMemberEmailJob { constructor( private readonly userWorkspaceService: UserWorkspaceService, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, ) {} @Process(UpdateWorkspaceMemberEmailJob.name) @@ -32,13 +33,23 @@ export class UpdateWorkspaceMemberEmailJob { const workspace = await this.userWorkspaceService.findFirstWorkspaceByUserId(userId); - const workspaceMemberRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspace.id, - 'workspaceMember', - { shouldBypassPermissionChecks: true }, - ); + const authContext = buildSystemAuthContext(workspace.id); - await workspaceMemberRepository.update({ userId }, { userEmail: email }); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workspaceMemberRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspace.id, + 'workspaceMember', + { shouldBypassPermissionChecks: true }, + ); + + await workspaceMemberRepository.update( + { userId }, + { userEmail: email }, + ); + }, + ); } } diff --git a/packages/twenty-server/src/engine/core-modules/user/services/user.service.spec.ts b/packages/twenty-server/src/engine/core-modules/user/services/user.service.spec.ts index 430f3f404b9..8a9c835ebc3 100644 --- a/packages/twenty-server/src/engine/core-modules/user/services/user.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/user/services/user.service.spec.ts @@ -20,15 +20,15 @@ import { PermissionsExceptionCode, } from 'src/engine/metadata-modules/permissions/permissions.exception'; import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; describe('UserService', () => { let service: UserService; let userRepository: Repository; let workspaceService: WorkspaceService; - let twentyORMGlobalManager: TwentyORMGlobalManager; + let globalWorkspaceOrmManager: GlobalWorkspaceOrmManager; let userRoleService: UserRoleService; const mockWorkspaceMemberRepo = { @@ -70,9 +70,13 @@ describe('UserService', () => { useValue: { add: jest.fn() }, }, { - provide: TwentyORMGlobalManager, + provide: GlobalWorkspaceOrmManager, useValue: { - getRepositoryForWorkspace: jest.fn(), + getRepository: jest.fn(), + executeInWorkspaceContext: jest + .fn() + + .mockImplementation((_authContext: any, fn: () => any) => fn()), }, }, { @@ -99,8 +103,8 @@ describe('UserService', () => { getRepositoryToken(UserEntity), ); userRoleService = module.get(UserRoleService); - twentyORMGlobalManager = module.get( - TwentyORMGlobalManager, + globalWorkspaceOrmManager = module.get( + GlobalWorkspaceOrmManager, ); workspaceService = module.get(WorkspaceService); }); @@ -115,9 +119,7 @@ describe('UserService', () => { ); expect(res).toBeNull(); - expect( - twentyORMGlobalManager.getRepositoryForWorkspace, - ).not.toHaveBeenCalled(); + expect(globalWorkspaceOrmManager.getRepository).not.toHaveBeenCalled(); }); it('fetches from workspace member repo when workspace active', async () => { @@ -127,7 +129,7 @@ describe('UserService', () => { } as WorkspaceMemberWorkspaceEntity); jest - .spyOn(twentyORMGlobalManager, 'getRepositoryForWorkspace') + .spyOn(globalWorkspaceOrmManager, 'getRepository') .mockResolvedValue(mockWorkspaceMemberRepo); const res = await service.loadWorkspaceMember( @@ -138,11 +140,13 @@ describe('UserService', () => { } as WorkspaceEntity, ); - expect( - twentyORMGlobalManager.getRepositoryForWorkspace, - ).toHaveBeenCalledWith('w1', 'workspaceMember', { - shouldBypassPermissionChecks: true, - }); + expect(globalWorkspaceOrmManager.getRepository).toHaveBeenCalledWith( + 'w1', + 'workspaceMember', + { + shouldBypassPermissionChecks: true, + }, + ); expect(mockWorkspaceMemberRepo.findOne).toHaveBeenCalledWith({ where: { userId: 'u1' }, }); @@ -158,9 +162,7 @@ describe('UserService', () => { } as WorkspaceEntity); expect(res).toEqual([]); - expect( - twentyORMGlobalManager.getRepositoryForWorkspace, - ).not.toHaveBeenCalled(); + expect(globalWorkspaceOrmManager.getRepository).not.toHaveBeenCalled(); }); it('fetches members withDeleted flag', async () => { @@ -168,7 +170,7 @@ describe('UserService', () => { .spyOn(mockWorkspaceMemberRepo, 'find') .mockResolvedValue([{ id: 'wm1' } as WorkspaceMemberWorkspaceEntity]); jest - .spyOn(twentyORMGlobalManager, 'getRepositoryForWorkspace') + .spyOn(globalWorkspaceOrmManager, 'getRepository') .mockResolvedValue(mockWorkspaceMemberRepo); const res = await service.loadWorkspaceMembers( @@ -203,7 +205,7 @@ describe('UserService', () => { { id: 'wm-del' } as WorkspaceMemberWorkspaceEntity, ]); jest - .spyOn(twentyORMGlobalManager, 'getRepositoryForWorkspace') + .spyOn(globalWorkspaceOrmManager, 'getRepository') .mockResolvedValue(mockWorkspaceMemberRepo); await service.loadDeletedWorkspaceMembersOnly({ @@ -323,7 +325,7 @@ describe('UserService', () => { { id: 'wm-2', userId: 'uX' } as WorkspaceMemberWorkspaceEntity, ]); jest - .spyOn(twentyORMGlobalManager, 'getRepositoryForWorkspace') + .spyOn(globalWorkspaceOrmManager, 'getRepository') .mockResolvedValue(mockWorkspaceMemberRepo); jest @@ -358,7 +360,7 @@ describe('UserService', () => { .spyOn(mockWorkspaceMemberRepo, 'find') .mockResolvedValue([wmForUser('u2')]); jest - .spyOn(twentyORMGlobalManager, 'getRepositoryForWorkspace') + .spyOn(globalWorkspaceOrmManager, 'getRepository') .mockResolvedValue(mockWorkspaceMemberRepo); (userRepository.softDelete as jest.Mock).mockResolvedValue({ diff --git a/packages/twenty-server/src/engine/core-modules/user/services/user.service.ts b/packages/twenty-server/src/engine/core-modules/user/services/user.service.ts index 78128c2f1d4..5b6d062da0b 100644 --- a/packages/twenty-server/src/engine/core-modules/user/services/user.service.ts +++ b/packages/twenty-server/src/engine/core-modules/user/services/user.service.ts @@ -37,7 +37,8 @@ import { PermissionsExceptionMessage, } from 'src/engine/metadata-modules/permissions/permissions.exception'; import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; // eslint-disable-next-line @nx/workspace-inject-workspace-repository @@ -48,7 +49,7 @@ export class UserService extends TypeOrmQueryService { private readonly workspaceDomainsService: WorkspaceDomainsService, private readonly emailVerificationService: EmailVerificationService, private readonly workspaceService: WorkspaceService, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly userRoleService: UserRoleService, private readonly userWorkspaceService: UserWorkspaceService, @InjectMessageQueue(MessageQueue.workspaceQueue) @@ -62,18 +63,25 @@ export class UserService extends TypeOrmQueryService { return null; } - const workspaceMemberRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspace.id, - 'workspaceMember', - { shouldBypassPermissionChecks: true }, - ); + const authContext = buildSystemAuthContext(workspace.id); - return await workspaceMemberRepository.findOne({ - where: { - userId: user.id, + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workspaceMemberRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspace.id, + 'workspaceMember', + { shouldBypassPermissionChecks: true }, + ); + + return await workspaceMemberRepository.findOne({ + where: { + userId: user.id, + }, + }); }, - }); + ); } async loadWorkspaceMembers(workspace: WorkspaceEntity, withDeleted = false) { @@ -81,14 +89,23 @@ export class UserService extends TypeOrmQueryService { return []; } - const workspaceMemberRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspace.id, - 'workspaceMember', - { shouldBypassPermissionChecks: true }, - ); + const authContext = buildSystemAuthContext(workspace.id); - return await workspaceMemberRepository.find({ withDeleted: withDeleted }); + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workspaceMemberRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspace.id, + 'workspaceMember', + { shouldBypassPermissionChecks: true }, + ); + + return await workspaceMemberRepository.find({ + withDeleted: withDeleted, + }); + }, + ); } async loadDeletedWorkspaceMembersOnly(workspace: WorkspaceEntity) { @@ -96,17 +113,24 @@ export class UserService extends TypeOrmQueryService { return []; } - const workspaceMemberRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspace.id, - 'workspaceMember', - { shouldBypassPermissionChecks: true }, - ); + const authContext = buildSystemAuthContext(workspace.id); - return await workspaceMemberRepository.find({ - where: { deletedAt: Not(IsNull()) }, - withDeleted: true, - }); + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workspaceMemberRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspace.id, + 'workspaceMember', + { shouldBypassPermissionChecks: true }, + ); + + return await workspaceMemberRepository.find({ + where: { deletedAt: Not(IsNull()) }, + withDeleted: true, + }); + }, + ); } async deleteUser(userId: string) { @@ -174,16 +198,23 @@ export class UserService extends TypeOrmQueryService { userWorkspace: UserWorkspaceEntity, ) { const workspaceId = userWorkspace.workspaceId; + const authContext = buildSystemAuthContext(workspaceId); - const workspaceMemberRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'workspaceMember', - { shouldBypassPermissionChecks: true }, + const workspaceMembers = + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workspaceMemberRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'workspaceMember', + { shouldBypassPermissionChecks: true }, + ); + + return workspaceMemberRepository.find(); + }, ); - const workspaceMembers = await workspaceMemberRepository.find(); - const userWorkspaceId = userWorkspace.id; if (workspaceMembers.length === 1) { @@ -224,7 +255,21 @@ export class UserService extends TypeOrmQueryService { assert(workspaceMember, 'WorkspaceMember not found'); - await workspaceMemberRepository.delete({ userId: userWorkspace.userId }); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workspaceMemberRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'workspaceMember', + { shouldBypassPermissionChecks: true }, + ); + + await workspaceMemberRepository.delete({ + userId: userWorkspace.userId, + }); + }, + ); await this.userWorkspaceService.deleteUserWorkspace({ userWorkspaceId, diff --git a/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts b/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts index 8ce40bed5d9..54dab8d7dd1 100644 --- a/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts @@ -14,10 +14,10 @@ import crypto from 'crypto'; import { msg } from '@lingui/core/macro'; import { GraphQLJSONObject } from 'graphql-type-json'; import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs'; +import { PermissionFlagType } from 'twenty-shared/constants'; import { isDefined } from 'twenty-shared/utils'; import { WorkspaceActivationStatus } from 'twenty-shared/workspace'; import { In, Repository } from 'typeorm'; -import { PermissionFlagType } from 'twenty-shared/constants'; import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface'; import { SupportDriver } from 'src/engine/core-modules/twenty-config/interfaces/support.interface'; @@ -73,7 +73,8 @@ import { type UserWorkspacePermissions } from 'src/engine/metadata-modules/permi import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; import { fromUserWorkspacePermissionsToUserWorkspacePermissionsDto } from 'src/engine/metadata-modules/role/utils/fromUserWorkspacePermissionsToUserWorkspacePermissionsDto'; import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { AccountsToReconnectKeys } from 'src/modules/connected-account/types/accounts-to-reconnect-key-value.type'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; import { streamToBuffer } from 'src/utils/stream-to-buffer'; @@ -103,7 +104,7 @@ export class UserResolver { private readonly permissionsService: PermissionsService, private readonly workspaceMemberTranspiler: WorkspaceMemberTranspiler, private readonly userWorkspaceService: UserWorkspaceService, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, ) {} private async getUserWorkspacePermissions({ @@ -425,18 +426,26 @@ export class UserResolver { ); } - const workspaceMemberRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspace.id, - 'workspaceMember', - { shouldBypassPermissionChecks: true }, - ); + const authContext = buildSystemAuthContext(workspace.id); - const workspaceMemberToDelete = await workspaceMemberRepository.findOne({ - where: { - id: workspaceMemberIdToDelete, - }, - }); + const workspaceMemberToDelete = + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workspaceMemberRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspace.id, + 'workspaceMember', + { shouldBypassPermissionChecks: true }, + ); + + return workspaceMemberRepository.findOne({ + where: { + id: workspaceMemberIdToDelete, + }, + }); + }, + ); if (!isDefined(workspaceMemberToDelete)) { throw new BadRequestException( diff --git a/packages/twenty-server/src/engine/core-modules/workflow/controllers/workflow-trigger.controller.ts b/packages/twenty-server/src/engine/core-modules/workflow/controllers/workflow-trigger.controller.ts index b4d2490d3cd..1e9a862cd15 100644 --- a/packages/twenty-server/src/engine/core-modules/workflow/controllers/workflow-trigger.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/workflow/controllers/workflow-trigger.controller.ts @@ -9,14 +9,15 @@ import { } from '@nestjs/common'; import { Request } from 'express'; -import { isDefined } from 'twenty-shared/utils'; import { FieldActorSource } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; import { WorkflowTriggerRestApiExceptionFilter } from 'src/engine/core-modules/workflow/filters/workflow-trigger-rest-api-exception.filter'; import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard'; import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard'; import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { WorkflowVersionStatus, type WorkflowVersionWorkspaceEntity, @@ -36,7 +37,7 @@ import { WorkflowTriggerWorkspaceService } from 'src/modules/workflow/workflow-t ) export class WorkflowTriggerController { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly workflowTriggerWorkspaceService: WorkflowTriggerWorkspaceService, ) {} @@ -72,68 +73,78 @@ export class WorkflowTriggerController { payload?: object; workspaceId: string; }) { - const workflowRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'workflow', - { shouldBypassPermissionChecks: true }, - ); + const authContext = buildSystemAuthContext(workspaceId); - const workflow = await workflowRepository.findOne({ - where: { id: workflowId }, - }); + const { workflow } = + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workflowRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'workflow', + { shouldBypassPermissionChecks: true }, + ); - if (!isDefined(workflow)) { - throw new WorkflowTriggerException( - `[Webhook trigger] Workflow ${workflowId} not found in workspace ${workspaceId}`, - WorkflowTriggerExceptionCode.NOT_FOUND, - ); - } + const workflow = await workflowRepository.findOne({ + where: { id: workflowId }, + }); - if ( - !isDefined(workflow.lastPublishedVersionId) || - workflow.lastPublishedVersionId === '' - ) { - throw new WorkflowTriggerException( - `[Webhook trigger] Workflow ${workflowId} has not been activated in workspace ${workspaceId}`, - WorkflowTriggerExceptionCode.INVALID_WORKFLOW_STATUS, - ); - } + if (!isDefined(workflow)) { + throw new WorkflowTriggerException( + `[Webhook trigger] Workflow ${workflowId} not found in workspace ${workspaceId}`, + WorkflowTriggerExceptionCode.NOT_FOUND, + ); + } - const workflowVersionRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'workflowVersion', - { shouldBypassPermissionChecks: true }, - ); - const workflowVersion = await workflowVersionRepository.findOne({ - where: { id: workflow.lastPublishedVersionId }, - }); + if ( + !isDefined(workflow.lastPublishedVersionId) || + workflow.lastPublishedVersionId === '' + ) { + throw new WorkflowTriggerException( + `[Webhook trigger] Workflow ${workflowId} has not been activated in workspace ${workspaceId}`, + WorkflowTriggerExceptionCode.INVALID_WORKFLOW_STATUS, + ); + } - if (!isDefined(workflowVersion)) { - throw new WorkflowTriggerException( - `[Webhook trigger] No workflow version activated for workflow ${workflowId} in workspace ${workspaceId}`, - WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION, - ); - } + const workflowVersionRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'workflowVersion', + { shouldBypassPermissionChecks: true }, + ); + const workflowVersion = await workflowVersionRepository.findOne({ + where: { id: workflow.lastPublishedVersionId }, + }); - if (workflowVersion.trigger?.type !== WorkflowTriggerType.WEBHOOK) { - throw new WorkflowTriggerException( - `[Webhook trigger] Workflow ${workflowId} does not have a Webhook trigger in workspace ${workspaceId}`, - WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER, - ); - } + if (!isDefined(workflowVersion)) { + throw new WorkflowTriggerException( + `[Webhook trigger] No workflow version activated for workflow ${workflowId} in workspace ${workspaceId}`, + WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION, + ); + } - if (workflowVersion.status !== WorkflowVersionStatus.ACTIVE) { - throw new WorkflowTriggerException( - `[Webhook trigger] Workflow version ${workflowVersion.id} is not active in workspace ${workspaceId}`, - WorkflowTriggerExceptionCode.INVALID_WORKFLOW_STATUS, + if (workflowVersion.trigger?.type !== WorkflowTriggerType.WEBHOOK) { + throw new WorkflowTriggerException( + `[Webhook trigger] Workflow ${workflowId} does not have a Webhook trigger in workspace ${workspaceId}`, + WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER, + ); + } + + if (workflowVersion.status !== WorkflowVersionStatus.ACTIVE) { + throw new WorkflowTriggerException( + `[Webhook trigger] Workflow version ${workflowVersion.id} is not active in workspace ${workspaceId}`, + WorkflowTriggerExceptionCode.INVALID_WORKFLOW_STATUS, + ); + } + + return { workflow, workflowVersion }; + }, ); - } const { workflowRunId } = await this.workflowTriggerWorkspaceService.runWorkflowVersion({ - workflowVersionId: workflow.lastPublishedVersionId, + workflowVersionId: workflow.lastPublishedVersionId!, payload: payload || {}, createdBy: { source: FieldActorSource.WEBHOOK, diff --git a/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-trigger.resolver.ts b/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-trigger.resolver.ts index 4b3ea514654..9913dd94d2d 100644 --- a/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-trigger.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-trigger.resolver.ts @@ -19,7 +19,8 @@ import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.g import { UserAuthGuard } from 'src/engine/guards/user-auth.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { WorkflowTriggerWorkspaceService } from 'src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; @@ -37,7 +38,7 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta ) export class WorkflowTriggerResolver { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly workflowTriggerWorkspaceService: WorkflowTriggerWorkspaceService, ) {} @@ -72,18 +73,26 @@ export class WorkflowTriggerResolver { @Args('input') { workflowVersionId, workflowRunId, payload }: RunWorkflowVersionInput, ) { - const workspaceMemberRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspace.id, - 'workspaceMember', - { shouldBypassPermissionChecks: true }, - ); + const authContext = buildSystemAuthContext(workspace.id); - const workspaceMember = await workspaceMemberRepository.findOneOrFail({ - where: { - userId: user.id, - }, - }); + const workspaceMember = + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workspaceMemberRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspace.id, + 'workspaceMember', + { shouldBypassPermissionChecks: true }, + ); + + return workspaceMemberRepository.findOneOrFail({ + where: { + userId: user.id, + }, + }); + }, + ); return this.workflowTriggerWorkspaceService.runWorkflowVersion({ workflowVersionId, diff --git a/packages/twenty-server/src/engine/core-modules/workspace-invitation/workspace-invitation.resolver.ts b/packages/twenty-server/src/engine/core-modules/workspace-invitation/workspace-invitation.resolver.ts index acc94d7e870..612c521b547 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace-invitation/workspace-invitation.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace-invitation/workspace-invitation.resolver.ts @@ -16,7 +16,8 @@ import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.g import { UserAuthGuard } from 'src/engine/guards/user-auth.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; import { SendInvitationsInput } from './dtos/send-invitations.input'; @@ -33,7 +34,7 @@ import { SendInvitationsInput } from './dtos/send-invitations.input'; @Resolver() export class WorkspaceInvitationResolver { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly workspaceInvitationService: WorkspaceInvitationService, ) {} @@ -55,18 +56,26 @@ export class WorkspaceInvitationResolver { @AuthWorkspace() workspace: WorkspaceEntity, @AuthUser() user: UserEntity, ) { - const workspaceMemberRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspace.id, - 'workspaceMember', - { shouldBypassPermissionChecks: true }, - ); + const authContext = buildSystemAuthContext(workspace.id); - const workspaceMember = await workspaceMemberRepository.findOneOrFail({ - where: { - userId: user.id, - }, - }); + const workspaceMember = + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workspaceMemberRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspace.id, + 'workspaceMember', + { shouldBypassPermissionChecks: true }, + ); + + return workspaceMemberRepository.findOneOrFail({ + where: { + userId: user.id, + }, + }); + }, + ); return this.workspaceInvitationService.resendWorkspaceInvitation( appTokenId, @@ -87,18 +96,26 @@ export class WorkspaceInvitationResolver { @AuthUser() user: UserEntity, @AuthWorkspace() workspace: WorkspaceEntity, ): Promise { - const workspaceMemberRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspace.id, - 'workspaceMember', - { shouldBypassPermissionChecks: true }, - ); + const authContext = buildSystemAuthContext(workspace.id); - const workspaceMember = await workspaceMemberRepository.findOneOrFail({ - where: { - userId: user.id, - }, - }); + const workspaceMember = + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workspaceMemberRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspace.id, + 'workspaceMember', + { shouldBypassPermissionChecks: true }, + ); + + return workspaceMemberRepository.findOneOrFail({ + where: { + userId: user.id, + }, + }); + }, + ); return await this.workspaceInvitationService.sendInvitations( sendInviteLinkInput.emails, diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/agent-actor-context.service.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/agent-actor-context.service.ts index b56025b6a68..3b501d8329d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/agent-actor-context.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/agent-actor-context.service.ts @@ -9,7 +9,8 @@ import { AgentExceptionCode, } from 'src/engine/metadata-modules/ai/ai-agent/agent.exception'; import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; export type AgentActorContext = { actorContext: ActorMetadata; @@ -22,13 +23,15 @@ export class AgentActorContextService { constructor( private readonly userWorkspaceService: UserWorkspaceService, private readonly userRoleService: UserRoleService, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, ) {} async buildUserAndAgentActorContext( userWorkspaceId: string, workspaceId: string, ): Promise { + const authContext = buildSystemAuthContext(workspaceId); + const userWorkspace = await this.userWorkspaceService.findById(userWorkspaceId); @@ -39,18 +42,24 @@ export class AgentActorContextService { ); } - const workspaceMemberRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'workspaceMember', - { shouldBypassPermissionChecks: true }, - ); + const workspaceMember = + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workspaceMemberRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'workspaceMember', + { shouldBypassPermissionChecks: true }, + ); - const workspaceMember = await workspaceMemberRepository.findOne({ - where: { - userId: userWorkspace.userId, - }, - }); + return workspaceMemberRepository.findOne({ + where: { + userId: userWorkspace.userId, + }, + }); + }, + ); if (!workspaceMember) { throw new AgentException( diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/chat-execution.service.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/chat-execution.service.ts index 9365aa441ce..bf3a6e82c3a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/chat-execution.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/chat-execution.service.ts @@ -44,8 +44,9 @@ import { CHAT_SYSTEM_PROMPTS } from 'src/engine/metadata-modules/ai/ai-chat/cons import { ModelProvider } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-models.const'; import { AI_TELEMETRY_CONFIG } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-telemetry.const'; import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; -import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { getWorkspaceContext } from 'src/engine/twenty-orm/storage/orm-workspace-context.storage'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; export type ChatExecutionOptions = { workspace: WorkspaceEntity; @@ -75,8 +76,7 @@ export class ChatExecutionService { private readonly aiModelRegistryService: AiModelRegistryService, private readonly aiBillingService: AIBillingService, private readonly agentActorContextService: AgentActorContextService, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, - private readonly workspaceCacheService: WorkspaceCacheService, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly workspaceDomainsService: WorkspaceDomainsService, ) {} @@ -212,106 +212,116 @@ export class ChatExecutionService { recordIdsByObjectMetadataNameSingular: RecordIdsByObjectMetadataNameSingularType, userWorkspaceId: string, ): Promise { - const { userWorkspaceRoleMap } = - await this.workspaceCacheService.getOrRecompute(workspace.id, [ - 'userWorkspaceRoleMap', - ]); + const authContext = buildSystemAuthContext(workspace.id); - const roleId = userWorkspaceRoleMap[userWorkspaceId]; + const contextFromRecords = + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const { + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, + permissionsPerRoleId: objectMetadataPermissions, + userWorkspaceRoleMap, + } = getWorkspaceContext(); - if (!roleId) { - throw new AgentException( - 'Failed to retrieve user role.', - AgentExceptionCode.ROLE_NOT_FOUND, - ); - } + const roleId = userWorkspaceRoleMap[userWorkspaceId]; - const workspaceDataSource = - await this.twentyORMGlobalManager.getDataSourceForWorkspace({ - workspaceId: workspace.id, - }); + if (!roleId) { + throw new AgentException( + 'Failed to retrieve user role.', + AgentExceptionCode.ROLE_NOT_FOUND, + ); + } - const flatObjectMetadataMaps = - workspaceDataSource.internalContext.flatObjectMetadataMaps; - const flatFieldMetadataMaps = - workspaceDataSource.internalContext.flatFieldMetadataMaps; - const objectIdByNameSingular = - workspaceDataSource.internalContext.objectIdByNameSingular; - const objectMetadataPermissions = workspaceDataSource.permissionsPerRoleId; - - const contextObject = ( - await Promise.all( - recordIdsByObjectMetadataNameSingular.map( - async (recordsWithObjectMetadataNameSingular) => { - if (recordsWithObjectMetadataNameSingular.recordIds.length === 0) { - return []; - } - - const objectMetadataId = - objectIdByNameSingular[ - recordsWithObjectMetadataNameSingular.objectMetadataNameSingular - ]; - const objectMetadataMapItem = objectMetadataId - ? flatObjectMetadataMaps.byId[objectMetadataId] - : undefined; - - if (!objectMetadataMapItem) { - this.logger.warn( - `Object metadata not found for ${recordsWithObjectMetadataNameSingular.objectMetadataNameSingular}`, - ); - - return []; - } - - const repository = workspaceDataSource.getRepository( - recordsWithObjectMetadataNameSingular.objectMetadataNameSingular, - { unionOf: [roleId] }, + const workspaceDataSource = + await this.globalWorkspaceOrmManager.getDataSourceForWorkspace( + workspace.id, ); - const restrictedFields = - objectMetadataPermissions?.[roleId]?.[objectMetadataMapItem.id] - ?.restrictedFields ?? {}; + const contextObject = ( + await Promise.all( + recordIdsByObjectMetadataNameSingular.map( + async (recordsWithObjectMetadataNameSingular) => { + if ( + recordsWithObjectMetadataNameSingular.recordIds.length === 0 + ) { + return []; + } - const hasRestrictedFields = Object.values(restrictedFields).some( - (field) => field.canRead === false, - ); + const objectMetadataId = + objectIdByNameSingular[ + recordsWithObjectMetadataNameSingular + .objectMetadataNameSingular + ]; + const objectMetadataMapItem = objectMetadataId + ? flatObjectMetadataMaps.byId[objectMetadataId] + : undefined; - const selectOptions = hasRestrictedFields - ? getAllSelectableColumnNames({ - restrictedFields, - objectMetadata: { - objectMetadataMapItem, - flatFieldMetadataMaps, - }, - }) - : undefined; + if (!objectMetadataMapItem) { + this.logger.warn( + `Object metadata not found for ${recordsWithObjectMetadataNameSingular.objectMetadataNameSingular}`, + ); - return ( - await repository.find({ - ...(selectOptions && { select: selectOptions }), - where: { - id: In(recordsWithObjectMetadataNameSingular.recordIds), + return []; + } + + const repository = workspaceDataSource.getRepository( + recordsWithObjectMetadataNameSingular.objectMetadataNameSingular, + { unionOf: [roleId] }, + ); + + const restrictedFields = + objectMetadataPermissions?.[roleId]?.[ + objectMetadataMapItem.id + ]?.restrictedFields ?? {}; + + const hasRestrictedFields = Object.values( + restrictedFields, + ).some((field) => field.canRead === false); + + const selectOptions = hasRestrictedFields + ? getAllSelectableColumnNames({ + restrictedFields, + objectMetadata: { + objectMetadataMapItem, + flatFieldMetadataMaps, + }, + }) + : undefined; + + return ( + await repository.find({ + ...(selectOptions && { select: selectOptions }), + where: { + id: In(recordsWithObjectMetadataNameSingular.recordIds), + }, + }) + ).map((record) => { + return { + ...record, + resourceUrl: + this.workspaceDomainsService.buildWorkspaceURL({ + workspace, + pathname: getAppPath(AppPath.RecordShowPage, { + objectNameSingular: + recordsWithObjectMetadataNameSingular.objectMetadataNameSingular, + objectRecordId: record.id, + }), + }), + }; + }); }, - }) - ).map((record) => { - return { - ...record, - resourceUrl: this.workspaceDomainsService.buildWorkspaceURL({ - workspace, - pathname: getAppPath(AppPath.RecordShowPage, { - objectNameSingular: - recordsWithObjectMetadataNameSingular.objectMetadataNameSingular, - objectRecordId: record.id, - }), - }), - }; - }); - }, - ), - ) - ).flat(2); + ), + ) + ).flat(2); - return JSON.stringify(contextObject); + return JSON.stringify(contextObject); + }, + ); + + return contextFromRecords; } private getLastUserMessage( diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts index c1c44434b29..830b1b69008 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts @@ -31,7 +31,8 @@ import { } from 'src/engine/metadata-modules/object-metadata/object-metadata.exception'; import { ViewKey } from 'src/engine/metadata-modules/view/enums/view-key.enum'; import { ViewType } from 'src/engine/metadata-modules/view/enums/view-type.enum'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; import { DEFAULT_VIEW_FIELD_SIZE } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/constants/DEFAULT_VIEW_FIELD_SIZE'; import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2'; @@ -46,7 +47,7 @@ export class ObjectMetadataService extends TypeOrmQueryService( - workspaceId, - 'favorite', - ); + const authContext = buildSystemAuthContext(workspaceId); - const favoriteCount = await favoriteRepository.count(); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const favoriteRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'favorite', + ); - await favoriteRepository.insert({ - viewId: view.id, - position: favoriteCount, - }); + const favoriteCount = await favoriteRepository.count(); + + await favoriteRepository.insert({ + viewId: view.id, + position: favoriteCount, + }); + }, + ); } public async deleteWorkspaceAllObjectMetadata({ diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout.service.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout.service.ts index 5cbfb8a53fe..66a47666505 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout/services/page-layout.service.ts @@ -30,7 +30,8 @@ import { } from 'src/engine/metadata-modules/page-layout/exceptions/page-layout.exception'; import { fromFlatPageLayoutToPageLayoutDto } from 'src/engine/metadata-modules/page-layout/utils/from-flat-page-layout-to-page-layout-dto.util'; import { fromFlatPageLayoutWithTabsAndWidgetsToPageLayoutDto } from 'src/engine/metadata-modules/page-layout/utils/from-flat-page-layout-with-tabs-and-widgets-to-page-layout-dto.util'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2'; import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service'; @@ -39,7 +40,7 @@ export class PageLayoutService { private readonly logger = new Logger(PageLayoutService.name); constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService, private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, private readonly applicationService: ApplicationService, @@ -376,23 +377,30 @@ export class PageLayoutService { pageLayoutId: string, workspaceId: string, ): Promise { + const authContext = buildSystemAuthContext(workspaceId); + try { - const dashboardRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'dashboard', - { shouldBypassPermissionChecks: true }, - ); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const dashboardRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'dashboard', + { shouldBypassPermissionChecks: true }, + ); - const dashboards = await dashboardRepository.find({ - where: { - pageLayoutId, + const dashboards = await dashboardRepository.find({ + where: { + pageLayoutId, + }, + }); + + for (const dashboard of dashboards) { + await dashboardRepository.delete(dashboard.id); + } }, - }); - - for (const dashboard of dashboards) { - await dashboardRepository.delete(dashboard.id); - } + ); } catch (error) { this.logger.error( `Failed to destroy associated dashboards for page layout ${pageLayoutId}: ${error}`, diff --git a/packages/twenty-server/src/engine/metadata-modules/user-role/user-role.service.ts b/packages/twenty-server/src/engine/metadata-modules/user-role/user-role.service.ts index a96087cabee..b28ff9fb1e5 100644 --- a/packages/twenty-server/src/engine/metadata-modules/user-role/user-role.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/user-role/user-role.service.ts @@ -13,7 +13,8 @@ import { import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity'; import { RoleTargetService } from 'src/engine/metadata-modules/role-target/services/role-target.service'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; import { ADMIN_ROLE } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-roles/roles/admin-role'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; @@ -24,7 +25,7 @@ export class UserRoleService { private readonly roleTargetRepository: Repository, @InjectRepository(UserWorkspaceEntity) private readonly userWorkspaceRepository: Repository, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly roleTargetService: RoleTargetService, private readonly workspaceCacheService: WorkspaceCacheService, ) {} @@ -137,6 +138,8 @@ export class UserRoleService { roleId: string, workspaceId: string, ): Promise { + const authContext = buildSystemAuthContext(workspaceId); + const userWorkspaceIdsWithRole = await this.getUserWorkspaceIdsAssignedToRole(roleId, workspaceId); @@ -150,20 +153,25 @@ export class UserRoleService { userWorkspaces.map((userWorkspace) => userWorkspace.userId), ); - const workspaceMemberRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'workspaceMember', - { shouldBypassPermissionChecks: true }, - ); + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workspaceMemberRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'workspaceMember', + { shouldBypassPermissionChecks: true }, + ); - const workspaceMembers = await workspaceMemberRepository.find({ - where: { - userId: In(userIds), + const workspaceMembers = await workspaceMemberRepository.find({ + where: { + userId: In(userIds), + }, + }); + + return workspaceMembers; }, - }); - - return workspaceMembers; + ); } public async getUserWorkspaceIdsAssignedToRole( diff --git a/packages/twenty-server/src/engine/object-metadata-repository/object-metadata-repository.module.ts b/packages/twenty-server/src/engine/object-metadata-repository/object-metadata-repository.module.ts index 5e75d86f0b7..91dca544203 100644 --- a/packages/twenty-server/src/engine/object-metadata-repository/object-metadata-repository.module.ts +++ b/packages/twenty-server/src/engine/object-metadata-repository/object-metadata-repository.module.ts @@ -8,7 +8,7 @@ import { import { capitalize } from 'twenty-shared/utils'; import { metadataToRepositoryMapping } from 'src/engine/object-metadata-repository/metadata-to-repository.mapping'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; import { convertClassNameToObjectMetadataName } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/convert-class-to-object-metadata-name.util'; @@ -31,10 +31,10 @@ export class ObjectMetadataRepositoryModule { provide: `${capitalize( convertClassNameToObjectMetadataName(objectMetadata.name), )}Repository`, - useFactory: (twentyORMGlobalManager: TwentyORMGlobalManager) => { - return new repositoryClass(twentyORMGlobalManager); + useFactory: (globalWorkspaceOrmManager: GlobalWorkspaceOrmManager) => { + return new repositoryClass(globalWorkspaceOrmManager); }, - inject: [TwentyORMGlobalManager], + inject: [GlobalWorkspaceOrmManager], }; }); diff --git a/packages/twenty-server/src/engine/trash-cleanup/services/__tests__/trash-cleanup.service.spec.ts b/packages/twenty-server/src/engine/trash-cleanup/services/__tests__/trash-cleanup.service.spec.ts index b597dc8d78f..fc6520e1fba 100644 --- a/packages/twenty-server/src/engine/trash-cleanup/services/__tests__/trash-cleanup.service.spec.ts +++ b/packages/twenty-server/src/engine/trash-cleanup/services/__tests__/trash-cleanup.service.spec.ts @@ -2,20 +2,23 @@ import { Test, type TestingModule } from '@nestjs/testing'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { TrashCleanupService } from 'src/engine/trash-cleanup/services/trash-cleanup.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; describe('TrashCleanupService', () => { let service: TrashCleanupService; let mockFlatEntityMapsCacheService: any; - let mockTwentyORMGlobalManager: any; + let mockGlobalWorkspaceOrmManager: any; beforeEach(async () => { mockFlatEntityMapsCacheService = { getOrRecomputeManyOrAllFlatEntityMaps: jest.fn(), }; - mockTwentyORMGlobalManager = { - getRepositoryForWorkspace: jest.fn(), + mockGlobalWorkspaceOrmManager = { + getRepository: jest.fn(), + executeInWorkspaceContext: jest + .fn() + .mockImplementation((_authContext: any, fn: () => any) => fn()), }; const module: TestingModule = await Test.createTestingModule({ @@ -26,8 +29,8 @@ describe('TrashCleanupService', () => { useValue: mockFlatEntityMapsCacheService, }, { - provide: TwentyORMGlobalManager, - useValue: mockTwentyORMGlobalManager, + provide: GlobalWorkspaceOrmManager, + useValue: mockGlobalWorkspaceOrmManager, }, ], }).compile(); @@ -99,7 +102,7 @@ describe('TrashCleanupService', () => { const companyRepository = createRepositoryMock('company', 2); const personRepository = createRepositoryMock('person', 1); - mockTwentyORMGlobalManager.getRepositoryForWorkspace + mockGlobalWorkspaceOrmManager.getRepository .mockResolvedValueOnce(companyRepository) .mockResolvedValueOnce(personRepository); @@ -130,7 +133,7 @@ describe('TrashCleanupService', () => { expect(result).toEqual(0); expect( - mockTwentyORMGlobalManager.getRepositoryForWorkspace, + mockGlobalWorkspaceOrmManager.getRepository, ).not.toHaveBeenCalled(); }); @@ -145,7 +148,7 @@ describe('TrashCleanupService', () => { const companyRepository = createRepositoryMock('company', 2); const personRepository = createRepositoryMock('person', 5); - mockTwentyORMGlobalManager.getRepositoryForWorkspace + mockGlobalWorkspaceOrmManager.getRepository .mockResolvedValueOnce(companyRepository) .mockResolvedValueOnce(personRepository); @@ -170,7 +173,7 @@ describe('TrashCleanupService', () => { const companyRepository = createRepositoryMock('company', 0); - mockTwentyORMGlobalManager.getRepositoryForWorkspace.mockResolvedValueOnce( + mockGlobalWorkspaceOrmManager.getRepository.mockResolvedValueOnce( companyRepository, ); @@ -188,7 +191,7 @@ describe('TrashCleanupService', () => { const companyRepository = createRepositoryMock('company', 5); - mockTwentyORMGlobalManager.getRepositoryForWorkspace.mockResolvedValueOnce( + mockGlobalWorkspaceOrmManager.getRepository.mockResolvedValueOnce( companyRepository, ); diff --git a/packages/twenty-server/src/engine/trash-cleanup/services/trash-cleanup.service.ts b/packages/twenty-server/src/engine/trash-cleanup/services/trash-cleanup.service.ts index 1728d0e2998..22a76eaa40a 100644 --- a/packages/twenty-server/src/engine/trash-cleanup/services/trash-cleanup.service.ts +++ b/packages/twenty-server/src/engine/trash-cleanup/services/trash-cleanup.service.ts @@ -6,7 +6,8 @@ import { In, LessThan } from 'typeorm'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { TRASH_CLEANUP_BATCH_SIZE } from 'src/engine/trash-cleanup/constants/trash-cleanup-batch-size.constant'; import { TRASH_CLEANUP_MAX_RECORDS_PER_WORKSPACE } from 'src/engine/trash-cleanup/constants/trash-cleanup-max-records-per-workspace.constant'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; export type TrashCleanupInput = { workspaceId: string; @@ -22,7 +23,7 @@ export class TrashCleanupService { constructor( private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, ) {} async cleanupWorkspaceTrash(input: TrashCleanupInput): Promise { @@ -96,41 +97,47 @@ export class TrashCleanupService { return 0; } - const repository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - objectName, - { shouldBypassPermissionChecks: true }, - ); + const authContext = buildSystemAuthContext(workspaceId); - let deleted = 0; + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const repository = await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + objectName, + { shouldBypassPermissionChecks: true }, + ); - while (deleted < remainingQuota) { - const take = Math.min(this.batchSize, remainingQuota - deleted); + let deleted = 0; - const recordsToDelete = await repository.find({ - withDeleted: true, - select: ['id'], - where: { - deletedAt: LessThan(cutoffDate), - }, - order: { deletedAt: 'ASC' }, - take, - loadEagerRelations: false, - }); + while (deleted < remainingQuota) { + const take = Math.min(this.batchSize, remainingQuota - deleted); - if (recordsToDelete.length === 0) { - break; - } + const recordsToDelete = await repository.find({ + withDeleted: true, + select: ['id'], + where: { + deletedAt: LessThan(cutoffDate), + }, + order: { deletedAt: 'ASC' }, + take, + loadEagerRelations: false, + }); - await repository.delete({ - id: In(recordsToDelete.map((record) => record.id)), - }); + if (recordsToDelete.length === 0) { + break; + } - deleted += recordsToDelete.length; - } + await repository.delete({ + id: In(recordsToDelete.map((record) => record.id)), + }); - return deleted; + deleted += recordsToDelete.length; + } + + return deleted; + }, + ); } private calculateCutoffDate(trashRetentionDays: number): Date { diff --git a/packages/twenty-server/src/engine/twenty-orm/datasource/workspace.datasource.ts b/packages/twenty-server/src/engine/twenty-orm/datasource/workspace.datasource.ts index 3e73c5ed7e0..a21dc4f984e 100644 --- a/packages/twenty-server/src/engine/twenty-orm/datasource/workspace.datasource.ts +++ b/packages/twenty-server/src/engine/twenty-orm/datasource/workspace.datasource.ts @@ -13,6 +13,7 @@ import { import { EntityManagerFactory } from 'typeorm/entity-manager/EntityManagerFactory'; import { type FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface'; +import { type WorkspaceDataSourceInterface } from 'src/engine/twenty-orm/interfaces/workspace-datasource.interface'; import { type WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface'; import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; @@ -29,7 +30,11 @@ type CreateQueryBuilderOptions = { calledByWorkspaceEntityManager?: boolean; }; -export class WorkspaceDataSource extends DataSource { +export class WorkspaceDataSource + extends DataSource + implements WorkspaceDataSourceInterface +{ + readonly isGlobalFlow = false; readonly internalContext: WorkspaceInternalContext; readonly manager: WorkspaceEntityManager; featureFlagMapVersion: string; diff --git a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.ts b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.ts index 36ccb5ccf46..6f630b0b7ea 100644 --- a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.ts +++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.ts @@ -34,6 +34,7 @@ import { type UpsertOptions } from 'typeorm/repository/UpsertOptions'; import { InstanceChecker } from 'typeorm/util/InstanceChecker'; import { type FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface'; +import { type WorkspaceDataSourceInterface } from 'src/engine/twenty-orm/interfaces/workspace-datasource.interface'; import { type WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface'; import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; @@ -57,12 +58,14 @@ import { } from 'src/engine/twenty-orm/repository/permissions.utils'; import { WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder'; import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; +import { getWorkspaceContext } from 'src/engine/twenty-orm/storage/orm-workspace-context.storage'; import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; import { computePermissionIntersection } from 'src/engine/twenty-orm/utils/compute-permission-intersection.util'; import { formatData } from 'src/engine/twenty-orm/utils/format-data.util'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; import { formatTwentyOrmEventToDatabaseBatchEvent } from 'src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util'; import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util'; +import { type WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; type PermissionOptions = { shouldBypassPermissionChecks?: boolean; @@ -70,19 +73,52 @@ type PermissionOptions = { }; export class WorkspaceEntityManager extends EntityManager { - private readonly internalContext: WorkspaceInternalContext; + private readonly _legacyInternalContext?: WorkspaceInternalContext; // eslint-disable-next-line @typescript-eslint/no-explicit-any readonly repositories: Map>; declare connection: WorkspaceDataSource; constructor( - internalContext: WorkspaceInternalContext, + legacyInternalContext: WorkspaceInternalContext | undefined, connection: WorkspaceDataSource | GlobalWorkspaceDataSource, queryRunner?: QueryRunner, ) { super(connection, queryRunner); - this.internalContext = internalContext; this.repositories = new Map(); + this._legacyInternalContext = legacyInternalContext; + } + + private get eventEmitterService(): WorkspaceEventEmitter { + const isGlobalFlow = (this.connection as WorkspaceDataSourceInterface) + .isGlobalFlow; + + if (isGlobalFlow) { + return (this.connection as unknown as GlobalWorkspaceDataSource) + .eventEmitterService; + } + + return this._legacyInternalContext!.eventEmitterService; + } + + get internalContext(): WorkspaceInternalContext { + const isGlobalFlow = (this.connection as WorkspaceDataSourceInterface) + .isGlobalFlow; + + if (isGlobalFlow) { + const context = getWorkspaceContext(); + + return { + workspaceId: context.authContext.workspace.id, + flatObjectMetadataMaps: context.flatObjectMetadataMaps, + flatFieldMetadataMaps: context.flatFieldMetadataMaps, + flatIndexMaps: context.flatIndexMaps, + objectIdByNameSingular: context.objectIdByNameSingular, + featureFlagsMap: context.featureFlagsMap, + eventEmitterService: this.eventEmitterService, + }; + } + + return this._legacyInternalContext!; } getFeatureFlagMap(): FeatureFlagMap { @@ -146,7 +182,6 @@ export class WorkspaceEntityManager extends EntityManager { } const newRepository = new WorkspaceRepository( - this.internalContext, target, this, dataSource.featureFlagMap, diff --git a/packages/twenty-server/src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource.module.ts b/packages/twenty-server/src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource.module.ts index 4ae82042a57..b7caa35e0f2 100644 --- a/packages/twenty-server/src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource.module.ts +++ b/packages/twenty-server/src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource.module.ts @@ -15,6 +15,7 @@ import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-sche import { GlobalWorkspaceDataSourceService } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource.service'; import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { WorkspaceORMEntityMetadatasCacheService } from 'src/engine/twenty-orm/global-workspace-datasource/workspace-orm-entity-metadatas-cache.service'; +import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; import { WorkspaceEventEmitterModule } from 'src/engine/workspace-event-emitter/workspace-event-emitter.module'; @@ -35,6 +36,7 @@ import { WorkspaceEventEmitterModule } from 'src/engine/workspace-event-emitter/ TwentyConfigModule, WorkspaceEventEmitterModule, WorkspaceCacheModule, + TwentyORMModule, ], providers: [ GlobalWorkspaceDataSourceService, diff --git a/packages/twenty-server/src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource.ts b/packages/twenty-server/src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource.ts index cb4f2e0212d..ecc167c1c15 100644 --- a/packages/twenty-server/src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource.ts +++ b/packages/twenty-server/src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource.ts @@ -15,6 +15,7 @@ import { EntityMetadataNotFoundError } from 'typeorm/error/EntityMetadataNotFoun import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface'; import { type FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface'; +import { type WorkspaceDataSourceInterface } from 'src/engine/twenty-orm/interfaces/workspace-datasource.interface'; import { PermissionsException, @@ -31,8 +32,12 @@ type CreateQueryBuilderOptions = { calledByWorkspaceEntityManager?: boolean; }; -export class GlobalWorkspaceDataSource extends DataSource { - private eventEmitterService: WorkspaceEventEmitter; +export class GlobalWorkspaceDataSource + extends DataSource + implements WorkspaceDataSourceInterface +{ + readonly isGlobalFlow = true; + readonly eventEmitterService: WorkspaceEventEmitter; private _isConstructing = true; dataSourceWithOverridenCreateQueryBuilder: GlobalWorkspaceDataSource; @@ -102,21 +107,7 @@ export class GlobalWorkspaceDataSource extends DataSource { return super.createEntityManager(queryRunner) as WorkspaceEntityManager; } - const context = getWorkspaceContext(); - - return new WorkspaceEntityManager( - { - workspaceId: context.authContext.workspace.id, - flatObjectMetadataMaps: context.flatObjectMetadataMaps, - flatFieldMetadataMaps: context.flatFieldMetadataMaps, - flatIndexMaps: context.flatIndexMaps, - objectIdByNameSingular: context.objectIdByNameSingular, - featureFlagsMap: context.featureFlagsMap, - eventEmitterService: this.eventEmitterService, - }, - this, - queryRunner, - ); + return new WorkspaceEntityManager(undefined, this, queryRunner); } override createQueryRunner( diff --git a/packages/twenty-server/src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager.ts b/packages/twenty-server/src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager.ts index b6f8a63d7b2..2355a2822ea 100644 --- a/packages/twenty-server/src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager.ts +++ b/packages/twenty-server/src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager.ts @@ -1,17 +1,20 @@ import { Injectable, type Type } from '@nestjs/common'; -import { ObjectLiteral } from 'typeorm'; +import { type ObjectLiteral } from 'typeorm'; -import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface'; +import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface'; +import type { WorkspaceDataSourceInterface } from 'src/engine/twenty-orm/interfaces/workspace-datasource.interface'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util'; import { GlobalWorkspaceDataSourceService } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource.service'; -import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; +import type { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; import { type ORMWorkspaceContext, withWorkspaceContext, } from 'src/engine/twenty-orm/storage/orm-workspace-context.storage'; -import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import type { RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; import { convertClassNameToObjectMetadataName } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/convert-class-to-object-metadata-name.util'; @@ -20,8 +23,21 @@ export class GlobalWorkspaceOrmManager { constructor( private readonly globalWorkspaceDataSourceService: GlobalWorkspaceDataSourceService, private readonly workspaceCacheService: WorkspaceCacheService, + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, ) {} + async isGlobalDataSourceFlow(workspaceId: string): Promise { + const { featureFlagsMap } = await this.workspaceCacheService.getOrRecompute( + workspaceId, + ['featureFlagsMap'], + ); + + return ( + featureFlagsMap[FeatureFlagKey.IS_GLOBAL_WORKSPACE_DATASOURCE_ENABLED] === + true + ); + } + async getRepository( workspaceId: string, workspaceEntity: Type, @@ -39,25 +55,57 @@ export class GlobalWorkspaceOrmManager { workspaceEntityOrObjectMetadataName: Type | string, permissionOptions?: RolePermissionConfig, ): Promise> { - let objectMetadataName: string; + const isGlobalFlow = await this.isGlobalDataSourceFlow(workspaceId); - if (typeof workspaceEntityOrObjectMetadataName === 'string') { - objectMetadataName = workspaceEntityOrObjectMetadataName; - } else { - objectMetadataName = convertClassNameToObjectMetadataName( - workspaceEntityOrObjectMetadataName.name, + if (isGlobalFlow) { + let objectMetadataName: string; + + if (typeof workspaceEntityOrObjectMetadataName === 'string') { + objectMetadataName = workspaceEntityOrObjectMetadataName; + } else { + objectMetadataName = convertClassNameToObjectMetadataName( + workspaceEntityOrObjectMetadataName.name, + ); + } + + const globalDataSource = + this.globalWorkspaceDataSourceService.getGlobalWorkspaceDataSource(); + + return globalDataSource.getRepository( + objectMetadataName, + permissionOptions, ); } - const globalDataSource = - this.globalWorkspaceDataSourceService.getGlobalWorkspaceDataSource(); + if (typeof workspaceEntityOrObjectMetadataName === 'string') { + return this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + workspaceEntityOrObjectMetadataName, + permissionOptions, + ); + } - return globalDataSource.getRepository( - objectMetadataName, + return this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + workspaceEntityOrObjectMetadataName, permissionOptions, ); } + async getDataSourceForWorkspace( + workspaceId: string, + ): Promise { + const isGlobalFlow = await this.isGlobalDataSourceFlow(workspaceId); + + if (isGlobalFlow) { + return this.globalWorkspaceDataSourceService.getGlobalWorkspaceDataSource(); + } + + return this.twentyORMGlobalManager.getDataSourceForWorkspace({ + workspaceId, + }); + } + async getGlobalWorkspaceDataSource() { return this.globalWorkspaceDataSourceService.getGlobalWorkspaceDataSource(); } @@ -71,6 +119,18 @@ export class GlobalWorkspaceOrmManager { return withWorkspaceContext(context, fn); } + async destroyDataSourceForWorkspace(workspaceId: string): Promise { + const isGlobalFlow = await this.isGlobalDataSourceFlow(workspaceId); + + if (isGlobalFlow) { + return; + } + + await this.twentyORMGlobalManager.destroyDataSourceForWorkspace( + workspaceId, + ); + } + private async loadWorkspaceContext( authContext: WorkspaceAuthContext, ): Promise { @@ -83,6 +143,7 @@ export class GlobalWorkspaceOrmManager { featureFlagsMap, rolesPermissions: permissionsPerRoleId, ORMEntityMetadatas: entityMetadatas, + userWorkspaceRoleMap, } = await this.workspaceCacheService.getOrRecompute(workspaceId, [ 'flatObjectMetadataMaps', 'flatFieldMetadataMaps', @@ -90,6 +151,7 @@ export class GlobalWorkspaceOrmManager { 'featureFlagsMap', 'rolesPermissions', 'ORMEntityMetadatas', + 'userWorkspaceRoleMap', ]); const { idByNameSingular: objectIdByNameSingular } = @@ -104,6 +166,7 @@ export class GlobalWorkspaceOrmManager { featureFlagsMap, permissionsPerRoleId, entityMetadatas, + userWorkspaceRoleMap, }; } } diff --git a/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-datasource.interface.ts b/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-datasource.interface.ts new file mode 100644 index 00000000000..01e20edbc24 --- /dev/null +++ b/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-datasource.interface.ts @@ -0,0 +1,49 @@ +import { type ObjectsPermissionsByRoleId } from 'twenty-shared/types'; +import { + type EntityManager, + type EntityTarget, + type ObjectLiteral, + type QueryRunner, + type ReplicationMode, +} from 'typeorm'; + +import { type FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface'; + +import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; +import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; +import { type WorkspaceQueryRunner } from 'src/engine/twenty-orm/query-runner/workspace-query-runner'; +import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; +import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; + +export interface WorkspaceDataSourceInterface { + readonly isGlobalFlow: boolean; + readonly manager: EntityManager; + + featureFlagMap: FeatureFlagMap; + permissionsPerRoleId: ObjectsPermissionsByRoleId; + + getRepository( + target: EntityTarget, + permissionOptions?: RolePermissionConfig, + authContext?: AuthContext, + ): WorkspaceRepository; + + createQueryRunner(mode?: ReplicationMode): WorkspaceQueryRunner; + + createEntityManager(queryRunner?: QueryRunner): WorkspaceEntityManager; + + createQueryRunnerForEntityPersistExecutor( + mode?: ReplicationMode, + ): QueryRunner; + + transaction( + runInTransaction: (entityManager: EntityManager) => Promise, + ): Promise; + + query( + query: string, + parameters?: unknown[], + queryRunner?: QueryRunner, + options?: { shouldBypassPermissionChecks?: boolean }, + ): Promise; +} diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/__tests__/workspace.repository.spec.ts b/packages/twenty-server/src/engine/twenty-orm/repository/__tests__/workspace.repository.spec.ts index bc29deba707..413bb442461 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/__tests__/workspace.repository.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/__tests__/workspace.repository.spec.ts @@ -59,6 +59,9 @@ describe('WorkspaceRepository', () => { decrement: jest.fn(), preload: jest.fn(), clear: jest.fn(), + get internalContext() { + return mockInternalContext; + }, } as unknown as jest.Mocked; const mockFieldMetadata: FlatFieldMetadata = { @@ -136,7 +139,6 @@ describe('WorkspaceRepository', () => { mockQueryRunner = {} as QueryRunner; repository = new WorkspaceRepository( - mockInternalContext, 'test-entity', mockEntityManager, mockFeatureFlagMap, diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts index 508fe747daa..ea7ac5652f0 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts @@ -37,7 +37,6 @@ import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/g export class WorkspaceRepository< T extends ObjectLiteral, > extends Repository { - private readonly _internalContext: WorkspaceInternalContext; private shouldBypassPermissionChecks: boolean; private featureFlagMap: FeatureFlagMap; public readonly objectRecordsPermissions?: ObjectsPermissions; @@ -45,11 +44,10 @@ export class WorkspaceRepository< declare manager: WorkspaceEntityManager; get internalContext(): WorkspaceInternalContext { - return this._internalContext; + return this.manager.internalContext; } constructor( - internalContext: WorkspaceInternalContext, target: EntityTarget, manager: WorkspaceEntityManager, featureFlagMap: FeatureFlagMap, @@ -59,7 +57,6 @@ export class WorkspaceRepository< authContext?: AuthContext, ) { super(target, manager, queryRunner); - this._internalContext = internalContext; this.featureFlagMap = featureFlagMap; this.objectRecordsPermissions = objectRecordsPermissions; this.shouldBypassPermissionChecks = shouldBypassPermissionChecks; @@ -83,7 +80,7 @@ export class WorkspaceRepository< return new WorkspaceSelectQueryBuilder( queryBuilder, this.objectRecordsPermissions, - this._internalContext, + this.internalContext, this.shouldBypassPermissionChecks, this.authContext ?? {}, this.featureFlagMap, @@ -948,10 +945,7 @@ export class WorkspaceRepository< * PRIVATE METHODS */ private async getObjectMetadataFromTarget() { - return getObjectMetadataFromEntityTarget( - this.target, - this._internalContext, - ); + return getObjectMetadataFromEntityTarget(this.target, this.internalContext); } private async transformOptions< @@ -978,7 +972,7 @@ export class WorkspaceRepository< return formatData( data, objectMetadata, - this._internalContext.flatFieldMetadataMaps, + this.internalContext.flatFieldMetadataMaps, ) as T; } } diff --git a/packages/twenty-server/src/engine/twenty-orm/storage/orm-workspace-context.storage.ts b/packages/twenty-server/src/engine/twenty-orm/storage/orm-workspace-context.storage.ts index 487f7fcb5d5..ec5170af16a 100644 --- a/packages/twenty-server/src/engine/twenty-orm/storage/orm-workspace-context.storage.ts +++ b/packages/twenty-server/src/engine/twenty-orm/storage/orm-workspace-context.storage.ts @@ -20,6 +20,7 @@ export type ORMWorkspaceContext = { featureFlagsMap: Record; permissionsPerRoleId: ObjectsPermissionsByRoleId; entityMetadatas: EntityMetadata[]; + userWorkspaceRoleMap: Record; }; export const workspaceContextStorage = diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/build-system-auth-context.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/build-system-auth-context.util.ts new file mode 100644 index 00000000000..1f48ec8d39d --- /dev/null +++ b/packages/twenty-server/src/engine/twenty-orm/utils/build-system-auth-context.util.ts @@ -0,0 +1,15 @@ +import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface'; + +// Builds a minimal WorkspaceAuthContext for system operations (jobs, commands, crons) +// that don't have a user context but need to operate on a workspace +export const buildSystemAuthContext = ( + workspaceId: string, +): WorkspaceAuthContext => { + return { + user: null, + workspace: { id: workspaceId } as WorkspaceAuthContext['workspace'], + workspaceMemberId: undefined, + userWorkspaceId: undefined, + apiKey: null, + } as WorkspaceAuthContext; +}; diff --git a/packages/twenty-server/src/engine/workspace-manager/__tests__/workspace-manager.service.spec.ts b/packages/twenty-server/src/engine/workspace-manager/__tests__/workspace-manager.service.spec.ts index 58ea4716ed7..c0dfedfbb96 100644 --- a/packages/twenty-server/src/engine/workspace-manager/__tests__/workspace-manager.service.spec.ts +++ b/packages/twenty-server/src/engine/workspace-manager/__tests__/workspace-manager.service.spec.ts @@ -21,7 +21,7 @@ import { RoleService } from 'src/engine/metadata-modules/role/role.service'; import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; import { WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service'; import { WorkspaceSyncMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service'; @@ -139,7 +139,7 @@ describe('WorkspaceManagerService', () => { }, }, { - provide: TwentyORMGlobalManager, + provide: GlobalWorkspaceOrmManager, useValue: { getDataSourceForWorkspace: jest.fn().mockResolvedValue({ transaction: jest.fn(), diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/services/cleaner.workspace-service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/services/cleaner.workspace-service.ts index 27ac58837e0..a965d8b5569 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/services/cleaner.workspace-service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/services/cleaner.workspace-service.ts @@ -24,7 +24,7 @@ import { UserService } from 'src/engine/core-modules/user/services/user.service' import { UserVarsService } from 'src/engine/core-modules/user/user-vars/services/user-vars.service'; import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { USER_WORKSPACE_DELETION_WARNING_SENT_KEY } from 'src/engine/workspace-manager/workspace-cleaner/constants/user-workspace-deletion-warning-sent-key.constant'; import { WorkspaceCleanerException, @@ -49,7 +49,7 @@ export class CleanerWorkspaceService { private readonly workspaceRepository: Repository, @InjectRepository(BillingSubscriptionEntity) private readonly billingSubscriptionRepository: Repository, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, @InjectRepository(UserWorkspaceEntity) private readonly userWorkspaceRepository: Repository, private readonly i18nService: I18nService, @@ -401,7 +401,7 @@ export class CleanerWorkspaceService { ); } - await this.twentyORMGlobalManager.destroyDataSourceForWorkspace( + await this.globalWorkspaceOrmManager.destroyDataSourceForWorkspace( workspace.id, ); } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts index 5a8a767cdcf..fd072c830eb 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts @@ -17,7 +17,7 @@ import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { RoleService } from 'src/engine/metadata-modules/role/role.service'; import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; import { prefillCoreViews } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-core-views'; import { standardObjectsPrefillData } from 'src/engine/workspace-manager/standard-objects-prefill-data/standard-objects-prefill-data'; @@ -47,7 +47,7 @@ export class WorkspaceManagerService { private readonly roleRepository: Repository, @InjectRepository(RoleTargetEntity) private readonly roleTargetRepository: Repository, - protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly applicationService: ApplicationService, private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, ) {} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command.ts index fc71e78ffb2..e2f87060e38 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command.ts @@ -4,12 +4,12 @@ import { Command } from 'nest-commander'; import { Repository } from 'typeorm'; import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; +import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { WorkspaceSyncMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service'; -import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; import { SyncWorkspaceLoggerService } from './services/sync-workspace-logger.service'; @@ -25,9 +25,9 @@ export class SyncWorkspaceMetadataCommand extends ActiveOrSuspendedWorkspacesMig protected readonly dataSourceService: DataSourceService, private readonly syncWorkspaceLoggerService: SyncWorkspaceLoggerService, private readonly featureFlagService: FeatureFlagService, - protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, ) { - super(workspaceRepository, twentyORMGlobalManager, dataSourceService); + super(workspaceRepository, globalWorkspaceOrmManager, dataSourceService); } override async runOnWorkspace({ diff --git a/packages/twenty-server/src/modules/blocklist/blocklist-validation-manager/services/blocklist-validation.service.ts b/packages/twenty-server/src/modules/blocklist/blocklist-validation-manager/services/blocklist-validation.service.ts index 821b85944e4..33aee25cfdd 100644 --- a/packages/twenty-server/src/modules/blocklist/blocklist-validation-manager/services/blocklist-validation.service.ts +++ b/packages/twenty-server/src/modules/blocklist/blocklist-validation-manager/services/blocklist-validation.service.ts @@ -8,7 +8,8 @@ import { } 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 { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; 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'; @@ -28,7 +29,7 @@ export class BlocklistValidationService { constructor( @InjectObjectMetadataRepository(BlocklistWorkspaceEntity) private readonly blocklistRepository: BlocklistRepository, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, ) {} public async validateBlocklistForCreateMany( @@ -83,15 +84,23 @@ export class BlocklistValidationService { userId: string, workspaceId: string, ) { - const workspaceMemberRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - WorkspaceMemberWorkspaceEntity, - ); + const authContext = buildSystemAuthContext(workspaceId); + const currentWorkspaceMember = - await workspaceMemberRepository.findOneByOrFail({ - userId, - }); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workspaceMemberRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + WorkspaceMemberWorkspaceEntity, + ); + + return workspaceMemberRepository.findOneByOrFail({ + userId, + }); + }, + ); const currentBlocklist = await this.blocklistRepository.getByWorkspaceMemberId( @@ -132,16 +141,23 @@ export class BlocklistValidationService { return; } - const workspaceMemberRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - WorkspaceMemberWorkspaceEntity, - ); + const authContext = buildSystemAuthContext(workspaceId); const currentWorkspaceMember = - await workspaceMemberRepository.findOneByOrFail({ - userId, - }); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workspaceMemberRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + WorkspaceMemberWorkspaceEntity, + ); + + return workspaceMemberRepository.findOneByOrFail({ + userId, + }); + }, + ); const currentBlocklist = await this.blocklistRepository.getByWorkspaceMemberId( diff --git a/packages/twenty-server/src/modules/blocklist/repositories/blocklist.repository.ts b/packages/twenty-server/src/modules/blocklist/repositories/blocklist.repository.ts index 7d16a3ab1b2..4b9545122c0 100644 --- a/packages/twenty-server/src/modules/blocklist/repositories/blocklist.repository.ts +++ b/packages/twenty-server/src/modules/blocklist/repositories/blocklist.repository.ts @@ -1,46 +1,61 @@ import { Injectable } from '@nestjs/common'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { BlocklistWorkspaceEntity } from 'src/modules/blocklist/standard-objects/blocklist.workspace-entity'; @Injectable() export class BlocklistRepository { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, ) {} public async getById( id: string, workspaceId: string, ): Promise { - const blockListRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - BlocklistWorkspaceEntity, - { - shouldBypassPermissionChecks: true, - }, - ); + const authContext = buildSystemAuthContext(workspaceId); - return blockListRepository.findOneBy({ - id, - }); + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const blockListRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + BlocklistWorkspaceEntity, + { + shouldBypassPermissionChecks: true, + }, + ); + + return blockListRepository.findOneBy({ + id, + }); + }, + ); } public async getByWorkspaceMemberId( workspaceMemberId: string, workspaceId: string, ): Promise { - const blockListRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - BlocklistWorkspaceEntity, - ); + const authContext = buildSystemAuthContext(workspaceId); - return blockListRepository.find({ - where: { - workspaceMemberId, + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const blockListRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + BlocklistWorkspaceEntity, + ); + + return blockListRepository.find({ + where: { + workspaceMemberId, + }, + }); }, - }); + ); } } diff --git a/packages/twenty-server/src/modules/calendar/blocklist-manager/jobs/blocklist-item-delete-calendar-events.job.ts b/packages/twenty-server/src/modules/calendar/blocklist-manager/jobs/blocklist-item-delete-calendar-events.job.ts index 32aa6d8bc2e..5c6e9aa770d 100644 --- a/packages/twenty-server/src/modules/calendar/blocklist-manager/jobs/blocklist-item-delete-calendar-events.job.ts +++ b/packages/twenty-server/src/modules/calendar/blocklist-manager/jobs/blocklist-item-delete-calendar-events.job.ts @@ -7,7 +7,8 @@ import { type ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emit import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator'; import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { type WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type'; import { type BlocklistWorkspaceEntity } from 'src/modules/blocklist/standard-objects/blocklist.workspace-entity'; import { CalendarEventCleanerService } from 'src/modules/calendar/calendar-event-cleaner/services/calendar-event-cleaner.service'; @@ -24,7 +25,7 @@ export type BlocklistItemDeleteCalendarEventsJobData = WorkspaceEventBatch< }) export class BlocklistItemDeleteCalendarEventsJob { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly calendarEventCleanerService: CalendarEventCleanerService, ) {} @@ -32,121 +33,128 @@ export class BlocklistItemDeleteCalendarEventsJob { async handle(data: BlocklistItemDeleteCalendarEventsJobData): Promise { const workspaceId = data.workspaceId; - const blocklistItemIds = data.events.map( - (eventPayload) => eventPayload.recordId, - ); + const authContext = buildSystemAuthContext(workspaceId); - const blocklistRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'blocklist', - ); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const blocklistItemIds = data.events.map( + (eventPayload) => eventPayload.recordId, + ); - const blocklist = await blocklistRepository.find({ - where: { - id: Any(blocklistItemIds), - }, - }); - - const handlesToDeleteByWorkspaceMemberIdMap = blocklist.reduce( - (acc, blocklistItem) => { - const { handle, workspaceMemberId } = blocklistItem; - - if (!acc.has(workspaceMemberId)) { - acc.set(workspaceMemberId, []); - } - - if (!isDefined(handle)) { - return acc; - } - - acc.get(workspaceMemberId)?.push(handle); - - return acc; - }, - new Map(), - ); - - const calendarChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'calendarChannel', - ); - - const calendarChannelEventAssociationRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'calendarChannelEventAssociation', - ); - - for (const workspaceMemberId of handlesToDeleteByWorkspaceMemberIdMap.keys()) { - const handles = - handlesToDeleteByWorkspaceMemberIdMap.get(workspaceMemberId); - - if (!handles) { - continue; - } - - const calendarChannels = await calendarChannelRepository.find({ - select: { - id: true, - handle: true, - connectedAccount: { - handleAliases: true, - }, - }, - where: { - connectedAccount: { - accountOwnerId: workspaceMemberId, - }, - }, - relations: ['connectedAccount'], - }); - - for (const calendarChannel of calendarChannels) { - const calendarChannelHandles = [calendarChannel.handle]; - - if (calendarChannel.connectedAccount.handleAliases) { - calendarChannelHandles.push( - ...calendarChannel.connectedAccount.handleAliases.split(','), + const blocklistRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'blocklist', ); - } - const handleConditions = handles.map((handle) => { - const isHandleDomain = handle.startsWith('@'); - - return isHandleDomain - ? { - handle: And( - Or(ILike(`%${handle}`), ILike(`%.${handle.slice(1)}`)), - Not(In(calendarChannelHandles)), - ), - } - : { handle }; + const blocklist = await blocklistRepository.find({ + where: { + id: Any(blocklistItemIds), + }, }); - const calendarEventsAssociationsToDelete = - await calendarChannelEventAssociationRepository.find({ - where: { - calendarChannelId: calendarChannel.id, - calendarEvent: { - calendarEventParticipants: handleConditions, + const handlesToDeleteByWorkspaceMemberIdMap = blocklist.reduce( + (acc, blocklistItem) => { + const { handle, workspaceMemberId } = blocklistItem; + + if (!acc.has(workspaceMemberId)) { + acc.set(workspaceMemberId, []); + } + + if (!isDefined(handle)) { + return acc; + } + + acc.get(workspaceMemberId)?.push(handle); + + return acc; + }, + new Map(), + ); + + const calendarChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'calendarChannel', + ); + + const calendarChannelEventAssociationRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'calendarChannelEventAssociation', + ); + + for (const workspaceMemberId of handlesToDeleteByWorkspaceMemberIdMap.keys()) { + const handles = + handlesToDeleteByWorkspaceMemberIdMap.get(workspaceMemberId); + + if (!handles) { + continue; + } + + const calendarChannels = await calendarChannelRepository.find({ + select: { + id: true, + handle: true, + connectedAccount: { + handleAliases: true, }, }, + where: { + connectedAccount: { + accountOwnerId: workspaceMemberId, + }, + }, + relations: ['connectedAccount'], }); - if (calendarEventsAssociationsToDelete.length === 0) { - continue; + for (const calendarChannel of calendarChannels) { + const calendarChannelHandles = [calendarChannel.handle]; + + if (calendarChannel.connectedAccount.handleAliases) { + calendarChannelHandles.push( + ...calendarChannel.connectedAccount.handleAliases.split(','), + ); + } + + const handleConditions = handles.map((handle) => { + const isHandleDomain = handle.startsWith('@'); + + return isHandleDomain + ? { + handle: And( + Or(ILike(`%${handle}`), ILike(`%.${handle.slice(1)}`)), + Not(In(calendarChannelHandles)), + ), + } + : { handle }; + }); + + const calendarEventsAssociationsToDelete = + await calendarChannelEventAssociationRepository.find({ + where: { + calendarChannelId: calendarChannel.id, + calendarEvent: { + calendarEventParticipants: handleConditions, + }, + }, + }); + + if (calendarEventsAssociationsToDelete.length === 0) { + continue; + } + + await calendarChannelEventAssociationRepository.delete( + calendarEventsAssociationsToDelete.map(({ id }) => id), + ); + } } - await calendarChannelEventAssociationRepository.delete( - calendarEventsAssociationsToDelete.map(({ id }) => id), + await this.calendarEventCleanerService.cleanWorkspaceCalendarEvents( + workspaceId, ); - } - } - - await this.calendarEventCleanerService.cleanWorkspaceCalendarEvents( - workspaceId, + }, ); } } diff --git a/packages/twenty-server/src/modules/calendar/blocklist-manager/jobs/blocklist-reimport-calendar-events.job.ts b/packages/twenty-server/src/modules/calendar/blocklist-manager/jobs/blocklist-reimport-calendar-events.job.ts index 679bfce6ace..7c4afd1663e 100644 --- a/packages/twenty-server/src/modules/calendar/blocklist-manager/jobs/blocklist-reimport-calendar-events.job.ts +++ b/packages/twenty-server/src/modules/calendar/blocklist-manager/jobs/blocklist-reimport-calendar-events.job.ts @@ -6,7 +6,8 @@ import { type ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emit import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator'; import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { type WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type'; import { type BlocklistWorkspaceEntity } from 'src/modules/blocklist/standard-objects/blocklist.workspace-entity'; import { CalendarChannelSyncStatusService } from 'src/modules/calendar/common/services/calendar-channel-sync-status.service'; @@ -25,7 +26,7 @@ export type BlocklistReimportCalendarEventsJobData = WorkspaceEventBatch< }) export class BlocklistReimportCalendarEventsJob { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly calendarChannelSyncStatusService: CalendarChannelSyncStatusService, ) {} @@ -33,32 +34,39 @@ export class BlocklistReimportCalendarEventsJob { async handle(data: BlocklistReimportCalendarEventsJobData): Promise { const workspaceId = data.workspaceId; - const calendarChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'calendarChannel', - ); + const authContext = buildSystemAuthContext(workspaceId); - for (const eventPayload of data.events) { - const workspaceMemberId = - eventPayload.properties.before.workspaceMemberId; + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const calendarChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'calendarChannel', + ); - const calendarChannels = await calendarChannelRepository.find({ - select: ['id'], - where: { - connectedAccount: { - accountOwnerId: workspaceMemberId, - }, - syncStage: Not( - CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING, - ), - }, - }); + for (const eventPayload of data.events) { + const workspaceMemberId = + eventPayload.properties.before.workspaceMemberId; - await this.calendarChannelSyncStatusService.resetAndMarkAsCalendarEventListFetchPending( - calendarChannels.map((calendarChannel) => calendarChannel.id), - workspaceId, - ); - } + const calendarChannels = await calendarChannelRepository.find({ + select: ['id'], + where: { + connectedAccount: { + accountOwnerId: workspaceMemberId, + }, + syncStage: Not( + CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING, + ), + }, + }); + + await this.calendarChannelSyncStatusService.resetAndMarkAsCalendarEventListFetchPending( + calendarChannels.map((calendarChannel) => calendarChannel.id), + workspaceId, + ); + } + }, + ); } } diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-cleaner/services/calendar-event-cleaner.service.ts b/packages/twenty-server/src/modules/calendar/calendar-event-cleaner/services/calendar-event-cleaner.service.ts index 556d2a2242b..e5373fdfe5e 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-cleaner/services/calendar-event-cleaner.service.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-cleaner/services/calendar-event-cleaner.service.ts @@ -2,40 +2,49 @@ import { Injectable } from '@nestjs/common'; import { Any, IsNull } from 'typeorm'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { deleteUsingPagination } from 'src/modules/messaging/message-cleaner/utils/delete-using-pagination.util'; @Injectable() export class CalendarEventCleanerService { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, ) {} public async cleanWorkspaceCalendarEvents(workspaceId: string) { - const calendarEventRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'calendarEvent', - ); + const authContext = buildSystemAuthContext(workspaceId); - await deleteUsingPagination( - workspaceId, - 500, - async (limit, offset) => { - const nonAssociatedCalendarEvents = await calendarEventRepository.find({ - where: { - calendarChannelEventAssociations: { - id: IsNull(), - }, + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const calendarEventRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'calendarEvent', + ); + + await deleteUsingPagination( + workspaceId, + 500, + async (limit, offset) => { + const nonAssociatedCalendarEvents = + await calendarEventRepository.find({ + where: { + calendarChannelEventAssociations: { + id: IsNull(), + }, + }, + take: limit, + skip: offset, + }); + + return nonAssociatedCalendarEvents.map(({ id }) => id); }, - take: limit, - skip: offset, - }); - - return nonAssociatedCalendarEvents.map(({ id }) => id); - }, - async (ids) => { - await calendarEventRepository.delete({ id: Any(ids) }); + async (ids) => { + await calendarEventRepository.delete({ id: Any(ids) }); + }, + ); }, ); } diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/jobs/calendar-event-list-fetch.job.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/jobs/calendar-event-list-fetch.job.ts index 3f542be4a58..82dd387a8b8 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/jobs/calendar-event-list-fetch.job.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/jobs/calendar-event-list-fetch.job.ts @@ -3,7 +3,8 @@ import { Scope } from '@nestjs/common'; import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator'; import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { CalendarFetchEventsService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-fetch-events.service'; import { CalendarChannelSyncStatusService } from 'src/modules/calendar/common/services/calendar-channel-sync-status.service'; import { @@ -23,7 +24,7 @@ export type CalendarEventListFetchJobData = { }) export class CalendarEventListFetchJob { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly calendarChannelSyncStatusService: CalendarChannelSyncStatusService, private readonly calendarFetchEventsService: CalendarFetchEventsService, ) {} @@ -32,50 +33,57 @@ export class CalendarEventListFetchJob { async handle(data: CalendarEventListFetchJobData): Promise { const { workspaceId, calendarChannelId } = data; - const calendarChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'calendarChannel', - ); + const authContext = buildSystemAuthContext(workspaceId); - const calendarChannel = await calendarChannelRepository.findOne({ - where: { - id: calendarChannelId, - isSyncEnabled: true, + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const calendarChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'calendarChannel', + ); + + const calendarChannel = await calendarChannelRepository.findOne({ + where: { + id: calendarChannelId, + isSyncEnabled: true, + }, + relations: ['connectedAccount'], + }); + + if (!calendarChannel) { + return; + } + + if ( + calendarChannel.syncStage !== + CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_SCHEDULED + ) { + return; + } + + if ( + isThrottled( + calendarChannel.syncStageStartedAt, + calendarChannel.throttleFailureCount, + ) + ) { + await this.calendarChannelSyncStatusService.markAsCalendarEventListFetchPending( + [calendarChannel.id], + workspaceId, + true, + ); + + return; + } + + await this.calendarFetchEventsService.fetchCalendarEvents( + calendarChannel, + calendarChannel.connectedAccount, + workspaceId, + ); }, - relations: ['connectedAccount'], - }); - - if (!calendarChannel) { - return; - } - - if ( - calendarChannel.syncStage !== - CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_SCHEDULED - ) { - return; - } - - if ( - isThrottled( - calendarChannel.syncStageStartedAt, - calendarChannel.throttleFailureCount, - ) - ) { - await this.calendarChannelSyncStatusService.markAsCalendarEventListFetchPending( - [calendarChannel.id], - workspaceId, - true, - ); - - return; - } - - await this.calendarFetchEventsService.fetchCalendarEvents( - calendarChannel, - calendarChannel.connectedAccount, - workspaceId, ); } } diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/jobs/calendar-events-import.job.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/jobs/calendar-events-import.job.ts index 52e015d01ec..c59ebfeb7fa 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/jobs/calendar-events-import.job.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/jobs/calendar-events-import.job.ts @@ -3,7 +3,8 @@ import { Scope } from '@nestjs/common'; import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator'; import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { CalendarEventsImportService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-events-import.service'; import { CalendarChannelSyncStatusService } from 'src/modules/calendar/common/services/calendar-channel-sync-status.service'; import { @@ -25,56 +26,63 @@ export class CalendarEventsImportJob { constructor( private readonly calendarEventsImportService: CalendarEventsImportService, private readonly calendarChannelSyncStatusService: CalendarChannelSyncStatusService, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, ) {} @Process(CalendarEventsImportJob.name) async handle(data: CalendarEventsImportJobData): Promise { const { calendarChannelId, workspaceId } = data; - const calendarChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'calendarChannel', - ); - const calendarChannel = await calendarChannelRepository.findOne({ - where: { - id: calendarChannelId, - isSyncEnabled: true, + const authContext = buildSystemAuthContext(workspaceId); + + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const calendarChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'calendarChannel', + ); + const calendarChannel = await calendarChannelRepository.findOne({ + where: { + id: calendarChannelId, + isSyncEnabled: true, + }, + relations: ['connectedAccount'], + }); + + if (!calendarChannel?.isSyncEnabled) { + return; + } + + if ( + calendarChannel.syncStage !== + CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_SCHEDULED + ) { + return; + } + + if ( + isThrottled( + calendarChannel.syncStageStartedAt, + calendarChannel.throttleFailureCount, + ) + ) { + await this.calendarChannelSyncStatusService.markAsCalendarEventsImportPending( + [calendarChannel.id], + workspaceId, + true, + ); + + return; + } + + await this.calendarEventsImportService.processCalendarEventsImport( + calendarChannel, + calendarChannel.connectedAccount, + workspaceId, + ); }, - relations: ['connectedAccount'], - }); - - if (!calendarChannel?.isSyncEnabled) { - return; - } - - if ( - calendarChannel.syncStage !== - CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_SCHEDULED - ) { - return; - } - - if ( - isThrottled( - calendarChannel.syncStageStartedAt, - calendarChannel.throttleFailureCount, - ) - ) { - await this.calendarChannelSyncStatusService.markAsCalendarEventsImportPending( - [calendarChannel.id], - workspaceId, - true, - ); - - return; - } - - await this.calendarEventsImportService.processCalendarEventsImport( - calendarChannel, - calendarChannel.connectedAccount, - workspaceId, ); } } diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/jobs/calendar-ongoing-stale.job.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/jobs/calendar-ongoing-stale.job.ts index c4c35d5f576..088ea2ae63c 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/jobs/calendar-ongoing-stale.job.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/jobs/calendar-ongoing-stale.job.ts @@ -5,7 +5,8 @@ import { In } from 'typeorm'; import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator'; import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { isSyncStale } from 'src/modules/calendar/calendar-event-import-manager/utils/is-sync-stale.util'; import { CalendarChannelSyncStatusService } from 'src/modules/calendar/common/services/calendar-channel-sync-status.service'; import { @@ -24,7 +25,7 @@ export type CalendarOngoingStaleJobData = { export class CalendarOngoingStaleJob { private readonly logger = new Logger(CalendarOngoingStaleJob.name); constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly calendarChannelSyncStatusService: CalendarChannelSyncStatusService, ) {} @@ -32,58 +33,65 @@ export class CalendarOngoingStaleJob { async handle(data: CalendarOngoingStaleJobData): Promise { const { workspaceId } = data; - const calendarChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'calendarChannel', - ); + const authContext = buildSystemAuthContext(workspaceId); - const calendarChannels = await calendarChannelRepository.find({ - where: { - syncStage: In([ - CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_ONGOING, - CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_ONGOING, - CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_SCHEDULED, - CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_SCHEDULED, - ]), - }, - }); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const calendarChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'calendarChannel', + ); - for (const calendarChannel of calendarChannels) { - if ( - calendarChannel.syncStageStartedAt && - isSyncStale(calendarChannel.syncStageStartedAt) - ) { - await this.calendarChannelSyncStatusService.resetSyncStageStartedAt( - [calendarChannel.id], - workspaceId, - ); + const calendarChannels = await calendarChannelRepository.find({ + where: { + syncStage: In([ + CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_ONGOING, + CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_ONGOING, + CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_SCHEDULED, + CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_SCHEDULED, + ]), + }, + }); - switch (calendarChannel.syncStage) { - case CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_ONGOING: - case CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_SCHEDULED: - this.logger.log( - `Sync for calendar channel ${calendarChannel.id} and workspace ${workspaceId} is stale. Setting sync stage to CALENDAR_EVENT_LIST_FETCH_PENDING`, - ); - await this.calendarChannelSyncStatusService.markAsCalendarEventListFetchPending( + for (const calendarChannel of calendarChannels) { + if ( + calendarChannel.syncStageStartedAt && + isSyncStale(calendarChannel.syncStageStartedAt) + ) { + await this.calendarChannelSyncStatusService.resetSyncStageStartedAt( [calendarChannel.id], workspaceId, ); - break; - case CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_ONGOING: - case CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_SCHEDULED: - this.logger.log( - `Sync for calendar channel ${calendarChannel.id} and workspace ${workspaceId} is stale. Setting sync stage to CALENDAR_EVENTS_IMPORT_PENDING`, - ); - await this.calendarChannelSyncStatusService.markAsCalendarEventsImportPending( - [calendarChannel.id], - workspaceId, - ); - break; - default: - break; + + switch (calendarChannel.syncStage) { + case CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_ONGOING: + case CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_SCHEDULED: + this.logger.log( + `Sync for calendar channel ${calendarChannel.id} and workspace ${workspaceId} is stale. Setting sync stage to CALENDAR_EVENT_LIST_FETCH_PENDING`, + ); + await this.calendarChannelSyncStatusService.markAsCalendarEventListFetchPending( + [calendarChannel.id], + workspaceId, + ); + break; + case CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_ONGOING: + case CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_SCHEDULED: + this.logger.log( + `Sync for calendar channel ${calendarChannel.id} and workspace ${workspaceId} is stale. Setting sync stage to CALENDAR_EVENTS_IMPORT_PENDING`, + ); + await this.calendarChannelSyncStatusService.markAsCalendarEventsImportPending( + [calendarChannel.id], + workspaceId, + ); + break; + default: + break; + } + } } - } - } + }, + ); } } diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/jobs/calendar-relaunch-failed-calendar-channel.job.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/jobs/calendar-relaunch-failed-calendar-channel.job.ts index 3c396243f3e..08dfa62ff4d 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/jobs/calendar-relaunch-failed-calendar-channel.job.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/jobs/calendar-relaunch-failed-calendar-channel.job.ts @@ -3,7 +3,8 @@ import { Scope } from '@nestjs/common'; import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator'; import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { CalendarChannelSyncStage, CalendarChannelSyncStatus, @@ -21,42 +22,50 @@ export type CalendarRelaunchFailedCalendarChannelJobData = { }) export class CalendarRelaunchFailedCalendarChannelJob { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, ) {} @Process(CalendarRelaunchFailedCalendarChannelJob.name) async handle(data: CalendarRelaunchFailedCalendarChannelJobData) { const { workspaceId, calendarChannelId } = data; - const calendarChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'calendarChannel', - { shouldBypassPermissionChecks: true }, - ); + const authContext = buildSystemAuthContext(workspaceId); - const calendarChannel = await calendarChannelRepository.findOne({ - where: { - id: calendarChannelId, + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const calendarChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'calendarChannel', + { shouldBypassPermissionChecks: true }, + ); + + const calendarChannel = await calendarChannelRepository.findOne({ + where: { + id: calendarChannelId, + }, + relations: { + connectedAccount: { + accountOwner: true, + }, + }, + }); + + if ( + !calendarChannel || + calendarChannel.syncStage !== CalendarChannelSyncStage.FAILED || + calendarChannel.syncStatus !== + CalendarChannelSyncStatus.FAILED_UNKNOWN + ) { + return; + } + + await calendarChannelRepository.update(calendarChannelId, { + syncStage: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING, + syncStatus: CalendarChannelSyncStatus.ACTIVE, + }); }, - relations: { - connectedAccount: { - accountOwner: true, - }, - }, - }); - - if ( - !calendarChannel || - calendarChannel.syncStage !== CalendarChannelSyncStage.FAILED || - calendarChannel.syncStatus !== CalendarChannelSyncStatus.FAILED_UNKNOWN - ) { - return; - } - - await calendarChannelRepository.update(calendarChannelId, { - syncStage: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING, - syncStatus: CalendarChannelSyncStatus.ACTIVE, - }); + ); } } diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-event-import-exception-handler.service.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-event-import-exception-handler.service.ts index 74a5a82e4b6..e7fc0a2f81d 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-event-import-exception-handler.service.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-event-import-exception-handler.service.ts @@ -5,7 +5,8 @@ import { type TwentyORMException, TwentyORMExceptionCode, } from 'src/engine/twenty-orm/exceptions/twenty-orm.exception'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { CALENDAR_THROTTLE_MAX_ATTEMPTS } from 'src/modules/calendar/calendar-event-import-manager/constants/calendar-throttle-max-attempts'; import { type CalendarEventImportDriverException, @@ -28,7 +29,7 @@ export class CalendarEventImportErrorHandlerService { CalendarEventImportErrorHandlerService.name, ); constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly calendarChannelSyncStatusService: CalendarChannelSyncStatusService, private readonly exceptionHandlerService: ExceptionHandlerService, ) {} @@ -130,20 +131,27 @@ export class CalendarEventImportErrorHandlerService { throw calendarEventImportException; } - const calendarChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'calendarChannel', - ); + const authContext = buildSystemAuthContext(workspaceId); - await calendarChannelRepository.increment( - { - id: calendarChannel.id, + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const calendarChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'calendarChannel', + ); + + await calendarChannelRepository.increment( + { + id: calendarChannel.id, + }, + 'throttleFailureCount', + 1, + undefined, + ['throttleFailureCount', 'id'], + ); }, - 'throttleFailureCount', - 1, - undefined, - ['throttleFailureCount', 'id'], ); switch (syncStep) { diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-events-import.service.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-events-import.service.ts index f428d201049..6c3eaabe134 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-events-import.service.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-events-import.service.ts @@ -7,7 +7,8 @@ import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decora import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service'; import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { BlocklistRepository } from 'src/modules/blocklist/repositories/blocklist.repository'; import { BlocklistWorkspaceEntity } from 'src/modules/blocklist/standard-objects/blocklist.workspace-entity'; import { CalendarEventCleanerService } from 'src/modules/calendar/calendar-event-cleaner/services/calendar-event-cleaner.service'; @@ -34,7 +35,7 @@ export class CalendarEventsImportService { constructor( @InjectCacheStorage(CacheStorageNamespace.ModuleCalendar) private readonly cacheStorage: CacheStorageService, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, @InjectObjectMetadataRepository(BlocklistWorkspaceEntity) private readonly blocklistRepository: BlocklistRepository, private readonly calendarEventCleanerService: CalendarEventCleanerService, @@ -55,115 +56,123 @@ export class CalendarEventsImportService { workspaceId, ); - let calendarEvents: FetchedCalendarEvent[] = []; + const authContext = buildSystemAuthContext(workspaceId); - try { - if (fetchedCalendarEvents) { - calendarEvents = fetchedCalendarEvents; - } else { - const eventIdsToFetch: string[] = await this.cacheStorage.setPop( - `calendar-events-to-import:${workspaceId}:${calendarChannel.id}`, - CALENDAR_EVENT_IMPORT_BATCH_SIZE, - ); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + let calendarEvents: FetchedCalendarEvent[] = []; + + try { + if (fetchedCalendarEvents) { + calendarEvents = fetchedCalendarEvents; + } else { + const eventIdsToFetch: string[] = await this.cacheStorage.setPop( + `calendar-events-to-import:${workspaceId}:${calendarChannel.id}`, + CALENDAR_EVENT_IMPORT_BATCH_SIZE, + ); + + if (!eventIdsToFetch || eventIdsToFetch.length === 0) { + await this.calendarChannelSyncStatusService.markAsCompletedAndMarkAsCalendarEventListFetchPending( + [calendarChannel.id], + workspaceId, + ); + + return; + } + + switch (connectedAccount.provider) { + case 'microsoft': + calendarEvents = + await this.microsoftCalendarImportEventService.getCalendarEvents( + connectedAccount, + eventIdsToFetch, + ); + break; + default: + break; + } + } + + if (!calendarEvents || calendarEvents?.length === 0) { + await this.calendarChannelSyncStatusService.markAsCalendarEventListFetchPending( + [calendarChannel.id], + workspaceId, + ); + } + + const blocklist = + await this.blocklistRepository.getByWorkspaceMemberId( + connectedAccount.accountOwnerId, + workspaceId, + ); + + if ( + !isDefined(connectedAccount.handleAliases) || + !isDefined(calendarChannel.handle) + ) { + throw new CalendarEventImportDriverException( + 'Calendar channel handle or Handle aliases are required', + CalendarEventImportDriverExceptionCode.CHANNEL_MISCONFIGURED, + ); + } + + const { filteredEvents, cancelledEvents } = + filterEventsAndReturnCancelledEvents( + [ + calendarChannel.handle, + ...connectedAccount.handleAliases.split(','), + ], + calendarEvents, + blocklist.map((blocklist) => blocklist.handle ?? ''), + ); + + const cancelledEventExternalIds = cancelledEvents.map( + (event) => event.id, + ); + + const BATCH_SIZE = 1000; + + for (let i = 0; i < filteredEvents.length; i = i + BATCH_SIZE) { + const eventsBatch = filteredEvents.slice(i, i + BATCH_SIZE); + + await this.calendarSaveEventsService.saveCalendarEventsAndEnqueueContactCreationJob( + eventsBatch, + calendarChannel, + connectedAccount, + workspaceId, + ); + } + const calendarChannelEventAssociationRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'calendarChannelEventAssociation', + ); + + await calendarChannelEventAssociationRepository.delete({ + eventExternalId: Any(cancelledEventExternalIds), + calendarChannel: { + id: calendarChannel.id, + }, + }); + + await this.calendarEventCleanerService.cleanWorkspaceCalendarEvents( + workspaceId, + ); - if (!eventIdsToFetch || eventIdsToFetch.length === 0) { await this.calendarChannelSyncStatusService.markAsCompletedAndMarkAsCalendarEventListFetchPending( [calendarChannel.id], workspaceId, ); - - return; + } catch (error) { + await this.calendarEventImportErrorHandlerService.handleDriverException( + error, + CalendarEventImportSyncStep.CALENDAR_EVENTS_IMPORT, + calendarChannel, + workspaceId, + ); } - - switch (connectedAccount.provider) { - case 'microsoft': - calendarEvents = - await this.microsoftCalendarImportEventService.getCalendarEvents( - connectedAccount, - eventIdsToFetch, - ); - break; - default: - break; - } - } - - if (!calendarEvents || calendarEvents?.length === 0) { - await this.calendarChannelSyncStatusService.markAsCalendarEventListFetchPending( - [calendarChannel.id], - workspaceId, - ); - } - - const blocklist = await this.blocklistRepository.getByWorkspaceMemberId( - connectedAccount.accountOwnerId, - workspaceId, - ); - - if ( - !isDefined(connectedAccount.handleAliases) || - !isDefined(calendarChannel.handle) - ) { - throw new CalendarEventImportDriverException( - 'Calendar channel handle or Handle aliases are required', - CalendarEventImportDriverExceptionCode.CHANNEL_MISCONFIGURED, - ); - } - - const { filteredEvents, cancelledEvents } = - filterEventsAndReturnCancelledEvents( - [ - calendarChannel.handle, - ...connectedAccount.handleAliases.split(','), - ], - calendarEvents, - blocklist.map((blocklist) => blocklist.handle ?? ''), - ); - - const cancelledEventExternalIds = cancelledEvents.map( - (event) => event.id, - ); - - const BATCH_SIZE = 1000; - - for (let i = 0; i < filteredEvents.length; i = i + BATCH_SIZE) { - const eventsBatch = filteredEvents.slice(i, i + BATCH_SIZE); - - await this.calendarSaveEventsService.saveCalendarEventsAndEnqueueContactCreationJob( - eventsBatch, - calendarChannel, - connectedAccount, - workspaceId, - ); - } - const calendarChannelEventAssociationRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'calendarChannelEventAssociation', - ); - - await calendarChannelEventAssociationRepository.delete({ - eventExternalId: Any(cancelledEventExternalIds), - calendarChannel: { - id: calendarChannel.id, - }, - }); - - await this.calendarEventCleanerService.cleanWorkspaceCalendarEvents( - workspaceId, - ); - - await this.calendarChannelSyncStatusService.markAsCompletedAndMarkAsCalendarEventListFetchPending( - [calendarChannel.id], - workspaceId, - ); - } catch (error) { - await this.calendarEventImportErrorHandlerService.handleDriverException( - error, - CalendarEventImportSyncStep.CALENDAR_EVENTS_IMPORT, - calendarChannel, - workspaceId, - ); - } + }, + ); } } diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-fetch-events.service.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-fetch-events.service.ts index 6705748988a..9bf68fcb940 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-fetch-events.service.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-fetch-events.service.ts @@ -5,7 +5,8 @@ import { isDefined } from 'twenty-shared/utils'; import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator'; import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service'; import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { CalendarEventImportDriverException, CalendarEventImportDriverExceptionCode, @@ -27,7 +28,7 @@ export class CalendarFetchEventsService { constructor( @InjectCacheStorage(CacheStorageNamespace.ModuleCalendar) private readonly cacheStorage: CacheStorageService, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly calendarChannelSyncStatusService: CalendarChannelSyncStatusService, private readonly getCalendarEventsService: CalendarGetCalendarEventsService, private readonly calendarEventImportErrorHandlerService: CalendarEventImportErrorHandlerService, @@ -45,111 +46,115 @@ export class CalendarFetchEventsService { workspaceId, ); - try { - const { accessToken, refreshToken } = - await this.calendarAccountAuthenticationService.validateAndRefreshConnectedAccountAuthentication( - { - connectedAccount, + const authContext = buildSystemAuthContext(workspaceId); + + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + try { + const { accessToken, refreshToken } = + await this.calendarAccountAuthenticationService.validateAndRefreshConnectedAccountAuthentication( + { + connectedAccount, + workspaceId, + calendarChannelId: calendarChannel.id, + }, + ); + + const connectedAccountWithFreshTokens = { + ...connectedAccount, + accessToken, + refreshToken, + }; + + if (!isDefined(calendarChannel.syncCursor)) { + throw new CalendarEventImportDriverException( + 'Sync cursor is required', + CalendarEventImportDriverExceptionCode.SYNC_CURSOR_ERROR, + ); + } + + const getCalendarEventsResponse = + await this.getCalendarEventsService.getCalendarEvents( + connectedAccountWithFreshTokens, + calendarChannel.syncCursor, + ); + + const hasFullEvents = getCalendarEventsResponse.fullEvents; + + const calendarEvents = hasFullEvents + ? getCalendarEventsResponse.calendarEvents + : null; + const calendarEventIds = getCalendarEventsResponse.calendarEventIds; + const nextSyncCursor = getCalendarEventsResponse.nextSyncCursor; + + const calendarChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'calendarChannel', + ); + + if (!calendarEvents || calendarEvents?.length === 0) { + await calendarChannelRepository.update( + { + id: calendarChannel.id, + }, + { + syncCursor: nextSyncCursor, + }, + ); + + await this.calendarChannelSyncStatusService.markAsCalendarEventListFetchPending( + [calendarChannel.id], + workspaceId, + ); + } + + await calendarChannelRepository.update( + { + id: calendarChannel.id, + }, + { + syncCursor: nextSyncCursor, + }, + ); + + if (hasFullEvents && calendarEvents) { + await this.calendarEventsImportService.processCalendarEventsImport( + calendarChannel, + connectedAccount, + workspaceId, + calendarEvents, + ); + } else if (!hasFullEvents && calendarEventIds) { + await this.cacheStorage.setAdd( + `calendar-events-to-import:${workspaceId}:${calendarChannel.id}`, + calendarEventIds, + ); + + await this.calendarChannelSyncStatusService.markAsCalendarEventsImportPending( + [calendarChannel.id], + workspaceId, + ); + } else { + throw new CalendarEventImportDriverException( + "Expected 'calendarEvents' or 'calendarEventIds' to be present", + CalendarEventImportDriverExceptionCode.UNKNOWN, + ); + } + } catch (error) { + this.logger.log( + `Calendar event fetch error for workspace ${workspaceId} and calendar channel ${calendarChannel.id}`, + ); + this.logger.error(error); + await this.calendarEventImportErrorHandlerService.handleDriverException( + error, + CalendarEventImportSyncStep.CALENDAR_EVENT_LIST_FETCH, + calendarChannel, workspaceId, - calendarChannelId: calendarChannel.id, - }, - ); - - const connectedAccountWithFreshTokens = { - ...connectedAccount, - accessToken, - refreshToken, - }; - - if (!isDefined(calendarChannel.syncCursor)) { - throw new CalendarEventImportDriverException( - 'Sync cursor is required', - CalendarEventImportDriverExceptionCode.SYNC_CURSOR_ERROR, - ); - } - - const getCalendarEventsResponse = - await this.getCalendarEventsService.getCalendarEvents( - connectedAccountWithFreshTokens, - calendarChannel.syncCursor, - ); - - const hasFullEvents = getCalendarEventsResponse.fullEvents; - - const calendarEvents = hasFullEvents - ? getCalendarEventsResponse.calendarEvents - : null; - const calendarEventIds = getCalendarEventsResponse.calendarEventIds; - const nextSyncCursor = getCalendarEventsResponse.nextSyncCursor; - - const calendarChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'calendarChannel', - ); - - if (!calendarEvents || calendarEvents?.length === 0) { - await calendarChannelRepository.update( - { - id: calendarChannel.id, - }, - { - syncCursor: nextSyncCursor, - }, - ); - - await this.calendarChannelSyncStatusService.markAsCalendarEventListFetchPending( - [calendarChannel.id], - workspaceId, - ); - } - - await calendarChannelRepository.update( - { - id: calendarChannel.id, - }, - { - syncCursor: nextSyncCursor, - }, - ); - - if (hasFullEvents && calendarEvents) { - // Event Import already done - await this.calendarEventsImportService.processCalendarEventsImport( - calendarChannel, - connectedAccount, - workspaceId, - calendarEvents, - ); - } else if (!hasFullEvents && calendarEventIds) { - // Event Import still needed - - await this.cacheStorage.setAdd( - `calendar-events-to-import:${workspaceId}:${calendarChannel.id}`, - calendarEventIds, - ); - - await this.calendarChannelSyncStatusService.markAsCalendarEventsImportPending( - [calendarChannel.id], - workspaceId, - ); - } else { - throw new CalendarEventImportDriverException( - "Expected 'calendarEvents' or 'calendarEventIds' to be present", - CalendarEventImportDriverExceptionCode.UNKNOWN, - ); - } - } catch (error) { - this.logger.log( - `Calendar event fetch error for workspace ${workspaceId} and calendar channel ${calendarChannel.id}`, - ); - this.logger.error(error); - await this.calendarEventImportErrorHandlerService.handleDriverException( - error, - CalendarEventImportSyncStep.CALENDAR_EVENT_LIST_FETCH, - calendarChannel, - workspaceId, - ); - } + ); + } + }, + ); } } diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-save-events.service.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-save-events.service.ts index 3cb8e2cf0b7..1e237fb0da9 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-save-events.service.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-save-events.service.ts @@ -3,7 +3,8 @@ import { Injectable } from '@nestjs/common'; import { Any } from 'typeorm'; import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { CalendarEventParticipantService } from 'src/modules/calendar/calendar-event-participant-manager/services/calendar-event-participant.service'; import { type CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity'; import { type CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; @@ -20,7 +21,7 @@ type FetchedCalendarEventWithDBEvent = { @Injectable() export class CalendarSaveEventsService { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly calendarEventParticipantService: CalendarEventParticipantService, ) {} @@ -30,227 +31,246 @@ export class CalendarSaveEventsService { connectedAccount: ConnectedAccountWorkspaceEntity, workspaceId: string, ): Promise { - const calendarEventRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'calendarEvent', - ); + const authContext = buildSystemAuthContext(workspaceId); - const calendarChannelEventAssociationRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'calendarChannelEventAssociation', - ); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const calendarEventRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'calendarEvent', + ); - const existingCalendarEvents = await calendarEventRepository.find({ - where: { - iCalUid: Any( - fetchedCalendarEvents.map((event) => event.iCalUid as string), - ), - }, - }); + const calendarChannelEventAssociationRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'calendarChannelEventAssociation', + ); - const fetchedCalendarEventsWithDBEvents: FetchedCalendarEventWithDBEvent[] = - fetchedCalendarEvents.map((event): FetchedCalendarEventWithDBEvent => { - const existingEventWithSameiCalUid = existingCalendarEvents.find( - (existingEvent) => existingEvent.iCalUid === event.iCalUid, - ); - - return { - fetchedCalendarEvent: event, - existingCalendarEvent: existingEventWithSameiCalUid ?? null, - newlyCreatedCalendarEvent: null, - }; - }); - - const workspaceDataSource = - await this.twentyORMGlobalManager.getDataSourceForWorkspace({ - workspaceId, - }); - - await workspaceDataSource.transaction( - async (transactionManager: WorkspaceEntityManager) => { - const savedCalendarEvents = await calendarEventRepository.save( - fetchedCalendarEventsWithDBEvents - .filter( - ({ existingCalendarEvent }) => existingCalendarEvent === null, - ) - .map( - ({ fetchedCalendarEvent }) => - ({ - iCalUid: fetchedCalendarEvent.iCalUid, - title: fetchedCalendarEvent.title, - description: fetchedCalendarEvent.description, - startsAt: fetchedCalendarEvent.startsAt, - endsAt: fetchedCalendarEvent.endsAt, - location: fetchedCalendarEvent.location, - isFullDay: fetchedCalendarEvent.isFullDay, - isCanceled: fetchedCalendarEvent.isCanceled, - conferenceSolution: fetchedCalendarEvent.conferenceSolution, - conferenceLink: { - primaryLinkLabel: fetchedCalendarEvent.conferenceLinkLabel, - primaryLinkUrl: fetchedCalendarEvent.conferenceLinkUrl, - secondaryLinks: [], - }, - externalCreatedAt: fetchedCalendarEvent.externalCreatedAt, - externalUpdatedAt: fetchedCalendarEvent.externalUpdatedAt, - }) satisfies Omit< - CalendarEventWorkspaceEntity, - | 'id' - | 'calendarChannelEventAssociations' - | 'calendarEventParticipants' - | 'createdAt' - | 'updatedAt' - | 'deletedAt' - >, + const existingCalendarEvents = await calendarEventRepository.find({ + where: { + iCalUid: Any( + fetchedCalendarEvents.map((event) => event.iCalUid as string), ), - {}, - transactionManager, - ); + }, + }); - const fetchedCalendarEventsWithDBEventsEnrichedWithSavedEvents: FetchedCalendarEventWithDBEvent[] = - fetchedCalendarEventsWithDBEvents.map( - ({ fetchedCalendarEvent, existingCalendarEvent }) => { - const savedCalendarEvent = savedCalendarEvents.find( - (savedCalendarEvent) => - savedCalendarEvent.iCalUid === fetchedCalendarEvent.iCalUid, + const fetchedCalendarEventsWithDBEvents: FetchedCalendarEventWithDBEvent[] = + fetchedCalendarEvents.map( + (event): FetchedCalendarEventWithDBEvent => { + const existingEventWithSameiCalUid = existingCalendarEvents.find( + (existingEvent) => existingEvent.iCalUid === event.iCalUid, ); return { - fetchedCalendarEvent, - existingCalendarEvent: existingCalendarEvent, - newlyCreatedCalendarEvent: savedCalendarEvent ?? null, + fetchedCalendarEvent: event, + existingCalendarEvent: existingEventWithSameiCalUid ?? null, + newlyCreatedCalendarEvent: null, }; }, ); - await calendarEventRepository.save( - fetchedCalendarEventsWithDBEventsEnrichedWithSavedEvents - .filter( - ({ existingCalendarEvent }) => existingCalendarEvent !== null, - ) - .map(({ fetchedCalendarEvent, existingCalendarEvent }) => { - if (!existingCalendarEvent) { - throw new Error( - `Existing calendar event with iCalUid ${fetchedCalendarEvent.iCalUid} not found - should never happen`, - ); - } - - return { - id: existingCalendarEvent.id, - iCalUid: fetchedCalendarEvent.iCalUid, - title: fetchedCalendarEvent.title, - description: fetchedCalendarEvent.description, - startsAt: fetchedCalendarEvent.startsAt, - endsAt: fetchedCalendarEvent.endsAt, - location: fetchedCalendarEvent.location, - isFullDay: fetchedCalendarEvent.isFullDay, - isCanceled: fetchedCalendarEvent.isCanceled, - conferenceSolution: fetchedCalendarEvent.conferenceSolution, - conferenceLink: { - primaryLinkLabel: fetchedCalendarEvent.conferenceLinkLabel, - primaryLinkUrl: fetchedCalendarEvent.conferenceLinkUrl, - secondaryLinks: [], - }, - externalCreatedAt: fetchedCalendarEvent.externalCreatedAt, - externalUpdatedAt: fetchedCalendarEvent.externalUpdatedAt, - } satisfies Omit< - CalendarEventWorkspaceEntity, - | 'calendarChannelEventAssociations' - | 'calendarEventParticipants' - | 'createdAt' - | 'updatedAt' - | 'deletedAt' - >; - }), - {}, - transactionManager, - ); - - const calendarChannelEventAssociationsToSave: Pick< - CalendarChannelEventAssociationWorkspaceEntity, - | 'calendarEventId' - | 'eventExternalId' - | 'calendarChannelId' - | 'recurringEventExternalId' - >[] = fetchedCalendarEventsWithDBEventsEnrichedWithSavedEvents.map( - ({ - fetchedCalendarEvent, - existingCalendarEvent, - newlyCreatedCalendarEvent, - }) => { - const calendarEventId = - existingCalendarEvent?.id ?? newlyCreatedCalendarEvent?.id; - - if (!calendarEventId) { - throw new Error( - `Calendar event id not found for event with iCalUid ${fetchedCalendarEvent.iCalUid} - should never happen`, - ); - } - - return { - calendarEventId, - eventExternalId: fetchedCalendarEvent.id, - calendarChannelId: calendarChannel.id, - recurringEventExternalId: - fetchedCalendarEvent.recurringEventExternalId ?? '', - }; - }, - ); - - await calendarChannelEventAssociationRepository.save( - calendarChannelEventAssociationsToSave, - {}, - transactionManager, - ); - - const participantsToCreate = - fetchedCalendarEventsWithDBEventsEnrichedWithSavedEvents - .filter( - ({ newlyCreatedCalendarEvent }) => - newlyCreatedCalendarEvent !== null, - ) - .flatMap(({ newlyCreatedCalendarEvent, fetchedCalendarEvent }) => { - if (!newlyCreatedCalendarEvent?.id) { - throw new Error( - `Newly created calendar event with iCalUid ${fetchedCalendarEvent.iCalUid} not found - should never happen`, - ); - } - - return fetchedCalendarEvent.participants.map((participant) => ({ - ...participant, - calendarEventId: newlyCreatedCalendarEvent.id, - })); - }); - - // todo: we should prevent duplicate rows on calendarEventAssociation by creating - // an index on calendarChannelId and calendarEventId - const participantsToUpdate = - fetchedCalendarEventsWithDBEventsEnrichedWithSavedEvents - .filter( - ({ existingCalendarEvent }) => existingCalendarEvent !== null, - ) - .flatMap(({ fetchedCalendarEvent, existingCalendarEvent }) => { - if (!existingCalendarEvent?.id) { - throw new Error( - `Existing calendar event with iCalUid ${fetchedCalendarEvent.iCalUid} not found - should never happen`, - ); - } - - return fetchedCalendarEvent.participants.map((participant) => ({ - ...participant, - calendarEventId: existingCalendarEvent.id, - })); - }); - - await this.calendarEventParticipantService.upsertAndDeleteCalendarEventParticipants( - { - participantsToCreate, - participantsToUpdate, - transactionManager, - calendarChannel, - connectedAccount, + const workspaceDataSource = + await this.globalWorkspaceOrmManager.getDataSourceForWorkspace( workspaceId, + ); + + await workspaceDataSource.transaction( + async (transactionManager: WorkspaceEntityManager) => { + const savedCalendarEvents = await calendarEventRepository.save( + fetchedCalendarEventsWithDBEvents + .filter( + ({ existingCalendarEvent }) => existingCalendarEvent === null, + ) + .map( + ({ fetchedCalendarEvent }) => + ({ + iCalUid: fetchedCalendarEvent.iCalUid, + title: fetchedCalendarEvent.title, + description: fetchedCalendarEvent.description, + startsAt: fetchedCalendarEvent.startsAt, + endsAt: fetchedCalendarEvent.endsAt, + location: fetchedCalendarEvent.location, + isFullDay: fetchedCalendarEvent.isFullDay, + isCanceled: fetchedCalendarEvent.isCanceled, + conferenceSolution: + fetchedCalendarEvent.conferenceSolution, + conferenceLink: { + primaryLinkLabel: + fetchedCalendarEvent.conferenceLinkLabel, + primaryLinkUrl: fetchedCalendarEvent.conferenceLinkUrl, + secondaryLinks: [], + }, + externalCreatedAt: fetchedCalendarEvent.externalCreatedAt, + externalUpdatedAt: fetchedCalendarEvent.externalUpdatedAt, + }) satisfies Omit< + CalendarEventWorkspaceEntity, + | 'id' + | 'calendarChannelEventAssociations' + | 'calendarEventParticipants' + | 'createdAt' + | 'updatedAt' + | 'deletedAt' + >, + ), + {}, + transactionManager, + ); + + const fetchedCalendarEventsWithDBEventsEnrichedWithSavedEvents: FetchedCalendarEventWithDBEvent[] = + fetchedCalendarEventsWithDBEvents.map( + ({ fetchedCalendarEvent, existingCalendarEvent }) => { + const savedCalendarEvent = savedCalendarEvents.find( + (savedCalendarEvent) => + savedCalendarEvent.iCalUid === + fetchedCalendarEvent.iCalUid, + ); + + return { + fetchedCalendarEvent, + existingCalendarEvent: existingCalendarEvent, + newlyCreatedCalendarEvent: savedCalendarEvent ?? null, + }; + }, + ); + + await calendarEventRepository.save( + fetchedCalendarEventsWithDBEventsEnrichedWithSavedEvents + .filter( + ({ existingCalendarEvent }) => existingCalendarEvent !== null, + ) + .map(({ fetchedCalendarEvent, existingCalendarEvent }) => { + if (!existingCalendarEvent) { + throw new Error( + `Existing calendar event with iCalUid ${fetchedCalendarEvent.iCalUid} not found - should never happen`, + ); + } + + return { + id: existingCalendarEvent.id, + iCalUid: fetchedCalendarEvent.iCalUid, + title: fetchedCalendarEvent.title, + description: fetchedCalendarEvent.description, + startsAt: fetchedCalendarEvent.startsAt, + endsAt: fetchedCalendarEvent.endsAt, + location: fetchedCalendarEvent.location, + isFullDay: fetchedCalendarEvent.isFullDay, + isCanceled: fetchedCalendarEvent.isCanceled, + conferenceSolution: fetchedCalendarEvent.conferenceSolution, + conferenceLink: { + primaryLinkLabel: + fetchedCalendarEvent.conferenceLinkLabel, + primaryLinkUrl: fetchedCalendarEvent.conferenceLinkUrl, + secondaryLinks: [], + }, + externalCreatedAt: fetchedCalendarEvent.externalCreatedAt, + externalUpdatedAt: fetchedCalendarEvent.externalUpdatedAt, + } satisfies Omit< + CalendarEventWorkspaceEntity, + | 'calendarChannelEventAssociations' + | 'calendarEventParticipants' + | 'createdAt' + | 'updatedAt' + | 'deletedAt' + >; + }), + {}, + transactionManager, + ); + + const calendarChannelEventAssociationsToSave: Pick< + CalendarChannelEventAssociationWorkspaceEntity, + | 'calendarEventId' + | 'eventExternalId' + | 'calendarChannelId' + | 'recurringEventExternalId' + >[] = fetchedCalendarEventsWithDBEventsEnrichedWithSavedEvents.map( + ({ + fetchedCalendarEvent, + existingCalendarEvent, + newlyCreatedCalendarEvent, + }) => { + const calendarEventId = + existingCalendarEvent?.id ?? newlyCreatedCalendarEvent?.id; + + if (!calendarEventId) { + throw new Error( + `Calendar event id not found for event with iCalUid ${fetchedCalendarEvent.iCalUid} - should never happen`, + ); + } + + return { + calendarEventId, + eventExternalId: fetchedCalendarEvent.id, + calendarChannelId: calendarChannel.id, + recurringEventExternalId: + fetchedCalendarEvent.recurringEventExternalId ?? '', + }; + }, + ); + + await calendarChannelEventAssociationRepository.save( + calendarChannelEventAssociationsToSave, + {}, + transactionManager, + ); + + const participantsToCreate = + fetchedCalendarEventsWithDBEventsEnrichedWithSavedEvents + .filter( + ({ newlyCreatedCalendarEvent }) => + newlyCreatedCalendarEvent !== null, + ) + .flatMap( + ({ newlyCreatedCalendarEvent, fetchedCalendarEvent }) => { + if (!newlyCreatedCalendarEvent?.id) { + throw new Error( + `Newly created calendar event with iCalUid ${fetchedCalendarEvent.iCalUid} not found - should never happen`, + ); + } + + return fetchedCalendarEvent.participants.map( + (participant) => ({ + ...participant, + calendarEventId: newlyCreatedCalendarEvent.id, + }), + ); + }, + ); + + // todo: we should prevent duplicate rows on calendarEventAssociation by creating + // an index on calendarChannelId and calendarEventId + const participantsToUpdate = + fetchedCalendarEventsWithDBEventsEnrichedWithSavedEvents + .filter( + ({ existingCalendarEvent }) => existingCalendarEvent !== null, + ) + .flatMap(({ fetchedCalendarEvent, existingCalendarEvent }) => { + if (!existingCalendarEvent?.id) { + throw new Error( + `Existing calendar event with iCalUid ${fetchedCalendarEvent.iCalUid} not found - should never happen`, + ); + } + + return fetchedCalendarEvent.participants.map( + (participant) => ({ + ...participant, + calendarEventId: existingCalendarEvent.id, + }), + ); + }); + + await this.calendarEventParticipantService.upsertAndDeleteCalendarEventParticipants( + { + participantsToCreate, + participantsToUpdate, + transactionManager, + calendarChannel, + connectedAccount, + workspaceId, + }, + ); }, ); }, diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/services/calendar-event-participant.service.ts b/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/services/calendar-event-participant.service.ts index ba3f31806a1..a009213b718 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/services/calendar-event-participant.service.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/services/calendar-event-participant.service.ts @@ -10,7 +10,8 @@ import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decora import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { type CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; import { type CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity'; import { type FetchedCalendarEventParticipant } from 'src/modules/calendar/common/types/fetched-calendar-event'; @@ -34,7 +35,7 @@ type FetchedCalendarEventParticipantWithCalendarEventIdAndExistingId = @Injectable() export class CalendarEventParticipantService { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly matchParticipantService: MatchParticipantService, @InjectMessageQueue(MessageQueue.contactCreationQueue) private readonly messageQueueService: MessageQueueService, @@ -55,125 +56,133 @@ export class CalendarEventParticipantService { connectedAccount: ConnectedAccountWorkspaceEntity; workspaceId: string; }): Promise { - const chunkedParticipantsToUpdate = chunk(participantsToUpdate, 200); + const authContext = buildSystemAuthContext(workspaceId); - const calendarEventParticipantRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'calendarEventParticipant', - ); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const chunkedParticipantsToUpdate = chunk(participantsToUpdate, 200); - for (const participantsToUpdateChunk of chunkedParticipantsToUpdate) { - const existingCalendarEventParticipants = - await calendarEventParticipantRepository.find({ - where: { - calendarEventId: Any( - participantsToUpdateChunk - .map((participant) => participant.calendarEventId) - .filter(isDefined), - ), - }, - }); + const calendarEventParticipantRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'calendarEventParticipant', + ); - const { - calendarEventParticipantsToUpdate, - newCalendarEventParticipants, - } = participantsToUpdateChunk.reduce<{ - calendarEventParticipantsToUpdate: FetchedCalendarEventParticipantWithCalendarEventIdAndExistingId[]; - newCalendarEventParticipants: FetchedCalendarEventParticipantWithCalendarEventId[]; - }>( - (acc, calendarEventParticipant) => { - const existingCalendarEventParticipant = - existingCalendarEventParticipants.find( - (existingCalendarEventParticipant) => - existingCalendarEventParticipant.handle === - calendarEventParticipant.handle && - existingCalendarEventParticipant.calendarEventId === - calendarEventParticipant.calendarEventId, + for (const participantsToUpdateChunk of chunkedParticipantsToUpdate) { + const existingCalendarEventParticipants = + await calendarEventParticipantRepository.find({ + where: { + calendarEventId: Any( + participantsToUpdateChunk + .map((participant) => participant.calendarEventId) + .filter(isDefined), + ), + }, + }); + + const { + calendarEventParticipantsToUpdate, + newCalendarEventParticipants, + } = participantsToUpdateChunk.reduce<{ + calendarEventParticipantsToUpdate: FetchedCalendarEventParticipantWithCalendarEventIdAndExistingId[]; + newCalendarEventParticipants: FetchedCalendarEventParticipantWithCalendarEventId[]; + }>( + (acc, calendarEventParticipant) => { + const existingCalendarEventParticipant = + existingCalendarEventParticipants.find( + (existingCalendarEventParticipant) => + existingCalendarEventParticipant.handle === + calendarEventParticipant.handle && + existingCalendarEventParticipant.calendarEventId === + calendarEventParticipant.calendarEventId, + ); + + if (existingCalendarEventParticipant) { + acc.calendarEventParticipantsToUpdate.push({ + ...calendarEventParticipant, + id: existingCalendarEventParticipant.id, + }); + } else { + acc.newCalendarEventParticipants.push(calendarEventParticipant); + } + + return acc; + }, + { + calendarEventParticipantsToUpdate: [], + newCalendarEventParticipants: [], + }, + ); + + const calendarEventParticipantsToDelete = differenceWith( + existingCalendarEventParticipants, + participantsToUpdateChunk, + (existingCalendarEventParticipant, participantToUpdate) => + existingCalendarEventParticipant.handle === + participantToUpdate.handle && + existingCalendarEventParticipant.calendarEventId === + participantToUpdate.calendarEventId, + ); + + await calendarEventParticipantRepository.delete( + { + id: Any( + calendarEventParticipantsToDelete.map( + (calendarEventParticipant) => calendarEventParticipant.id, + ), + ), + }, + transactionManager, + ); + + await calendarEventParticipantRepository.updateMany( + calendarEventParticipantsToUpdate.map((participant) => ({ + criteria: participant.id, + partialEntity: participant, + })), + transactionManager, + ); + participantsToCreate.push(...newCalendarEventParticipants); + } + + const chunkedParticipantsToCreate = chunk(participantsToCreate, 200); + const savedParticipants: CalendarEventParticipantWorkspaceEntity[] = []; + + for (const participantsToCreateChunk of chunkedParticipantsToCreate) { + const savedParticipantsChunk = + await calendarEventParticipantRepository.insert( + participantsToCreateChunk, + transactionManager, ); - if (existingCalendarEventParticipant) { - acc.calendarEventParticipantsToUpdate.push({ - ...calendarEventParticipant, - id: existingCalendarEventParticipant.id, - }); - } else { - acc.newCalendarEventParticipants.push(calendarEventParticipant); - } + savedParticipants.push(...savedParticipantsChunk.raw); + } - return acc; - }, - { - calendarEventParticipantsToUpdate: [], - newCalendarEventParticipants: [], - }, - ); + if (calendarChannel.isContactAutoCreationEnabled) { + await this.messageQueueService.add( + CreateCompanyAndContactJob.name, + { + workspaceId, + connectedAccount, + contactsToCreate: savedParticipants.map((participant) => ({ + handle: participant.handle ?? '', + displayName: + participant.displayName ?? participant.handle ?? '', + })), + source: FieldActorSource.CALENDAR, + }, + ); + } - const calendarEventParticipantsToDelete = differenceWith( - existingCalendarEventParticipants, - participantsToUpdateChunk, - (existingCalendarEventParticipant, participantToUpdate) => - existingCalendarEventParticipant.handle === - participantToUpdate.handle && - existingCalendarEventParticipant.calendarEventId === - participantToUpdate.calendarEventId, - ); - - await calendarEventParticipantRepository.delete( - { - id: Any( - calendarEventParticipantsToDelete.map( - (calendarEventParticipant) => calendarEventParticipant.id, - ), - ), - }, - transactionManager, - ); - - await calendarEventParticipantRepository.updateMany( - calendarEventParticipantsToUpdate.map((participant) => ({ - criteria: participant.id, - partialEntity: participant, - })), - transactionManager, - ); - participantsToCreate.push(...newCalendarEventParticipants); - } - - const chunkedParticipantsToCreate = chunk(participantsToCreate, 200); - const savedParticipants: CalendarEventParticipantWorkspaceEntity[] = []; - - for (const participantsToCreateChunk of chunkedParticipantsToCreate) { - const savedParticipantsChunk = - await calendarEventParticipantRepository.insert( - participantsToCreateChunk, + await this.matchParticipantService.matchParticipants({ + participants: savedParticipants, + objectMetadataName: 'calendarEventParticipant', transactionManager, - ); - - savedParticipants.push(...savedParticipantsChunk.raw); - } - - if (calendarChannel.isContactAutoCreationEnabled) { - await this.messageQueueService.add( - CreateCompanyAndContactJob.name, - { + matchWith: 'workspaceMemberAndPerson', workspaceId, - connectedAccount, - contactsToCreate: savedParticipants.map((participant) => ({ - handle: participant.handle ?? '', - displayName: participant.displayName ?? participant.handle ?? '', - })), - source: FieldActorSource.CALENDAR, - }, - ); - } - - await this.matchParticipantService.matchParticipants({ - participants: savedParticipants, - objectMetadataName: 'calendarEventParticipant', - transactionManager, - matchWith: 'workspaceMemberAndPerson', - workspaceId, - }); + }); + }, + ); } } diff --git a/packages/twenty-server/src/modules/calendar/common/query-hooks/calendar-event/services/apply-calendar-events-visibility-restrictions.service.spec.ts b/packages/twenty-server/src/modules/calendar/common/query-hooks/calendar-event/services/apply-calendar-events-visibility-restrictions.service.spec.ts index 485f44c40b6..d0e2393cf19 100644 --- a/packages/twenty-server/src/modules/calendar/common/query-hooks/calendar-event/services/apply-calendar-events-visibility-restrictions.service.spec.ts +++ b/packages/twenty-server/src/modules/calendar/common/query-hooks/calendar-event/services/apply-calendar-events-visibility-restrictions.service.spec.ts @@ -2,7 +2,7 @@ import { Test, type TestingModule } from '@nestjs/testing'; import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { CalendarChannelVisibility } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; import { type CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity'; @@ -52,20 +52,21 @@ describe('ApplyCalendarEventsVisibilityRestrictionsService', () => { findOneByOrFail: jest.fn(), }; - const mockTwentyORMGlobalManager = { - getRepositoryForWorkspace: jest + const mockGlobalWorkspaceOrmManager = { + getRepository: jest.fn().mockImplementation((workspaceId, name) => { + if (name === 'calendarChannelEventAssociation') { + return mockCalendarEventAssociationRepository; + } + if (name === 'connectedAccount') { + return mockConnectedAccountRepository; + } + if (name === 'workspaceMember') { + return mockWorkspaceMemberRepository; + } + }), + executeInWorkspaceContext: jest .fn() - .mockImplementation((workspaceId, name) => { - if (name === 'calendarChannelEventAssociation') { - return mockCalendarEventAssociationRepository; - } - if (name === 'connectedAccount') { - return mockConnectedAccountRepository; - } - if (name === 'workspaceMember') { - return mockWorkspaceMemberRepository; - } - }), + .mockImplementation((_authContext: any, fn: () => any) => fn()), }; beforeEach(async () => { @@ -73,8 +74,8 @@ describe('ApplyCalendarEventsVisibilityRestrictionsService', () => { providers: [ ApplyCalendarEventsVisibilityRestrictionsService, { - provide: TwentyORMGlobalManager, - useValue: mockTwentyORMGlobalManager, + provide: GlobalWorkspaceOrmManager, + useValue: mockGlobalWorkspaceOrmManager, }, ], }).compile(); diff --git a/packages/twenty-server/src/modules/calendar/common/query-hooks/calendar-event/services/apply-calendar-events-visibility-restrictions.service.ts b/packages/twenty-server/src/modules/calendar/common/query-hooks/calendar-event/services/apply-calendar-events-visibility-restrictions.service.ts index 9aa29422288..d127a9809b3 100644 --- a/packages/twenty-server/src/modules/calendar/common/query-hooks/calendar-event/services/apply-calendar-events-visibility-restrictions.service.ts +++ b/packages/twenty-server/src/modules/calendar/common/query-hooks/calendar-event/services/apply-calendar-events-visibility-restrictions.service.ts @@ -5,7 +5,8 @@ import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/ import { isDefined } from 'twenty-shared/utils'; import { In } from 'typeorm'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { type CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity'; import { CalendarChannelVisibility } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; import { type CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity'; @@ -15,7 +16,7 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta @Injectable() export class ApplyCalendarEventsVisibilityRestrictionsService { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, ) {} public async applyCalendarEventsVisibilityRestrictions( @@ -23,90 +24,99 @@ export class ApplyCalendarEventsVisibilityRestrictionsService { workspaceId: string, userId?: string, // undefined when request is made with api key ) { - const calendarChannelEventAssociationRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'calendarChannelEventAssociation', - ); + const authContext = buildSystemAuthContext(workspaceId); - const calendarChannelCalendarEventsAssociations = - await calendarChannelEventAssociationRepository.find({ - where: { - calendarEventId: In(calendarEvents.map((event) => event.id)), - }, - relations: ['calendarChannel'], - }); + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const calendarChannelEventAssociationRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'calendarChannelEventAssociation', + ); - const connectedAccountRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'connectedAccount', - ); - - const workspaceMemberRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'workspaceMember', - ); - - for (let i = calendarEvents.length - 1; i >= 0; i--) { - const calendarChannelCalendarEventAssociations = - calendarChannelCalendarEventsAssociations.filter( - (association) => association.calendarEventId === calendarEvents[i].id, - ); - - const calendarChannels = calendarChannelCalendarEventAssociations.map( - (association) => association.calendarChannel, - ); - - const calendarChannelsGroupByVisibility = groupBy( - calendarChannels, - (channel) => channel.visibility, - ); - - if ( - calendarChannelsGroupByVisibility[ - CalendarChannelVisibility.SHARE_EVERYTHING - ] - ) { - continue; - } - - if (isDefined(userId)) { - const workspaceMember = await workspaceMemberRepository.findOneByOrFail( - { - userId: userId, - }, - ); - - const connectedAccounts = await connectedAccountRepository.find({ - select: ['id'], - where: { - calendarChannels: { - id: In(calendarChannels.map((channel) => channel.id)), + const calendarChannelCalendarEventsAssociations = + await calendarChannelEventAssociationRepository.find({ + where: { + calendarEventId: In(calendarEvents.map((event) => event.id)), }, - accountOwnerId: workspaceMember.id, - }, - }); + relations: ['calendarChannel'], + }); - if (connectedAccounts.length > 0) { - continue; + const connectedAccountRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'connectedAccount', + ); + + const workspaceMemberRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'workspaceMember', + ); + + for (let i = calendarEvents.length - 1; i >= 0; i--) { + const calendarChannelCalendarEventAssociations = + calendarChannelCalendarEventsAssociations.filter( + (association) => + association.calendarEventId === calendarEvents[i].id, + ); + + const calendarChannels = calendarChannelCalendarEventAssociations.map( + (association) => association.calendarChannel, + ); + + const calendarChannelsGroupByVisibility = groupBy( + calendarChannels, + (channel) => channel.visibility, + ); + + if ( + calendarChannelsGroupByVisibility[ + CalendarChannelVisibility.SHARE_EVERYTHING + ] + ) { + continue; + } + + if (isDefined(userId)) { + const workspaceMember = + await workspaceMemberRepository.findOneByOrFail({ + userId: userId, + }); + + const connectedAccounts = await connectedAccountRepository.find({ + select: ['id'], + where: { + calendarChannels: { + id: In(calendarChannels.map((channel) => channel.id)), + }, + accountOwnerId: workspaceMember.id, + }, + }); + + if (connectedAccounts.length > 0) { + continue; + } + } + + if ( + calendarChannelsGroupByVisibility[ + CalendarChannelVisibility.METADATA + ] + ) { + calendarEvents[i].title = + FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED; + calendarEvents[i].description = + FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED; + continue; + } + + calendarEvents.splice(i, 1); } - } - if ( - calendarChannelsGroupByVisibility[CalendarChannelVisibility.METADATA] - ) { - calendarEvents[i].title = - FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED; - calendarEvents[i].description = - FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED; - continue; - } - - calendarEvents.splice(i, 1); - } - - return calendarEvents; + return calendarEvents; + }, + ); } } diff --git a/packages/twenty-server/src/modules/calendar/common/services/calendar-channel-sync-status.service.ts b/packages/twenty-server/src/modules/calendar/common/services/calendar-channel-sync-status.service.ts index 45e518d0cb7..1363c775ab2 100644 --- a/packages/twenty-server/src/modules/calendar/common/services/calendar-channel-sync-status.service.ts +++ b/packages/twenty-server/src/modules/calendar/common/services/calendar-channel-sync-status.service.ts @@ -7,7 +7,8 @@ import { CacheStorageService } from 'src/engine/core-modules/cache-storage/servi import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service'; import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { CalendarChannelSyncStage, CalendarChannelSyncStatus, @@ -20,7 +21,7 @@ import { AccountsToReconnectKeys } from 'src/modules/connected-account/types/acc @Injectable() export class CalendarChannelSyncStatusService { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, @InjectCacheStorage(CacheStorageNamespace.ModuleCalendar) private readonly cacheStorage: CacheStorageService, private readonly accountsToReconnectService: AccountsToReconnectService, @@ -36,16 +37,23 @@ export class CalendarChannelSyncStatusService { return; } - const calendarChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'calendarChannel', - ); + const authContext = buildSystemAuthContext(workspaceId); - await calendarChannelRepository.update(calendarChannelIds, { - syncStage: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING, - ...(!preserveSyncStageStartedAt ? { syncStageStartedAt: null } : {}), - }); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const calendarChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'calendarChannel', + ); + + await calendarChannelRepository.update(calendarChannelIds, { + syncStage: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING, + ...(!preserveSyncStageStartedAt ? { syncStageStartedAt: null } : {}), + }); + }, + ); } public async markAsCalendarEventListFetchOngoing( @@ -56,17 +64,24 @@ export class CalendarChannelSyncStatusService { return; } - const calendarChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'calendarChannel', - ); + const authContext = buildSystemAuthContext(workspaceId); - await calendarChannelRepository.update(calendarChannelIds, { - syncStage: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_ONGOING, - syncStatus: CalendarChannelSyncStatus.ONGOING, - syncStageStartedAt: new Date().toISOString(), - }); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const calendarChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'calendarChannel', + ); + + await calendarChannelRepository.update(calendarChannelIds, { + syncStage: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_ONGOING, + syncStatus: CalendarChannelSyncStatus.ONGOING, + syncStageStartedAt: new Date().toISOString(), + }); + }, + ); } public async resetAndMarkAsCalendarEventListFetchPending( @@ -83,17 +98,24 @@ export class CalendarChannelSyncStatusService { ); } - const calendarChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'calendarChannel', - ); + const authContext = buildSystemAuthContext(workspaceId); - await calendarChannelRepository.update(calendarChannelIds, { - syncCursor: '', - syncStageStartedAt: null, - throttleFailureCount: 0, - }); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const calendarChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'calendarChannel', + ); + + await calendarChannelRepository.update(calendarChannelIds, { + syncCursor: '', + syncStageStartedAt: null, + throttleFailureCount: 0, + }); + }, + ); await this.markAsCalendarEventListFetchPending( calendarChannelIds, @@ -109,15 +131,22 @@ export class CalendarChannelSyncStatusService { return; } - const calendarChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'calendarChannel', - ); + const authContext = buildSystemAuthContext(workspaceId); - await calendarChannelRepository.update(calendarChannelIds, { - syncStageStartedAt: null, - }); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const calendarChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'calendarChannel', + ); + + await calendarChannelRepository.update(calendarChannelIds, { + syncStageStartedAt: null, + }); + }, + ); } public async markAsCalendarEventsImportPending( @@ -129,16 +158,23 @@ export class CalendarChannelSyncStatusService { return; } - const calendarChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'calendarChannel', - ); + const authContext = buildSystemAuthContext(workspaceId); - await calendarChannelRepository.update(calendarChannelIds, { - syncStage: CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_PENDING, - ...(!preserveSyncStageStartedAt ? { syncStageStartedAt: null } : {}), - }); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const calendarChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'calendarChannel', + ); + + await calendarChannelRepository.update(calendarChannelIds, { + syncStage: CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_PENDING, + ...(!preserveSyncStageStartedAt ? { syncStageStartedAt: null } : {}), + }); + }, + ); } public async markAsCalendarEventsImportOngoing( @@ -149,16 +185,23 @@ export class CalendarChannelSyncStatusService { return; } - const calendarChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'calendarChannel', - ); + const authContext = buildSystemAuthContext(workspaceId); - await calendarChannelRepository.update(calendarChannelIds, { - syncStage: CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_ONGOING, - syncStatus: CalendarChannelSyncStatus.ONGOING, - }); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const calendarChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'calendarChannel', + ); + + await calendarChannelRepository.update(calendarChannelIds, { + syncStage: CalendarChannelSyncStage.CALENDAR_EVENTS_IMPORT_ONGOING, + syncStatus: CalendarChannelSyncStatus.ONGOING, + }); + }, + ); } public async markAsCompletedAndMarkAsCalendarEventListFetchPending( @@ -169,19 +212,26 @@ export class CalendarChannelSyncStatusService { return; } - const calendarChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'calendarChannel', - ); + const authContext = buildSystemAuthContext(workspaceId); - await calendarChannelRepository.update(calendarChannelIds, { - syncStage: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING, - syncStatus: CalendarChannelSyncStatus.ACTIVE, - throttleFailureCount: 0, - syncStageStartedAt: null, - syncedAt: new Date().toISOString(), - }); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const calendarChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'calendarChannel', + ); + + await calendarChannelRepository.update(calendarChannelIds, { + syncStage: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING, + syncStatus: CalendarChannelSyncStatus.ACTIVE, + throttleFailureCount: 0, + syncStageStartedAt: null, + syncedAt: new Date().toISOString(), + }); + }, + ); await this.markAsCalendarEventListFetchPending( calendarChannelIds, @@ -202,22 +252,29 @@ export class CalendarChannelSyncStatusService { return; } - const calendarChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'calendarChannel', - ); - for (const calendarChannelId of calendarChannelIds) { await this.cacheStorage.del( `calendar-events-to-import:${workspaceId}:${calendarChannelId}`, ); } - await calendarChannelRepository.update(calendarChannelIds, { - syncStatus: CalendarChannelSyncStatus.FAILED_UNKNOWN, - syncStage: CalendarChannelSyncStage.FAILED, - }); + const authContext = buildSystemAuthContext(workspaceId); + + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const calendarChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'calendarChannel', + ); + + await calendarChannelRepository.update(calendarChannelIds, { + syncStatus: CalendarChannelSyncStatus.FAILED_UNKNOWN, + syncStage: CalendarChannelSyncStage.FAILED, + }); + }, + ); await this.metricsService.batchIncrementCounter({ key: MetricsKeys.CalendarEventSyncJobFailedUnknown, @@ -233,49 +290,57 @@ export class CalendarChannelSyncStatusService { return; } - const calendarChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'calendarChannel', - ); - for (const calendarChannelId of calendarChannelIds) { await this.cacheStorage.del( `calendar-events-to-import:${workspaceId}:${calendarChannelId}`, ); } - await calendarChannelRepository.update(calendarChannelIds, { - syncStatus: CalendarChannelSyncStatus.FAILED_INSUFFICIENT_PERMISSIONS, - syncStage: CalendarChannelSyncStage.FAILED, - }); - const connectedAccountRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'connectedAccount', - ); + const authContext = buildSystemAuthContext(workspaceId); - const calendarChannels = await calendarChannelRepository.find({ - select: ['id', 'connectedAccountId'], - where: { id: Any(calendarChannelIds) }, - }); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const calendarChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'calendarChannel', + ); - const connectedAccountIds = calendarChannels.map( - (calendarChannel) => calendarChannel.connectedAccountId, - ); + await calendarChannelRepository.update(calendarChannelIds, { + syncStatus: CalendarChannelSyncStatus.FAILED_INSUFFICIENT_PERMISSIONS, + syncStage: CalendarChannelSyncStage.FAILED, + }); - await connectedAccountRepository.update( - { id: Any(connectedAccountIds) }, - { - authFailedAt: new Date(), + const connectedAccountRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'connectedAccount', + ); + + const calendarChannels = await calendarChannelRepository.find({ + select: ['id', 'connectedAccountId'], + where: { id: Any(calendarChannelIds) }, + }); + + const connectedAccountIds = calendarChannels.map( + (calendarChannel) => calendarChannel.connectedAccountId, + ); + + await connectedAccountRepository.update( + { id: Any(connectedAccountIds) }, + { + authFailedAt: new Date(), + }, + ); + + await this.addToAccountsToReconnect( + calendarChannels.map((calendarChannel) => calendarChannel.id), + workspaceId, + ); }, ); - await this.addToAccountsToReconnect( - calendarChannels.map((calendarChannel) => calendarChannel.id), - workspaceId, - ); - await this.metricsService.batchIncrementCounter({ key: MetricsKeys.CalendarEventSyncJobFailedInsufficientPermissions, eventIds: calendarChannelIds, @@ -291,7 +356,7 @@ export class CalendarChannelSyncStatusService { } const calendarChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( + await this.globalWorkspaceOrmManager.getRepository( workspaceId, 'calendarChannel', ); diff --git a/packages/twenty-server/src/modules/connected-account/channel-sync/services/channel-sync.service.ts b/packages/twenty-server/src/modules/connected-account/channel-sync/services/channel-sync.service.ts index f05a5ed78e4..a284d085fb5 100644 --- a/packages/twenty-server/src/modules/connected-account/channel-sync/services/channel-sync.service.ts +++ b/packages/twenty-server/src/modules/connected-account/channel-sync/services/channel-sync.service.ts @@ -3,7 +3,8 @@ import { Injectable } from '@nestjs/common'; import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { CalendarEventListFetchJob, type CalendarEventListFetchJobData, @@ -31,7 +32,7 @@ export type StartChannelSyncInput = { @Injectable() export class ChannelSyncService { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, @InjectMessageQueue(MessageQueue.messagingQueue) private readonly messageQueueService: MessageQueueService, @InjectMessageQueue(MessageQueue.calendarQueue) @@ -50,65 +51,80 @@ export class ChannelSyncService { connectedAccountId: string, workspaceId: string, ): Promise { - const messageChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'messageChannel', - ); + const authContext = buildSystemAuthContext(workspaceId); - const messageChannels = await messageChannelRepository.find({ - where: { - connectedAccountId, - syncStage: MessageChannelSyncStage.PENDING_CONFIGURATION, + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const messageChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'messageChannel', + ); + + const messageChannels = await messageChannelRepository.find({ + where: { + connectedAccountId, + syncStage: MessageChannelSyncStage.PENDING_CONFIGURATION, + }, + }); + + for (const messageChannel of messageChannels) { + await this.messageChannelSyncStatusService.markAsMessagesListFetchScheduled( + [messageChannel.id], + workspaceId, + ); + + await this.messageQueueService.add( + MessagingMessageListFetchJob.name, + { + workspaceId, + messageChannelId: messageChannel.id, + }, + ); + } }, - }); - - for (const messageChannel of messageChannels) { - await this.messageChannelSyncStatusService.markAsMessagesListFetchScheduled( - [messageChannel.id], - workspaceId, - ); - - await this.messageQueueService.add( - MessagingMessageListFetchJob.name, - { - workspaceId, - messageChannelId: messageChannel.id, - }, - ); - } + ); } private async startCalendarChannelSync( connectedAccountId: string, workspaceId: string, ): Promise { - const calendarChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'calendarChannel', - ); + const authContext = buildSystemAuthContext(workspaceId); - const calendarChannels = await calendarChannelRepository.find({ - where: { - connectedAccountId, - syncStage: CalendarChannelSyncStage.PENDING_CONFIGURATION, + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const calendarChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'calendarChannel', + ); + + const calendarChannels = await calendarChannelRepository.find({ + where: { + connectedAccountId, + syncStage: CalendarChannelSyncStage.PENDING_CONFIGURATION, + }, + }); + + for (const calendarChannel of calendarChannels) { + await calendarChannelRepository.update(calendarChannel.id, { + syncStage: + CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_SCHEDULED, + syncStatus: CalendarChannelSyncStatus.ONGOING, + }); + + await this.calendarQueueService.add( + CalendarEventListFetchJob.name, + { + workspaceId, + calendarChannelId: calendarChannel.id, + }, + ); + } }, - }); - - for (const calendarChannel of calendarChannels) { - await calendarChannelRepository.update(calendarChannel.id, { - syncStage: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_SCHEDULED, - syncStatus: CalendarChannelSyncStatus.ONGOING, - }); - - await this.calendarQueueService.add( - CalendarEventListFetchJob.name, - { - workspaceId, - calendarChannelId: calendarChannel.id, - }, - ); - } + ); } } diff --git a/packages/twenty-server/src/modules/connected-account/email-alias-manager/services/email-alias-manager.service.spec.ts b/packages/twenty-server/src/modules/connected-account/email-alias-manager/services/email-alias-manager.service.spec.ts index 53ad647be37..18b45138b7b 100644 --- a/packages/twenty-server/src/modules/connected-account/email-alias-manager/services/email-alias-manager.service.spec.ts +++ b/packages/twenty-server/src/modules/connected-account/email-alias-manager/services/email-alias-manager.service.spec.ts @@ -3,7 +3,7 @@ import { Test, type TestingModule } from '@nestjs/testing'; import { ConnectedAccountProvider } from 'twenty-shared/types'; import { type Repository } from 'typeorm'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { GoogleEmailAliasManagerService } from 'src/modules/connected-account/email-alias-manager/drivers/google/services/google-email-alias-manager.service'; import { microsoftGraphMeResponseWithProxyAddresses } from 'src/modules/connected-account/email-alias-manager/drivers/microsoft/mocks/microsoft-api-examples'; import { MicrosoftEmailAliasManagerService } from 'src/modules/connected-account/email-alias-manager/drivers/microsoft/services/microsoft-email-alias-manager.service'; @@ -28,11 +28,14 @@ describe('Email Alias Manager Service', () => { const module: TestingModule = await Test.createTestingModule({ providers: [ { - provide: TwentyORMGlobalManager, + provide: GlobalWorkspaceOrmManager, useValue: { - getRepositoryForWorkspace: jest + getRepository: jest .fn() .mockResolvedValue(connectedAccountRepository), + executeInWorkspaceContext: jest + .fn() + .mockImplementation((_authContext: any, fn: () => any) => fn()), }, }, EmailAliasManagerService, diff --git a/packages/twenty-server/src/modules/connected-account/email-alias-manager/services/email-alias-manager.service.ts b/packages/twenty-server/src/modules/connected-account/email-alias-manager/services/email-alias-manager.service.ts index ba454cbb2b8..beb06710b14 100644 --- a/packages/twenty-server/src/modules/connected-account/email-alias-manager/services/email-alias-manager.service.ts +++ b/packages/twenty-server/src/modules/connected-account/email-alias-manager/services/email-alias-manager.service.ts @@ -3,7 +3,8 @@ import { Injectable } from '@nestjs/common'; import { ConnectedAccountProvider } from 'twenty-shared/types'; import { assertUnreachable } from 'twenty-shared/utils'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { GoogleEmailAliasManagerService } from 'src/modules/connected-account/email-alias-manager/drivers/google/services/google-email-alias-manager.service'; import { MicrosoftEmailAliasManagerService } from 'src/modules/connected-account/email-alias-manager/drivers/microsoft/services/microsoft-email-alias-manager.service'; import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; @@ -13,7 +14,7 @@ export class EmailAliasManagerService { constructor( private readonly googleEmailAliasManagerService: GoogleEmailAliasManagerService, private readonly microsoftEmailAliasManagerService: MicrosoftEmailAliasManagerService, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, ) {} public async refreshHandleAliases( @@ -46,16 +47,23 @@ export class EmailAliasManagerService { ); } - const connectedAccountRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'connectedAccount', - ); + const authContext = buildSystemAuthContext(workspaceId); - await connectedAccountRepository.update( - { id: connectedAccount.id }, - { - handleAliases: handleAliases.join(','), // TODO: modify handleAliases to be of fieldmetadatatype array + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const connectedAccountRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'connectedAccount', + ); + + await connectedAccountRepository.update( + { id: connectedAccount.id }, + { + handleAliases: handleAliases.join(','), // TODO: modify handleAliases to be of fieldmetadatatype array + }, + ); }, ); } diff --git a/packages/twenty-server/src/modules/connected-account/jobs/delete-workspace-member-connected-accounts.job.ts b/packages/twenty-server/src/modules/connected-account/jobs/delete-workspace-member-connected-accounts.job.ts index d9d9ee8c882..e926150be56 100644 --- a/packages/twenty-server/src/modules/connected-account/jobs/delete-workspace-member-connected-accounts.job.ts +++ b/packages/twenty-server/src/modules/connected-account/jobs/delete-workspace-member-connected-accounts.job.ts @@ -1,7 +1,8 @@ import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator'; import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; export type DeleteWorkspaceMemberConnectedAccountsCleanupJobData = { @@ -12,7 +13,7 @@ export type DeleteWorkspaceMemberConnectedAccountsCleanupJobData = { @Processor(MessageQueue.deleteCascadeQueue) export class DeleteWorkspaceMemberConnectedAccountsCleanupJob { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, ) {} @Process(DeleteWorkspaceMemberConnectedAccountsCleanupJob.name) @@ -21,14 +22,21 @@ export class DeleteWorkspaceMemberConnectedAccountsCleanupJob { ): Promise { const { workspaceId, workspaceMemberId } = data; - const connectedAccountRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'connectedAccount', - ); + const authContext = buildSystemAuthContext(workspaceId); - await connectedAccountRepository.delete({ - accountOwnerId: workspaceMemberId, - }); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const connectedAccountRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'connectedAccount', + ); + + await connectedAccountRepository.delete({ + accountOwnerId: workspaceMemberId, + }); + }, + ); } } diff --git a/packages/twenty-server/src/modules/connected-account/listeners/connected-account.listener.ts b/packages/twenty-server/src/modules/connected-account/listeners/connected-account.listener.ts index 5571f2ada3b..c4448759efe 100644 --- a/packages/twenty-server/src/modules/connected-account/listeners/connected-account.listener.ts +++ b/packages/twenty-server/src/modules/connected-account/listeners/connected-account.listener.ts @@ -3,7 +3,8 @@ import { Injectable } from '@nestjs/common'; import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator'; import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; import { type ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type'; import { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service'; import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; @@ -12,7 +13,7 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta @Injectable() export class ConnectedAccountListener { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly accountsToReconnectService: AccountsToReconnectService, ) {} @@ -22,28 +23,39 @@ export class ConnectedAccountListener { ObjectRecordDeleteEvent >, ) { - for (const eventPayload of payload.events) { - const workspaceMemberId = eventPayload.properties.before.accountOwnerId; - const workspaceId = payload.workspaceId; - const workspaceMemberRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'workspaceMember', - { shouldBypassPermissionChecks: true }, - ); - const workspaceMember = await workspaceMemberRepository.findOneOrFail({ - where: { id: workspaceMemberId }, - }); + const workspaceId = payload.workspaceId; + const authContext = buildSystemAuthContext(workspaceId); - const userId = workspaceMember.userId; + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + for (const eventPayload of payload.events) { + const workspaceMemberId = + eventPayload.properties.before.accountOwnerId; - const connectedAccountId = eventPayload.properties.before.id; + const workspaceMemberRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'workspaceMember', + { shouldBypassPermissionChecks: true }, + ); + const workspaceMember = await workspaceMemberRepository.findOneOrFail( + { + where: { id: workspaceMemberId }, + }, + ); - await this.accountsToReconnectService.removeAccountToReconnect( - userId, - workspaceId, - connectedAccountId, - ); - } + const userId = workspaceMember.userId; + + const connectedAccountId = eventPayload.properties.before.id; + + await this.accountsToReconnectService.removeAccountToReconnect( + userId, + workspaceId, + connectedAccountId, + ); + } + }, + ); } } diff --git a/packages/twenty-server/src/modules/connected-account/query-hooks/connected-account-delete-one.pre-query.hook.ts b/packages/twenty-server/src/modules/connected-account/query-hooks/connected-account-delete-one.pre-query.hook.ts index 6fd3bbe8476..d7c9cc40aeb 100644 --- a/packages/twenty-server/src/modules/connected-account/query-hooks/connected-account-delete-one.pre-query.hook.ts +++ b/packages/twenty-server/src/modules/connected-account/query-hooks/connected-account-delete-one.pre-query.hook.ts @@ -5,6 +5,7 @@ import { Repository } from 'typeorm'; import { type WorkspacePreQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface'; import { type DeleteOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; +import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface'; import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator'; @@ -12,7 +13,7 @@ import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-contex import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception'; import { fromObjectMetadataEntityToFlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-object-metadata-entity-to-flat-object-metadata.util'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; @@ -21,7 +22,7 @@ export class ConnectedAccountDeleteOnePreQueryHook implements WorkspacePreQueryHookInstance { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly workspaceEventEmitter: WorkspaceEventEmitter, @InjectRepository(ObjectMetadataEntity) private readonly objectMetadataRepository: Repository, @@ -38,15 +39,21 @@ export class ConnectedAccountDeleteOnePreQueryHook assertIsDefinedOrThrow(workspace, WorkspaceNotFoundDefaultError); - const messageChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspace.id, - 'messageChannel', - ); + const messageChannels = + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext as WorkspaceAuthContext, + async () => { + const messageChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspace.id, + 'messageChannel', + ); - const messageChannels = await messageChannelRepository.findBy({ - connectedAccountId, - }); + return messageChannelRepository.findBy({ + connectedAccountId, + }); + }, + ); const objectMetadataEntity = await this.objectMetadataRepository.findOneOrFail({ diff --git a/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/services/connected-account-refresh-tokens.service.spec.ts b/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/services/connected-account-refresh-tokens.service.spec.ts index 0445b3e1c3a..193f8950112 100644 --- a/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/services/connected-account-refresh-tokens.service.spec.ts +++ b/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/services/connected-account-refresh-tokens.service.spec.ts @@ -2,7 +2,7 @@ import { Test, type TestingModule } from '@nestjs/testing'; import { ConnectedAccountProvider } from 'twenty-shared/types'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { GoogleAPIRefreshAccessTokenService } from 'src/modules/connected-account/refresh-tokens-manager/drivers/google/services/google-api-refresh-tokens.service'; import { MicrosoftAPIRefreshAccessTokenService } from 'src/modules/connected-account/refresh-tokens-manager/drivers/microsoft/services/microsoft-api-refresh-tokens.service'; import { @@ -17,7 +17,7 @@ describe('ConnectedAccountRefreshTokensService', () => { let service: ConnectedAccountRefreshTokensService; let googleAPIRefreshAccessTokenService: GoogleAPIRefreshAccessTokenService; let microsoftAPIRefreshAccessTokenService: MicrosoftAPIRefreshAccessTokenService; - let twentyORMGlobalManager: TwentyORMGlobalManager; + let globalWorkspaceOrmManager: GlobalWorkspaceOrmManager; const mockWorkspaceId = 'workspace-123'; const mockConnectedAccountId = 'account-456'; @@ -42,9 +42,13 @@ describe('ConnectedAccountRefreshTokensService', () => { }, }, { - provide: TwentyORMGlobalManager, + provide: GlobalWorkspaceOrmManager, useValue: { - getRepositoryForWorkspace: jest.fn(), + getRepository: jest.fn(), + executeInWorkspaceContext: jest + .fn() + + .mockImplementation((_authContext: any, fn: () => any) => fn()), }, }, ], @@ -61,8 +65,8 @@ describe('ConnectedAccountRefreshTokensService', () => { module.get( MicrosoftAPIRefreshAccessTokenService, ); - twentyORMGlobalManager = module.get( - TwentyORMGlobalManager, + globalWorkspaceOrmManager = module.get( + GlobalWorkspaceOrmManager, ); }); @@ -92,9 +96,7 @@ describe('ConnectedAccountRefreshTokensService', () => { expect( microsoftAPIRefreshAccessTokenService.refreshTokens, ).not.toHaveBeenCalled(); - expect( - twentyORMGlobalManager.getRepositoryForWorkspace, - ).not.toHaveBeenCalled(); + expect(globalWorkspaceOrmManager.getRepository).not.toHaveBeenCalled(); }); it('should refresh and save new Microsoft token when expired (lastCredentialsRefreshedAt is old)', async () => { @@ -116,7 +118,7 @@ describe('ConnectedAccountRefreshTokensService', () => { .spyOn(microsoftAPIRefreshAccessTokenService, 'refreshTokens') .mockResolvedValue(newTokens); jest - .spyOn(twentyORMGlobalManager, 'getRepositoryForWorkspace') + .spyOn(globalWorkspaceOrmManager, 'getRepository') .mockResolvedValue(mockRepository as any); const result = await service.refreshAndSaveTokens( @@ -156,7 +158,7 @@ describe('ConnectedAccountRefreshTokensService', () => { .spyOn(googleAPIRefreshAccessTokenService, 'refreshTokens') .mockResolvedValue(newTokens); jest - .spyOn(twentyORMGlobalManager, 'getRepositoryForWorkspace') + .spyOn(globalWorkspaceOrmManager, 'getRepository') .mockResolvedValue(mockRepository as any); const result = await service.refreshAndSaveTokens( @@ -196,7 +198,7 @@ describe('ConnectedAccountRefreshTokensService', () => { .spyOn(microsoftAPIRefreshAccessTokenService, 'refreshTokens') .mockResolvedValue(newTokens); jest - .spyOn(twentyORMGlobalManager, 'getRepositoryForWorkspace') + .spyOn(globalWorkspaceOrmManager, 'getRepository') .mockResolvedValue(mockRepository as any); const result = await service.refreshAndSaveTokens( diff --git a/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/services/connected-account-refresh-tokens.service.ts b/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/services/connected-account-refresh-tokens.service.ts index a63c6665535..9b11dd16da8 100644 --- a/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/services/connected-account-refresh-tokens.service.ts +++ b/packages/twenty-server/src/modules/connected-account/refresh-tokens-manager/services/connected-account-refresh-tokens.service.ts @@ -3,7 +3,8 @@ import { Injectable, Logger } from '@nestjs/common'; import { ConnectedAccountProvider } from 'twenty-shared/types'; import { assertUnreachable, isDefined } from 'twenty-shared/utils'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { GoogleAPIRefreshAccessTokenService } from 'src/modules/connected-account/refresh-tokens-manager/drivers/google/services/google-api-refresh-tokens.service'; import { MicrosoftAPIRefreshAccessTokenService } from 'src/modules/connected-account/refresh-tokens-manager/drivers/microsoft/services/microsoft-api-refresh-tokens.service'; import { @@ -29,7 +30,7 @@ export class ConnectedAccountRefreshTokensService { constructor( private readonly googleAPIRefreshAccessTokenService: GoogleAPIRefreshAccessTokenService, private readonly microsoftAPIRefreshAccessTokenService: MicrosoftAPIRefreshAccessTokenService, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, ) {} async refreshAndSaveTokens( @@ -75,17 +76,24 @@ export class ConnectedAccountRefreshTokensService { workspaceId, ); - const connectedAccountRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'connectedAccount', - ); + const authContext = buildSystemAuthContext(workspaceId); - await connectedAccountRepository.update( - { id: connectedAccount.id }, - { - ...connectedAccountTokens, - lastCredentialsRefreshedAt: new Date(), + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const connectedAccountRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'connectedAccount', + ); + + await connectedAccountRepository.update( + { id: connectedAccount.id }, + { + ...connectedAccountTokens, + lastCredentialsRefreshedAt: new Date(), + }, + ); }, ); diff --git a/packages/twenty-server/src/modules/connected-account/services/imap-smtp-caldav-apis.service.spec.ts b/packages/twenty-server/src/modules/connected-account/services/imap-smtp-caldav-apis.service.spec.ts index f1f8c08ca9b..b35f28dfd5a 100644 --- a/packages/twenty-server/src/modules/connected-account/services/imap-smtp-caldav-apis.service.spec.ts +++ b/packages/twenty-server/src/modules/connected-account/services/imap-smtp-caldav-apis.service.spec.ts @@ -5,7 +5,7 @@ import { ConnectedAccountProvider } from 'twenty-shared/types'; import { CreateCalendarChannelService } from 'src/engine/core-modules/auth/services/create-calendar-channel.service'; import { CreateMessageChannelService } from 'src/engine/core-modules/auth/services/create-message-channel.service'; import { type EmailAccountConnectionParameters } from 'src/engine/core-modules/imap-smtp-caldav-connection/dtos/imap-smtp-caldav-connection.dto'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { type CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; import { ImapSmtpCalDavAPIService } from 'src/modules/connected-account/services/imap-smtp-caldav-apis.service'; import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; @@ -50,9 +50,9 @@ describe('ImapSmtpCalDavAPIService', () => { providers: [ ImapSmtpCalDavAPIService, { - provide: TwentyORMGlobalManager, + provide: GlobalWorkspaceOrmManager, useValue: { - getRepositoryForWorkspace: jest + getRepository: jest .fn() .mockImplementation((_workspaceId, entity) => { if (entity === 'connectedAccount') @@ -67,6 +67,10 @@ describe('ImapSmtpCalDavAPIService', () => { getDataSourceForWorkspace: jest .fn() .mockImplementation(() => mockWorkspaceDataSource), + executeInWorkspaceContext: jest + .fn() + + .mockImplementation((_authContext: any, fn: () => any) => fn()), }, }, { diff --git a/packages/twenty-server/src/modules/connected-account/services/imap-smtp-caldav-apis.service.ts b/packages/twenty-server/src/modules/connected-account/services/imap-smtp-caldav-apis.service.ts index d549a20d2d8..9860252497e 100644 --- a/packages/twenty-server/src/modules/connected-account/services/imap-smtp-caldav-apis.service.ts +++ b/packages/twenty-server/src/modules/connected-account/services/imap-smtp-caldav-apis.service.ts @@ -8,7 +8,8 @@ import { CreateCalendarChannelService } from 'src/engine/core-modules/auth/servi import { CreateMessageChannelService } from 'src/engine/core-modules/auth/services/create-message-channel.service'; import { type EmailAccountConnectionParameters } from 'src/engine/core-modules/imap-smtp-caldav-connection/dtos/imap-smtp-caldav-connection.dto'; import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { type CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; @@ -16,11 +17,33 @@ import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common @Injectable() export class ImapSmtpCalDavAPIService { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly createMessageChannelService: CreateMessageChannelService, private readonly createCalendarChannelService: CreateCalendarChannelService, ) {} + async getImapSmtpCaldavConnectedAccount( + workspaceId: string, + id: string, + ): Promise { + const authContext = buildSystemAuthContext(workspaceId); + + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const connectedAccountRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'connectedAccount', + ); + + return connectedAccountRepository.findOne({ + where: { id, provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV }, + }); + }, + ); + } + async processAccount(input: { handle: string; workspaceMemberId: string; @@ -31,94 +54,101 @@ export class ImapSmtpCalDavAPIService { const { handle, workspaceId, workspaceMemberId, connectedAccountId } = input; - const connectedAccountRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'connectedAccount', - ); + const authContext = buildSystemAuthContext(workspaceId); - const messageChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'messageChannel', - ); + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const connectedAccountRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'connectedAccount', + ); - const calendarChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'calendarChannel', - ); + const messageChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'messageChannel', + ); - const existingAccount = connectedAccountId - ? await connectedAccountRepository.findOne({ - where: { id: connectedAccountId }, - }) - : await connectedAccountRepository.findOne({ - where: { handle, accountOwnerId: workspaceMemberId }, - }); + const calendarChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'calendarChannel', + ); - const newOrExistingAccountId = - existingAccount?.id ?? connectedAccountId ?? v4(); + const existingAccount = connectedAccountId + ? await connectedAccountRepository.findOne({ + where: { id: connectedAccountId }, + }) + : await connectedAccountRepository.findOne({ + where: { handle, accountOwnerId: workspaceMemberId }, + }); - const workspaceDataSource = - await this.twentyORMGlobalManager.getDataSourceForWorkspace({ - workspaceId, - }); + const newOrExistingAccountId = + existingAccount?.id ?? connectedAccountId ?? v4(); - const existingMessageChannel = existingAccount - ? await messageChannelRepository.findOne({ - where: { connectedAccountId: existingAccount.id }, - }) - : null; + const workspaceDataSource = + await this.globalWorkspaceOrmManager.getDataSourceForWorkspace( + workspaceId, + ); - const existingCalendarChannel = existingAccount - ? await calendarChannelRepository.findOne({ - where: { connectedAccountId: existingAccount.id }, - }) - : null; + const existingMessageChannel = existingAccount + ? await messageChannelRepository.findOne({ + where: { connectedAccountId: existingAccount.id }, + }) + : null; - const shouldCreateMessageChannel = - !isDefined(existingMessageChannel) && - Boolean(input.connectionParameters.IMAP); + const existingCalendarChannel = existingAccount + ? await calendarChannelRepository.findOne({ + where: { connectedAccountId: existingAccount.id }, + }) + : null; - const shouldCreateCalendarChannel = - !isDefined(existingCalendarChannel) && - Boolean(input.connectionParameters.CALDAV); + const shouldCreateMessageChannel = + !isDefined(existingMessageChannel) && + Boolean(input.connectionParameters.IMAP); - await workspaceDataSource.transaction( - async (manager: WorkspaceEntityManager) => { - await connectedAccountRepository.save( - { - id: newOrExistingAccountId, - handle, - provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV, - connectionParameters: input.connectionParameters, - accountOwnerId: workspaceMemberId, + const shouldCreateCalendarChannel = + !isDefined(existingCalendarChannel) && + Boolean(input.connectionParameters.CALDAV); + + await workspaceDataSource.transaction( + async (manager: WorkspaceEntityManager) => { + await connectedAccountRepository.save( + { + id: newOrExistingAccountId, + handle, + provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV, + connectionParameters: input.connectionParameters, + accountOwnerId: workspaceMemberId, + }, + {}, + manager, + ); + + if (shouldCreateMessageChannel) { + await this.createMessageChannelService.createMessageChannel({ + workspaceId, + connectedAccountId: newOrExistingAccountId, + handle, + manager, + }); + } + + if (shouldCreateCalendarChannel) { + await this.createCalendarChannelService.createCalendarChannel({ + workspaceId, + connectedAccountId: newOrExistingAccountId, + handle, + manager, + }); + } }, - {}, - manager, ); - if (shouldCreateMessageChannel) { - await this.createMessageChannelService.createMessageChannel({ - workspaceId, - connectedAccountId: newOrExistingAccountId, - handle, - manager, - }); - } - - if (shouldCreateCalendarChannel) { - await this.createCalendarChannelService.createCalendarChannel({ - workspaceId, - connectedAccountId: newOrExistingAccountId, - handle, - manager, - }); - } + return newOrExistingAccountId; }, ); - - return newOrExistingAccountId; } } diff --git a/packages/twenty-server/src/modules/contact-creation-manager/services/__tests__/create-company-and-contact.service.spec.ts b/packages/twenty-server/src/modules/contact-creation-manager/services/__tests__/create-company-and-contact.service.spec.ts index 688ed99fe40..48d8c5a9212 100644 --- a/packages/twenty-server/src/modules/contact-creation-manager/services/__tests__/create-company-and-contact.service.spec.ts +++ b/packages/twenty-server/src/modules/contact-creation-manager/services/__tests__/create-company-and-contact.service.spec.ts @@ -3,7 +3,7 @@ import { Test, type TestingModule } from '@nestjs/testing'; import { FieldActorSource } from 'twenty-shared/types'; import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { CreateCompanyAndPersonService } from 'src/modules/contact-creation-manager/services/create-company-and-contact.service'; import { CreateCompanyService } from 'src/modules/contact-creation-manager/services/create-company.service'; @@ -41,7 +41,7 @@ describe('CreateCompanyAndPersonService', () => { useValue: mockCreatePersonService, }, { - provide: TwentyORMGlobalManager, + provide: GlobalWorkspaceOrmManager, useValue: {}, }, { diff --git a/packages/twenty-server/src/modules/contact-creation-manager/services/__tests__/create-company.service.spec.ts b/packages/twenty-server/src/modules/contact-creation-manager/services/__tests__/create-company.service.spec.ts index 97e65a5c7e4..a6d3c90911a 100644 --- a/packages/twenty-server/src/modules/contact-creation-manager/services/__tests__/create-company.service.spec.ts +++ b/packages/twenty-server/src/modules/contact-creation-manager/services/__tests__/create-company.service.spec.ts @@ -2,14 +2,14 @@ import { Test, type TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import axios from 'axios'; +import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata'; import { ConnectedAccountProvider, FieldActorSource, } from 'twenty-shared/types'; -import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { type CompanyToCreate, CreateCompanyService, @@ -115,11 +115,12 @@ describe('CreateCompanyService', () => { providers: [ CreateCompanyService, { - provide: TwentyORMGlobalManager, + provide: GlobalWorkspaceOrmManager, useValue: { - getRepositoryForWorkspace: jest + getRepository: jest.fn().mockResolvedValue(mockCompanyRepository), + executeInWorkspaceContext: jest .fn() - .mockResolvedValue(mockCompanyRepository), + .mockImplementation((_authContext: any, fn: () => any) => fn()), }, }, { diff --git a/packages/twenty-server/src/modules/contact-creation-manager/services/create-company-and-contact.service.ts b/packages/twenty-server/src/modules/contact-creation-manager/services/create-company-and-contact.service.ts index 0c534e71112..884ab2439b7 100644 --- a/packages/twenty-server/src/modules/contact-creation-manager/services/create-company-and-contact.service.ts +++ b/packages/twenty-server/src/modules/contact-creation-manager/services/create-company-and-contact.service.ts @@ -12,7 +12,8 @@ import { type DeepPartial } from 'typeorm'; import { v4 } from 'uuid'; import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { CONTACTS_CREATION_BATCH_SIZE } from 'src/modules/contact-creation-manager/constants/contacts-creation-batch-size.constant'; import { CreateCompanyService } from 'src/modules/contact-creation-manager/services/create-company.service'; @@ -27,12 +28,13 @@ import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/perso import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; import { computeDisplayName } from 'src/utils/compute-display-name'; import { isWorkDomain, isWorkEmail } from 'src/utils/is-work-email'; + @Injectable() export class CreateCompanyAndPersonService { constructor( private readonly createPersonService: CreatePersonService, private readonly createCompaniesService: CreateCompanyService, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly exceptionHandlerService: ExceptionHandlerService, ) {} @@ -46,96 +48,103 @@ export class CreateCompanyAndPersonService { return []; } - const personRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - PersonWorkspaceEntity, - { - shouldBypassPermissionChecks: true, - }, - ); + const authContext = buildSystemAuthContext(workspaceId); - const workspaceMemberRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - WorkspaceMemberWorkspaceEntity, - ); + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const personRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + PersonWorkspaceEntity, + { + shouldBypassPermissionChecks: true, + }, + ); - const workspaceMembers = await workspaceMemberRepository.find(); + const workspaceMemberRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + WorkspaceMemberWorkspaceEntity, + ); - const peopleToCreateFromOtherCompanies = - filterOutContactsThatBelongToSelfOrWorkspaceMembers( - contactsToCreate, - connectedAccount, - workspaceMembers, - ); + const workspaceMembers = await workspaceMemberRepository.find(); - const { uniqueContacts, uniqueHandles } = getUniqueContactsAndHandles( - peopleToCreateFromOtherCompanies, - ); + const peopleToCreateFromOtherCompanies = + filterOutContactsThatBelongToSelfOrWorkspaceMembers( + contactsToCreate, + connectedAccount, + workspaceMembers, + ); - if (uniqueHandles.length === 0) { - return []; - } + const { uniqueContacts, uniqueHandles } = getUniqueContactsAndHandles( + peopleToCreateFromOtherCompanies, + ); - const queryBuilder = addPersonEmailFiltersToQueryBuilder({ - queryBuilder: personRepository.createQueryBuilder('person'), - emails: uniqueHandles, - }); + if (uniqueHandles.length === 0) { + return []; + } - const alreadyCreatedPeople = await queryBuilder - .orderBy('person.createdAt', 'ASC') - .withDeleted() - .getMany(); + const queryBuilder = addPersonEmailFiltersToQueryBuilder({ + queryBuilder: personRepository.createQueryBuilder('person'), + emails: uniqueHandles, + }); - const { - contactsThatNeedPersonCreate, - contactsThatNeedPersonRestore, - workDomainNamesToCreate, - shouldCreateOrRestorePeopleByHandleMap, - } = - this.computeContactsThatNeedPersonCreateAndRestoreAndWorkDomainNamesToCreate( - uniqueContacts, - alreadyCreatedPeople, - source, - connectedAccount, - ); + const alreadyCreatedPeople = await queryBuilder + .orderBy('person.createdAt', 'ASC') + .withDeleted() + .getMany(); - const companiesMap = - await this.createCompaniesService.createOrRestoreCompanies( - workDomainNamesToCreate, - workspaceId, - ); + const { + contactsThatNeedPersonCreate, + contactsThatNeedPersonRestore, + workDomainNamesToCreate, + shouldCreateOrRestorePeopleByHandleMap, + } = + this.computeContactsThatNeedPersonCreateAndRestoreAndWorkDomainNamesToCreate( + uniqueContacts, + alreadyCreatedPeople, + source, + connectedAccount, + ); - const peopleToCreate = this.formatPeopleToCreateFromContacts({ - contactsToCreate: contactsThatNeedPersonCreate, - createdBy: { - source: source, - workspaceMember: connectedAccount.accountOwner, - context: { - provider: connectedAccount.provider, - }, + const companiesMap = + await this.createCompaniesService.createOrRestoreCompanies( + workDomainNamesToCreate, + workspaceId, + ); + + const peopleToCreate = this.formatPeopleToCreateFromContacts({ + contactsToCreate: contactsThatNeedPersonCreate, + createdBy: { + source: source, + workspaceMember: connectedAccount.accountOwner, + context: { + provider: connectedAccount.provider, + }, + }, + companiesMap, + }); + + const createdPeople = await this.createPersonService.createPeople( + peopleToCreate, + workspaceId, + ); + + const peopleToRestore = this.formatPeopleToRestoreFromContacts({ + contactsToRestore: contactsThatNeedPersonRestore, + companiesMap, + shouldCreateOrRestorePeopleByHandleMap, + }); + + const restoredPeople = await this.createPersonService.restorePeople( + peopleToRestore, + workspaceId, + ); + + return { ...createdPeople, ...restoredPeople }; }, - companiesMap, - }); - - const createdPeople = await this.createPersonService.createPeople( - peopleToCreate, - workspaceId, ); - - const peopleToRestore = this.formatPeopleToRestoreFromContacts({ - contactsToRestore: contactsThatNeedPersonRestore, - companiesMap, - shouldCreateOrRestorePeopleByHandleMap, - }); - - const restoredPeople = await this.createPersonService.restorePeople( - peopleToRestore, - workspaceId, - ); - - return { ...createdPeople, ...restoredPeople }; } async createCompaniesAndPeopleAndUpdateParticipants( @@ -149,27 +158,34 @@ export class CreateCompanyAndPersonService { CONTACTS_CREATION_BATCH_SIZE, ); - if (!connectedAccount.accountOwner) { - const workspaceMemberRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - WorkspaceMemberWorkspaceEntity, - ); + const authContext = buildSystemAuthContext(workspaceId); - const workspaceMember = await workspaceMemberRepository.findOne({ - where: { - id: connectedAccount.accountOwnerId, - }, - }); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + if (!connectedAccount.accountOwner) { + const workspaceMemberRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + WorkspaceMemberWorkspaceEntity, + ); - if (!workspaceMember) { - throw new Error( - `Workspace member with id ${connectedAccount.accountOwnerId} not found in workspace ${workspaceId}`, - ); - } + const workspaceMember = await workspaceMemberRepository.findOne({ + where: { + id: connectedAccount.accountOwnerId, + }, + }); - connectedAccount.accountOwner = workspaceMember; - } + if (!workspaceMember) { + throw new Error( + `Workspace member with id ${connectedAccount.accountOwnerId} not found in workspace ${workspaceId}`, + ); + } + + connectedAccount.accountOwner = workspaceMember; + } + }, + ); for (const contactsBatch of contactsBatches) { try { diff --git a/packages/twenty-server/src/modules/contact-creation-manager/services/create-company.service.ts b/packages/twenty-server/src/modules/contact-creation-manager/services/create-company.service.ts index fd2ea0f158c..1c33ac2dc79 100644 --- a/packages/twenty-server/src/modules/contact-creation-manager/services/create-company.service.ts +++ b/packages/twenty-server/src/modules/contact-creation-manager/services/create-company.service.ts @@ -13,8 +13,9 @@ import { } from 'twenty-shared/utils'; import { type DeepPartial, ILike } from 'typeorm'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { CompanyWorkspaceEntity } from 'src/modules/company/standard-objects/company.workspace-entity'; import { extractDomainFromLink } from 'src/modules/contact-creation-manager/utils/extract-domain-from-link.util'; import { getCompanyNameFromDomainName } from 'src/modules/contact-creation-manager/utils/get-company-name-from-domain-name.util'; @@ -34,7 +35,9 @@ export type CompanyToCreate = { export class CreateCompanyService { private readonly httpService: AxiosInstance; - constructor(private readonly twentyORMGlobalManager: TwentyORMGlobalManager) { + constructor( + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, + ) { this.httpService = axios.create({ baseURL: TWENTY_COMPANIES_BASE_URL, }); @@ -50,97 +53,111 @@ export class CreateCompanyService { return {}; } - const companyRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - CompanyWorkspaceEntity, - { - shouldBypassPermissionChecks: true, - }, - ); + const authContext = buildSystemAuthContext(workspaceId); - const companiesWithoutTrailingSlash = companies.map((company) => ({ - ...company, - domainName: company.domainName - ? lowercaseUrlOriginAndRemoveTrailingSlash(company.domainName) - : undefined, - })); + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const companyRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + CompanyWorkspaceEntity, + { + shouldBypassPermissionChecks: true, + }, + ); - const uniqueCompanies = uniqBy(companiesWithoutTrailingSlash, 'domainName'); - const conditions = uniqueCompanies.map((companyToCreate) => ({ - domainName: { - primaryLinkUrl: ILike(`%${companyToCreate.domainName}%`), - }, - })); + const companiesWithoutTrailingSlash = companies.map((company) => ({ + ...company, + domainName: company.domainName + ? lowercaseUrlOriginAndRemoveTrailingSlash(company.domainName) + : undefined, + })); - const existingCompanies = await companyRepository.find({ - where: conditions, - withDeleted: true, - }); - const existingCompanyIdsMap = this.createCompanyMap(existingCompanies); - - const newCompaniesToCreate = uniqueCompanies.filter( - (company) => - !existingCompanies.some( - (existingCompany) => - existingCompany.domainName && - extractDomainFromLink(existingCompany.domainName.primaryLinkUrl) === - company.domainName, - ), - ); - - const companiesToRestore = this.filterCompaniesToRestore( - uniqueCompanies, - existingCompanies, - ); - - if (newCompaniesToCreate.length === 0 && companiesToRestore.length === 0) { - return existingCompanyIdsMap; - } - - let lastCompanyPosition = - await this.getLastCompanyPosition(companyRepository); - const newCompaniesData = await Promise.all( - newCompaniesToCreate.map((company) => - this.prepareCompanyData(company, ++lastCompanyPosition), - ), - ); - - const createdCompanies = await companyRepository.save(newCompaniesData); - - const restoredCompanies = await companyRepository.updateMany( - companiesToRestore.map((company) => { - return { - criteria: company.id, - partialEntity: { - deletedAt: null, - }, - }; - }), - undefined, - ['domainNamePrimaryLinkUrl', 'id'], - ); - - const formattedRestoredCompanies = restoredCompanies.raw.map( - (row: { id: string; domainNamePrimaryLinkUrl: string }) => { - return { - id: row.id, + const uniqueCompanies = uniqBy( + companiesWithoutTrailingSlash, + 'domainName', + ); + const conditions = uniqueCompanies.map((companyToCreate) => ({ domainName: { - primaryLinkUrl: row.domainNamePrimaryLinkUrl, + primaryLinkUrl: ILike(`%${companyToCreate.domainName}%`), }, + })); + + const existingCompanies = await companyRepository.find({ + where: conditions, + withDeleted: true, + }); + const existingCompanyIdsMap = this.createCompanyMap(existingCompanies); + + const newCompaniesToCreate = uniqueCompanies.filter( + (company) => + !existingCompanies.some( + (existingCompany) => + existingCompany.domainName && + extractDomainFromLink( + existingCompany.domainName.primaryLinkUrl, + ) === company.domainName, + ), + ); + + const companiesToRestore = this.filterCompaniesToRestore( + uniqueCompanies, + existingCompanies, + ); + + if ( + newCompaniesToCreate.length === 0 && + companiesToRestore.length === 0 + ) { + return existingCompanyIdsMap; + } + + let lastCompanyPosition = + await this.getLastCompanyPosition(companyRepository); + const newCompaniesData = await Promise.all( + newCompaniesToCreate.map((company) => + this.prepareCompanyData(company, ++lastCompanyPosition), + ), + ); + + const createdCompanies = await companyRepository.save(newCompaniesData); + + const restoredCompanies = await companyRepository.updateMany( + companiesToRestore.map((company) => { + return { + criteria: company.id, + partialEntity: { + deletedAt: null, + }, + }; + }), + undefined, + ['domainNamePrimaryLinkUrl', 'id'], + ); + + const formattedRestoredCompanies = restoredCompanies.raw.map( + (row: { id: string; domainNamePrimaryLinkUrl: string }) => { + return { + id: row.id, + domainName: { + primaryLinkUrl: row.domainNamePrimaryLinkUrl, + }, + }; + }, + ); + + return { + ...existingCompanyIdsMap, + ...(createdCompanies.length > 0 + ? this.createCompanyMap(createdCompanies) + : {}), + ...(formattedRestoredCompanies.length > 0 + ? this.createCompanyMap(formattedRestoredCompanies) + : {}), }; }, ); - - return { - ...existingCompanyIdsMap, - ...(createdCompanies.length > 0 - ? this.createCompanyMap(createdCompanies) - : {}), - ...(formattedRestoredCompanies.length > 0 - ? this.createCompanyMap(formattedRestoredCompanies) - : {}), - }; } private filterCompaniesToRestore( diff --git a/packages/twenty-server/src/modules/contact-creation-manager/services/create-person.service.ts b/packages/twenty-server/src/modules/contact-creation-manager/services/create-person.service.ts index 7a8747e5577..07189ceb91f 100644 --- a/packages/twenty-server/src/modules/contact-creation-manager/services/create-person.service.ts +++ b/packages/twenty-server/src/modules/contact-creation-manager/services/create-person.service.ts @@ -2,14 +2,15 @@ import { Injectable } from '@nestjs/common'; import { DeepPartial } from 'typeorm'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity'; @Injectable() export class CreatePersonService { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, ) {} public async createPeople( @@ -18,28 +19,35 @@ export class CreatePersonService { ): Promise[]> { if (peopleToCreate.length === 0) return []; - const personRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - PersonWorkspaceEntity, - { - shouldBypassPermissionChecks: true, - }, - ); + const authContext = buildSystemAuthContext(workspaceId); - const lastPersonPosition = - await this.getLastPersonPosition(personRepository); + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const personRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + PersonWorkspaceEntity, + { + shouldBypassPermissionChecks: true, + }, + ); - const createdPeople = await personRepository.insert( - peopleToCreate.map((person, index) => ({ - ...person, - position: lastPersonPosition + index, - })), - undefined, - ['companyId', 'id'], + const lastPersonPosition = + await this.getLastPersonPosition(personRepository); + + const createdPeople = await personRepository.insert( + peopleToCreate.map((person, index) => ({ + ...person, + position: lastPersonPosition + index, + })), + undefined, + ['companyId', 'id'], + ); + + return createdPeople.raw; + }, ); - - return createdPeople.raw; } public async restorePeople( @@ -50,28 +58,35 @@ export class CreatePersonService { return []; } - const personRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - PersonWorkspaceEntity, - { - shouldBypassPermissionChecks: true, - }, - ); + const authContext = buildSystemAuthContext(workspaceId); - const restoredPeople = await personRepository.updateMany( - people.map(({ personId, companyId }) => ({ - criteria: personId, - partialEntity: { - deletedAt: null, - companyId, - }, - })), - undefined, - ['companyId', 'id'], + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const personRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + PersonWorkspaceEntity, + { + shouldBypassPermissionChecks: true, + }, + ); + + const restoredPeople = await personRepository.updateMany( + people.map(({ personId, companyId }) => ({ + criteria: personId, + partialEntity: { + deletedAt: null, + companyId, + }, + })), + undefined, + ['companyId', 'id'], + ); + + return restoredPeople.raw; + }, ); - - return restoredPeople.raw; } private async getLastPersonPosition( diff --git a/packages/twenty-server/src/modules/favorite-folder/listeners/favorite-folder.listener.ts b/packages/twenty-server/src/modules/favorite-folder/listeners/favorite-folder.listener.ts index 378aa2494c6..9fb5ad694b0 100644 --- a/packages/twenty-server/src/modules/favorite-folder/listeners/favorite-folder.listener.ts +++ b/packages/twenty-server/src/modules/favorite-folder/listeners/favorite-folder.listener.ts @@ -3,7 +3,8 @@ import { Injectable } from '@nestjs/common'; import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator'; import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; import { type ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type'; import { type FavoriteFolderWorkspaceEntity } from 'src/modules/favorite-folder/standard-objects/favorite-folder.workspace-entity'; import { type FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity'; @@ -11,7 +12,7 @@ import { type FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-obje @Injectable() export class FavoriteFolderDeletionListener { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, ) {} @OnDatabaseBatchEvent('favoriteFolder', DatabaseEventAction.DELETED) @@ -20,17 +21,25 @@ export class FavoriteFolderDeletionListener { ObjectRecordDeleteEvent >, ) { - for (const eventPayload of payload.events) { - const favoriteRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - payload.workspaceId, - 'favorite', - ); + const workspaceId = payload.workspaceId; + const authContext = buildSystemAuthContext(workspaceId); - await favoriteRepository.update( - { favoriteFolderId: eventPayload.recordId }, - { deletedAt: new Date().toISOString() }, - ); - } + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + for (const eventPayload of payload.events) { + const favoriteRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'favorite', + ); + + await favoriteRepository.update( + { favoriteFolderId: eventPayload.recordId }, + { deletedAt: new Date().toISOString() }, + ); + } + }, + ); } } diff --git a/packages/twenty-server/src/modules/favorite/services/favorite-deletion.service.ts b/packages/twenty-server/src/modules/favorite/services/favorite-deletion.service.ts index fbcff62d4d6..0421db9117f 100644 --- a/packages/twenty-server/src/modules/favorite/services/favorite-deletion.service.ts +++ b/packages/twenty-server/src/modules/favorite/services/favorite-deletion.service.ts @@ -6,7 +6,8 @@ import { In, Repository } from 'typeorm'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { FAVORITE_DELETION_BATCH_SIZE } from 'src/modules/favorite/constants/favorite-deletion-batch-size'; import { type FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity'; @@ -18,69 +19,77 @@ export class FavoriteDeletionService { @InjectRepository(FieldMetadataEntity) private readonly fieldMetadataRepository: Repository, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, ) {} async deleteFavoritesForDeletedRecords( deletedRecordIds: string[], workspaceId: string, ): Promise { - const favoriteRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'favorite', - ); + const authContext = buildSystemAuthContext(workspaceId); - const favoriteObjectMetadata = await this.objectMetadataRepository.findOne({ - where: { - nameSingular: 'favorite', - workspaceId, + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const favoriteRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'favorite', + ); + + const favoriteObjectMetadata = + await this.objectMetadataRepository.findOne({ + where: { + nameSingular: 'favorite', + workspaceId, + }, + }); + + if (!favoriteObjectMetadata) { + throw new Error('Favorite object metadata not found'); + } + + const favoriteFields = await this.fieldMetadataRepository.find({ + where: { + objectMetadataId: favoriteObjectMetadata.id, + type: FieldMetadataType.RELATION, + }, + }); + + const favoritesToDelete = await favoriteRepository.find({ + select: { + id: true, + }, + where: favoriteFields.map((field) => ({ + [`${field.name}Id`]: In(deletedRecordIds), + })), + withDeleted: true, + }); + + if (favoritesToDelete.length === 0) { + return; + } + + const favoriteIdsToDelete = favoritesToDelete.map( + (favorite) => favorite.id, + ); + + const batches: string[][] = []; + + for ( + let i = 0; + i < favoriteIdsToDelete.length; + i += FAVORITE_DELETION_BATCH_SIZE + ) { + batches.push( + favoriteIdsToDelete.slice(i, i + FAVORITE_DELETION_BATCH_SIZE), + ); + } + + for (const batch of batches) { + await favoriteRepository.delete(batch); + } }, - }); - - if (!favoriteObjectMetadata) { - throw new Error('Favorite object metadata not found'); - } - - const favoriteFields = await this.fieldMetadataRepository.find({ - where: { - objectMetadataId: favoriteObjectMetadata.id, - type: FieldMetadataType.RELATION, - }, - }); - - const favoritesToDelete = await favoriteRepository.find({ - select: { - id: true, - }, - where: favoriteFields.map((field) => ({ - [`${field.name}Id`]: In(deletedRecordIds), - })), - withDeleted: true, - }); - - if (favoritesToDelete.length === 0) { - return; - } - - const favoriteIdsToDelete = favoritesToDelete.map( - (favorite) => favorite.id, ); - - const batches: string[][] = []; - - for ( - let i = 0; - i < favoriteIdsToDelete.length; - i += FAVORITE_DELETION_BATCH_SIZE - ) { - batches.push( - favoriteIdsToDelete.slice(i, i + FAVORITE_DELETION_BATCH_SIZE), - ); - } - - for (const batch of batches) { - await favoriteRepository.delete(batch); - } } } diff --git a/packages/twenty-server/src/modules/match-participant/match-participant.service.ts b/packages/twenty-server/src/modules/match-participant/match-participant.service.ts index 98c9f310d6d..7bec88a6aa9 100644 --- a/packages/twenty-server/src/modules/match-participant/match-participant.service.ts +++ b/packages/twenty-server/src/modules/match-participant/match-participant.service.ts @@ -5,7 +5,8 @@ import { isDefined } from 'twenty-shared/utils'; import { Any, In } from 'typeorm'; import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; import { type CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity'; import { addPersonEmailFiltersToQueryBuilder } from 'src/modules/match-participant/utils/add-person-email-filters-to-query-builder'; @@ -59,7 +60,7 @@ export class MatchParticipantService< > { constructor( private readonly workspaceEventEmitter: WorkspaceEventEmitter, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, ) {} private async getParticipantRepository( @@ -67,13 +68,13 @@ export class MatchParticipantService< objectMetadataName: 'messageParticipant' | 'calendarEventParticipant', ) { if (objectMetadataName === 'messageParticipant') { - return await this.twentyORMGlobalManager.getRepositoryForWorkspace( + return await this.globalWorkspaceOrmManager.getRepository( workspaceId, objectMetadataName, ); } - return await this.twentyORMGlobalManager.getRepositoryForWorkspace( + return await this.globalWorkspaceOrmManager.getRepository( workspaceId, objectMetadataName, ); @@ -91,7 +92,7 @@ export class MatchParticipantService< } const personRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( + await this.globalWorkspaceOrmManager.getRepository( workspaceId, 'person', { shouldBypassPermissionChecks: true }, @@ -103,7 +104,7 @@ export class MatchParticipantService< ); const workspaceMemberRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( + await this.globalWorkspaceOrmManager.getRepository( workspaceId, 'workspaceMember', { shouldBypassPermissionChecks: true }, @@ -207,30 +208,38 @@ export class MatchParticipantService< objectMetadataName, workspaceId, }: MatchParticipantsForWorkspaceMembersArgs) { - const participantRepository = await this.getParticipantRepository( - workspaceId, - objectMetadataName, - ); + const authContext = buildSystemAuthContext(workspaceId); - const participants = await participantRepository.find({ - where: { - workspaceMemberId: In(participantMatching.workspaceMemberIds), + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const participantRepository = await this.getParticipantRepository( + workspaceId, + objectMetadataName, + ); + + const participants = await participantRepository.find({ + where: { + workspaceMemberId: In(participantMatching.workspaceMemberIds), + }, + }); + + const tobeRematchedParticipants = participants.map((participant) => { + return { + ...participant, + workspaceMemberId: null, + }; + }); + + await this.matchParticipants({ + matchWith: 'workspaceMemberOnly', + participants: + tobeRematchedParticipants as ParticipantWorkspaceEntity[], + objectMetadataName, + workspaceId, + }); }, - }); - - const tobeRematchedParticipants = participants.map((participant) => { - return { - ...participant, - workspaceMemberId: null, - }; - }); - - await this.matchParticipants({ - matchWith: 'workspaceMemberOnly', - participants: tobeRematchedParticipants as ParticipantWorkspaceEntity[], - objectMetadataName, - workspaceId, - }); + ); } public async matchParticipantsForPeople({ @@ -238,49 +247,58 @@ export class MatchParticipantService< objectMetadataName, workspaceId, }: MatchParticipantsForPeopleArgs) { - const participantRepository = await this.getParticipantRepository( - workspaceId, - objectMetadataName, + const authContext = buildSystemAuthContext(workspaceId); + + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const participantRepository = await this.getParticipantRepository( + workspaceId, + objectMetadataName, + ); + + let participantsMatchingPersonEmails: ParticipantWorkspaceEntity[] = []; + let participantsMatchingPersonId: ParticipantWorkspaceEntity[] = []; + + if (participantMatching.personIds.length > 0) { + participantsMatchingPersonId = (await participantRepository.find({ + where: { + personId: In(participantMatching.personIds), + }, + })) as ParticipantWorkspaceEntity[]; + } + + if (participantMatching.personEmails.length > 0) { + participantsMatchingPersonEmails = (await participantRepository.find({ + where: { + handle: In(participantMatching.personEmails), + }, + })) as ParticipantWorkspaceEntity[]; + } + + const uniqueParticipants = [ + ...new Set([ + ...participantsMatchingPersonId, + ...participantsMatchingPersonEmails, + ]), + ]; + + const tobeRematchedParticipants = uniqueParticipants.map( + (participant) => { + return { + ...participant, + personId: null, + }; + }, + ); + + await this.matchParticipants({ + matchWith: 'personOnly', + participants: tobeRematchedParticipants, + objectMetadataName, + workspaceId, + }); + }, ); - - let participantsMatchingPersonEmails: ParticipantWorkspaceEntity[] = []; - let participantsMatchingPersonId: ParticipantWorkspaceEntity[] = []; - - if (participantMatching.personIds.length > 0) { - participantsMatchingPersonId = (await participantRepository.find({ - where: { - personId: In(participantMatching.personIds), - }, - })) as ParticipantWorkspaceEntity[]; - } - - if (participantMatching.personEmails.length > 0) { - participantsMatchingPersonEmails = (await participantRepository.find({ - where: { - handle: In(participantMatching.personEmails), - }, - })) as ParticipantWorkspaceEntity[]; - } - - const uniqueParticipants = [ - ...new Set([ - ...participantsMatchingPersonId, - ...participantsMatchingPersonEmails, - ]), - ]; - - const tobeRematchedParticipants = uniqueParticipants.map((participant) => { - return { - ...participant, - personId: null, - }; - }); - - await this.matchParticipants({ - matchWith: 'personOnly', - participants: tobeRematchedParticipants, - objectMetadataName, - workspaceId, - }); } } diff --git a/packages/twenty-server/src/modules/messaging/blocklist-manager/jobs/messaging-blocklist-item-delete-messages.job.ts b/packages/twenty-server/src/modules/messaging/blocklist-manager/jobs/messaging-blocklist-item-delete-messages.job.ts index aedc03c2d13..549fe681818 100644 --- a/packages/twenty-server/src/modules/messaging/blocklist-manager/jobs/messaging-blocklist-item-delete-messages.job.ts +++ b/packages/twenty-server/src/modules/messaging/blocklist-manager/jobs/messaging-blocklist-item-delete-messages.job.ts @@ -1,14 +1,15 @@ import { Scope } from '@nestjs/common'; +import { MessageParticipantRole } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { And, Any, ILike, In, Not, Or } from 'typeorm'; -import { MessageParticipantRole } from 'twenty-shared/types'; import { type ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event'; import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator'; import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { type WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type'; import { type BlocklistWorkspaceEntity } from 'src/modules/blocklist/standard-objects/blocklist.workspace-entity'; import { type MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity'; @@ -26,132 +27,141 @@ export type BlocklistItemDeleteMessagesJobData = WorkspaceEventBatch< export class BlocklistItemDeleteMessagesJob { constructor( private readonly threadCleanerService: MessagingMessageCleanerService, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, ) {} @Process(BlocklistItemDeleteMessagesJob.name) async handle(data: BlocklistItemDeleteMessagesJobData): Promise { const workspaceId = data.workspaceId; - const blocklistItemIds = data.events.map( - (eventPayload) => eventPayload.recordId, - ); + const authContext = buildSystemAuthContext(workspaceId); - const blocklistRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'blocklist', - ); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const blocklistItemIds = data.events.map( + (eventPayload) => eventPayload.recordId, + ); - const blocklist = await blocklistRepository.find({ - where: { - id: Any(blocklistItemIds), - }, - }); - - const handlesToDeleteByWorkspaceMemberIdMap = blocklist.reduce( - (acc, blocklistItem) => { - const { handle, workspaceMemberId } = blocklistItem; - - if (!acc.has(workspaceMemberId)) { - acc.set(workspaceMemberId, []); - } - - if (!isDefined(handle)) { - return acc; - } - - acc.get(workspaceMemberId)?.push(handle); - - return acc; - }, - new Map(), - ); - - const messageChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'messageChannel', - ); - - const messageChannelMessageAssociationRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'messageChannelMessageAssociation', - ); - - for (const workspaceMemberId of handlesToDeleteByWorkspaceMemberIdMap.keys()) { - const handles = - handlesToDeleteByWorkspaceMemberIdMap.get(workspaceMemberId); - - if (!handles) { - continue; - } - - const rolesToDelete = [ - MessageParticipantRole.FROM, - MessageParticipantRole.TO, - ] as const; - - const messageChannels = await messageChannelRepository.find({ - select: { - id: true, - handle: true, - connectedAccount: { - handleAliases: true, - }, - }, - where: { - connectedAccount: { - accountOwnerId: workspaceMemberId, - }, - }, - relations: ['connectedAccount'], - }); - - for (const messageChannel of messageChannels) { - const messageChannelHandles = [messageChannel.handle]; - - if (messageChannel.connectedAccount.handleAliases) { - messageChannelHandles.push( - ...messageChannel.connectedAccount.handleAliases.split(','), + const blocklistRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'blocklist', ); - } - const handleConditions = handles.map((handle) => { - const isHandleDomain = handle.startsWith('@'); - - return isHandleDomain - ? { - handle: And( - Or(ILike(`%${handle}`), ILike(`%.${handle.slice(1)}`)), - Not(In(messageChannelHandles)), - ), - role: In(rolesToDelete), - } - : { handle, role: In(rolesToDelete) }; + const blocklist = await blocklistRepository.find({ + where: { + id: Any(blocklistItemIds), + }, }); - const messageChannelMessageAssociationsToDelete = - await messageChannelMessageAssociationRepository.find({ - where: { - messageChannelId: messageChannel.id, - message: { - messageParticipants: handleConditions, + const handlesToDeleteByWorkspaceMemberIdMap = blocklist.reduce( + (acc, blocklistItem) => { + const { handle, workspaceMemberId } = blocklistItem; + + if (!acc.has(workspaceMemberId)) { + acc.set(workspaceMemberId, []); + } + + if (!isDefined(handle)) { + return acc; + } + + acc.get(workspaceMemberId)?.push(handle); + + return acc; + }, + new Map(), + ); + + const messageChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'messageChannel', + ); + + const messageChannelMessageAssociationRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'messageChannelMessageAssociation', + ); + + for (const workspaceMemberId of handlesToDeleteByWorkspaceMemberIdMap.keys()) { + const handles = + handlesToDeleteByWorkspaceMemberIdMap.get(workspaceMemberId); + + if (!handles) { + continue; + } + + const rolesToDelete = [ + MessageParticipantRole.FROM, + MessageParticipantRole.TO, + ] as const; + + const messageChannels = await messageChannelRepository.find({ + select: { + id: true, + handle: true, + connectedAccount: { + handleAliases: true, }, }, + where: { + connectedAccount: { + accountOwnerId: workspaceMemberId, + }, + }, + relations: ['connectedAccount'], }); - if (messageChannelMessageAssociationsToDelete.length === 0) { - continue; + for (const messageChannel of messageChannels) { + const messageChannelHandles = [messageChannel.handle]; + + if (messageChannel.connectedAccount.handleAliases) { + messageChannelHandles.push( + ...messageChannel.connectedAccount.handleAliases.split(','), + ); + } + + const handleConditions = handles.map((handle) => { + const isHandleDomain = handle.startsWith('@'); + + return isHandleDomain + ? { + handle: And( + Or(ILike(`%${handle}`), ILike(`%.${handle.slice(1)}`)), + Not(In(messageChannelHandles)), + ), + role: In(rolesToDelete), + } + : { handle, role: In(rolesToDelete) }; + }); + + const messageChannelMessageAssociationsToDelete = + await messageChannelMessageAssociationRepository.find({ + where: { + messageChannelId: messageChannel.id, + message: { + messageParticipants: handleConditions, + }, + }, + }); + + if (messageChannelMessageAssociationsToDelete.length === 0) { + continue; + } + + await messageChannelMessageAssociationRepository.delete( + messageChannelMessageAssociationsToDelete.map(({ id }) => id), + ); + } } - await messageChannelMessageAssociationRepository.delete( - messageChannelMessageAssociationsToDelete.map(({ id }) => id), + await this.threadCleanerService.cleanOrphanMessagesAndThreads( + workspaceId, ); - } - } - - await this.threadCleanerService.cleanOrphanMessagesAndThreads(workspaceId); + }, + ); } } diff --git a/packages/twenty-server/src/modules/messaging/blocklist-manager/jobs/messaging-blocklist-reimport-messages.job.ts b/packages/twenty-server/src/modules/messaging/blocklist-manager/jobs/messaging-blocklist-reimport-messages.job.ts index 80bcdc31df5..dbb14065009 100644 --- a/packages/twenty-server/src/modules/messaging/blocklist-manager/jobs/messaging-blocklist-reimport-messages.job.ts +++ b/packages/twenty-server/src/modules/messaging/blocklist-manager/jobs/messaging-blocklist-reimport-messages.job.ts @@ -6,7 +6,8 @@ import { type ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emit import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator'; import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { type WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type'; import { type BlocklistWorkspaceEntity } from 'src/modules/blocklist/standard-objects/blocklist.workspace-entity'; import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service'; @@ -25,7 +26,7 @@ export type BlocklistReimportMessagesJobData = WorkspaceEventBatch< }) export class BlocklistReimportMessagesJob { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly messagingChannelSyncStatusService: MessageChannelSyncStatusService, ) {} @@ -33,30 +34,39 @@ export class BlocklistReimportMessagesJob { async handle(data: BlocklistReimportMessagesJobData): Promise { const workspaceId = data.workspaceId; - const messageChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'messageChannel', - ); + const authContext = buildSystemAuthContext(workspaceId); - for (const eventPayload of data.events) { - const workspaceMemberId = - eventPayload.properties.before.workspaceMemberId; + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const messageChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'messageChannel', + ); - const messageChannels = await messageChannelRepository.find({ - select: ['id'], - where: { - connectedAccount: { - accountOwnerId: workspaceMemberId, - }, - syncStage: Not(MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING), - }, - }); + for (const eventPayload of data.events) { + const workspaceMemberId = + eventPayload.properties.before.workspaceMemberId; - await this.messagingChannelSyncStatusService.resetAndMarkAsMessagesListFetchPending( - messageChannels.map((messageChannel) => messageChannel.id), - workspaceId, - ); - } + const messageChannels = await messageChannelRepository.find({ + select: ['id'], + where: { + connectedAccount: { + accountOwnerId: workspaceMemberId, + }, + syncStage: Not( + MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING, + ), + }, + }); + + await this.messagingChannelSyncStatusService.resetAndMarkAsMessagesListFetchPending( + messageChannels.map((messageChannel) => messageChannel.id), + workspaceId, + ); + } + }, + ); } } diff --git a/packages/twenty-server/src/modules/messaging/common/query-hooks/message/apply-messages-visibility-restrictions.service.spec.ts b/packages/twenty-server/src/modules/messaging/common/query-hooks/message/apply-messages-visibility-restrictions.service.spec.ts index 4ec789c9f62..f88b5f75a30 100644 --- a/packages/twenty-server/src/modules/messaging/common/query-hooks/message/apply-messages-visibility-restrictions.service.spec.ts +++ b/packages/twenty-server/src/modules/messaging/common/query-hooks/message/apply-messages-visibility-restrictions.service.spec.ts @@ -2,7 +2,7 @@ import { Test, type TestingModule } from '@nestjs/testing'; import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; import { type MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message.workspace-entity'; @@ -42,20 +42,21 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => { findOneByOrFail: jest.fn(), }; - const mockTwentyORMGlobalManager = { - getRepositoryForWorkspace: jest + const mockGlobalWorkspaceOrmManager = { + getRepository: jest.fn().mockImplementation((workspaceId, name) => { + if (name === 'messageChannelMessageAssociation') { + return mockMessageChannelMessageAssociationRepository; + } + if (name === 'connectedAccount') { + return mockConnectedAccountRepository; + } + if (name === 'workspaceMember') { + return mockWorkspaceMemberRepository; + } + }), + executeInWorkspaceContext: jest .fn() - .mockImplementation((workspaceId, name) => { - if (name === 'messageChannelMessageAssociation') { - return mockMessageChannelMessageAssociationRepository; - } - if (name === 'connectedAccount') { - return mockConnectedAccountRepository; - } - if (name === 'workspaceMember') { - return mockWorkspaceMemberRepository; - } - }), + .mockImplementation((_authContext: any, fn: () => any) => fn()), }; beforeEach(async () => { @@ -63,8 +64,8 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => { providers: [ ApplyMessagesVisibilityRestrictionsService, { - provide: TwentyORMGlobalManager, - useValue: mockTwentyORMGlobalManager, + provide: GlobalWorkspaceOrmManager, + useValue: mockGlobalWorkspaceOrmManager, }, ], }).compile(); diff --git a/packages/twenty-server/src/modules/messaging/common/query-hooks/message/apply-messages-visibility-restrictions.service.ts b/packages/twenty-server/src/modules/messaging/common/query-hooks/message/apply-messages-visibility-restrictions.service.ts index 8a04faf82bd..c098c50d741 100644 --- a/packages/twenty-server/src/modules/messaging/common/query-hooks/message/apply-messages-visibility-restrictions.service.ts +++ b/packages/twenty-server/src/modules/messaging/common/query-hooks/message/apply-messages-visibility-restrictions.service.ts @@ -6,7 +6,8 @@ import { isDefined } from 'twenty-shared/utils'; import { In } from 'typeorm'; import { NotFoundError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { type MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity'; import { MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; @@ -16,105 +17,117 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta @Injectable() export class ApplyMessagesVisibilityRestrictionsService { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, ) {} public async applyMessagesVisibilityRestrictions( messages: MessageWorkspaceEntity[], workspaceId: string, - userId?: string, // undefined when request is made with api key + userId?: string, ) { - const messageChannelMessageAssociationRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'messageChannelMessageAssociation', - ); + const authContext = buildSystemAuthContext(workspaceId); - const messageChannelMessagesAssociations = - await messageChannelMessageAssociationRepository.find({ - where: { - messageId: In(messages.map((message) => message.id)), - }, - relations: ['messageChannel'], - }); + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const messageChannelMessageAssociationRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'messageChannelMessageAssociation', + ); - const connectedAccountRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'connectedAccount', - ); - - const workspaceMemberRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'workspaceMember', - ); - - for (let i = messages.length - 1; i >= 0; i--) { - const messageChannelMessageAssociations = - messageChannelMessagesAssociations.filter( - (association) => association.messageId === messages[i].id, - ); - - const messageChannels = messageChannelMessageAssociations - .map((association) => association.messageChannel) - .filter( - (channel): channel is NonNullable => channel !== null, - ); - - if (messageChannels.length === 0) { - throw new NotFoundError('Associated message channels not found'); - } - - const messageChannelsGroupByVisibility = groupBy( - messageChannels, - (channel) => channel.visibility, - ); - - if ( - messageChannelsGroupByVisibility[ - MessageChannelVisibility.SHARE_EVERYTHING - ] - ) { - continue; - } - - if (isDefined(userId)) { - const workspaceMember = await workspaceMemberRepository.findOneByOrFail( - { - userId, - }, - ); - - const connectedAccounts = await connectedAccountRepository.find({ - select: ['id'], - where: { - messageChannels: { - id: In(messageChannels.map((channel) => channel.id)), + const messageChannelMessagesAssociations = + await messageChannelMessageAssociationRepository.find({ + where: { + messageId: In(messages.map((message) => message.id)), }, - accountOwnerId: workspaceMember.id, - }, - }); + relations: ['messageChannel'], + }); - if (connectedAccounts.length > 0) { - continue; + const connectedAccountRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'connectedAccount', + ); + + const workspaceMemberRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'workspaceMember', + ); + + for (let i = messages.length - 1; i >= 0; i--) { + const messageChannelMessageAssociations = + messageChannelMessagesAssociations.filter( + (association) => association.messageId === messages[i].id, + ); + + const messageChannels = messageChannelMessageAssociations + .map((association) => association.messageChannel) + .filter( + (channel): channel is NonNullable => + channel !== null, + ); + + if (messageChannels.length === 0) { + throw new NotFoundError('Associated message channels not found'); + } + + const messageChannelsGroupByVisibility = groupBy( + messageChannels, + (channel) => channel.visibility, + ); + + if ( + messageChannelsGroupByVisibility[ + MessageChannelVisibility.SHARE_EVERYTHING + ] + ) { + continue; + } + + if (isDefined(userId)) { + const workspaceMember = + await workspaceMemberRepository.findOneByOrFail({ + userId, + }); + + const connectedAccounts = await connectedAccountRepository.find({ + select: ['id'], + where: { + messageChannels: { + id: In(messageChannels.map((channel) => channel.id)), + }, + accountOwnerId: workspaceMember.id, + }, + }); + + if (connectedAccounts.length > 0) { + continue; + } + } + + if ( + messageChannelsGroupByVisibility[MessageChannelVisibility.SUBJECT] + ) { + messages[i].text = FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED; + continue; + } + + if ( + messageChannelsGroupByVisibility[MessageChannelVisibility.METADATA] + ) { + messages[i].subject = + FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED; + messages[i].text = FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED; + continue; + } + + messages.splice(i, 1); } - } - if (messageChannelsGroupByVisibility[MessageChannelVisibility.SUBJECT]) { - messages[i].text = FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED; - continue; - } - - if (messageChannelsGroupByVisibility[MessageChannelVisibility.METADATA]) { - messages[i].subject = FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED; - messages[i].text = FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED; - continue; - } - - messages.splice(i, 1); - } - - return messages; + return messages; + }, + ); } } diff --git a/packages/twenty-server/src/modules/messaging/common/query-hooks/message/message-channel-update-one.pre-query.hook.ts b/packages/twenty-server/src/modules/messaging/common/query-hooks/message/message-channel-update-one.pre-query.hook.ts index 6438f5e5335..3eeb1674900 100644 --- a/packages/twenty-server/src/modules/messaging/common/query-hooks/message/message-channel-update-one.pre-query.hook.ts +++ b/packages/twenty-server/src/modules/messaging/common/query-hooks/message/message-channel-update-one.pre-query.hook.ts @@ -14,7 +14,8 @@ import { } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception'; import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { MessageChannelPendingGroupEmailsAction, MessageChannelSyncStage, @@ -39,7 +40,7 @@ export class MessageChannelUpdateOnePreQueryHook ); constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly messagingProcessGroupEmailActionsService: MessagingProcessGroupEmailActionsService, ) {} @@ -52,89 +53,97 @@ export class MessageChannelUpdateOnePreQueryHook assertIsDefinedOrThrow(workspace, WorkspaceNotFoundDefaultError); - const messageChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspace.id, - 'messageChannel', - ); + const systemAuthContext = buildSystemAuthContext(workspace.id); - const messageChannel = await messageChannelRepository.findOne({ - where: { id: payload.id }, - }); + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + systemAuthContext, + async () => { + const messageChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspace.id, + 'messageChannel', + ); - if (!isDefined(messageChannel)) { - throw new WorkspaceQueryRunnerException( - 'Message channel not found', - WorkspaceQueryRunnerExceptionCode.DATA_NOT_FOUND, - { - userFriendlyMessage: msg`Message channel not found`, - }, - ); - } + const messageChannel = await messageChannelRepository.findOne({ + where: { id: payload.id }, + }); - const isSyncOngoing = ONGOING_SYNC_STAGES.includes( - messageChannel.syncStage, + if (!isDefined(messageChannel)) { + throw new WorkspaceQueryRunnerException( + 'Message channel not found', + WorkspaceQueryRunnerExceptionCode.DATA_NOT_FOUND, + { + userFriendlyMessage: msg`Message channel not found`, + }, + ); + } + + const isSyncOngoing = ONGOING_SYNC_STAGES.includes( + messageChannel.syncStage, + ); + + const messageFolderRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspace.id, + 'messageFolder', + ); + + const messageFoldersWithPendingActionCount = + await messageFolderRepository.count({ + where: { + messageChannelId: messageChannel.id, + pendingSyncAction: Not(MessageFolderPendingSyncAction.NONE), + }, + }); + + const hasPendingFolderActions = + messageFoldersWithPendingActionCount > 0; + + const hasPendingGroupEmailsAction = + messageChannel.pendingGroupEmailsAction !== + MessageChannelPendingGroupEmailsAction.NONE; + + if ( + isSyncOngoing && + (hasPendingFolderActions || hasPendingGroupEmailsAction) + ) { + throw new WorkspaceQueryRunnerException( + 'Cannot update message channel while sync is ongoing with pending actions', + WorkspaceQueryRunnerExceptionCode.INVALID_QUERY_INPUT, + { + userFriendlyMessage: msg`Cannot update message channel while sync is ongoing. Please wait for the sync to complete.`, + }, + ); + } + + const hasCompletedConfiguration = + messageChannel.syncStage !== + MessageChannelSyncStage.PENDING_CONFIGURATION; + + if (!hasCompletedConfiguration) { + this.logger.log( + `MessageChannelId: ${messageChannel.id} - Skipping pending action for message channel in PENDING_CONFIGURATION state`, + ); + + return payload; + } + + const excludeGroupEmailsChanged = + isDefined(payload.data.excludeGroupEmails) && + payload.data.excludeGroupEmails !== messageChannel.excludeGroupEmails; + + if (excludeGroupEmailsChanged) { + await this.messagingProcessGroupEmailActionsService.markMessageChannelAsPendingGroupEmailsAction( + messageChannel, + workspace.id, + payload.data.excludeGroupEmails + ? MessageChannelPendingGroupEmailsAction.GROUP_EMAILS_DELETION + : MessageChannelPendingGroupEmailsAction.GROUP_EMAILS_IMPORT, + ); + } + + return payload; + }, ); - - const messageFolderRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspace.id, - 'messageFolder', - ); - - const messageFoldersWithPendingActionCount = - await messageFolderRepository.count({ - where: { - messageChannelId: messageChannel.id, - pendingSyncAction: Not(MessageFolderPendingSyncAction.NONE), - }, - }); - - const hasPendingFolderActions = messageFoldersWithPendingActionCount > 0; - - const hasPendingGroupEmailsAction = - messageChannel.pendingGroupEmailsAction !== - MessageChannelPendingGroupEmailsAction.NONE; - - if ( - isSyncOngoing && - (hasPendingFolderActions || hasPendingGroupEmailsAction) - ) { - throw new WorkspaceQueryRunnerException( - 'Cannot update message channel while sync is ongoing with pending actions', - WorkspaceQueryRunnerExceptionCode.INVALID_QUERY_INPUT, - { - userFriendlyMessage: msg`Cannot update message channel while sync is ongoing. Please wait for the sync to complete.`, - }, - ); - } - - const hasCompletedConfiguration = - messageChannel.syncStage !== - MessageChannelSyncStage.PENDING_CONFIGURATION; - - if (!hasCompletedConfiguration) { - this.logger.log( - `MessageChannelId: ${messageChannel.id} - Skipping pending action for message channel in PENDING_CONFIGURATION state`, - ); - - return payload; - } - - const excludeGroupEmailsChanged = - isDefined(payload.data.excludeGroupEmails) && - payload.data.excludeGroupEmails !== messageChannel.excludeGroupEmails; - - if (excludeGroupEmailsChanged) { - await this.messagingProcessGroupEmailActionsService.markMessageChannelAsPendingGroupEmailsAction( - messageChannel, - workspace.id, - payload.data.excludeGroupEmails - ? MessageChannelPendingGroupEmailsAction.GROUP_EMAILS_DELETION - : MessageChannelPendingGroupEmailsAction.GROUP_EMAILS_IMPORT, - ); - } - - return payload; } } diff --git a/packages/twenty-server/src/modules/messaging/common/services/message-channel-sync-status.service.ts b/packages/twenty-server/src/modules/messaging/common/services/message-channel-sync-status.service.ts index e2add2eb351..ff9a29e4b2d 100644 --- a/packages/twenty-server/src/modules/messaging/common/services/message-channel-sync-status.service.ts +++ b/packages/twenty-server/src/modules/messaging/common/services/message-channel-sync-status.service.ts @@ -7,7 +7,8 @@ import { CacheStorageService } from 'src/engine/core-modules/cache-storage/servi import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service'; import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service'; import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { AccountsToReconnectKeys } from 'src/modules/connected-account/types/accounts-to-reconnect-key-value.type'; @@ -27,7 +28,7 @@ export class MessageChannelSyncStatusService { constructor( @InjectCacheStorage(CacheStorageNamespace.ModuleMessaging) private readonly cacheStorage: CacheStorageService, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly accountsToReconnectService: AccountsToReconnectService, private readonly metricsService: MetricsService, ) {} @@ -41,16 +42,23 @@ export class MessageChannelSyncStatusService { return; } - const messageChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'messageChannel', - ); + const authContext = buildSystemAuthContext(workspaceId); - await messageChannelRepository.update(messageChannelIds, { - syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING, - ...(!preserveSyncStageStartedAt ? { syncStageStartedAt: null } : {}), - }); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const messageChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'messageChannel', + ); + + await messageChannelRepository.update(messageChannelIds, { + syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING, + ...(!preserveSyncStageStartedAt ? { syncStageStartedAt: null } : {}), + }); + }, + ); } public async markAsMessagesImportPending( @@ -62,16 +70,23 @@ export class MessageChannelSyncStatusService { return; } - const messageChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'messageChannel', - ); + const authContext = buildSystemAuthContext(workspaceId); - await messageChannelRepository.update(messageChannelIds, { - syncStage: MessageChannelSyncStage.MESSAGES_IMPORT_PENDING, - ...(!preserveSyncStageStartedAt ? { syncStageStartedAt: null } : {}), - }); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const messageChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'messageChannel', + ); + + await messageChannelRepository.update(messageChannelIds, { + syncStage: MessageChannelSyncStage.MESSAGES_IMPORT_PENDING, + ...(!preserveSyncStageStartedAt ? { syncStageStartedAt: null } : {}), + }); + }, + ); } public async resetAndMarkAsMessagesListFetchPending( @@ -88,30 +103,37 @@ export class MessageChannelSyncStatusService { ); } - const messageChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'messageChannel', - ); + const authContext = buildSystemAuthContext(workspaceId); - const messageFolderRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'messageFolder', - ); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const messageChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'messageChannel', + ); - await messageChannelRepository.update(messageChannelIds, { - syncCursor: '', - syncStageStartedAt: null, - throttleFailureCount: 0, - pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction.NONE, - }); + const messageFolderRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'messageFolder', + ); - await messageFolderRepository.update( - { messageChannelId: In(messageChannelIds) }, - { - syncCursor: '', - pendingSyncAction: MessageFolderPendingSyncAction.NONE, + await messageChannelRepository.update(messageChannelIds, { + syncCursor: '', + syncStageStartedAt: null, + throttleFailureCount: 0, + pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction.NONE, + }); + + await messageFolderRepository.update( + { messageChannelId: In(messageChannelIds) }, + { + syncCursor: '', + pendingSyncAction: MessageFolderPendingSyncAction.NONE, + }, + ); }, ); @@ -126,15 +148,22 @@ export class MessageChannelSyncStatusService { return; } - const messageChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'messageChannel', - ); + const authContext = buildSystemAuthContext(workspaceId); - await messageChannelRepository.update(messageChannelIds, { - syncStageStartedAt: null, - }); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const messageChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'messageChannel', + ); + + await messageChannelRepository.update(messageChannelIds, { + syncStageStartedAt: null, + }); + }, + ); } public async markAsMessagesListFetchScheduled( @@ -145,17 +174,24 @@ export class MessageChannelSyncStatusService { return; } - const messageChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'messageChannel', - ); + const authContext = buildSystemAuthContext(workspaceId); - await messageChannelRepository.update(messageChannelIds, { - syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_SCHEDULED, - syncStatus: MessageChannelSyncStatus.ONGOING, - syncStageStartedAt: new Date().toISOString(), - }); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const messageChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'messageChannel', + ); + + await messageChannelRepository.update(messageChannelIds, { + syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_SCHEDULED, + syncStatus: MessageChannelSyncStatus.ONGOING, + syncStageStartedAt: new Date().toISOString(), + }); + }, + ); } public async markAsMessagesListFetchOngoing( @@ -166,16 +202,23 @@ export class MessageChannelSyncStatusService { return; } - const messageChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'messageChannel', - ); + const authContext = buildSystemAuthContext(workspaceId); - await messageChannelRepository.update(messageChannelIds, { - syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_ONGOING, - syncStatus: MessageChannelSyncStatus.ONGOING, - }); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const messageChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'messageChannel', + ); + + await messageChannelRepository.update(messageChannelIds, { + syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_ONGOING, + syncStatus: MessageChannelSyncStatus.ONGOING, + }); + }, + ); } public async markAsCompletedAndMarkAsMessagesListFetchPending( @@ -186,19 +229,26 @@ export class MessageChannelSyncStatusService { return; } - const messageChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'messageChannel', - ); + const authContext = buildSystemAuthContext(workspaceId); - await messageChannelRepository.update(messageChannelIds, { - syncStatus: MessageChannelSyncStatus.ACTIVE, - syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING, - throttleFailureCount: 0, - syncStageStartedAt: null, - syncedAt: new Date().toISOString(), - }); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const messageChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'messageChannel', + ); + + await messageChannelRepository.update(messageChannelIds, { + syncStatus: MessageChannelSyncStatus.ACTIVE, + syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING, + throttleFailureCount: 0, + syncStageStartedAt: null, + syncedAt: new Date().toISOString(), + }); + }, + ); await this.metricsService.batchIncrementCounter({ key: MetricsKeys.MessageChannelSyncJobActive, @@ -214,15 +264,22 @@ export class MessageChannelSyncStatusService { return; } - const messageChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'messageChannel', - ); + const authContext = buildSystemAuthContext(workspaceId); - await messageChannelRepository.update(messageChannelIds, { - syncStage: MessageChannelSyncStage.MESSAGES_IMPORT_SCHEDULED, - }); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const messageChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'messageChannel', + ); + + await messageChannelRepository.update(messageChannelIds, { + syncStage: MessageChannelSyncStage.MESSAGES_IMPORT_SCHEDULED, + }); + }, + ); } public async markAsMessagesImportOngoing( @@ -233,16 +290,23 @@ export class MessageChannelSyncStatusService { return; } - const messageChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'messageChannel', - ); + const authContext = buildSystemAuthContext(workspaceId); - await messageChannelRepository.update(messageChannelIds, { - syncStage: MessageChannelSyncStage.MESSAGES_IMPORT_ONGOING, - syncStageStartedAt: new Date().toISOString(), - }); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const messageChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'messageChannel', + ); + + await messageChannelRepository.update(messageChannelIds, { + syncStage: MessageChannelSyncStage.MESSAGES_IMPORT_ONGOING, + syncStageStartedAt: new Date().toISOString(), + }); + }, + ); } public async markAsFailed( @@ -256,57 +320,66 @@ export class MessageChannelSyncStatusService { return; } - const messageChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'messageChannel', - ); + const authContext = buildSystemAuthContext(workspaceId); - await messageChannelRepository.update(messageChannelIds, { - syncStage: MessageChannelSyncStage.FAILED, - syncStatus: syncStatus, - }); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const messageChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'messageChannel', + ); - const metricsKey = - syncStatus === MessageChannelSyncStatus.FAILED_INSUFFICIENT_PERMISSIONS - ? MetricsKeys.MessageChannelSyncJobFailedInsufficientPermissions - : MetricsKeys.MessageChannelSyncJobFailedUnknown; + await messageChannelRepository.update(messageChannelIds, { + syncStage: MessageChannelSyncStage.FAILED, + syncStatus: syncStatus, + }); - await this.metricsService.batchIncrementCounter({ - key: metricsKey, - eventIds: messageChannelIds, - }); + const metricsKey = + syncStatus === + MessageChannelSyncStatus.FAILED_INSUFFICIENT_PERMISSIONS + ? MetricsKeys.MessageChannelSyncJobFailedInsufficientPermissions + : MetricsKeys.MessageChannelSyncJobFailedUnknown; - if ( - syncStatus === MessageChannelSyncStatus.FAILED_INSUFFICIENT_PERMISSIONS - ) { - const connectedAccountRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'connectedAccount', - ); + await this.metricsService.batchIncrementCounter({ + key: metricsKey, + eventIds: messageChannelIds, + }); - const messageChannels = await messageChannelRepository.find({ - select: ['id', 'connectedAccountId'], - where: { id: Any(messageChannelIds) }, - }); + if ( + syncStatus === + MessageChannelSyncStatus.FAILED_INSUFFICIENT_PERMISSIONS + ) { + const connectedAccountRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'connectedAccount', + ); - const connectedAccountIds = messageChannels.map( - (messageChannel) => messageChannel.connectedAccountId, - ); + const messageChannels = await messageChannelRepository.find({ + select: ['id', 'connectedAccountId'], + where: { id: Any(messageChannelIds) }, + }); - await connectedAccountRepository.update( - { id: Any(connectedAccountIds) }, - { - authFailedAt: new Date(), - }, - ); + const connectedAccountIds = messageChannels.map( + (messageChannel) => messageChannel.connectedAccountId, + ); - await this.addToAccountsToReconnect( - messageChannels.map((messageChannel) => messageChannel.id), - workspaceId, - ); - } + await connectedAccountRepository.update( + { id: Any(connectedAccountIds) }, + { + authFailedAt: new Date(), + }, + ); + + await this.addToAccountsToReconnect( + messageChannels.map((messageChannel) => messageChannel.id), + workspaceId, + ); + } + }, + ); } private async addToAccountsToReconnect( @@ -318,7 +391,7 @@ export class MessageChannelSyncStatusService { } const messageChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( + await this.globalWorkspaceOrmManager.getRepository( workspaceId, 'messageChannel', ); diff --git a/packages/twenty-server/src/modules/messaging/message-cleaner/commands/messaging-message-clearner-remove-orphans.command.ts b/packages/twenty-server/src/modules/messaging/message-cleaner/commands/messaging-message-clearner-remove-orphans.command.ts index 93ed34f6c16..4234c3566dc 100644 --- a/packages/twenty-server/src/modules/messaging/message-cleaner/commands/messaging-message-clearner-remove-orphans.command.ts +++ b/packages/twenty-server/src/modules/messaging/message-cleaner/commands/messaging-message-clearner-remove-orphans.command.ts @@ -7,7 +7,7 @@ import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/ import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { MessagingMessageCleanerService } from 'src/modules/messaging/message-cleaner/services/messaging-message-cleaner.service'; @Command({ @@ -18,11 +18,11 @@ export class MessagingMessageCleanerRemoveOrphansCommand extends ActiveOrSuspend constructor( @InjectRepository(WorkspaceEntity) protected readonly workspaceRepository: Repository, - protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly messagingMessageCleanerService: MessagingMessageCleanerService, protected readonly dataSourceService: DataSourceService, ) { - super(workspaceRepository, twentyORMGlobalManager, dataSourceService); + super(workspaceRepository, globalWorkspaceOrmManager, dataSourceService); } override async runOnWorkspace({ diff --git a/packages/twenty-server/src/modules/messaging/message-cleaner/commands/messaging-reset-channel.command.ts b/packages/twenty-server/src/modules/messaging/message-cleaner/commands/messaging-reset-channel.command.ts index 27387315619..62f2bb5ad16 100644 --- a/packages/twenty-server/src/modules/messaging/message-cleaner/commands/messaging-reset-channel.command.ts +++ b/packages/twenty-server/src/modules/messaging/message-cleaner/commands/messaging-reset-channel.command.ts @@ -3,7 +3,8 @@ import { Logger } from '@nestjs/common'; import { Command, CommandRunner, Option } from 'nest-commander'; import { isDefined } from 'twenty-shared/utils'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service'; import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; import { MessagingMessageCleanerService } from 'src/modules/messaging/message-cleaner/services/messaging-message-cleaner.service'; @@ -22,7 +23,7 @@ export class MessagingResetChannelCommand extends CommandRunner { private readonly logger = new Logger(MessagingResetChannelCommand.name); constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly messagingChannelSyncStatusService: MessageChannelSyncStatusService, private readonly messagingMessageCleanerService: MessagingMessageCleanerService, ) { @@ -35,44 +36,53 @@ export class MessagingResetChannelCommand extends CommandRunner { ): Promise { const { workspaceId, messageChannelId } = options; - const messageChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'messageChannel', - ); + const authContext = buildSystemAuthContext(workspaceId); - this.logger.log( - `No message channel ID provided, resetting all message channels in workspace ${workspaceId}`, - ); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const messageChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'messageChannel', + ); - const messageChannels = await messageChannelRepository.find({ - where: { - ...(isDefined(messageChannelId) ? { id: messageChannelId } : {}), + this.logger.log( + `No message channel ID provided, resetting all message channels in workspace ${workspaceId}`, + ); + + const messageChannels = await messageChannelRepository.find({ + where: { + ...(isDefined(messageChannelId) ? { id: messageChannelId } : {}), + }, + }); + + if (messageChannels.length === 0) { + this.logger.log( + `No message channels found in workspace ${workspaceId}`, + ); + + return; + } + + this.logger.log( + `Found ${messageChannels.length} message channels to reset`, + ); + + for (const messageChannel of messageChannels) { + await this.messagingChannelSyncStatusService.resetAndMarkAsMessagesListFetchPending( + [messageChannel.id], + workspaceId, + ); + await this.messagingMessageCleanerService.cleanOrphanMessagesAndThreads( + workspaceId, + ); + } + + this.logger.log( + `Successfully reset all ${messageChannels.length} message channels in workspace ${workspaceId}`, + ); }, - }); - - if (messageChannels.length === 0) { - this.logger.log(`No message channels found in workspace ${workspaceId}`); - - return; - } - - this.logger.log( - `Found ${messageChannels.length} message channels to reset`, - ); - - for (const messageChannel of messageChannels) { - await this.messagingChannelSyncStatusService.resetAndMarkAsMessagesListFetchPending( - [messageChannel.id], - workspaceId, - ); - await this.messagingMessageCleanerService.cleanOrphanMessagesAndThreads( - workspaceId, - ); - } - - this.logger.log( - `Successfully reset all ${messageChannels.length} message channels in workspace ${workspaceId}`, ); } diff --git a/packages/twenty-server/src/modules/messaging/message-cleaner/services/messaging-message-cleaner.service.ts b/packages/twenty-server/src/modules/messaging/message-cleaner/services/messaging-message-cleaner.service.ts index 173333fa27d..cdf545c182e 100644 --- a/packages/twenty-server/src/modules/messaging/message-cleaner/services/messaging-message-cleaner.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-cleaner/services/messaging-message-cleaner.service.ts @@ -4,7 +4,8 @@ import chunk from 'lodash.chunk'; import { In, IsNull } from 'typeorm'; import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity'; import { type MessageThreadWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-thread.workspace-entity'; import { type MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message.workspace-entity'; @@ -14,7 +15,7 @@ import { deleteUsingPagination } from 'src/modules/messaging/message-cleaner/uti export class MessagingMessageCleanerService { private readonly logger = new Logger(MessagingMessageCleanerService.name); constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, ) {} async deleteMessagesChannelMessageAssociationsAndRelatedOrphans({ @@ -26,183 +27,199 @@ export class MessagingMessageCleanerService { messageExternalIds: string[]; messageChannelId: string; }) { - const messageRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'message', - ); + const authContext = buildSystemAuthContext(workspaceId); - const messageChannelMessageAssociationRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'messageChannelMessageAssociation', - ); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const messageRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'message', + ); - const messageThreadRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'messageThread', - ); + const messageChannelMessageAssociationRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'messageChannelMessageAssociation', + ); - const messageExternalIdsChunks = chunk(messageExternalIds, 500); + const messageThreadRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'messageThread', + ); - for (const messageExternalIdsChunk of messageExternalIdsChunks) { - const messageChannelMessageAssociationsToDelete = - await messageChannelMessageAssociationRepository.find({ - where: { - messageExternalId: In(messageExternalIdsChunk), - messageChannelId, - }, - }); + const messageExternalIdsChunks = chunk(messageExternalIds, 500); - if (messageChannelMessageAssociationsToDelete.length <= 0) { - continue; - } + for (const messageExternalIdsChunk of messageExternalIdsChunks) { + const messageChannelMessageAssociationsToDelete = + await messageChannelMessageAssociationRepository.find({ + where: { + messageExternalId: In(messageExternalIdsChunk), + messageChannelId, + }, + }); - await messageChannelMessageAssociationRepository.delete( - messageChannelMessageAssociationsToDelete.map(({ id }) => id), - ); + if (messageChannelMessageAssociationsToDelete.length <= 0) { + continue; + } - this.logger.log( - `WorkspaceId: ${workspaceId} Deleting ${messageChannelMessageAssociationsToDelete.length} message channel message associations`, - ); + await messageChannelMessageAssociationRepository.delete( + messageChannelMessageAssociationsToDelete.map(({ id }) => id), + ); - const orphanMessages = await messageRepository.find({ - where: { - id: In( - messageChannelMessageAssociationsToDelete.map( - ({ messageId }) => messageId, - ), - ), - messageChannelMessageAssociations: { - id: IsNull(), - }, - }, - }); + this.logger.log( + `WorkspaceId: ${workspaceId} Deleting ${messageChannelMessageAssociationsToDelete.length} message channel message associations`, + ); - if (orphanMessages.length <= 0) { - continue; - } + const orphanMessages = await messageRepository.find({ + where: { + id: In( + messageChannelMessageAssociationsToDelete.map( + ({ messageId }) => messageId, + ), + ), + messageChannelMessageAssociations: { + id: IsNull(), + }, + }, + }); - this.logger.log( - `WorkspaceId: ${workspaceId} Deleting ${orphanMessages.length} orphan messages`, - ); + if (orphanMessages.length <= 0) { + continue; + } - await messageRepository.delete(orphanMessages.map(({ id }) => id)); + this.logger.log( + `WorkspaceId: ${workspaceId} Deleting ${orphanMessages.length} orphan messages`, + ); - const orphanMessageThreads = await messageThreadRepository.find({ - where: { - id: In(orphanMessages.map(({ messageThreadId }) => messageThreadId)), - messages: { - id: IsNull(), - }, - }, - }); + await messageRepository.delete(orphanMessages.map(({ id }) => id)); - if (orphanMessageThreads.length <= 0) { - continue; - } + const orphanMessageThreads = await messageThreadRepository.find({ + where: { + id: In( + orphanMessages.map(({ messageThreadId }) => messageThreadId), + ), + messages: { + id: IsNull(), + }, + }, + }); - this.logger.log( - `WorkspaceId: ${workspaceId} Deleting ${orphanMessageThreads.length} orphan message threads`, - ); + if (orphanMessageThreads.length <= 0) { + continue; + } - await messageThreadRepository.delete( - orphanMessageThreads.map(({ id }) => id), - ); - } + this.logger.log( + `WorkspaceId: ${workspaceId} Deleting ${orphanMessageThreads.length} orphan message threads`, + ); + + await messageThreadRepository.delete( + orphanMessageThreads.map(({ id }) => id), + ); + } + }, + ); } public async cleanOrphanMessagesAndThreads(workspaceId: string) { - const messageThreadRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'messageThread', - ); + const authContext = buildSystemAuthContext(workspaceId); - const messageRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'message', - ); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const messageThreadRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'messageThread', + ); - const workspaceDataSource = - await this.twentyORMGlobalManager.getDataSourceForWorkspace({ - workspaceId, - }); + const messageRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'message', + ); - await workspaceDataSource.transaction( - async (transactionManager: WorkspaceEntityManager) => { - await deleteUsingPagination( - workspaceId, - 500, - async ( - limit: number, - offset: number, - _workspaceId: string, - transactionManager: WorkspaceEntityManager, - ) => { - const nonAssociatedMessages = await messageRepository.find( - { - where: { - messageChannelMessageAssociations: { - id: IsNull(), + const workspaceDataSource = + await this.globalWorkspaceOrmManager.getDataSourceForWorkspace( + workspaceId, + ); + + await workspaceDataSource.transaction( + async (transactionManager: WorkspaceEntityManager) => { + await deleteUsingPagination( + workspaceId, + 500, + async ( + limit: number, + offset: number, + _workspaceId: string, + transactionManager: WorkspaceEntityManager, + ) => { + const nonAssociatedMessages = await messageRepository.find( + { + where: { + messageChannelMessageAssociations: { + id: IsNull(), + }, + }, + take: limit, + skip: offset, + relations: ['messageChannelMessageAssociations'], }, - }, - take: limit, - skip: offset, - relations: ['messageChannelMessageAssociations'], + transactionManager, + ); + + return nonAssociatedMessages.map(({ id }) => id); + }, + async ( + ids: string[], + workspaceId: string, + transactionManager?: WorkspaceEntityManager, + ) => { + this.logger.log( + `WorkspaceId: ${workspaceId} Deleting ${ids.length} messages from message cleaner`, + ); + await messageRepository.delete(ids, transactionManager); }, transactionManager, ); - return nonAssociatedMessages.map(({ id }) => id); - }, - async ( - ids: string[], - workspaceId: string, - transactionManager?: WorkspaceEntityManager, - ) => { - this.logger.log( - `WorkspaceId: ${workspaceId} Deleting ${ids.length} messages from message cleaner`, - ); - await messageRepository.delete(ids, transactionManager); - }, - transactionManager, - ); - - await deleteUsingPagination( - workspaceId, - 500, - async ( - limit: number, - offset: number, - _workspaceId: string, - transactionManager?: WorkspaceEntityManager, - ) => { - const orphanThreads = await messageThreadRepository.find( - { - where: { - messages: { - id: IsNull(), + await deleteUsingPagination( + workspaceId, + 500, + async ( + limit: number, + offset: number, + _workspaceId: string, + transactionManager?: WorkspaceEntityManager, + ) => { + const orphanThreads = await messageThreadRepository.find( + { + where: { + messages: { + id: IsNull(), + }, + }, + take: limit, + skip: offset, }, - }, - take: limit, - skip: offset, + transactionManager, + ); + + return orphanThreads.map(({ id }) => id); + }, + async ( + ids: string[], + _workspaceId: string, + transactionManager?: WorkspaceEntityManager, + ) => { + await messageThreadRepository.delete(ids, transactionManager); }, transactionManager, ); - - return orphanThreads.map(({ id }) => id); }, - async ( - ids: string[], - _workspaceId: string, - transactionManager?: WorkspaceEntityManager, - ) => { - await messageThreadRepository.delete(ids, transactionManager); - }, - transactionManager, ); }, ); diff --git a/packages/twenty-server/src/modules/messaging/message-folder-manager/query-hooks/message-folder-update-one.pre-query.hook.ts b/packages/twenty-server/src/modules/messaging/message-folder-manager/query-hooks/message-folder-update-one.pre-query.hook.ts index 57679e67440..7419f0d624a 100644 --- a/packages/twenty-server/src/modules/messaging/message-folder-manager/query-hooks/message-folder-update-one.pre-query.hook.ts +++ b/packages/twenty-server/src/modules/messaging/message-folder-manager/query-hooks/message-folder-update-one.pre-query.hook.ts @@ -12,7 +12,8 @@ import { } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception'; import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { MessageFolderImportPolicy, type MessageChannelWorkspaceEntity, @@ -24,7 +25,7 @@ export class MessageFolderUpdateOnePreQueryHook implements WorkspacePreQueryHookInstance { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, ) {} async execute( @@ -36,68 +37,75 @@ export class MessageFolderUpdateOnePreQueryHook assertIsDefinedOrThrow(workspace, WorkspaceNotFoundDefaultError); - const messageFolderRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspace.id, - 'messageFolder', - ); + const systemAuthContext = buildSystemAuthContext(workspace.id); - const messageFolder = await messageFolderRepository.findOne({ - where: { id: payload.id }, - }); + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + systemAuthContext, + async () => { + const messageFolderRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspace.id, + 'messageFolder', + ); - if (!messageFolder) { - throw new WorkspaceQueryRunnerException( - 'Message folder not found', - WorkspaceQueryRunnerExceptionCode.DATA_NOT_FOUND, - { - userFriendlyMessage: msg`Message folder not found`, - }, - ); - } + const messageFolder = await messageFolderRepository.findOne({ + where: { id: payload.id }, + }); - if (payload.data.isSynced !== false) { - return payload; - } + if (!messageFolder) { + throw new WorkspaceQueryRunnerException( + 'Message folder not found', + WorkspaceQueryRunnerExceptionCode.DATA_NOT_FOUND, + { + userFriendlyMessage: msg`Message folder not found`, + }, + ); + } - const messageChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspace.id, - 'messageChannel', - ); + if (payload.data.isSynced !== false) { + return payload; + } - const messageChannel = await messageChannelRepository.findOne({ - where: { id: messageFolder.messageChannelId }, - }); + const messageChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspace.id, + 'messageChannel', + ); - if ( - messageChannel?.messageFolderImportPolicy !== - MessageFolderImportPolicy.SELECTED_FOLDERS - ) { - return payload; - } + const messageChannel = await messageChannelRepository.findOne({ + where: { id: messageFolder.messageChannelId }, + }); - const syncedFoldersCount = await messageFolderRepository.count({ - where: { - messageChannelId: messageFolder.messageChannelId, - isSynced: true, + if ( + messageChannel?.messageFolderImportPolicy !== + MessageFolderImportPolicy.SELECTED_FOLDERS + ) { + return payload; + } + + const syncedFoldersCount = await messageFolderRepository.count({ + where: { + messageChannelId: messageFolder.messageChannelId, + isSynced: true, + }, + }); + + if ( + isDefined(syncedFoldersCount) && + isNumber(syncedFoldersCount) && + syncedFoldersCount <= 1 + ) { + throw new WorkspaceQueryRunnerException( + 'Cannot unsync the last folder when folder import policy is set to selected folders', + WorkspaceQueryRunnerExceptionCode.INVALID_QUERY_INPUT, + { + userFriendlyMessage: msg`At least one folder must be synced.`, + }, + ); + } + + return payload; }, - }); - - if ( - isDefined(syncedFoldersCount) && - isNumber(syncedFoldersCount) && - syncedFoldersCount <= 1 - ) { - throw new WorkspaceQueryRunnerException( - 'Cannot unsync the last folder when folder import policy is set to selected folders', - WorkspaceQueryRunnerExceptionCode.INVALID_QUERY_INPUT, - { - userFriendlyMessage: msg`At least one folder must be synced.`, - }, - ); - } - - return payload; + ); } } diff --git a/packages/twenty-server/src/modules/messaging/message-folder-manager/services/sync-message-folders.service.ts b/packages/twenty-server/src/modules/messaging/message-folder-manager/services/sync-message-folders.service.ts index 1167808d934..11e632a4671 100644 --- a/packages/twenty-server/src/modules/messaging/message-folder-manager/services/sync-message-folders.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-folder-manager/services/sync-message-folders.service.ts @@ -9,8 +9,9 @@ import { v4 } from 'uuid'; import { MessageFolder } from 'src/modules/messaging/message-folder-manager/interfaces/message-folder-driver.interface'; import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; import { MessageFolderPendingSyncAction, @@ -52,7 +53,7 @@ type MessageFolderToUpdate = Partial< @Injectable() export class SyncMessageFoldersService { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly gmailGetAllFoldersService: GmailGetAllFoldersService, private readonly microsoftGetAllFoldersService: MicrosoftGetAllFoldersService, private readonly imapGetAllFoldersService: ImapGetAllFoldersService, @@ -61,17 +62,24 @@ export class SyncMessageFoldersService { async syncMessageFolders(input: SyncMessageFoldersInput): Promise { const { workspaceId, messageChannel, manager } = input; - const folders = await this.discoverAllFolders( - messageChannel.connectedAccount, - messageChannel, - ); + const authContext = buildSystemAuthContext(workspaceId); - await this.upsertDiscoveredFolders({ - workspaceId, - messageChannelId: messageChannel.id, - folders, - manager, - }); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const folders = await this.discoverAllFolders( + messageChannel.connectedAccount, + messageChannel, + ); + + await this.upsertDiscoveredFolders({ + workspaceId, + messageChannelId: messageChannel.id, + folders, + manager, + }); + }, + ); } private async upsertDiscoveredFolders({ @@ -86,7 +94,7 @@ export class SyncMessageFoldersService { manager: WorkspaceEntityManager; }): Promise { const messageFolderRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( + await this.globalWorkspaceOrmManager.getRepository( workspaceId, 'messageFolder', ); diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job.ts index 32ad89c0daa..632ef76cd00 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job.ts @@ -3,7 +3,8 @@ import { Scope } from '@nestjs/common'; import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator'; import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { isThrottled } from 'src/modules/connected-account/utils/is-throttled'; import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service'; import { @@ -30,7 +31,7 @@ export class MessagingMessageListFetchJob { constructor( private readonly messagingMessageListFetchService: MessagingMessageListFetchService, private readonly messagingMonitoringService: MessagingMonitoringService, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly messageImportErrorHandlerService: MessageImportExceptionHandlerService, private readonly messageChannelSyncStatusService: MessageChannelSyncStatusService, ) {} @@ -45,77 +46,84 @@ export class MessagingMessageListFetchJob { workspaceId, }); - const messageChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'messageChannel', - ); + const authContext = buildSystemAuthContext(workspaceId); - const messageChannel = await messageChannelRepository.findOne({ - where: { - id: messageChannelId, + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const messageChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'messageChannel', + ); + + const messageChannel = await messageChannelRepository.findOne({ + where: { + id: messageChannelId, + }, + relations: ['connectedAccount', 'messageFolders'], + }); + + if (!messageChannel) { + await this.messagingMonitoringService.track({ + eventName: 'message_list_fetch_job.error.message_channel_not_found', + messageChannelId, + workspaceId, + }); + + return; + } + + if ( + messageChannel.syncStage !== + MessageChannelSyncStage.MESSAGE_LIST_FETCH_SCHEDULED + ) { + return; + } + + try { + if ( + isThrottled( + messageChannel.syncStageStartedAt, + messageChannel.throttleFailureCount, + ) + ) { + await this.messageChannelSyncStatusService.markAsMessagesListFetchPending( + [messageChannel.id], + workspaceId, + true, + ); + + return; + } + + await this.messagingMonitoringService.track({ + eventName: 'message_list_fetch.started', + workspaceId, + connectedAccountId: messageChannel.connectedAccount.id, + messageChannelId: messageChannel.id, + }); + + await this.messagingMessageListFetchService.processMessageListFetch( + messageChannel, + workspaceId, + ); + + await this.messagingMonitoringService.track({ + eventName: 'message_list_fetch.completed', + workspaceId, + connectedAccountId: messageChannel.connectedAccount.id, + messageChannelId: messageChannel.id, + }); + } catch (error) { + await this.messageImportErrorHandlerService.handleDriverException( + error, + MessageImportSyncStep.MESSAGE_LIST_FETCH, + messageChannel, + workspaceId, + ); + } }, - relations: ['connectedAccount', 'messageFolders'], - }); - - if (!messageChannel) { - await this.messagingMonitoringService.track({ - eventName: 'message_list_fetch_job.error.message_channel_not_found', - messageChannelId, - workspaceId, - }); - - return; - } - - if ( - messageChannel.syncStage !== - MessageChannelSyncStage.MESSAGE_LIST_FETCH_SCHEDULED - ) { - return; - } - - try { - if ( - isThrottled( - messageChannel.syncStageStartedAt, - messageChannel.throttleFailureCount, - ) - ) { - await this.messageChannelSyncStatusService.markAsMessagesListFetchPending( - [messageChannel.id], - workspaceId, - true, - ); - - return; - } - - await this.messagingMonitoringService.track({ - eventName: 'message_list_fetch.started', - workspaceId, - connectedAccountId: messageChannel.connectedAccount.id, - messageChannelId: messageChannel.id, - }); - - await this.messagingMessageListFetchService.processMessageListFetch( - messageChannel, - workspaceId, - ); - - await this.messagingMonitoringService.track({ - eventName: 'message_list_fetch.completed', - workspaceId, - connectedAccountId: messageChannel.connectedAccount.id, - messageChannelId: messageChannel.id, - }); - } catch (error) { - await this.messageImportErrorHandlerService.handleDriverException( - error, - MessageImportSyncStep.MESSAGE_LIST_FETCH, - messageChannel, - workspaceId, - ); - } + ); } } diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-messages-import.job.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-messages-import.job.ts index f04d711f869..fd9bce545a4 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-messages-import.job.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-messages-import.job.ts @@ -3,7 +3,8 @@ import { Scope } from '@nestjs/common'; import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator'; import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { isThrottled } from 'src/modules/connected-account/utils/is-throttled'; import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service'; import { @@ -12,6 +13,7 @@ import { } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; import { MessagingMessagesImportService } from 'src/modules/messaging/message-import-manager/services/messaging-messages-import.service'; import { MessagingMonitoringService } from 'src/modules/messaging/monitoring/services/messaging-monitoring.service'; + export type MessagingMessagesImportJobData = { messageChannelId: string; workspaceId: string; @@ -26,7 +28,7 @@ export class MessagingMessagesImportJob { private readonly messagingMessagesImportService: MessagingMessagesImportService, private readonly messagingMonitoringService: MessagingMonitoringService, private readonly messageChannelSyncStatusService: MessageChannelSyncStatusService, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, ) {} @Process(MessagingMessagesImportJob.name) @@ -39,59 +41,66 @@ export class MessagingMessagesImportJob { messageChannelId, }); - const messageChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'messageChannel', - ); + const authContext = buildSystemAuthContext(workspaceId); - const messageChannel = await messageChannelRepository.findOne({ - where: { - id: messageChannelId, + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const messageChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'messageChannel', + ); + + const messageChannel = await messageChannelRepository.findOne({ + where: { + id: messageChannelId, + }, + relations: ['connectedAccount'], + }); + + if (!messageChannel) { + await this.messagingMonitoringService.track({ + eventName: 'messages_import.error.message_channel_not_found', + messageChannelId, + workspaceId, + }); + + return; + } + + if (!messageChannel?.isSyncEnabled) { + return; + } + + if ( + messageChannel.syncStage !== + MessageChannelSyncStage.MESSAGES_IMPORT_SCHEDULED + ) { + return; + } + + if ( + isThrottled( + messageChannel.syncStageStartedAt, + messageChannel.throttleFailureCount, + ) + ) { + await this.messageChannelSyncStatusService.markAsMessagesImportPending( + [messageChannel.id], + workspaceId, + true, + ); + + return; + } + + await this.messagingMessagesImportService.processMessageBatchImport( + messageChannel, + messageChannel.connectedAccount, + workspaceId, + ); }, - relations: ['connectedAccount'], - }); - - if (!messageChannel) { - await this.messagingMonitoringService.track({ - eventName: 'messages_import.error.message_channel_not_found', - messageChannelId, - workspaceId, - }); - - return; - } - - if (!messageChannel?.isSyncEnabled) { - return; - } - - if ( - messageChannel.syncStage !== - MessageChannelSyncStage.MESSAGES_IMPORT_SCHEDULED - ) { - return; - } - - if ( - isThrottled( - messageChannel.syncStageStartedAt, - messageChannel.throttleFailureCount, - ) - ) { - await this.messageChannelSyncStatusService.markAsMessagesImportPending( - [messageChannel.id], - workspaceId, - true, - ); - - return; - } - - await this.messagingMessagesImportService.processMessageBatchImport( - messageChannel, - messageChannel.connectedAccount, - workspaceId, ); } } diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-ongoing-stale.job.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-ongoing-stale.job.ts index 8886cb85016..f013a3942db 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-ongoing-stale.job.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-ongoing-stale.job.ts @@ -5,7 +5,8 @@ import { In } from 'typeorm'; import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator'; import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service'; import { MessageChannelSyncStage, @@ -24,7 +25,7 @@ export type MessagingOngoingStaleJobData = { export class MessagingOngoingStaleJob { private readonly logger = new Logger(MessagingOngoingStaleJob.name); constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly messageChannelSyncStatusService: MessageChannelSyncStatusService, ) {} @@ -32,58 +33,65 @@ export class MessagingOngoingStaleJob { async handle(data: MessagingOngoingStaleJobData): Promise { const { workspaceId } = data; - const messageChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'messageChannel', - ); + const authContext = buildSystemAuthContext(workspaceId); - const messageChannels = await messageChannelRepository.find({ - where: { - syncStage: In([ - MessageChannelSyncStage.MESSAGES_IMPORT_ONGOING, - MessageChannelSyncStage.MESSAGE_LIST_FETCH_ONGOING, - MessageChannelSyncStage.MESSAGES_IMPORT_SCHEDULED, - MessageChannelSyncStage.MESSAGE_LIST_FETCH_SCHEDULED, - ]), - }, - }); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const messageChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'messageChannel', + ); - for (const messageChannel of messageChannels) { - if ( - messageChannel.syncStageStartedAt && - isSyncStale(messageChannel.syncStageStartedAt) - ) { - await this.messageChannelSyncStatusService.resetSyncStageStartedAt( - [messageChannel.id], - workspaceId, - ); + const messageChannels = await messageChannelRepository.find({ + where: { + syncStage: In([ + MessageChannelSyncStage.MESSAGES_IMPORT_ONGOING, + MessageChannelSyncStage.MESSAGE_LIST_FETCH_ONGOING, + MessageChannelSyncStage.MESSAGES_IMPORT_SCHEDULED, + MessageChannelSyncStage.MESSAGE_LIST_FETCH_SCHEDULED, + ]), + }, + }); - switch (messageChannel.syncStage) { - case MessageChannelSyncStage.MESSAGE_LIST_FETCH_ONGOING: - case MessageChannelSyncStage.MESSAGE_LIST_FETCH_SCHEDULED: - this.logger.log( - `Sync for message channel ${messageChannel.id} and workspace ${workspaceId} is stale. Setting sync stage to MESSAGE_LIST_FETCH_PENDING`, - ); - await this.messageChannelSyncStatusService.markAsMessagesListFetchPending( + for (const messageChannel of messageChannels) { + if ( + messageChannel.syncStageStartedAt && + isSyncStale(messageChannel.syncStageStartedAt) + ) { + await this.messageChannelSyncStatusService.resetSyncStageStartedAt( [messageChannel.id], workspaceId, ); - break; - case MessageChannelSyncStage.MESSAGES_IMPORT_ONGOING: - case MessageChannelSyncStage.MESSAGES_IMPORT_SCHEDULED: - this.logger.log( - `Sync for message channel ${messageChannel.id} and workspace ${workspaceId} is stale. Setting sync stage to MESSAGES_IMPORT_PENDING`, - ); - await this.messageChannelSyncStatusService.markAsMessagesImportPending( - [messageChannel.id], - workspaceId, - ); - break; - default: - break; + + switch (messageChannel.syncStage) { + case MessageChannelSyncStage.MESSAGE_LIST_FETCH_ONGOING: + case MessageChannelSyncStage.MESSAGE_LIST_FETCH_SCHEDULED: + this.logger.log( + `Sync for message channel ${messageChannel.id} and workspace ${workspaceId} is stale. Setting sync stage to MESSAGE_LIST_FETCH_PENDING`, + ); + await this.messageChannelSyncStatusService.markAsMessagesListFetchPending( + [messageChannel.id], + workspaceId, + ); + break; + case MessageChannelSyncStage.MESSAGES_IMPORT_ONGOING: + case MessageChannelSyncStage.MESSAGES_IMPORT_SCHEDULED: + this.logger.log( + `Sync for message channel ${messageChannel.id} and workspace ${workspaceId} is stale. Setting sync stage to MESSAGES_IMPORT_PENDING`, + ); + await this.messageChannelSyncStatusService.markAsMessagesImportPending( + [messageChannel.id], + workspaceId, + ); + break; + default: + break; + } + } } - } - } + }, + ); } } diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-relaunch-failed-message-channel.job.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-relaunch-failed-message-channel.job.ts index f2b52c42b54..c35b80ff704 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-relaunch-failed-message-channel.job.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-relaunch-failed-message-channel.job.ts @@ -3,7 +3,8 @@ import { Scope } from '@nestjs/common'; import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator'; import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { MessageChannelSyncStage, MessageChannelSyncStatus, @@ -21,42 +22,49 @@ export type MessagingRelaunchFailedMessageChannelJobData = { }) export class MessagingRelaunchFailedMessageChannelJob { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, ) {} @Process(MessagingRelaunchFailedMessageChannelJob.name) async handle(data: MessagingRelaunchFailedMessageChannelJobData) { const { workspaceId, messageChannelId } = data; - const messageChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'messageChannel', - { shouldBypassPermissionChecks: true }, - ); + const authContext = buildSystemAuthContext(workspaceId); - const messageChannel = await messageChannelRepository.findOne({ - where: { - id: messageChannelId, + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const messageChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'messageChannel', + { shouldBypassPermissionChecks: true }, + ); + + const messageChannel = await messageChannelRepository.findOne({ + where: { + id: messageChannelId, + }, + relations: { + connectedAccount: { + accountOwner: true, + }, + }, + }); + + if ( + !messageChannel || + messageChannel.syncStage !== MessageChannelSyncStage.FAILED || + messageChannel.syncStatus !== MessageChannelSyncStatus.FAILED_UNKNOWN + ) { + return; + } + + await messageChannelRepository.update(messageChannelId, { + syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING, + syncStatus: MessageChannelSyncStatus.ACTIVE, + }); }, - relations: { - connectedAccount: { - accountOwner: true, - }, - }, - }); - - if ( - !messageChannel || - messageChannel.syncStage !== MessageChannelSyncStage.FAILED || - messageChannel.syncStatus !== MessageChannelSyncStatus.FAILED_UNKNOWN - ) { - return; - } - - await messageChannelRepository.update(messageChannelId, { - syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING, - syncStatus: MessageChannelSyncStatus.ACTIVE, - }); + ); } } diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/services/__tests__/messaging-message-list-fetch.service.spec.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/services/__tests__/messaging-message-list-fetch.service.spec.ts index f11e006289f..7eb0162aea9 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/services/__tests__/messaging-message-list-fetch.service.spec.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/services/__tests__/messaging-message-list-fetch.service.spec.ts @@ -4,7 +4,7 @@ import { ConnectedAccountProvider } from 'twenty-shared/types'; import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service'; import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service'; import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; import { type MessageFolderWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity'; @@ -24,7 +24,7 @@ describe('MessagingMessageListFetchService', () => { let messagingGetMessageListService: MessagingGetMessageListService; let messagingAccountAuthenticationService: MessagingAccountAuthenticationService; let messageChannelSyncStatusService: MessageChannelSyncStatusService; - let twentyORMGlobalManager: TwentyORMGlobalManager; + let globalWorkspaceOrmManager: GlobalWorkspaceOrmManager; let messagingCursorService: MessagingCursorService; let mockMicrosoftMessageChannel: MessageChannelWorkspaceEntity; @@ -196,21 +196,23 @@ describe('MessagingMessageListFetchService', () => { }, }, { - provide: TwentyORMGlobalManager, + provide: GlobalWorkspaceOrmManager, useValue: { getDataSourceForWorkspace: jest.fn().mockResolvedValue({ manager: {}, }), - getRepositoryForWorkspace: jest + executeInWorkspaceContext: jest .fn() - .mockImplementation((workspaceId, name) => { - if (name === 'messageChannelMessageAssociation') { - return mockMessageChannelMessageAssociationRepository; - } - if (name === 'messageFolder') { - return mockMessageFolderRepository; - } - }), + + .mockImplementation((_authContext: any, fn: () => any) => fn()), + getRepository: jest.fn().mockImplementation((workspaceId, name) => { + if (name === 'messageChannelMessageAssociation') { + return mockMessageChannelMessageAssociationRepository; + } + if (name === 'messageFolder') { + return mockMessageFolderRepository; + } + }), }, }, { @@ -275,8 +277,8 @@ describe('MessagingMessageListFetchService', () => { module.get( MessageChannelSyncStatusService, ); - twentyORMGlobalManager = module.get( - TwentyORMGlobalManager, + globalWorkspaceOrmManager = module.get( + GlobalWorkspaceOrmManager, ); messagingCursorService = module.get( MessagingCursorService, @@ -320,9 +322,10 @@ describe('MessagingMessageListFetchService', () => { ], ); - expect( - twentyORMGlobalManager.getRepositoryForWorkspace, - ).toHaveBeenCalledWith(workspaceId, 'messageChannelMessageAssociation'); + expect(globalWorkspaceOrmManager.getRepository).toHaveBeenCalledWith( + workspaceId, + 'messageChannelMessageAssociation', + ); expect(messagingCursorService.updateCursor).toHaveBeenCalledWith( { @@ -380,9 +383,10 @@ describe('MessagingMessageListFetchService', () => { ], ); - expect( - twentyORMGlobalManager.getRepositoryForWorkspace, - ).toHaveBeenCalledWith(workspaceId, 'messageChannelMessageAssociation'); + expect(globalWorkspaceOrmManager.getRepository).toHaveBeenCalledWith( + workspaceId, + 'messageChannelMessageAssociation', + ); expect(messagingCursorService.updateCursor).toHaveBeenCalledWith( { diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/services/__tests__/messaging-messages-import.service.spec.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/services/__tests__/messaging-messages-import.service.spec.ts index 281163c70ae..34dea3ba733 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/services/__tests__/messaging-messages-import.service.spec.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/services/__tests__/messaging-messages-import.service.spec.ts @@ -5,7 +5,7 @@ import { ConnectedAccountProvider } from 'twenty-shared/types'; import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service'; import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { BlocklistRepository } from 'src/modules/blocklist/repositories/blocklist.repository'; import { EmailAliasManagerService } from 'src/modules/connected-account/email-alias-manager/services/email-alias-manager.service'; import { ConnectedAccountRefreshTokensService } from 'src/modules/connected-account/refresh-tokens-manager/services/connected-account-refresh-tokens.service'; @@ -106,11 +106,14 @@ describe('MessagingMessagesImportService', () => { }, }, { - provide: TwentyORMGlobalManager, + provide: GlobalWorkspaceOrmManager, useValue: { - getRepositoryForWorkspace: jest.fn().mockResolvedValue({ + getRepository: jest.fn().mockResolvedValue({ update: jest.fn().mockResolvedValue(undefined), }), + executeInWorkspaceContext: jest + .fn() + .mockImplementation((_authContext: any, fn: () => any) => fn()), }, }, { diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-cursor.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-cursor.service.ts index f58c6376a0d..3b6ce11fc30 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-cursor.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-cursor.service.ts @@ -1,13 +1,14 @@ import { Injectable } from '@nestjs/common'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; import { type MessageFolderWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity'; @Injectable() export class MessagingCursorService { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, ) {} public async updateCursor( @@ -16,50 +17,57 @@ export class MessagingCursorService { workspaceId: string, folderId?: string, ) { - const messageChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'messageChannel', - ); - const folderRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'messageFolder', - ); + const authContext = buildSystemAuthContext(workspaceId); - if (!folderId) { - await messageChannelRepository.update( - { - id: messageChannel.id, - }, - { - throttleFailureCount: 0, - syncStageStartedAt: null, - syncCursor: - !messageChannel.syncCursor || - nextSyncCursor > messageChannel.syncCursor - ? nextSyncCursor - : messageChannel.syncCursor, - }, - ); - } else { - await folderRepository.update( - { - id: folderId, - }, - { - syncCursor: nextSyncCursor, - }, - ); - await messageChannelRepository.update( - { - id: messageChannel.id, - }, - { - throttleFailureCount: 0, - syncStageStartedAt: null, - }, - ); - } + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const messageChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'messageChannel', + ); + const folderRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'messageFolder', + ); + + if (!folderId) { + await messageChannelRepository.update( + { + id: messageChannel.id, + }, + { + throttleFailureCount: 0, + syncStageStartedAt: null, + syncCursor: + !messageChannel.syncCursor || + nextSyncCursor > messageChannel.syncCursor + ? nextSyncCursor + : messageChannel.syncCursor, + }, + ); + } else { + await folderRepository.update( + { + id: folderId, + }, + { + syncCursor: nextSyncCursor, + }, + ); + await messageChannelRepository.update( + { + id: messageChannel.id, + }, + { + throttleFailureCount: 0, + syncStageStartedAt: null, + }, + ); + } + }, + ); } } diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-delete-group-email-messages.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-delete-group-email-messages.service.ts index 74d289b7984..d0619709a2e 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-delete-group-email-messages.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-delete-group-email-messages.service.ts @@ -1,10 +1,11 @@ import { Injectable, Logger } from '@nestjs/common'; import chunk from 'lodash.chunk'; -import { isDefined } from 'twenty-shared/utils'; import { MessageParticipantRole } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity'; import { MessagingMessageCleanerService } from 'src/modules/messaging/message-cleaner/services/messaging-message-cleaner.service'; import { isGroupEmail } from 'src/modules/messaging/message-import-manager/utils/is-group-email'; @@ -24,7 +25,7 @@ export class MessagingDeleteGroupEmailMessagesService { ); constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly messagingMessageCleanerService: MessagingMessageCleanerService, ) {} @@ -36,92 +37,99 @@ export class MessagingDeleteGroupEmailMessagesService { `WorkspaceId: ${workspaceId}, MessageChannelId: ${messageChannelId} - Deleting messages from group email addresses`, ); - const messageChannelMessageAssociationRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'messageChannelMessageAssociation', - ); + const authContext = buildSystemAuthContext(workspaceId); - let offset = 0; - let totalDeletedCount = 0; - - while (true) { - const batch = await messageChannelMessageAssociationRepository - .createQueryBuilder('mcma') - .select('mcma.messageId', 'messageId') - .addSelect('mcma.messageExternalId', 'messageExternalId') - .addSelect('participant.handle', 'participantHandle') - .innerJoin('mcma.message', 'message') - .innerJoin( - 'message.messageParticipants', - 'participant', - 'participant.role = :role', - { role: MessageParticipantRole.FROM }, - ) - .where('mcma.messageChannelId = :messageChannelId', { - messageChannelId, - }) - .skip(offset) - .take(MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_BATCH_SIZE) - .getRawMany(); - - if (batch.length === 0) { - break; - } - - const groupEmailRecords = batch.filter( - (record) => - isDefined(record.participantHandle) && - isGroupEmail(record.participantHandle), - ); - - if (groupEmailRecords.length > 0) { - const uniqueMessageIds = new Set( - groupEmailRecords.map((r) => r.messageId), - ); - - const messageExternalIdsToDelete = batch - .filter((record) => uniqueMessageIds.has(record.messageId)) - .map((record) => record.messageExternalId) - .filter(isDefined); - - if (messageExternalIdsToDelete.length > 0) { - const messageExternalIdsChunks = chunk( - messageExternalIdsToDelete, - 200, + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const messageChannelMessageAssociationRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'messageChannelMessageAssociation', ); - for (const messageExternalIdsChunk of messageExternalIdsChunks) { - await this.messagingMessageCleanerService.deleteMessagesChannelMessageAssociationsAndRelatedOrphans( - { - workspaceId, - messageExternalIds: messageExternalIdsChunk, - messageChannelId, - }, + let offset = 0; + let totalDeletedCount = 0; + + while (true) { + const batch = await messageChannelMessageAssociationRepository + .createQueryBuilder('mcma') + .select('mcma.messageId', 'messageId') + .addSelect('mcma.messageExternalId', 'messageExternalId') + .addSelect('participant.handle', 'participantHandle') + .innerJoin('mcma.message', 'message') + .innerJoin( + 'message.messageParticipants', + 'participant', + 'participant.role = :role', + { role: MessageParticipantRole.FROM }, + ) + .where('mcma.messageChannelId = :messageChannelId', { + messageChannelId, + }) + .skip(offset) + .take(MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_BATCH_SIZE) + .getRawMany(); + + if (batch.length === 0) { + break; + } + + const groupEmailRecords = batch.filter( + (record) => + isDefined(record.participantHandle) && + isGroupEmail(record.participantHandle), + ); + + if (groupEmailRecords.length > 0) { + const uniqueMessageIds = new Set( + groupEmailRecords.map((r) => r.messageId), ); - totalDeletedCount += messageExternalIdsChunk.length; + const messageExternalIdsToDelete = batch + .filter((record) => uniqueMessageIds.has(record.messageId)) + .map((record) => record.messageExternalId) + .filter(isDefined); - this.logger.log( - `WorkspaceId: ${workspaceId}, MessageChannelId: ${messageChannelId} - Deleted ${messageExternalIdsChunk.length} group email messages`, - ); + if (messageExternalIdsToDelete.length > 0) { + const messageExternalIdsChunks = chunk( + messageExternalIdsToDelete, + 200, + ); + + for (const messageExternalIdsChunk of messageExternalIdsChunks) { + await this.messagingMessageCleanerService.deleteMessagesChannelMessageAssociationsAndRelatedOrphans( + { + workspaceId, + messageExternalIds: messageExternalIdsChunk, + messageChannelId, + }, + ); + + totalDeletedCount += messageExternalIdsChunk.length; + + this.logger.log( + `WorkspaceId: ${workspaceId}, MessageChannelId: ${messageChannelId} - Deleted ${messageExternalIdsChunk.length} group email messages`, + ); + } + } + } + + if (batch.length < MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_BATCH_SIZE) { + break; + } + + if (groupEmailRecords.length === 0) { + offset += MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_BATCH_SIZE; } } - } - if (batch.length < MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_BATCH_SIZE) { - break; - } + this.logger.log( + `WorkspaceId: ${workspaceId}, MessageChannelId: ${messageChannelId} - Completed deleting ${totalDeletedCount} group email messages`, + ); - if (groupEmailRecords.length === 0) { - offset += MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_BATCH_SIZE; - } - } - - this.logger.log( - `WorkspaceId: ${workspaceId}, MessageChannelId: ${messageChannelId} - Completed deleting ${totalDeletedCount} group email messages`, + return totalDeletedCount; + }, ); - - return totalDeletedCount; } } diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-import-exception-handler.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-import-exception-handler.service.ts index 322746622bc..9fc65f051d3 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-import-exception-handler.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-import-exception-handler.service.ts @@ -5,7 +5,8 @@ import { type TwentyORMException, TwentyORMExceptionCode, } from 'src/engine/twenty-orm/exceptions/twenty-orm.exception'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service'; import { MessageChannelSyncStatus, @@ -27,7 +28,7 @@ export enum MessageImportSyncStep { @Injectable() export class MessageImportExceptionHandlerService { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly messageChannelSyncStatusService: MessageChannelSyncStatusService, private readonly exceptionHandlerService: ExceptionHandlerService, ) {} @@ -148,18 +149,25 @@ export class MessageImportExceptionHandlerService { return; } - const messageChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'messageChannel', - ); + const authContext = buildSystemAuthContext(workspaceId); - await messageChannelRepository.increment( - { id: messageChannel.id }, - 'throttleFailureCount', - 1, - undefined, - ['throttleFailureCount', 'id'], + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const messageChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'messageChannel', + ); + + await messageChannelRepository.increment( + { id: messageChannel.id }, + 'throttleFailureCount', + 1, + undefined, + ['throttleFailureCount', 'id'], + ); + }, ); switch (syncStep) { diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-message-list-fetch.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-message-list-fetch.service.ts index 92561ff4e03..0e255466b9b 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-message-list-fetch.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-message-list-fetch.service.ts @@ -8,7 +8,9 @@ import { In, MoreThanOrEqual } from 'typeorm'; import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator'; import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service'; import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service'; import { type MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity'; import { @@ -43,7 +45,7 @@ export class MessagingMessageListFetchService { @InjectCacheStorage(CacheStorageNamespace.ModuleMessaging) private readonly cacheStorage: CacheStorageService, private readonly messageChannelSyncStatusService: MessageChannelSyncStatusService, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly messagingGetMessageListService: MessagingGetMessageListService, private readonly messageImportErrorHandlerService: MessageImportExceptionHandlerService, private readonly messagingMessageCleanerService: MessagingMessageCleanerService, @@ -59,264 +61,275 @@ export class MessagingMessageListFetchService { messageChannel: MessageChannelWorkspaceEntity, workspaceId: string, ) { - try { - const pendingGroupEmailActionsProcessed = - await this.processPendingGroupEmailActions(messageChannel, workspaceId); + const authContext = buildSystemAuthContext(workspaceId); - const pendingFolderActionsProcessed = - await this.processPendingFolderActions(messageChannel, workspaceId); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + try { + const pendingGroupEmailActionsProcessed = + await this.processPendingGroupEmailActions( + messageChannel, + workspaceId, + ); - await this.messageChannelSyncStatusService.markAsMessagesListFetchOngoing( - [messageChannel.id], - workspaceId, - ); + const pendingFolderActionsProcessed = + await this.processPendingFolderActions(messageChannel, workspaceId); - this.logger.log( - `messageChannelId: ${messageChannel.id} Processing message list fetch`, - ); - - const messageChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'messageChannel', - ); - - const freshMessageChannel = - pendingGroupEmailActionsProcessed || pendingFolderActionsProcessed - ? await messageChannelRepository.findOne({ - where: { - id: messageChannel.id, - }, - relations: ['connectedAccount', 'messageFolders'], - }) - : messageChannel; - - if (!isDefined(freshMessageChannel)) { - this.logger.error( - `error processing message list fetch: messageChannelId: ${messageChannel.id} Message channel not found`, - ); - - return; - } - - const { accessToken, refreshToken } = - await this.messagingAccountAuthenticationService.validateAndRefreshConnectedAccountAuthentication( - { - connectedAccount: freshMessageChannel.connectedAccount, + await this.messageChannelSyncStatusService.markAsMessagesListFetchOngoing( + [messageChannel.id], workspaceId, - messageChannelId: freshMessageChannel.id, - }, - ); + ); - const messageChannelWithFreshTokens = { - ...freshMessageChannel, - connectedAccount: { - ...freshMessageChannel.connectedAccount, - accessToken, - refreshToken, - }, - }; + this.logger.log( + `messageChannelId: ${messageChannel.id} Processing message list fetch`, + ); - const datasource = - await this.twentyORMGlobalManager.getDataSourceForWorkspace({ - workspaceId, - }); + const messageChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'messageChannel', + ); - await this.syncMessageFoldersService.syncMessageFolders({ - workspaceId, - messageChannel: messageChannelWithFreshTokens, - manager: datasource.manager, - }); + const freshMessageChannel = + pendingGroupEmailActionsProcessed || pendingFolderActionsProcessed + ? await messageChannelRepository.findOne({ + where: { + id: messageChannel.id, + }, + relations: ['connectedAccount', 'messageFolders'], + }) + : messageChannel; - const messageFolderRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'messageFolder', - ); + if (!isDefined(freshMessageChannel)) { + this.logger.error( + `error processing message list fetch: messageChannelId: ${messageChannel.id} Message channel not found`, + ); - const messageFolders = await messageFolderRepository.find({ - where: { - messageChannelId: freshMessageChannel.id, - pendingSyncAction: MessageFolderPendingSyncAction.NONE, - }, - }); + return; + } - const messageFoldersToSync = - messageChannelWithFreshTokens.messageFolderImportPolicy === - MessageFolderImportPolicy.ALL_FOLDERS - ? messageFolders - : messageFolders.filter((folder) => folder.isSynced); + const { accessToken, refreshToken } = + await this.messagingAccountAuthenticationService.validateAndRefreshConnectedAccountAuthentication( + { + connectedAccount: freshMessageChannel.connectedAccount, + workspaceId, + messageChannelId: freshMessageChannel.id, + }, + ); - const messageLists = - await this.messagingGetMessageListService.getMessageLists( - messageChannelWithFreshTokens, - messageFoldersToSync, - ); + const messageChannelWithFreshTokens = { + ...freshMessageChannel, + connectedAccount: { + ...freshMessageChannel.connectedAccount, + accessToken, + refreshToken, + }, + }; - await this.cacheStorage.del( - `messages-to-import:${workspaceId}:${freshMessageChannel.id}`, - ); + const datasource = + await this.globalWorkspaceOrmManager.getDataSourceForWorkspace( + workspaceId, + ); - const messageExternalIds = messageLists.flatMap( - (messageList) => messageList.messageExternalIds, - ); + await this.syncMessageFoldersService.syncMessageFolders({ + workspaceId, + messageChannel: messageChannelWithFreshTokens, + manager: datasource.manager as WorkspaceEntityManager, + }); - const messageExternalIdsToDelete = messageLists.flatMap( - (messageList) => messageList.messageExternalIdsToDelete, - ); + const messageFolderRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'messageFolder', + ); - const isFullSync = - messageLists.every( - (messageList) => !isNonEmptyString(messageList.previousSyncCursor), - ) && !isNonEmptyString(freshMessageChannel.syncCursor); - - let totalMessagesToImportCount = 0; - - this.logger.log( - `messageChannelId: ${freshMessageChannel.id} Is full sync: ${isFullSync} and toImportCount: ${messageExternalIds.length}, toDeleteCount: ${messageExternalIdsToDelete.length}, cursors: ${messageLists.map( - (messageList) => { - messageList.nextSyncCursor; - }, - )}`, - ); - - const messageChannelMessageAssociationRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'messageChannelMessageAssociation', - ); - - const messageExternalIdsChunks = chunk(messageExternalIds, 200); - - for (const [ - index, - messageExternalIdsChunk, - ] of messageExternalIdsChunks.entries()) { - const existingMessageChannelMessageAssociations = - await messageChannelMessageAssociationRepository.find({ + const messageFolders = await messageFolderRepository.find({ where: { messageChannelId: freshMessageChannel.id, - messageExternalId: In(messageExternalIdsChunk), + pendingSyncAction: MessageFolderPendingSyncAction.NONE, }, }); - const existingMessageChannelMessageAssociationsExternalIds = - existingMessageChannelMessageAssociations.map( - (messageChannelMessageAssociation) => - messageChannelMessageAssociation.messageExternalId, + const messageFoldersToSync = + messageChannelWithFreshTokens.messageFolderImportPolicy === + MessageFolderImportPolicy.ALL_FOLDERS + ? messageFolders + : messageFolders.filter((folder) => folder.isSynced); + + const messageLists = + await this.messagingGetMessageListService.getMessageLists( + messageChannelWithFreshTokens, + messageFoldersToSync, + ); + + await this.cacheStorage.del( + `messages-to-import:${workspaceId}:${freshMessageChannel.id}`, ); - const messageExternalIdsToImport = messageExternalIdsChunk.filter( - (messageExternalId) => - !existingMessageChannelMessageAssociationsExternalIds.includes( - messageExternalId, - ), - ); + const messageExternalIds = messageLists.flatMap( + (messageList) => messageList.messageExternalIds, + ); + + const messageExternalIdsToDelete = messageLists.flatMap( + (messageList) => messageList.messageExternalIdsToDelete, + ); + + const isFullSync = + messageLists.every( + (messageList) => + !isNonEmptyString(messageList.previousSyncCursor), + ) && !isNonEmptyString(freshMessageChannel.syncCursor); + + let totalMessagesToImportCount = 0; - if (messageExternalIdsToImport.length) { this.logger.log( - `messageChannelId: ${freshMessageChannel.id} Adding ${messageExternalIdsToImport.length} message external ids to import in batch ${index + 1}`, + `messageChannelId: ${freshMessageChannel.id} Is full sync: ${isFullSync} and toImportCount: ${messageExternalIds.length}, toDeleteCount: ${messageExternalIdsToDelete.length}, cursors: ${messageLists.map( + (messageList) => { + messageList.nextSyncCursor; + }, + )}`, ); - totalMessagesToImportCount += messageExternalIdsToImport.length; - - await this.cacheStorage.setAdd( - `messages-to-import:${workspaceId}:${messageChannelWithFreshTokens.id}`, - messageExternalIdsToImport, - ONE_WEEK_IN_MILLISECONDS, - ); - } - } - - for (const messageList of messageLists) { - const { nextSyncCursor, folderId } = messageList; - - await this.messagingCursorService.updateCursor( - messageChannelWithFreshTokens, - nextSyncCursor, - workspaceId, - folderId, - ); - } - - const fullSyncMessageChannelMessageAssociationsToDelete = isFullSync - ? await this.computeFullSyncMessageChannelMessageAssociationsToDelete( - freshMessageChannel, - messageExternalIds, - workspaceId, - ) - : []; - - const allMessageExternalIdsToDelete = [ - ...messageExternalIdsToDelete, - ...fullSyncMessageChannelMessageAssociationsToDelete.map( - (messageChannelMessageAssociation) => - messageChannelMessageAssociation.messageExternalId, - ), - ]; - - if (allMessageExternalIdsToDelete.length) { - this.logger.log( - `messageChannelId: ${freshMessageChannel.id} Deleting ${allMessageExternalIdsToDelete.length} message channel message associations`, - ); - - const toDeleteChunks = chunk(allMessageExternalIdsToDelete, 200); - - for (const [index, toDeleteChunk] of toDeleteChunks.entries()) { - this.logger.log( - `messageChannelId: ${freshMessageChannel.id} Deleting ${toDeleteChunk.length} message channel message associations in batch ${index + 1}`, - ); - - await this.messagingMessageCleanerService.deleteMessagesChannelMessageAssociationsAndRelatedOrphans( - { + const messageChannelMessageAssociationRepository = + await this.globalWorkspaceOrmManager.getRepository( workspaceId, - messageExternalIds: toDeleteChunk.filter((messageExternalId) => - isNonEmptyString(messageExternalId), - ), - messageChannelId: messageChannelWithFreshTokens.id, + 'messageChannelMessageAssociation', + ); + + const messageExternalIdsChunks = chunk(messageExternalIds, 200); + + for (const [ + index, + messageExternalIdsChunk, + ] of messageExternalIdsChunks.entries()) { + const existingMessageChannelMessageAssociations = + await messageChannelMessageAssociationRepository.find({ + where: { + messageChannelId: freshMessageChannel.id, + messageExternalId: In(messageExternalIdsChunk), + }, + }); + + const existingMessageChannelMessageAssociationsExternalIds = + existingMessageChannelMessageAssociations.map( + (messageChannelMessageAssociation) => + messageChannelMessageAssociation.messageExternalId, + ); + + const messageExternalIdsToImport = messageExternalIdsChunk.filter( + (messageExternalId) => + !existingMessageChannelMessageAssociationsExternalIds.includes( + messageExternalId, + ), + ); + + if (messageExternalIdsToImport.length) { + this.logger.log( + `messageChannelId: ${freshMessageChannel.id} Adding ${messageExternalIdsToImport.length} message external ids to import in batch ${index + 1}`, + ); + + totalMessagesToImportCount += messageExternalIdsToImport.length; + + await this.cacheStorage.setAdd( + `messages-to-import:${workspaceId}:${messageChannelWithFreshTokens.id}`, + messageExternalIdsToImport, + ONE_WEEK_IN_MILLISECONDS, + ); + } + } + + for (const messageList of messageLists) { + const { nextSyncCursor, folderId } = messageList; + + await this.messagingCursorService.updateCursor( + messageChannelWithFreshTokens, + nextSyncCursor, + workspaceId, + folderId, + ); + } + + const fullSyncMessageChannelMessageAssociationsToDelete = isFullSync + ? await this.computeFullSyncMessageChannelMessageAssociationsToDelete( + freshMessageChannel, + messageExternalIds, + workspaceId, + ) + : []; + + const allMessageExternalIdsToDelete = [ + ...messageExternalIdsToDelete, + ...fullSyncMessageChannelMessageAssociationsToDelete.map( + (messageChannelMessageAssociation) => + messageChannelMessageAssociation.messageExternalId, + ), + ]; + + if (allMessageExternalIdsToDelete.length) { + this.logger.log( + `messageChannelId: ${freshMessageChannel.id} Deleting ${allMessageExternalIdsToDelete.length} message channel message associations`, + ); + + const toDeleteChunks = chunk(allMessageExternalIdsToDelete, 200); + + for (const [index, toDeleteChunk] of toDeleteChunks.entries()) { + this.logger.log( + `messageChannelId: ${freshMessageChannel.id} Deleting ${toDeleteChunk.length} message channel message associations in batch ${index + 1}`, + ); + + await this.messagingMessageCleanerService.deleteMessagesChannelMessageAssociationsAndRelatedOrphans( + { + workspaceId, + messageExternalIds: toDeleteChunk.filter( + (messageExternalId) => isNonEmptyString(messageExternalId), + ), + messageChannelId: messageChannelWithFreshTokens.id, + }, + ); + } + } + + this.logger.log( + `messageChannelId: ${freshMessageChannel.id} Total messages to import count: ${totalMessagesToImportCount}`, + ); + + if (totalMessagesToImportCount === 0) { + await this.messageChannelSyncStatusService.markAsCompletedAndMarkAsMessagesListFetchPending( + [messageChannelWithFreshTokens.id], + workspaceId, + ); + + return; + } + + this.logger.log( + `messageChannelId: ${freshMessageChannel.id} Scheduling direct messages import`, + ); + + await this.messageChannelSyncStatusService.markAsMessagesImportScheduled( + [messageChannelWithFreshTokens.id], + workspaceId, + ); + + await this.messagingMessagesImportService.processMessageBatchImport( + { + ...messageChannelWithFreshTokens, + syncStage: MessageChannelSyncStage.MESSAGES_IMPORT_SCHEDULED, }, + messageChannelWithFreshTokens.connectedAccount, + workspaceId, + ); + } catch (error) { + await this.messageImportErrorHandlerService.handleDriverException( + error, + MessageImportSyncStep.MESSAGE_LIST_FETCH, + messageChannel, + workspaceId, ); } - } - - this.logger.log( - `messageChannelId: ${freshMessageChannel.id} Total messages to import count: ${totalMessagesToImportCount}`, - ); - - if (totalMessagesToImportCount === 0) { - await this.messageChannelSyncStatusService.markAsCompletedAndMarkAsMessagesListFetchPending( - [messageChannelWithFreshTokens.id], - workspaceId, - ); - - return; - } - - this.logger.log( - `messageChannelId: ${freshMessageChannel.id} Scheduling direct messages import`, - ); - - await this.messageChannelSyncStatusService.markAsMessagesImportScheduled( - [messageChannelWithFreshTokens.id], - workspaceId, - ); - - await this.messagingMessagesImportService.processMessageBatchImport( - { - ...messageChannelWithFreshTokens, - syncStage: MessageChannelSyncStage.MESSAGES_IMPORT_SCHEDULED, - }, - messageChannelWithFreshTokens.connectedAccount, - workspaceId, - ); - } catch (error) { - await this.messageImportErrorHandlerService.handleDriverException( - error, - MessageImportSyncStep.MESSAGE_LIST_FETCH, - messageChannel, - workspaceId, - ); - } + }, + ); } private async processPendingGroupEmailActions( @@ -350,7 +363,7 @@ export class MessagingMessageListFetchService { workspaceId: string, ): Promise { const messageFolderRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( + await this.globalWorkspaceOrmManager.getRepository( workspaceId, 'messageFolder', ); @@ -385,7 +398,7 @@ export class MessagingMessageListFetchService { workspaceId: string, ) { const messageChannelMessageAssociationRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( + await this.globalWorkspaceOrmManager.getRepository( workspaceId, 'messageChannelMessageAssociation', ); diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-message.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-message.service.ts index 2baca3aca73..c2c5cf0880a 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-message.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-message.service.ts @@ -5,7 +5,8 @@ import { In } from 'typeorm'; import { v4 } from 'uuid'; import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { type MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity'; import { type MessageThreadWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-thread.workspace-entity'; import { type MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message.workspace-entity'; @@ -39,7 +40,7 @@ export class MessagingMessageService { private readonly logger = new Logger(MessagingMessageService.name); constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, ) {} public async saveMessagesWithinTransaction( @@ -51,183 +52,200 @@ export class MessagingMessageService { createdMessages: Partial[]; messageExternalIdsAndIdsMap: Map; }> { - const messageChannelMessageAssociationRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'messageChannelMessageAssociation', - ); + const authContext = buildSystemAuthContext(workspaceId); - const messageRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'message', - ); + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const messageChannelMessageAssociationRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'messageChannelMessageAssociation', + ); - const messageThreadRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'messageThread', - ); + const messageRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'message', + ); - const messageAccumulatorMap = new Map(); + const messageThreadRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'messageThread', + ); - const existingMessagesInDB = await messageRepository.find({ - where: { - headerMessageId: In(messages.map((message) => message.headerMessageId)), - }, - }); + const messageAccumulatorMap = new Map(); - const messageChannelMessageAssociationsReferencingMessageThread = - await messageChannelMessageAssociationRepository.find( - { + const existingMessagesInDB = await messageRepository.find({ where: { - messageThreadExternalId: In( - messages.map((message) => message.messageThreadExternalId), + headerMessageId: In( + messages.map((message) => message.headerMessageId), ), - messageChannelId, }, - relations: ['message'], - }, - transactionManager, - ); + }); - const existingMessageChannelMessageAssociations = - await messageChannelMessageAssociationRepository.find({ - where: { - messageId: In(existingMessagesInDB.map((message) => message.id)), - messageChannelId, - }, - }); + const messageChannelMessageAssociationsReferencingMessageThread = + await messageChannelMessageAssociationRepository.find( + { + where: { + messageThreadExternalId: In( + messages.map((message) => message.messageThreadExternalId), + ), + messageChannelId, + }, + relations: ['message'], + }, + transactionManager, + ); - await this.enrichMessageAccumulatorWithExistingMessages( - messages, - messageAccumulatorMap, - existingMessagesInDB, - ); + const existingMessageChannelMessageAssociations = + await messageChannelMessageAssociationRepository.find({ + where: { + messageId: In(existingMessagesInDB.map((message) => message.id)), + messageChannelId, + }, + }); - await this.enrichMessageAccumulatorWithExistingMessageThreadIds( - messages, - messageAccumulatorMap, - messageChannelMessageAssociationsReferencingMessageThread, - workspaceId, - ); - - await this.enrichMessageAccumulatorWithExistingMessageChannelMessageAssociations( - messages, - messageAccumulatorMap, - existingMessageChannelMessageAssociations, - ); - - await this.enrichMessageAccumulatorWithMessageThreadToCreate( - messages, - messageAccumulatorMap, - ); - - for (const message of messages) { - const messageAccumulator = messageAccumulatorMap.get(message.externalId); - - if (!isDefined(messageAccumulator)) { - throw new Error( - `Message accumulator should reference the message, this should never happen`, + await this.enrichMessageAccumulatorWithExistingMessages( + messages, + messageAccumulatorMap, + existingMessagesInDB, ); - } - const messageThreadId = - messageAccumulator.threadToCreate?.id ?? - messageAccumulator.existingThreadInDB?.id; - - if (!isDefined(messageThreadId)) { - throw new Error( - `Message thread id should be defined, either in the threadToCreate or existingThreadInDB`, + await this.enrichMessageAccumulatorWithExistingMessageThreadIds( + messages, + messageAccumulatorMap, + messageChannelMessageAssociationsReferencingMessageThread, + workspaceId, ); - } - let newOrExistingMessageId: string; + await this.enrichMessageAccumulatorWithExistingMessageChannelMessageAssociations( + messages, + messageAccumulatorMap, + existingMessageChannelMessageAssociations, + ); - if (!isDefined(messageAccumulator.existingMessageInDB)) { - newOrExistingMessageId = v4(); + await this.enrichMessageAccumulatorWithMessageThreadToCreate( + messages, + messageAccumulatorMap, + ); - const messageToCreate = { - id: newOrExistingMessageId, - headerMessageId: message.headerMessageId, - subject: message.subject, - receivedAt: message.receivedAt, - text: message.text, - messageThreadId, - }; + for (const message of messages) { + const messageAccumulator = messageAccumulatorMap.get( + message.externalId, + ); - messageAccumulator.messageToCreate = messageToCreate; - } else { - newOrExistingMessageId = messageAccumulator.existingMessageInDB.id; - } + if (!isDefined(messageAccumulator)) { + throw new Error( + `Message accumulator should reference the message, this should never happen`, + ); + } - if ( - !isDefined( - messageAccumulator.existingMessageChannelMessageAssociationInDB, + const messageThreadId = + messageAccumulator.threadToCreate?.id ?? + messageAccumulator.existingThreadInDB?.id; + + if (!isDefined(messageThreadId)) { + throw new Error( + `Message thread id should be defined, either in the threadToCreate or existingThreadInDB`, + ); + } + + let newOrExistingMessageId: string; + + if (!isDefined(messageAccumulator.existingMessageInDB)) { + newOrExistingMessageId = v4(); + + const messageToCreate = { + id: newOrExistingMessageId, + headerMessageId: message.headerMessageId, + subject: message.subject, + receivedAt: message.receivedAt, + text: message.text, + messageThreadId, + }; + + messageAccumulator.messageToCreate = messageToCreate; + } else { + newOrExistingMessageId = messageAccumulator.existingMessageInDB.id; + } + + if ( + !isDefined( + messageAccumulator.existingMessageChannelMessageAssociationInDB, + ) + ) { + messageAccumulator.messageChannelMessageAssociationToCreate = { + messageChannelId, + messageId: newOrExistingMessageId, + messageExternalId: message.externalId, + messageThreadExternalId: message.messageThreadExternalId, + direction: message.direction, + }; + + messageAccumulatorMap.set(message.externalId, messageAccumulator); + } + } + + const messageThreadsToCreate = Array.from( + messageAccumulatorMap.values(), ) - ) { - messageAccumulator.messageChannelMessageAssociationToCreate = { - messageChannelId, - messageId: newOrExistingMessageId, - messageExternalId: message.externalId, - messageThreadExternalId: message.messageThreadExternalId, - direction: message.direction, + .map((accumulator) => accumulator.threadToCreate) + .filter(isDefined); + + await messageThreadRepository.insert( + messageThreadsToCreate, + transactionManager, + ); + + const messagesToCreate = Array.from(messageAccumulatorMap.values()) + .map((accumulator) => accumulator.messageToCreate) + .filter(isDefined); + + await messageRepository.insert(messagesToCreate, transactionManager); + + const messageChannelMessageAssociationsToCreate = Array.from( + messageAccumulatorMap.values(), + ) + .map( + (accumulator) => + accumulator.messageChannelMessageAssociationToCreate, + ) + .filter(isDefined); + + await messageChannelMessageAssociationRepository.insert( + messageChannelMessageAssociationsToCreate, + transactionManager, + ); + + const messageExternalIdsAndIdsMap = new Map(); + + for (const [ + externalId, + accumulator, + ] of messageAccumulatorMap.entries()) { + if (isDefined(accumulator.messageToCreate)) { + messageExternalIdsAndIdsMap.set( + externalId, + accumulator.messageToCreate.id, + ); + } + + if (isDefined(accumulator.existingMessageInDB)) { + messageExternalIdsAndIdsMap.set( + externalId, + accumulator.existingMessageInDB.id, + ); + } + } + + return { + createdMessages: messagesToCreate, + messageExternalIdsAndIdsMap, }; - - messageAccumulatorMap.set(message.externalId, messageAccumulator); - } - } - - const messageThreadsToCreate = Array.from(messageAccumulatorMap.values()) - .map((accumulator) => accumulator.threadToCreate) - .filter(isDefined); - - await messageThreadRepository.insert( - messageThreadsToCreate, - transactionManager, + }, ); - - const messagesToCreate = Array.from(messageAccumulatorMap.values()) - .map((accumulator) => accumulator.messageToCreate) - .filter(isDefined); - - await messageRepository.insert(messagesToCreate, transactionManager); - - const messageChannelMessageAssociationsToCreate = Array.from( - messageAccumulatorMap.values(), - ) - .map( - (accumulator) => accumulator.messageChannelMessageAssociationToCreate, - ) - .filter(isDefined); - - await messageChannelMessageAssociationRepository.insert( - messageChannelMessageAssociationsToCreate, - transactionManager, - ); - - const messageExternalIdsAndIdsMap = new Map(); - - for (const [externalId, accumulator] of messageAccumulatorMap.entries()) { - if (isDefined(accumulator.messageToCreate)) { - messageExternalIdsAndIdsMap.set( - externalId, - accumulator.messageToCreate.id, - ); - } - - if (isDefined(accumulator.existingMessageInDB)) { - messageExternalIdsAndIdsMap.set( - externalId, - accumulator.existingMessageInDB.id, - ); - } - } - - return { - createdMessages: messagesToCreate, - messageExternalIdsAndIdsMap, - }; } private async enrichMessageAccumulatorWithExistingMessages( @@ -307,11 +325,6 @@ export class MessagingMessageService { existingThreadIdInDBIfMessageIsExistingInDB !== existingThreadIdInDBIfMessageIsReferencedInMessageChannelMessageAssociation ) { - // TODO: this can be handled better - // If we find a messageThreadId different on the existingMessage (found by messageHeaderId which is cross channel) - // And on the the one associatied to the messageThreadExternalId (found by which is channel specific) - // this means that we have to channels that have imported messages separately and that this message is the first connection between the two channels - // we should merge messageThreads this.logger.warn( ` WorkspaceId: ${workspaceId} / diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-messages-import.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-messages-import.service.ts index 62f02069edb..feecb7b1741 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-messages-import.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-messages-import.service.ts @@ -6,7 +6,8 @@ import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decora import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service'; import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { BlocklistRepository } from 'src/modules/blocklist/repositories/blocklist.repository'; import { BlocklistWorkspaceEntity } from 'src/modules/blocklist/standard-objects/blocklist.workspace-entity'; import { EmailAliasManagerService } from 'src/modules/connected-account/email-alias-manager/services/email-alias-manager.service'; @@ -30,6 +31,7 @@ import { import { MessagingSaveMessagesAndEnqueueContactCreationService } from 'src/modules/messaging/message-import-manager/services/messaging-save-messages-and-enqueue-contact-creation.service'; import { filterEmails } from 'src/modules/messaging/message-import-manager/utils/filter-emails.util'; import { MessagingMonitoringService } from 'src/modules/messaging/monitoring/services/messaging-monitoring.service'; + @Injectable() export class MessagingMessagesImportService { private readonly logger = new Logger(MessagingMessagesImportService.name); @@ -43,7 +45,7 @@ export class MessagingMessagesImportService { @InjectObjectMetadataRepository(BlocklistWorkspaceEntity) private readonly blocklistRepository: BlocklistRepository, private readonly emailAliasManagerService: EmailAliasManagerService, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly messagingGetMessagesService: MessagingGetMessagesService, private readonly messageImportErrorHandlerService: MessageImportExceptionHandlerService, private readonly messagingAccountAuthenticationService: MessagingAccountAuthenticationService, @@ -56,162 +58,171 @@ export class MessagingMessagesImportService { ) { let messageIdsToFetch: string[] = []; - try { - if ( - messageChannel.syncStage !== - MessageChannelSyncStage.MESSAGES_IMPORT_SCHEDULED - ) { - return; - } + const authContext = buildSystemAuthContext(workspaceId); - await this.messagingMonitoringService.track({ - eventName: 'messages_import.started', - workspaceId, - connectedAccountId: messageChannel.connectedAccountId, - messageChannelId: messageChannel.id, - }); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + try { + if ( + messageChannel.syncStage !== + MessageChannelSyncStage.MESSAGES_IMPORT_SCHEDULED + ) { + return; + } - await this.messageChannelSyncStatusService.markAsMessagesImportOngoing( - [messageChannel.id], - workspaceId, - ); - - const { accessToken, refreshToken } = - await this.messagingAccountAuthenticationService.validateAndRefreshConnectedAccountAuthentication( - { - connectedAccount, + await this.messagingMonitoringService.track({ + eventName: 'messages_import.started', workspaceId, + connectedAccountId: messageChannel.connectedAccountId, messageChannelId: messageChannel.id, - }, - ); + }); - const connectedAccountWithFreshTokens = { - ...connectedAccount, - accessToken, - refreshToken, - }; + await this.messageChannelSyncStatusService.markAsMessagesImportOngoing( + [messageChannel.id], + workspaceId, + ); - await this.emailAliasManagerService.refreshHandleAliases( - connectedAccountWithFreshTokens, - workspaceId, - ); + const { accessToken, refreshToken } = + await this.messagingAccountAuthenticationService.validateAndRefreshConnectedAccountAuthentication( + { + connectedAccount, + workspaceId, + messageChannelId: messageChannel.id, + }, + ); - messageIdsToFetch = await this.cacheStorage.setPop( - `messages-to-import:${workspaceId}:${messageChannel.id}`, - MESSAGING_GMAIL_USERS_MESSAGES_GET_BATCH_SIZE, - ); + const connectedAccountWithFreshTokens = { + ...connectedAccount, + accessToken, + refreshToken, + }; - if (!messageIdsToFetch?.length) { - await this.messageChannelSyncStatusService.markAsCompletedAndMarkAsMessagesListFetchPending( - [messageChannel.id], - workspaceId, - ); + await this.emailAliasManagerService.refreshHandleAliases( + connectedAccountWithFreshTokens, + workspaceId, + ); - return await this.trackMessageImportCompleted( - messageChannel, - workspaceId, - ); - } + messageIdsToFetch = await this.cacheStorage.setPop( + `messages-to-import:${workspaceId}:${messageChannel.id}`, + MESSAGING_GMAIL_USERS_MESSAGES_GET_BATCH_SIZE, + ); - const allMessages = await this.messagingGetMessagesService.getMessages( - messageIdsToFetch, - connectedAccountWithFreshTokens, - ); + if (!messageIdsToFetch?.length) { + await this.messageChannelSyncStatusService.markAsCompletedAndMarkAsMessagesListFetchPending( + [messageChannel.id], + workspaceId, + ); - const blocklist = await this.blocklistRepository.getByWorkspaceMemberId( - connectedAccountWithFreshTokens.accountOwnerId, - workspaceId, - ); + return await this.trackMessageImportCompleted( + messageChannel, + workspaceId, + ); + } - if (!isDefined(messageChannel.handle)) { - throw new MessageImportDriverException( - 'Message channel handle is required', - MessageImportDriverExceptionCode.CHANNEL_MISCONFIGURED, - ); - } + const allMessages = + await this.messagingGetMessagesService.getMessages( + messageIdsToFetch, + connectedAccountWithFreshTokens, + ); - if (!isDefined(connectedAccountWithFreshTokens.handleAliases)) { - throw new MessageImportDriverException( - 'Message channel handle is required', - MessageImportDriverExceptionCode.CHANNEL_MISCONFIGURED, - ); - } + const blocklist = + await this.blocklistRepository.getByWorkspaceMemberId( + connectedAccountWithFreshTokens.accountOwnerId, + workspaceId, + ); - const messagesToSave = filterEmails( - messageChannel.handle, - [...connectedAccountWithFreshTokens.handleAliases.split(',')], - allMessages, - blocklist - .map((blocklistItem) => blocklistItem.handle) - .filter(isDefined), - messageChannel.excludeGroupEmails, - ); + if (!isDefined(messageChannel.handle)) { + throw new MessageImportDriverException( + 'Message channel handle is required', + MessageImportDriverExceptionCode.CHANNEL_MISCONFIGURED, + ); + } - if (messagesToSave.length > 0) { - await this.saveMessagesAndEnqueueContactCreationService.saveMessagesAndEnqueueContactCreation( - messagesToSave, - messageChannel, - connectedAccountWithFreshTokens, - workspaceId, - ); - } + if (!isDefined(connectedAccountWithFreshTokens.handleAliases)) { + throw new MessageImportDriverException( + 'Message channel handle is required', + MessageImportDriverExceptionCode.CHANNEL_MISCONFIGURED, + ); + } - if ( - messageIdsToFetch.length < MESSAGING_GMAIL_USERS_MESSAGES_GET_BATCH_SIZE - ) { - await this.messageChannelSyncStatusService.markAsCompletedAndMarkAsMessagesListFetchPending( - [messageChannel.id], - workspaceId, - ); - } else { - await this.messageChannelSyncStatusService.markAsMessagesImportPending( - [messageChannel.id], - workspaceId, - ); - } + const messagesToSave = filterEmails( + messageChannel.handle, + [...connectedAccountWithFreshTokens.handleAliases.split(',')], + allMessages, + blocklist + .map((blocklistItem) => blocklistItem.handle) + .filter(isDefined), + messageChannel.excludeGroupEmails, + ); - const messageChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'messageChannel', - ); + if (messagesToSave.length > 0) { + await this.saveMessagesAndEnqueueContactCreationService.saveMessagesAndEnqueueContactCreation( + messagesToSave, + messageChannel, + connectedAccountWithFreshTokens, + workspaceId, + ); + } - await messageChannelRepository.update( - { - id: messageChannel.id, - }, - { - throttleFailureCount: 0, - syncStageStartedAt: null, - }, - ); + if ( + messageIdsToFetch.length < + MESSAGING_GMAIL_USERS_MESSAGES_GET_BATCH_SIZE + ) { + await this.messageChannelSyncStatusService.markAsCompletedAndMarkAsMessagesListFetchPending( + [messageChannel.id], + workspaceId, + ); + } else { + await this.messageChannelSyncStatusService.markAsMessagesImportPending( + [messageChannel.id], + workspaceId, + ); + } - return await this.trackMessageImportCompleted( - messageChannel, - workspaceId, - ); - } catch (error) { - // TODO: remove this log once we catch better the error codes - this.logger.error( - `Error (${error.code}) importing messages for workspace ${workspaceId.slice(0, 8)} and account ${connectedAccount.id.slice(0, 8)}: ${error.message} - ${error.body}`, - ); - await this.cacheStorage.setAdd( - `messages-to-import:${workspaceId}:${messageChannel.id}`, - messageIdsToFetch, - ); + const messageChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'messageChannel', + ); - await this.messageImportErrorHandlerService.handleDriverException( - error, - MessageImportSyncStep.MESSAGES_IMPORT_ONGOING, - messageChannel, - workspaceId, - ); + await messageChannelRepository.update( + { + id: messageChannel.id, + }, + { + throttleFailureCount: 0, + syncStageStartedAt: null, + }, + ); - return await this.trackMessageImportCompleted( - messageChannel, - workspaceId, - ); - } + return await this.trackMessageImportCompleted( + messageChannel, + workspaceId, + ); + } catch (error) { + this.logger.error( + `Error (${error.code}) importing messages for workspace ${workspaceId.slice(0, 8)} and account ${connectedAccount.id.slice(0, 8)}: ${error.message} - ${error.body}`, + ); + await this.cacheStorage.setAdd( + `messages-to-import:${workspaceId}:${messageChannel.id}`, + messageIdsToFetch, + ); + + await this.messageImportErrorHandlerService.handleDriverException( + error, + MessageImportSyncStep.MESSAGES_IMPORT_ONGOING, + messageChannel, + workspaceId, + ); + + return await this.trackMessageImportCompleted( + messageChannel, + workspaceId, + ); + } + }, + ); } private async trackMessageImportCompleted( diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-process-folder-actions.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-process-folder-actions.service.ts index 518eb138b86..66204478f7d 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-process-folder-actions.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-process-folder-actions.service.ts @@ -4,7 +4,8 @@ import { isDefined } from 'twenty-shared/utils'; import { In } from 'typeorm'; import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; import { MessageFolderPendingSyncAction, @@ -19,7 +20,7 @@ export class MessagingProcessFolderActionsService { ); constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly messagingDeleteFolderMessagesService: MessagingDeleteFolderMessagesService, ) {} @@ -86,41 +87,48 @@ export class MessagingProcessFolderActionsService { } if (processedFolderIds.length > 0 || folderIdsToDelete.length > 0) { - const workspaceDataSource = - await this.twentyORMGlobalManager.getDataSourceForWorkspace({ - workspaceId, - }); + const authContext = buildSystemAuthContext(workspaceId); - await workspaceDataSource?.transaction( - async (transactionManager: WorkspaceEntityManager) => { - const messageFolderRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workspaceDataSource = + await this.globalWorkspaceOrmManager.getDataSourceForWorkspace( workspaceId, - 'messageFolder', ); - if (processedFolderIds.length > 0) { - await messageFolderRepository.update( - { id: In(processedFolderIds) }, - { pendingSyncAction: MessageFolderPendingSyncAction.NONE }, - transactionManager, - ); + await workspaceDataSource?.transaction( + async (transactionManager: WorkspaceEntityManager) => { + const messageFolderRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'messageFolder', + ); - this.logger.log( - `WorkspaceId: ${workspaceId}, MessageChannelId: ${messageChannel.id} - Reset pendingSyncAction to NONE for ${processedFolderIds.length} folders`, - ); - } + if (processedFolderIds.length > 0) { + await messageFolderRepository.update( + { id: In(processedFolderIds) }, + { pendingSyncAction: MessageFolderPendingSyncAction.NONE }, + transactionManager, + ); - if (folderIdsToDelete.length > 0) { - await messageFolderRepository.delete( - { id: In(folderIdsToDelete) }, - transactionManager, - ); + this.logger.log( + `WorkspaceId: ${workspaceId}, MessageChannelId: ${messageChannel.id} - Reset pendingSyncAction to NONE for ${processedFolderIds.length} folders`, + ); + } - this.logger.log( - `WorkspaceId: ${workspaceId}, MessageChannelId: ${messageChannel.id} - Deleted ${folderIdsToDelete.length} folders`, - ); - } + if (folderIdsToDelete.length > 0) { + await messageFolderRepository.delete( + { id: In(folderIdsToDelete) }, + transactionManager, + ); + + this.logger.log( + `WorkspaceId: ${workspaceId}, MessageChannelId: ${messageChannel.id} - Deleted ${folderIdsToDelete.length} folders`, + ); + } + }, + ); }, ); } diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-process-group-email-actions.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-process-group-email-actions.service.ts index 4a7503d0ba4..c93129dc4b4 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-process-group-email-actions.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-process-group-email-actions.service.ts @@ -3,7 +3,8 @@ import { Injectable, Logger } from '@nestjs/common'; import { isDefined } from 'twenty-shared/utils'; import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { MessageChannelPendingGroupEmailsAction, MessageChannelWorkspaceEntity, @@ -18,7 +19,7 @@ export class MessagingProcessGroupEmailActionsService { ); constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly messagingDeleteGroupEmailMessagesService: MessagingDeleteGroupEmailMessagesService, ) {} @@ -27,19 +28,26 @@ export class MessagingProcessGroupEmailActionsService { workspaceId: string, pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction, ): Promise { - const messageChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'messageChannel', - ); + const authContext = buildSystemAuthContext(workspaceId); - await messageChannelRepository.update( - { id: messageChannel.id }, - { pendingGroupEmailsAction }, - ); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const messageChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'messageChannel', + ); - this.logger.log( - `WorkspaceId: ${workspaceId}, MessageChannelId: ${messageChannel.id} - Marked message channel as pending group emails action: ${pendingGroupEmailsAction}`, + await messageChannelRepository.update( + { id: messageChannel.id }, + { pendingGroupEmailsAction }, + ); + + this.logger.log( + `WorkspaceId: ${workspaceId}, MessageChannelId: ${messageChannel.id} - Marked message channel as pending group emails action: ${pendingGroupEmailsAction}`, + ); + }, ); } @@ -60,56 +68,63 @@ export class MessagingProcessGroupEmailActionsService { `WorkspaceId: ${workspaceId}, MessageChannelId: ${messageChannel.id} - Processing group email action: ${pendingGroupEmailsAction}`, ); - const workspaceDataSource = - await this.twentyORMGlobalManager.getDataSourceForWorkspace({ - workspaceId, - }); + const authContext = buildSystemAuthContext(workspaceId); - await workspaceDataSource?.transaction( - async (transactionManager: WorkspaceEntityManager) => { - try { - const messageChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'messageChannel', - ); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workspaceDataSource = + await this.globalWorkspaceOrmManager.getDataSourceForWorkspace( + workspaceId, + ); - switch (pendingGroupEmailsAction) { - case MessageChannelPendingGroupEmailsAction.GROUP_EMAILS_DELETION: - await this.handleGroupEmailsDeletion( - workspaceId, - messageChannel.id, + await workspaceDataSource?.transaction( + async (transactionManager: WorkspaceEntityManager) => { + try { + const messageChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'messageChannel', + ); + + switch (pendingGroupEmailsAction) { + case MessageChannelPendingGroupEmailsAction.GROUP_EMAILS_DELETION: + await this.handleGroupEmailsDeletion( + workspaceId, + messageChannel.id, + transactionManager, + ); + break; + case MessageChannelPendingGroupEmailsAction.GROUP_EMAILS_IMPORT: + await this.handleGroupEmailsImport( + workspaceId, + messageChannel.id, + transactionManager, + ); + break; + } + + await messageChannelRepository.update( + { id: messageChannel.id }, + { + pendingGroupEmailsAction: + MessageChannelPendingGroupEmailsAction.NONE, + }, transactionManager, ); - break; - case MessageChannelPendingGroupEmailsAction.GROUP_EMAILS_IMPORT: - await this.handleGroupEmailsImport( - workspaceId, - messageChannel.id, - transactionManager, + + this.logger.log( + `WorkspaceId: ${workspaceId}, MessageChannelId: ${messageChannel.id} - Reset pendingGroupEmailsAction to NONE`, ); - break; - } - - await messageChannelRepository.update( - { id: messageChannel.id }, - { - pendingGroupEmailsAction: - MessageChannelPendingGroupEmailsAction.NONE, - }, - transactionManager, - ); - - this.logger.log( - `WorkspaceId: ${workspaceId}, MessageChannelId: ${messageChannel.id} - Reset pendingGroupEmailsAction to NONE`, - ); - } catch (error) { - this.logger.error( - `WorkspaceId: ${workspaceId}, MessageChannelId: ${messageChannel.id} - Error processing group email action: ${error.message}`, - error.stack, - ); - throw error; - } + } catch (error) { + this.logger.error( + `WorkspaceId: ${workspaceId}, MessageChannelId: ${messageChannel.id} - Error processing group email action: ${error.message}`, + error.stack, + ); + throw error; + } + }, + ); }, ); } @@ -161,7 +176,7 @@ export class MessagingProcessGroupEmailActionsService { transactionManager: WorkspaceEntityManager; }) { const messageChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( + await this.globalWorkspaceOrmManager.getRepository( workspaceId, 'messageChannel', ); @@ -175,7 +190,7 @@ export class MessagingProcessGroupEmailActionsService { ); const messageFolderRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( + await this.globalWorkspaceOrmManager.getRepository( workspaceId, 'messageFolder', ); diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-save-messages-and-enqueue-contact-creation.service.spec.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-save-messages-and-enqueue-contact-creation.service.spec.ts index ccc804670be..fd890b6fae4 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-save-messages-and-enqueue-contact-creation.service.spec.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-save-messages-and-enqueue-contact-creation.service.spec.ts @@ -7,7 +7,7 @@ import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queu import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; import { getQueueToken } from 'src/engine/core-modules/message-queue/utils/get-queue-token.util'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { CreateCompanyAndContactJob } from 'src/modules/contact-creation-manager/jobs/create-company-and-contact.job'; import { MessageDirection } from 'src/modules/messaging/common/enums/message-direction.enum'; @@ -152,11 +152,14 @@ describe('MessagingSaveMessagesAndEnqueueContactCreationService', () => { }, }, { - provide: TwentyORMGlobalManager, + provide: GlobalWorkspaceOrmManager, useValue: { getDataSourceForWorkspace: jest .fn() .mockResolvedValue(datasourceInstance), + executeInWorkspaceContext: jest + .fn() + .mockImplementation((_authContext: any, fn: () => any) => fn()), }, }, ], diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-save-messages-and-enqueue-contact-creation.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-save-messages-and-enqueue-contact-creation.service.ts index bffa6656d43..04e4dd9c483 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-save-messages-and-enqueue-contact-creation.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-save-messages-and-enqueue-contact-creation.service.ts @@ -6,7 +6,8 @@ import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decora import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { CreateCompanyAndContactJob, @@ -32,7 +33,7 @@ export class MessagingSaveMessagesAndEnqueueContactCreationService { private readonly messageQueueService: MessageQueueService, private readonly messageService: MessagingMessageService, private readonly messageParticipantService: MessagingMessageParticipantService, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, ) {} async saveMessagesAndEnqueueContactCreation( @@ -42,76 +43,88 @@ export class MessagingSaveMessagesAndEnqueueContactCreationService { workspaceId: string, ) { const handleAliases = connectedAccount.handleAliases?.split(',') || []; + const authContext = buildSystemAuthContext(workspaceId); - const workspaceDataSource = - await this.twentyORMGlobalManager.getDataSourceForWorkspace({ - workspaceId, - }); + const participantsWithMessageId = + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workspaceDataSource = + await this.globalWorkspaceOrmManager.getDataSourceForWorkspace( + workspaceId, + ); - const participantsWithMessageId = await workspaceDataSource?.transaction( - async (transactionManager: WorkspaceEntityManager) => { - const { messageExternalIdsAndIdsMap } = - await this.messageService.saveMessagesWithinTransaction( - messagesToSave, - messageChannel.id, - transactionManager, - workspaceId, + return workspaceDataSource?.transaction( + async (transactionManager: WorkspaceEntityManager) => { + const { messageExternalIdsAndIdsMap } = + await this.messageService.saveMessagesWithinTransaction( + messagesToSave, + messageChannel.id, + transactionManager, + workspaceId, + ); + + const participantsWithMessageId: (ParticipantWithMessageId & { + shouldCreateContact: boolean; + })[] = messagesToSave.flatMap((message) => { + const messageId = messageExternalIdsAndIdsMap.get( + message.externalId, + ); + + return messageId + ? message.participants.map((participant: Participant) => { + const fromHandle = + message.participants.find( + (p) => p.role === MessageParticipantRole.FROM, + )?.handle || ''; + + const isMessageSentByConnectedAccount = + handleAliases.includes(fromHandle) || + fromHandle === connectedAccount.handle; + + const isParticipantConnectedAccount = + handleAliases.includes(participant.handle) || + participant.handle === connectedAccount.handle; + + const isExcludedByNonProfessionalEmails = + messageChannel.excludeNonProfessionalEmails && + !isWorkEmail(participant.handle); + + const shouldCreateContact = + !!participant.handle && + !isParticipantConnectedAccount && + !isExcludedByNonProfessionalEmails && + (messageChannel.contactAutoCreationPolicy === + MessageChannelContactAutoCreationPolicy.SENT_AND_RECEIVED || + (messageChannel.contactAutoCreationPolicy === + MessageChannelContactAutoCreationPolicy.SENT && + isMessageSentByConnectedAccount)); + + return { + ...participant, + messageId, + shouldCreateContact, + }; + }) + : []; + }); + + await this.messageParticipantService.saveMessageParticipants( + participantsWithMessageId, + workspaceId, + transactionManager, + ); + + return participantsWithMessageId; + }, ); + }, + ); - const participantsWithMessageId: (ParticipantWithMessageId & { - shouldCreateContact: boolean; - })[] = messagesToSave.flatMap((message) => { - const messageId = messageExternalIdsAndIdsMap.get(message.externalId); - - return messageId - ? message.participants.map((participant: Participant) => { - const fromHandle = - message.participants.find( - (p) => p.role === MessageParticipantRole.FROM, - )?.handle || ''; - - const isMessageSentByConnectedAccount = - handleAliases.includes(fromHandle) || - fromHandle === connectedAccount.handle; - - const isParticipantConnectedAccount = - handleAliases.includes(participant.handle) || - participant.handle === connectedAccount.handle; - - const isExcludedByNonProfessionalEmails = - messageChannel.excludeNonProfessionalEmails && - !isWorkEmail(participant.handle); - - const shouldCreateContact = - !!participant.handle && - !isParticipantConnectedAccount && - !isExcludedByNonProfessionalEmails && - (messageChannel.contactAutoCreationPolicy === - MessageChannelContactAutoCreationPolicy.SENT_AND_RECEIVED || - (messageChannel.contactAutoCreationPolicy === - MessageChannelContactAutoCreationPolicy.SENT && - isMessageSentByConnectedAccount)); - - return { - ...participant, - messageId, - shouldCreateContact, - }; - }) - : []; - }); - - await this.messageParticipantService.saveMessageParticipants( - participantsWithMessageId, - workspaceId, - transactionManager, - ); - - return participantsWithMessageId; - }, - ); - - if (messageChannel.isContactAutoCreationEnabled) { + if ( + messageChannel.isContactAutoCreationEnabled && + participantsWithMessageId + ) { const contactsToCreate = participantsWithMessageId.filter( (participant) => participant.shouldCreateContact, ); diff --git a/packages/twenty-server/src/modules/messaging/message-participant-manager/services/messaging-message-participant.service.ts b/packages/twenty-server/src/modules/messaging/message-participant-manager/services/messaging-message-participant.service.ts index 684e5507613..fa7a9635faf 100644 --- a/packages/twenty-server/src/modules/messaging/message-participant-manager/services/messaging-message-participant.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-participant-manager/services/messaging-message-participant.service.ts @@ -3,7 +3,8 @@ import { Injectable } from '@nestjs/common'; import { In } from 'typeorm'; import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { MatchParticipantService } from 'src/modules/match-participant/match-participant.service'; import { type MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity'; import { type ParticipantWithMessageId } from 'src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-message.type'; @@ -11,7 +12,7 @@ import { type ParticipantWithMessageId } from 'src/modules/messaging/message-imp @Injectable() export class MessagingMessageParticipantService { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly matchParticipantService: MatchParticipantService, ) {} @@ -20,55 +21,62 @@ export class MessagingMessageParticipantService { workspaceId: string, transactionManager?: WorkspaceEntityManager, ): Promise { - const messageParticipantRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'messageParticipant', - ); + const authContext = buildSystemAuthContext(workspaceId); - const existingParticipantsBasedOnMessageIds = - await messageParticipantRepository.find({ - where: { - messageId: In( - participants.map((participant) => participant.messageId), - ), - }, - }); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const messageParticipantRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'messageParticipant', + ); - const participantsToCreate: Pick< - MessageParticipantWorkspaceEntity, - 'messageId' | 'handle' | 'displayName' | 'role' - >[] = participants - .filter( - (participant) => - !existingParticipantsBasedOnMessageIds.find( - (existingParticipant) => - existingParticipant.messageId === participant.messageId && - existingParticipant.handle === participant.handle && - existingParticipant.displayName === participant.displayName && - existingParticipant.role === participant.role, - ), - ) - .map((participant) => { - return { - messageId: participant.messageId, - handle: participant.handle, - displayName: participant.displayName, - role: participant.role, - }; - }); + const existingParticipantsBasedOnMessageIds = + await messageParticipantRepository.find({ + where: { + messageId: In( + participants.map((participant) => participant.messageId), + ), + }, + }); - const createdParticipants = await messageParticipantRepository.insert( - participantsToCreate, - transactionManager, + const participantsToCreate: Pick< + MessageParticipantWorkspaceEntity, + 'messageId' | 'handle' | 'displayName' | 'role' + >[] = participants + .filter( + (participant) => + !existingParticipantsBasedOnMessageIds.find( + (existingParticipant) => + existingParticipant.messageId === participant.messageId && + existingParticipant.handle === participant.handle && + existingParticipant.displayName === participant.displayName && + existingParticipant.role === participant.role, + ), + ) + .map((participant) => { + return { + messageId: participant.messageId, + handle: participant.handle, + displayName: participant.displayName, + role: participant.role, + }; + }); + + const createdParticipants = await messageParticipantRepository.insert( + participantsToCreate, + transactionManager, + ); + + await this.matchParticipantService.matchParticipants({ + participants: createdParticipants.raw ?? [], + objectMetadataName: 'messageParticipant', + transactionManager, + matchWith: 'workspaceMemberAndPerson', + workspaceId, + }); + }, ); - - await this.matchParticipantService.matchParticipants({ - participants: createdParticipants.raw ?? [], - objectMetadataName: 'messageParticipant', - transactionManager, - matchWith: 'workspaceMemberAndPerson', - workspaceId, - }); } } diff --git a/packages/twenty-server/src/modules/messaging/monitoring/crons/jobs/messaging-message-channel-sync-status-monitoring.cron.job.ts b/packages/twenty-server/src/modules/messaging/monitoring/crons/jobs/messaging-message-channel-sync-status-monitoring.cron.job.ts index dfe1697d543..553f66d6f94 100644 --- a/packages/twenty-server/src/modules/messaging/monitoring/crons/jobs/messaging-message-channel-sync-status-monitoring.cron.job.ts +++ b/packages/twenty-server/src/modules/messaging/monitoring/crons/jobs/messaging-message-channel-sync-status-monitoring.cron.job.ts @@ -10,7 +10,8 @@ import { Process } from 'src/engine/core-modules/message-queue/decorators/proces import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; import { MessagingMonitoringService } from 'src/modules/messaging/monitoring/services/messaging-monitoring.service'; @@ -23,7 +24,7 @@ export class MessagingMessageChannelSyncStatusMonitoringCronJob { @InjectRepository(WorkspaceEntity) private readonly workspaceRepository: Repository, private readonly messagingMonitoringService: MessagingMonitoringService, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly exceptionHandlerService: ExceptionHandlerService, ) {} @@ -46,29 +47,36 @@ export class MessagingMessageChannelSyncStatusMonitoringCronJob { for (const activeWorkspace of activeWorkspaces) { try { - const messageChannelRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - activeWorkspace.id, - 'messageChannel', - ); - const messageChannels = await messageChannelRepository.find({ - select: ['id', 'syncStatus', 'connectedAccountId'], - }); + const authContext = buildSystemAuthContext(activeWorkspace.id); - for (const messageChannel of messageChannels) { - if (!messageChannel.syncStatus) { - continue; - } - await this.messagingMonitoringService.track({ - eventName: `message_channel.monitoring.sync_status.${snakeCase( - messageChannel.syncStatus, - )}`, - workspaceId: activeWorkspace.id, - connectedAccountId: messageChannel.connectedAccountId, - messageChannelId: messageChannel.id, - message: messageChannel.syncStatus, - }); - } + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const messageChannelRepository = + await this.globalWorkspaceOrmManager.getRepository( + activeWorkspace.id, + 'messageChannel', + ); + const messageChannels = await messageChannelRepository.find({ + select: ['id', 'syncStatus', 'connectedAccountId'], + }); + + for (const messageChannel of messageChannels) { + if (!messageChannel.syncStatus) { + continue; + } + await this.messagingMonitoringService.track({ + eventName: `message_channel.monitoring.sync_status.${snakeCase( + messageChannel.syncStatus, + )}`, + workspaceId: activeWorkspace.id, + connectedAccountId: messageChannel.connectedAccountId, + messageChannelId: messageChannel.id, + message: messageChannel.syncStatus, + }); + } + }, + ); } catch (error) { this.exceptionHandlerService.captureExceptions([error], { workspace: { diff --git a/packages/twenty-server/src/modules/timeline/jobs/upsert-timeline-activity-from-internal-event.job.ts b/packages/twenty-server/src/modules/timeline/jobs/upsert-timeline-activity-from-internal-event.job.ts index c3f38031673..a287d552cc8 100644 --- a/packages/twenty-server/src/modules/timeline/jobs/upsert-timeline-activity-from-internal-event.job.ts +++ b/packages/twenty-server/src/modules/timeline/jobs/upsert-timeline-activity-from-internal-event.job.ts @@ -5,7 +5,8 @@ import { type ObjectRecordNonDestructiveEvent } from 'src/engine/core-modules/ev import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator'; import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type'; import { TimelineActivityService } from 'src/modules/timeline/services/timeline-activity.service'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; @@ -14,7 +15,7 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta export class UpsertTimelineActivityFromInternalEvent { constructor( private readonly timelineActivityService: TimelineActivityService, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, ) {} @Process(UpsertTimelineActivityFromInternalEvent.name) @@ -33,33 +34,40 @@ export class UpsertTimelineActivityFromInternalEvent { return; } - const workspaceMemberRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceEventBatch.workspaceId, - WorkspaceMemberWorkspaceEntity, - { - shouldBypassPermissionChecks: true, - }, - ); + const authContext = buildSystemAuthContext(workspaceEventBatch.workspaceId); - const userIds = workspaceEventBatch.events - .map((event) => event.userId) - .filter(isDefined); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workspaceMemberRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceEventBatch.workspaceId, + WorkspaceMemberWorkspaceEntity, + { + shouldBypassPermissionChecks: true, + }, + ); - const workspaceMembers = await workspaceMemberRepository.findBy({ - userId: In(userIds), - }); + const userIds = workspaceEventBatch.events + .map((event) => event.userId) + .filter(isDefined); - for (const eventData of workspaceEventBatch.events) { - const workspaceMember = workspaceMembers.find( - (workspaceMember) => workspaceMember.userId === eventData.userId, - ); + const workspaceMembers = await workspaceMemberRepository.findBy({ + userId: In(userIds), + }); - if (eventData.userId && workspaceMember) { - eventData.workspaceMemberId = workspaceMember.id; - } - } + for (const eventData of workspaceEventBatch.events) { + const workspaceMember = workspaceMembers.find( + (workspaceMember) => workspaceMember.userId === eventData.userId, + ); - await this.timelineActivityService.upsertEvents(workspaceEventBatch); + if (eventData.userId && workspaceMember) { + eventData.workspaceMemberId = workspaceMember.id; + } + } + + await this.timelineActivityService.upsertEvents(workspaceEventBatch); + }, + ); } } diff --git a/packages/twenty-server/src/modules/timeline/repositories/timeline-activity.repository.ts b/packages/twenty-server/src/modules/timeline/repositories/timeline-activity.repository.ts index af2a86cc20c..a7bb64ca30d 100644 --- a/packages/twenty-server/src/modules/timeline/repositories/timeline-activity.repository.ts +++ b/packages/twenty-server/src/modules/timeline/repositories/timeline-activity.repository.ts @@ -5,7 +5,8 @@ import { type ObjectRecord } from 'twenty-shared/types'; import { In, MoreThan } from 'typeorm'; import { objectRecordDiffMerge } from 'src/engine/core-modules/event-emitter/utils/object-record-diff-merge'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { type TimelineActivityPayload } from 'src/modules/timeline/types/timeline-activity-payload'; type TimelineActivityPayloadWorkspaceIdAndObjectSingularName = { @@ -19,7 +20,7 @@ type TimelineActivityPayloadWorkspaceIdAndObjectSingularName = { @Injectable() export class TimelineActivityRepository { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, ) {} async upsertTimelineActivities({ @@ -27,61 +28,73 @@ export class TimelineActivityRepository { workspaceId, payloads, }: TimelineActivityPayloadWorkspaceIdAndObjectSingularName) { - const recentTimelineActivities = await this.findRecentTimelineActivities({ - objectSingularName, - workspaceId, - payloads, - }); + const authContext = buildSystemAuthContext(workspaceId); - const payloadsWithDiff = payloads - .filter(({ properties }) => { - const isDiffEmpty = - properties.diff !== null && - properties.diff && - Object.keys(properties.diff).length === 0; + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const recentTimelineActivities = + await this.findRecentTimelineActivities({ + objectSingularName, + workspaceId, + payloads, + }); - return !isDiffEmpty; - }) - .map(({ properties, ...rest }) => ({ - ...rest, - properties: isDefined(properties.diff) ? { diff: properties.diff } : {}, - })); + const payloadsWithDiff = payloads + .filter(({ properties }) => { + const isDiffEmpty = + properties.diff !== null && + properties.diff && + Object.keys(properties.diff).length === 0; - const payloadsToInsert: TimelineActivityPayloadWorkspaceIdAndObjectSingularName['payloads'] = - []; + return !isDiffEmpty; + }) + .map(({ properties, ...rest }) => ({ + ...rest, + properties: isDefined(properties.diff) + ? { diff: properties.diff } + : {}, + })); - for (const payload of payloadsWithDiff) { - const recentTimelineActivity = recentTimelineActivities.find( - (timelineActivity) => - timelineActivity[`${objectSingularName}Id`] === payload.recordId && - timelineActivity.workspaceMemberId === payload.workspaceMemberId && - (!isDefined(payload.linkedRecordId) || - timelineActivity.linkedRecordId === payload.linkedRecordId) && - timelineActivity.name === payload.name, - ); + const payloadsToInsert: TimelineActivityPayloadWorkspaceIdAndObjectSingularName['payloads'] = + []; - if (recentTimelineActivity) { - const mergedProperties = objectRecordDiffMerge( - recentTimelineActivity.properties, - payload.properties, - ); + for (const payload of payloadsWithDiff) { + const recentTimelineActivity = recentTimelineActivities.find( + (timelineActivity) => + timelineActivity[`${objectSingularName}Id`] === + payload.recordId && + timelineActivity.workspaceMemberId === + payload.workspaceMemberId && + (!isDefined(payload.linkedRecordId) || + timelineActivity.linkedRecordId === payload.linkedRecordId) && + timelineActivity.name === payload.name, + ); - await this.updateTimelineActivity({ - id: recentTimelineActivity.id, - properties: mergedProperties, - workspaceMemberId: payload.workspaceMemberId, + if (recentTimelineActivity) { + const mergedProperties = objectRecordDiffMerge( + recentTimelineActivity.properties, + payload.properties, + ); + + await this.updateTimelineActivity({ + id: recentTimelineActivity.id, + properties: mergedProperties, + workspaceMemberId: payload.workspaceMemberId, + workspaceId, + }); + } else { + payloadsToInsert.push(payload); + } + } + + await this.insertTimelineActivities({ + objectSingularName, + payloads: payloadsToInsert, workspaceId, }); - } else { - payloadsToInsert.push(payload); - } - } - - await this.insertTimelineActivities({ - objectSingularName, - payloads: payloadsToInsert, - workspaceId, - }); + }, + ); } private async findRecentTimelineActivities({ @@ -90,7 +103,7 @@ export class TimelineActivityRepository { payloads, }: TimelineActivityPayloadWorkspaceIdAndObjectSingularName) { const timelineActivityTypeORMRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( + await this.globalWorkspaceOrmManager.getRepository( workspaceId, 'timelineActivity', { @@ -128,7 +141,7 @@ export class TimelineActivityRepository { } const timelineActivityTypeORMRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( + await this.globalWorkspaceOrmManager.getRepository( workspaceId, 'timelineActivity', { @@ -161,7 +174,7 @@ export class TimelineActivityRepository { workspaceId: string; }) { const timelineActivityTypeORMRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( + await this.globalWorkspaceOrmManager.getRepository( workspaceId, 'timelineActivity', { diff --git a/packages/twenty-server/src/modules/timeline/services/timeline-activity.service.ts b/packages/twenty-server/src/modules/timeline/services/timeline-activity.service.ts index 6f684eed443..41a25f52f6f 100644 --- a/packages/twenty-server/src/modules/timeline/services/timeline-activity.service.ts +++ b/packages/twenty-server/src/modules/timeline/services/timeline-activity.service.ts @@ -8,7 +8,8 @@ import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/work import { type ObjectRecordBaseEvent } from 'src/engine/core-modules/event-emitter/types/object-record.base.event'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type'; import { parseEventNameOrThrow } from 'src/engine/workspace-event-emitter/utils/parse-event-name'; import { NoteWorkspaceEntity } from 'src/modules/note/standard-objects/note.workspace-entity'; @@ -24,7 +25,7 @@ export class TimelineActivityService { constructor( @InjectObjectMetadataRepository(TimelineActivityWorkspaceEntity) private readonly timelineActivityRepository: TimelineActivityRepository, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, ) {} @@ -177,21 +178,29 @@ export class TimelineActivityService { const { action } = parseEventNameOrThrow(name); - const activityTargetRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - this.targetObjects[activityType], - { - shouldBypassPermissionChecks: true, + const authContext = buildSystemAuthContext(workspaceId); + + const activityTargets = + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const activityTargetRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + this.targetObjects[activityType], + { + shouldBypassPermissionChecks: true, + }, + ); + + return activityTargetRepository.find({ + where: { + [`${activityType}Id`]: In(events.map((event) => event.recordId)), + }, + }); }, ); - const activityTargets = await activityTargetRepository.find({ - where: { - [`${activityType}Id`]: In(events.map((event) => event.recordId)), - }, - }); - if (activityTargets.length === 0) { return []; } @@ -254,30 +263,38 @@ export class TimelineActivityService { }): Promise { const { action } = parseEventNameOrThrow(name); - const activityRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - activityType, - { - shouldBypassPermissionChecks: true, + const authContext = buildSystemAuthContext(workspaceId); + + const activities = + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const activityRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + activityType, + { + shouldBypassPermissionChecks: true, + }, + ); + + return activityRepository.find({ + where: { + id: In( + events + .map((event) => + this.extractActivityIdFromActivityTargetEvent( + event, + activityType, + ), + ) + .filter(isDefined), + ), + }, + }); }, ); - const activities = await activityRepository.find({ - where: { - id: In( - events - .map((event) => - this.extractActivityIdFromActivityTargetEvent( - event, - activityType, - ), - ) - .filter(isDefined), - ), - }, - }); - if (activities.length === 0) { return []; } diff --git a/packages/twenty-server/src/modules/workflow/common/query-hooks/workflow-create-many.post-query.hook.ts b/packages/twenty-server/src/modules/workflow/common/query-hooks/workflow-create-many.post-query.hook.ts index 3f23693ece5..84b2f501790 100644 --- a/packages/twenty-server/src/modules/workflow/common/query-hooks/workflow-create-many.post-query.hook.ts +++ b/packages/twenty-server/src/modules/workflow/common/query-hooks/workflow-create-many.post-query.hook.ts @@ -1,18 +1,19 @@ import { assertIsDefinedOrThrow } from 'twenty-shared/utils'; import { type WorkspacePostQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface'; +import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface'; import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator'; import { WorkspaceQueryHookType } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type'; import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { WorkflowVersionStatus, type WorkflowVersionWorkspaceEntity, } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity'; import { type WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity'; -import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception'; @WorkspaceQueryHook({ key: `workflow.createMany`, @@ -22,7 +23,7 @@ export class WorkflowCreateManyPostQueryHook implements WorkspacePostQueryHookInstance { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly recordPositionService: RecordPositionService, ) {} @@ -35,32 +36,37 @@ export class WorkflowCreateManyPostQueryHook assertIsDefinedOrThrow(workspace, WorkspaceNotFoundDefaultError); - const workflowVersionRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspace.id, - 'workflowVersion', - ); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext as WorkspaceAuthContext, + async () => { + const workflowVersionRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspace.id, + 'workflowVersion', + ); - const position = await this.recordPositionService.buildRecordPosition({ - value: 'first', - objectMetadata: { - isCustom: false, - nameSingular: 'workflowVersion', + const position = await this.recordPositionService.buildRecordPosition({ + value: 'first', + objectMetadata: { + isCustom: false, + nameSingular: 'workflowVersion', + }, + workspaceId: workspace.id, + }); + + const workflowVersionsToCreate = payload.map((workflow) => ({ + workflowId: workflow.id, + status: WorkflowVersionStatus.DRAFT, + name: 'v1', + position, + })); + + await Promise.all( + workflowVersionsToCreate.map((workflowVersion) => { + return workflowVersionRepository.insert(workflowVersion); + }), + ); }, - workspaceId: workspace.id, - }); - - const workflowVersionsToCreate = payload.map((workflow) => ({ - workflowId: workflow.id, - status: WorkflowVersionStatus.DRAFT, - name: 'v1', - position, - })); - - await Promise.all( - workflowVersionsToCreate.map((workflowVersion) => { - return workflowVersionRepository.insert(workflowVersion); - }), ); } } diff --git a/packages/twenty-server/src/modules/workflow/common/query-hooks/workflow-create-one.post-query.hook.ts b/packages/twenty-server/src/modules/workflow/common/query-hooks/workflow-create-one.post-query.hook.ts index ee2ed9c7b07..566d2bf7232 100644 --- a/packages/twenty-server/src/modules/workflow/common/query-hooks/workflow-create-one.post-query.hook.ts +++ b/packages/twenty-server/src/modules/workflow/common/query-hooks/workflow-create-one.post-query.hook.ts @@ -1,18 +1,19 @@ import { assertIsDefinedOrThrow } from 'twenty-shared/utils'; import { type WorkspacePostQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface'; +import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface'; import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator'; import { WorkspaceQueryHookType } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type'; import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { WorkflowVersionStatus, type WorkflowVersionWorkspaceEntity, } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity'; import { type WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity'; -import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception'; @WorkspaceQueryHook({ key: `workflow.createOne`, @@ -22,7 +23,7 @@ export class WorkflowCreateOnePostQueryHook implements WorkspacePostQueryHookInstance { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly recordPositionService: RecordPositionService, ) {} @@ -37,26 +38,31 @@ export class WorkflowCreateOnePostQueryHook const workflow = payload[0]; - const workflowVersionRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspace.id, - 'workflowVersion', - ); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext as WorkspaceAuthContext, + async () => { + const workflowVersionRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspace.id, + 'workflowVersion', + ); - const position = await this.recordPositionService.buildRecordPosition({ - value: 'first', - objectMetadata: { - isCustom: false, - nameSingular: 'workflowVersion', + const position = await this.recordPositionService.buildRecordPosition({ + value: 'first', + objectMetadata: { + isCustom: false, + nameSingular: 'workflowVersion', + }, + workspaceId: workspace.id, + }); + + await workflowVersionRepository.insert({ + workflowId: workflow.id, + status: WorkflowVersionStatus.DRAFT, + name: 'v1', + position, + }); }, - workspaceId: workspace.id, - }); - - await workflowVersionRepository.insert({ - workflowId: workflow.id, - status: WorkflowVersionStatus.DRAFT, - name: 'v1', - position, - }); + ); } } diff --git a/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-common.workspace-service.ts b/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-common.workspace-service.ts index dc10507df3c..ed44369c088 100644 --- a/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-common.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-common.workspace-service.ts @@ -2,14 +2,15 @@ import { Injectable } from '@nestjs/common'; import { isDefined } from 'twenty-shared/utils'; -import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util'; import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { WorkflowCommonException, WorkflowCommonExceptionCode, @@ -39,7 +40,7 @@ export type ObjectMetadataInfo = { @Injectable() export class WorkflowCommonWorkspaceService { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly serverlessFunctionService: ServerlessFunctionService, private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, ) {} @@ -58,20 +59,27 @@ export class WorkflowCommonWorkspaceService { ); } - const workflowVersionRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'workflowVersion', - { shouldBypassPermissionChecks: true }, // settings permissions are checked at resolver-level - ); + const authContext = buildSystemAuthContext(workspaceId); - const workflowVersion = await workflowVersionRepository.findOne({ - where: { - id: workflowVersionId, + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workflowVersionRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'workflowVersion', + { shouldBypassPermissionChecks: true }, + ); + + const workflowVersion = await workflowVersionRepository.findOne({ + where: { + id: workflowVersionId, + }, + }); + + return this.getValidWorkflowVersionOrFail(workflowVersion); }, - }); - - return this.getValidWorkflowVersionOrFail(workflowVersion); + ); } async getValidWorkflowVersionOrFail( @@ -84,14 +92,6 @@ export class WorkflowCommonWorkspaceService { ); } - // FIXME: For now we will make the trigger optional. Later, we'll have to ensure the trigger is defined when publishing the flow. - // if (!workflowVersion.trigger) { - // throw new WorkflowTriggerException( - // 'Workflow version does not contains trigger', - // WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION, - // ); - // } - return { ...workflowVersion, trigger: workflowVersion.trigger }; } @@ -163,73 +163,80 @@ export class WorkflowCommonWorkspaceService { workspaceId: string; operation: 'restore' | 'delete' | 'destroy'; }): Promise { - const workflowVersionRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'workflowVersion', - { shouldBypassPermissionChecks: true }, // settings permissions are checked at resolver-level - ); + const authContext = buildSystemAuthContext(workspaceId); - const workflowRunRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'workflowRun', - { shouldBypassPermissionChecks: true }, // settings permissions are checked at resolver-level - ); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workflowVersionRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'workflowVersion', + { shouldBypassPermissionChecks: true }, + ); - const workflowAutomatedTriggerRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'workflowAutomatedTrigger', - { shouldBypassPermissionChecks: true }, // settings permissions are checked at resolver-level - ); + const workflowRunRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'workflowRun', + { shouldBypassPermissionChecks: true }, + ); - for (const workflowId of workflowIds) { - switch (operation) { - case 'delete': - await workflowAutomatedTriggerRepository.softDelete({ + const workflowAutomatedTriggerRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'workflowAutomatedTrigger', + { shouldBypassPermissionChecks: true }, + ); + + for (const workflowId of workflowIds) { + switch (operation) { + case 'delete': + await workflowAutomatedTriggerRepository.softDelete({ + workflowId, + }); + + await workflowRunRepository.softDelete({ + workflowId, + }); + + await workflowVersionRepository.softDelete({ + workflowId, + }); + + break; + case 'restore': + await workflowAutomatedTriggerRepository.restore({ + workflowId, + }); + + await workflowRunRepository.restore({ + workflowId, + }); + + await workflowVersionRepository.restore({ + workflowId, + }); + + break; + } + + await this.deactivateVersionOnDelete({ + workflowVersionRepository, workflowId, + workspaceId, + operation, }); - await workflowRunRepository.softDelete({ + await this.handleServerlessFunctionSubEntities({ + workflowVersionRepository, workflowId, + workspaceId, + operation, }); - - await workflowVersionRepository.softDelete({ - workflowId, - }); - - break; - case 'restore': - await workflowAutomatedTriggerRepository.restore({ - workflowId, - }); - - await workflowRunRepository.restore({ - workflowId, - }); - - await workflowVersionRepository.restore({ - workflowId, - }); - - break; - } - - await this.deactivateVersionOnDelete({ - workflowVersionRepository, - workflowId, - workspaceId, - operation, - }); - - await this.handleServerlessFunctionSubEntities({ - workflowVersionRepository, - workflowId, - workspaceId, - operation, - }); - } + } + }, + ); } private async deactivateVersionOnDelete({ @@ -248,10 +255,10 @@ export class WorkflowCommonWorkspaceService { } const workflowRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( + await this.globalWorkspaceOrmManager.getRepository( workspaceId, 'workflow', - { shouldBypassPermissionChecks: true }, // settings permissions are checked at resolver-level + { shouldBypassPermissionChecks: true }, ); const workflow = await workflowRepository.findOne({ diff --git a/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-version-validation.workspace-service.ts b/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-version-validation.workspace-service.ts index 15ab8394108..cea81f7800d 100644 --- a/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-version-validation.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-version-validation.workspace-service.ts @@ -9,7 +9,8 @@ import { type UpdateOneResolverArgs, } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { WorkflowQueryValidationException, WorkflowQueryValidationExceptionCode, @@ -25,7 +26,7 @@ import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/work export class WorkflowVersionValidationWorkspaceService { constructor( private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, ) {} async validateWorkflowVersionForCreateOne( @@ -45,32 +46,38 @@ export class WorkflowVersionValidationWorkspaceService { ); } - const workflowVersionRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'workflowVersion', - { shouldBypassPermissionChecks: true }, // settings permissions are checked at resolver-level - ); + const authContext = buildSystemAuthContext(workspaceId); - const workflowAlreadyHasDraftVersion = - await workflowVersionRepository.exists({ - where: { - workflowId: payload.data.workflowId, - status: WorkflowVersionStatus.DRAFT, - // FIXME: soft-deleted rows selection will have to be improved globally - deletedAt: IsNull(), - }, - }); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workflowVersionRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'workflowVersion', + { shouldBypassPermissionChecks: true }, + ); - if (workflowAlreadyHasDraftVersion) { - throw new WorkflowQueryValidationException( - 'Cannot create multiple draft versions for the same workflow', - WorkflowQueryValidationExceptionCode.FORBIDDEN, - { - userFriendlyMessage: msg`Cannot create multiple draft versions for the same workflow`, - }, - ); - } + const workflowAlreadyHasDraftVersion = + await workflowVersionRepository.exists({ + where: { + workflowId: payload.data.workflowId, + status: WorkflowVersionStatus.DRAFT, + deletedAt: IsNull(), + }, + }); + + if (workflowAlreadyHasDraftVersion) { + throw new WorkflowQueryValidationException( + 'Cannot create multiple draft versions for the same workflow', + WorkflowQueryValidationExceptionCode.FORBIDDEN, + { + userFriendlyMessage: msg`Cannot create multiple draft versions for the same workflow`, + }, + ); + } + }, + ); } async validateWorkflowVersionForUpdateOne({ @@ -86,8 +93,6 @@ export class WorkflowVersionValidationWorkspaceService { workflowVersionId: payload.id, }); - // If the only field updated is the name, we can update the workflow version - // Otherwise, we need to assert that the workflow version is a draft if (!(Object.keys(payload.data).length === 1 && payload.data.name)) { assertWorkflowVersionIsDraft(workflowVersion); } @@ -123,29 +128,37 @@ export class WorkflowVersionValidationWorkspaceService { assertWorkflowVersionIsDraft(workflowVersion); - const workflowVersionRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'workflowVersion', - { shouldBypassPermissionChecks: true }, // settings permissions are checked at resolver-level - ); + const authContext = buildSystemAuthContext(workspaceId); - const otherWorkflowVersionsExist = await workflowVersionRepository.exists({ - where: { - workflowId: workflowVersion.workflowId, - deletedAt: IsNull(), - id: Not(workflowVersion.id), + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workflowVersionRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'workflowVersion', + { shouldBypassPermissionChecks: true }, + ); + + const otherWorkflowVersionsExist = + await workflowVersionRepository.exists({ + where: { + workflowId: workflowVersion.workflowId, + deletedAt: IsNull(), + id: Not(workflowVersion.id), + }, + }); + + if (!otherWorkflowVersionsExist) { + throw new WorkflowQueryValidationException( + 'The initial version of a workflow can not be deleted', + WorkflowQueryValidationExceptionCode.FORBIDDEN, + { + userFriendlyMessage: msg`The initial version of a workflow can not be deleted`, + }, + ); + } }, - }); - - if (!otherWorkflowVersionsExist) { - throw new WorkflowQueryValidationException( - 'The initial version of a workflow can not be deleted', - WorkflowQueryValidationExceptionCode.FORBIDDEN, - { - userFriendlyMessage: msg`The initial version of a workflow can not be deleted`, - }, - ); - } + ); } } diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-edge/__tests__/workflow-version-edge.workspace-service.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-edge/__tests__/workflow-version-edge.workspace-service.spec.ts index 26982a07e25..267ea7f9f9c 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-edge/__tests__/workflow-version-edge.workspace-service.spec.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-edge/__tests__/workflow-version-edge.workspace-service.spec.ts @@ -2,8 +2,8 @@ import { Test, type TestingModule } from '@nestjs/testing'; import { TRIGGER_STEP_ID } from 'twenty-shared/workflow'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { type WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity'; import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; import { WorkflowVersionEdgeWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-edge/workflow-version-edge.workspace-service'; @@ -73,7 +73,7 @@ const mockWorkflowVersion = { } as WorkflowVersionWorkspaceEntity; describe('WorkflowVersionEdgeWorkspaceService', () => { - let twentyORMGlobalManager: jest.Mocked; + let globalWorkspaceOrmManager: jest.Mocked; let workflowCommonWorkspaceService: jest.Mocked; let service: WorkflowVersionEdgeWorkspaceService; let mockWorkflowVersionWorkspaceRepository: MockWorkspaceRepository; @@ -88,11 +88,14 @@ describe('WorkflowVersionEdgeWorkspaceService', () => { mockWorkflowVersion, ); - twentyORMGlobalManager = { - getRepositoryForWorkspace: jest + globalWorkspaceOrmManager = { + executeInWorkspaceContext: jest + .fn() + .mockImplementation(async (_authContext, callback) => callback()), + getRepository: jest .fn() .mockResolvedValue(mockWorkflowVersionWorkspaceRepository), - } as unknown as jest.Mocked; + } as unknown as jest.Mocked; workflowCommonWorkspaceService = { getWorkflowVersionOrFail: jest @@ -104,8 +107,8 @@ describe('WorkflowVersionEdgeWorkspaceService', () => { providers: [ WorkflowVersionEdgeWorkspaceService, { - provide: TwentyORMGlobalManager, - useValue: twentyORMGlobalManager, + provide: GlobalWorkspaceOrmManager, + useValue: globalWorkspaceOrmManager, }, { provide: WorkflowCommonWorkspaceService, diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-edge/workflow-version-edge.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-edge/workflow-version-edge.workspace-service.ts index cbffeac99e3..221444fc1f1 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-edge/workflow-version-edge.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-edge/workflow-version-edge.workspace-service.ts @@ -4,8 +4,9 @@ import { isDefined } from 'twenty-shared/utils'; import { TRIGGER_STEP_ID } from 'twenty-shared/workflow'; import { type WorkflowVersionStepChangesDTO } from 'src/engine/core-modules/workflow/dtos/workflow-version-step-changes.dto'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { WorkflowVersionEdgeException, WorkflowVersionEdgeExceptionCode, @@ -24,7 +25,7 @@ import { type WorkflowTrigger } from 'src/modules/workflow/workflow-trigger/type @Injectable() export class WorkflowVersionEdgeWorkspaceService { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService, ) {} @@ -41,54 +42,61 @@ export class WorkflowVersionEdgeWorkspaceService { workspaceId: string; sourceConnectionOptions?: WorkflowStepConnectionOptions; }): Promise { - const workflowVersionRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'workflowVersion', - { shouldBypassPermissionChecks: true }, - ); + const authContext = buildSystemAuthContext(workspaceId); - const workflowVersion = - await this.workflowCommonWorkspaceService.getWorkflowVersionOrFail({ - workflowVersionId, - workspaceId, - }); + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workflowVersionRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'workflowVersion', + { shouldBypassPermissionChecks: true }, + ); - assertWorkflowVersionIsDraft(workflowVersion); + const workflowVersion = + await this.workflowCommonWorkspaceService.getWorkflowVersionOrFail({ + workflowVersionId, + workspaceId, + }); - const trigger = workflowVersion.trigger; - const steps = workflowVersion.steps || []; + assertWorkflowVersionIsDraft(workflowVersion); - const targetStep = steps.find((step) => step.id === target); + const trigger = workflowVersion.trigger; + const steps = workflowVersion.steps || []; - if (!isDefined(targetStep)) { - throw new WorkflowVersionEdgeException( - `Target step '${target}' not found in workflowVersion '${workflowVersionId}'`, - WorkflowVersionEdgeExceptionCode.NOT_FOUND, - ); - } + const targetStep = steps.find((step) => step.id === target); - const isSourceTrigger = source === TRIGGER_STEP_ID; + if (!isDefined(targetStep)) { + throw new WorkflowVersionEdgeException( + `Target step '${target}' not found in workflowVersion '${workflowVersionId}'`, + WorkflowVersionEdgeExceptionCode.NOT_FOUND, + ); + } - if (isSourceTrigger) { - return this.createTriggerEdge({ - trigger, - steps, - target, - workflowVersion, - workflowVersionRepository, - }); - } else { - return this.createStepEdge({ - trigger, - steps, - source, - target, - sourceConnectionOptions, - workflowVersion, - workflowVersionRepository, - }); - } + const isSourceTrigger = source === TRIGGER_STEP_ID; + + if (isSourceTrigger) { + return this.createTriggerEdge({ + trigger, + steps, + target, + workflowVersion, + workflowVersionRepository, + }); + } else { + return this.createStepEdge({ + trigger, + steps, + source, + target, + sourceConnectionOptions, + workflowVersion, + workflowVersionRepository, + }); + } + }, + ); } async deleteWorkflowVersionEdge({ @@ -104,54 +112,61 @@ export class WorkflowVersionEdgeWorkspaceService { workspaceId: string; sourceConnectionOptions?: WorkflowStepConnectionOptions; }): Promise { - const workflowVersionRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'workflowVersion', - { shouldBypassPermissionChecks: true }, - ); + const authContext = buildSystemAuthContext(workspaceId); - const workflowVersion = - await this.workflowCommonWorkspaceService.getWorkflowVersionOrFail({ - workflowVersionId, - workspaceId, - }); + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workflowVersionRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'workflowVersion', + { shouldBypassPermissionChecks: true }, + ); - assertWorkflowVersionIsDraft(workflowVersion); + const workflowVersion = + await this.workflowCommonWorkspaceService.getWorkflowVersionOrFail({ + workflowVersionId, + workspaceId, + }); - const trigger = workflowVersion.trigger; - const steps = workflowVersion.steps || []; + assertWorkflowVersionIsDraft(workflowVersion); - const targetStep = steps.find((step) => step.id === target); + const trigger = workflowVersion.trigger; + const steps = workflowVersion.steps || []; - if (!isDefined(targetStep)) { - throw new WorkflowVersionEdgeException( - `Target step '${target}' not found in workflowVersion '${workflowVersionId}'`, - WorkflowVersionEdgeExceptionCode.NOT_FOUND, - ); - } + const targetStep = steps.find((step) => step.id === target); - const isSourceTrigger = source === TRIGGER_STEP_ID; + if (!isDefined(targetStep)) { + throw new WorkflowVersionEdgeException( + `Target step '${target}' not found in workflowVersion '${workflowVersionId}'`, + WorkflowVersionEdgeExceptionCode.NOT_FOUND, + ); + } - if (isSourceTrigger) { - return this.deleteTriggerEdge({ - trigger, - steps, - target, - workflowVersion, - workflowVersionRepository, - }); - } else { - return this.deleteStepEdge({ - trigger, - steps, - source, - target, - workflowVersion, - workflowVersionRepository, - sourceConnectionOptions, - }); - } + const isSourceTrigger = source === TRIGGER_STEP_ID; + + if (isSourceTrigger) { + return this.deleteTriggerEdge({ + trigger, + steps, + target, + workflowVersion, + workflowVersionRepository, + }); + } else { + return this.deleteStepEdge({ + trigger, + steps, + source, + target, + workflowVersion, + workflowVersionRepository, + sourceConnectionOptions, + }); + } + }, + ); } private async createTriggerEdge({ diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/__tests__/workflow-version-step-operations.workspace-service.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/__tests__/workflow-version-step-operations.workspace-service.spec.ts index e95ca52b68c..ec14c30d533 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/__tests__/workflow-version-step-operations.workspace-service.spec.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/__tests__/workflow-version-step-operations.workspace-service.spec.ts @@ -8,7 +8,7 @@ import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-t import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { type ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; import { WorkflowVersionStepOperationsWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-operations.workspace-service'; @@ -21,7 +21,7 @@ const mockWorkspaceId = 'workspace-id'; describe('WorkflowVersionStepOperationsWorkspaceService', () => { let service: WorkflowVersionStepOperationsWorkspaceService; - let twentyORMGlobalManager: jest.Mocked; + let globalWorkspaceOrmManager: jest.Mocked; let serverlessFunctionService: jest.Mocked; let agentRepository: jest.Mocked; let roleTargetRepository: jest.Mocked; @@ -67,20 +67,19 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => { deleteAgentOnlyRoleIfUnused: jest.fn(), } as unknown as jest.Mocked; + globalWorkspaceOrmManager = { + getRepository: jest.fn(), + } as unknown as jest.Mocked; workspaceCacheService = { flush: jest.fn(), } as unknown as jest.Mocked; - twentyORMGlobalManager = { - getRepositoryForWorkspace: jest.fn(), - } as unknown as jest.Mocked; - const module: TestingModule = await Test.createTestingModule({ providers: [ WorkflowVersionStepOperationsWorkspaceService, { - provide: TwentyORMGlobalManager, - useValue: twentyORMGlobalManager, + provide: GlobalWorkspaceOrmManager, + useValue: globalWorkspaceOrmManager, }, { provide: ServerlessFunctionService, diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/__tests__/workflow-version-step.workspace-service.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/__tests__/workflow-version-step.workspace-service.spec.ts index 718f2fbad44..8d265c2ab28 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/__tests__/workflow-version-step.workspace-service.spec.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/__tests__/workflow-version-step.workspace-service.spec.ts @@ -2,17 +2,17 @@ import { Test, type TestingModule } from '@nestjs/testing'; import { TRIGGER_STEP_ID } from 'twenty-shared/workflow'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { type WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity'; import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; import { WorkflowSchemaWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service'; -import { WorkflowVersionStepOperationsWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-operations.workspace-service'; -import { WorkflowVersionStepWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step.workspace-service'; -import { WorkflowVersionStepHelpersWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-helpers.workspace-service'; import { WorkflowVersionStepCreationWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-creation.workspace-service'; -import { WorkflowVersionStepUpdateWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-update.workspace-service'; import { WorkflowVersionStepDeletionWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-deletion.workspace-service'; +import { WorkflowVersionStepHelpersWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-helpers.workspace-service'; +import { WorkflowVersionStepOperationsWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-operations.workspace-service'; +import { WorkflowVersionStepUpdateWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-update.workspace-service'; +import { WorkflowVersionStepWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step.workspace-service'; import { type WorkflowAction, WorkflowActionType, @@ -86,7 +86,7 @@ const mockWorkflowVersion = { } as WorkflowVersionWorkspaceEntity; describe('WorkflowVersionStepWorkspaceService', () => { - let twentyORMGlobalManager: jest.Mocked; + let globalWorkspaceOrmManager: jest.Mocked; let service: WorkflowVersionStepWorkspaceService; let mockWorkflowVersionWorkspaceRepository: MockWorkspaceRepository; let mockComputeWorkflowVersionStepChanges: jest.Mock; @@ -108,11 +108,15 @@ describe('WorkflowVersionStepWorkspaceService', () => { mockWorkflowVersion, ); - twentyORMGlobalManager = { - getRepositoryForWorkspace: jest + globalWorkspaceOrmManager = { + getRepository: jest .fn() .mockResolvedValue(mockWorkflowVersionWorkspaceRepository), - } as unknown as jest.Mocked; + executeInWorkspaceContext: jest + .fn() + + .mockImplementation((_authContext: any, fn: () => any) => fn()), + } as unknown as jest.Mocked; const module: TestingModule = await Test.createTestingModule({ providers: [ @@ -122,8 +126,8 @@ describe('WorkflowVersionStepWorkspaceService', () => { WorkflowVersionStepUpdateWorkspaceService, WorkflowVersionStepDeletionWorkspaceService, { - provide: TwentyORMGlobalManager, - useValue: twentyORMGlobalManager, + provide: GlobalWorkspaceOrmManager, + useValue: globalWorkspaceOrmManager, }, { provide: WorkflowSchemaWorkspaceService, diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-helpers.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-helpers.workspace-service.ts index e6cb9e5875d..644496fd570 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-helpers.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-helpers.workspace-service.ts @@ -1,6 +1,7 @@ import { Injectable } from '@nestjs/common'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { type WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity'; import { assertWorkflowVersionIsDraft } from 'src/modules/workflow/common/utils/assert-workflow-version-is-draft.util'; import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; @@ -10,7 +11,7 @@ import { type WorkflowTrigger } from 'src/modules/workflow/workflow-trigger/type @Injectable() export class WorkflowVersionStepHelpersWorkspaceService { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService, ) {} @@ -43,23 +44,30 @@ export class WorkflowVersionStepHelpersWorkspaceService { steps?: WorkflowAction[] | null; trigger?: WorkflowTrigger | null; }): Promise { - const workflowVersionRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'workflowVersion', - { shouldBypassPermissionChecks: true }, - ); + const authContext = buildSystemAuthContext(workspaceId); - const updateData: Partial = {}; + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workflowVersionRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'workflowVersion', + { shouldBypassPermissionChecks: true }, + ); - if (steps !== undefined) { - updateData.steps = steps; - } + const updateData: Partial = {}; - if (trigger !== undefined) { - updateData.trigger = trigger; - } + if (steps !== undefined) { + updateData.steps = steps; + } - await workflowVersionRepository.update(workflowVersionId, updateData); + if (trigger !== undefined) { + updateData.trigger = trigger; + } + + await workflowVersionRepository.update(workflowVersionId, updateData); + }, + ); } } diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-operations.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-operations.workspace-service.ts index ecbcaf36e85..132d185c8fe 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-operations.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-operations.workspace-service.ts @@ -15,7 +15,8 @@ import { DEFAULT_SMART_MODEL } from 'src/engine/metadata-modules/ai/ai-models/co import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity'; import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; import { WorkflowVersionStepException, @@ -52,7 +53,7 @@ const ITERATOR_EMPTY_STEP_POSITION_OFFSET = { @Injectable() export class WorkflowVersionStepOperationsWorkspaceService { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly serverlessFunctionService: ServerlessFunctionService, @InjectRepository(AgentEntity) private readonly agentRepository: Repository, @@ -359,7 +360,6 @@ export class WorkflowVersionStepOperationsWorkspaceService { }; } case WorkflowActionType.AI_AGENT: { - // Get workflow version to use workflow ID and name in agent name const workflowVersion = await this.workflowCommonWorkspaceService.getWorkflowVersionOrFail({ workflowVersionId, @@ -465,66 +465,75 @@ export class WorkflowVersionStepOperationsWorkspaceService { step: WorkflowFormAction; response: object; }) { - const responseKeys = Object.keys(response); + const authContext = buildSystemAuthContext(workspaceId); - const enrichedResponses = await Promise.all( - responseKeys.map(async (key) => { - // @ts-expect-error legacy noImplicitAny - if (!isDefined(response[key])) { - // @ts-expect-error legacy noImplicitAny - return { key, value: response[key] }; - } + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const responseKeys = Object.keys(response); - const field = step.settings.input.find((field) => field.name === key); - - if ( - field?.type === 'RECORD' && - field?.settings?.objectName && - // @ts-expect-error legacy noImplicitAny - isDefined(response[key].id) && - // @ts-expect-error legacy noImplicitAny - isValidUuid(response[key].id) - ) { - const { flatObjectMetadata, flatFieldMetadataMaps } = - await this.workflowCommonWorkspaceService.getObjectMetadataInfo( - field.settings.objectName, - workspaceId, - ); - - const relationFieldsNames = getFlatFieldsFromFlatObjectMetadata( - flatObjectMetadata, - flatFieldMetadataMaps, - ) - .filter((field) => field.type === FieldMetadataType.RELATION) - .map((field) => field.name); - - const repository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - field.settings.objectName, - { shouldBypassPermissionChecks: true }, - ); - - const record = await repository.findOne({ + const enrichedResponses = await Promise.all( + responseKeys.map(async (key) => { // @ts-expect-error legacy noImplicitAny - where: { id: response[key].id }, - relations: relationFieldsNames, - }); + if (!isDefined(response[key])) { + // @ts-expect-error legacy noImplicitAny + return { key, value: response[key] }; + } - return { key, value: record }; - } else { + const field = step.settings.input.find( + (field) => field.name === key, + ); + + if ( + field?.type === 'RECORD' && + field?.settings?.objectName && + // @ts-expect-error legacy noImplicitAny + isDefined(response[key].id) && + // @ts-expect-error legacy noImplicitAny + isValidUuid(response[key].id) + ) { + const { flatObjectMetadata, flatFieldMetadataMaps } = + await this.workflowCommonWorkspaceService.getObjectMetadataInfo( + field.settings.objectName, + workspaceId, + ); + + const relationFieldsNames = getFlatFieldsFromFlatObjectMetadata( + flatObjectMetadata, + flatFieldMetadataMaps, + ) + .filter((field) => field.type === FieldMetadataType.RELATION) + .map((field) => field.name); + + const repository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + field.settings.objectName, + { shouldBypassPermissionChecks: true }, + ); + + const record = await repository.findOne({ + // @ts-expect-error legacy noImplicitAny + where: { id: response[key].id }, + relations: relationFieldsNames, + }); + + return { key, value: record }; + } else { + // @ts-expect-error legacy noImplicitAny + return { key, value: response[key] }; + } + }), + ); + + return enrichedResponses.reduce((acc, { key, value }) => { // @ts-expect-error legacy noImplicitAny - return { key, value: response[key] }; - } - }), + acc[key] = value; + + return acc; + }, {}); + }, ); - - return enrichedResponses.reduce((acc, { key, value }) => { - // @ts-expect-error legacy noImplicitAny - acc[key] = value; - - return acc; - }, {}); } async cloneStep({ @@ -650,49 +659,60 @@ export class WorkflowVersionStepOperationsWorkspaceService { workspaceId: string; iteratorPosition?: WorkflowStepPositionInput; }): Promise { - const workflowVersionRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'workflowVersion', - { shouldBypassPermissionChecks: true }, - ); + const authContext = buildSystemAuthContext(workspaceId); - const workflowVersion = await workflowVersionRepository.findOne({ - where: { - id: workflowVersionId, + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workflowVersionRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'workflowVersion', + { shouldBypassPermissionChecks: true }, + ); + + const workflowVersion = await workflowVersionRepository.findOne({ + where: { + id: workflowVersionId, + }, + }); + + if (!isDefined(workflowVersion)) { + throw new WorkflowVersionStepException( + 'WorkflowVersion not found', + WorkflowVersionStepExceptionCode.NOT_FOUND, + ); + } + + const existingSteps = workflowVersion.steps ?? []; + + const emptyNodeStep: WorkflowEmptyAction = { + id: v4(), + name: 'Add an Action', + type: WorkflowActionType.EMPTY, + valid: true, + nextStepIds: [iteratorStepId], + settings: { + ...BASE_STEP_DEFINITION, + input: {}, + }, + position: { + x: + (iteratorPosition?.x ?? 0) + + ITERATOR_EMPTY_STEP_POSITION_OFFSET.x, + y: + (iteratorPosition?.y ?? 0) + + ITERATOR_EMPTY_STEP_POSITION_OFFSET.y, + }, + }; + + await workflowVersionRepository.update(workflowVersion.id, { + steps: [...existingSteps, emptyNodeStep], + }); + + return emptyNodeStep; }, - }); - - if (!isDefined(workflowVersion)) { - throw new WorkflowVersionStepException( - 'WorkflowVersion not found', - WorkflowVersionStepExceptionCode.NOT_FOUND, - ); - } - - const existingSteps = workflowVersion.steps ?? []; - - const emptyNodeStep: WorkflowEmptyAction = { - id: v4(), - name: 'Add an Action', - type: WorkflowActionType.EMPTY, - valid: true, - nextStepIds: [iteratorStepId], - settings: { - ...BASE_STEP_DEFINITION, - input: {}, - }, - position: { - x: (iteratorPosition?.x ?? 0) + ITERATOR_EMPTY_STEP_POSITION_OFFSET.x, - y: (iteratorPosition?.y ?? 0) + ITERATOR_EMPTY_STEP_POSITION_OFFSET.y, - }, - }; - - await workflowVersionRepository.update(workflowVersion.id, { - steps: [...existingSteps, emptyNodeStep], - }); - - return emptyNodeStep; + ); } async createDraftStep({ diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version/workflow-version.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version/workflow-version.workspace-service.ts index 7b8f7795112..fc52faa7b2f 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version/workflow-version.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version/workflow-version.workspace-service.ts @@ -5,7 +5,8 @@ import { TRIGGER_STEP_ID } from 'twenty-shared/workflow'; import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service'; import { type WorkflowStepPositionUpdateInput } from 'src/engine/core-modules/workflow/dtos/update-workflow-step-position-update-input.dto'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { WorkflowVersionStepException, WorkflowVersionStepExceptionCode, @@ -31,7 +32,7 @@ import { @Injectable() export class WorkflowVersionWorkspaceService { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly workflowVersionStepWorkspaceService: WorkflowVersionStepWorkspaceService, private readonly workflowVersionStepOperationsWorkspaceService: WorkflowVersionStepOperationsWorkspaceService, private readonly recordPositionService: RecordPositionService, @@ -46,90 +47,99 @@ export class WorkflowVersionWorkspaceService { workflowId: string; workflowVersionIdToCopy: string; }) { - const workflowVersionRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'workflowVersion', - { shouldBypassPermissionChecks: true }, - ); + const authContext = buildSystemAuthContext(workspaceId); - const workflowVersionToCopy = await workflowVersionRepository.findOne({ - where: { - id: workflowVersionIdToCopy, - workflowId, - }, - }); + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workflowVersionRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'workflowVersion', + { shouldBypassPermissionChecks: true }, + ); - if (!isDefined(workflowVersionToCopy)) { - throw new WorkflowVersionStepException( - 'WorkflowVersion to copy not found', - WorkflowVersionStepExceptionCode.NOT_FOUND, - ); - } - - assertWorkflowVersionTriggerIsDefined(workflowVersionToCopy); - assertWorkflowVersionHasSteps(workflowVersionToCopy); - - let draftWorkflowVersion = await workflowVersionRepository.findOne({ - where: { - workflowId, - status: WorkflowVersionStatus.DRAFT, - }, - }); - - if (!isDefined(draftWorkflowVersion)) { - const workflowVersionsCount = await workflowVersionRepository.count({ - where: { - workflowId, - }, - }); - - const position = await this.recordPositionService.buildRecordPosition({ - value: 'first', - objectMetadata: { - isCustom: false, - nameSingular: 'workflowVersion', - }, - workspaceId, - }); - - const insertResult = await workflowVersionRepository.insert({ - workflowId, - name: `v${workflowVersionsCount + 1}`, - status: WorkflowVersionStatus.DRAFT, - position, - }); - - draftWorkflowVersion = insertResult - .generatedMaps[0] as WorkflowVersionWorkspaceEntity; - } - - assertWorkflowVersionIsDraft(draftWorkflowVersion); - - const newWorkflowVersionTrigger = workflowVersionToCopy.trigger; - const newWorkflowVersionSteps: WorkflowAction[] = []; - - for (const step of workflowVersionToCopy.steps) { - const duplicatedStep = - await this.workflowVersionStepWorkspaceService.createDraftStep({ - step, - workspaceId, + const workflowVersionToCopy = await workflowVersionRepository.findOne({ + where: { + id: workflowVersionIdToCopy, + workflowId, + }, }); - newWorkflowVersionSteps.push(duplicatedStep); - } + if (!isDefined(workflowVersionToCopy)) { + throw new WorkflowVersionStepException( + 'WorkflowVersion to copy not found', + WorkflowVersionStepExceptionCode.NOT_FOUND, + ); + } - await workflowVersionRepository.update(draftWorkflowVersion.id, { - steps: newWorkflowVersionSteps, - trigger: newWorkflowVersionTrigger, - }); + assertWorkflowVersionTriggerIsDefined(workflowVersionToCopy); + assertWorkflowVersionHasSteps(workflowVersionToCopy); - return { - ...draftWorkflowVersion, - name: draftWorkflowVersion.name ?? '', - steps: newWorkflowVersionSteps, - trigger: newWorkflowVersionTrigger, - }; + let draftWorkflowVersion = await workflowVersionRepository.findOne({ + where: { + workflowId, + status: WorkflowVersionStatus.DRAFT, + }, + }); + + if (!isDefined(draftWorkflowVersion)) { + const workflowVersionsCount = await workflowVersionRepository.count({ + where: { + workflowId, + }, + }); + + const position = await this.recordPositionService.buildRecordPosition( + { + value: 'first', + objectMetadata: { + isCustom: false, + nameSingular: 'workflowVersion', + }, + workspaceId, + }, + ); + + const insertResult = await workflowVersionRepository.insert({ + workflowId, + name: `v${workflowVersionsCount + 1}`, + status: WorkflowVersionStatus.DRAFT, + position, + }); + + draftWorkflowVersion = insertResult + .generatedMaps[0] as WorkflowVersionWorkspaceEntity; + } + + assertWorkflowVersionIsDraft(draftWorkflowVersion); + + const newWorkflowVersionTrigger = workflowVersionToCopy.trigger; + const newWorkflowVersionSteps: WorkflowAction[] = []; + + for (const step of workflowVersionToCopy.steps) { + const duplicatedStep = + await this.workflowVersionStepWorkspaceService.createDraftStep({ + step, + workspaceId, + }); + + newWorkflowVersionSteps.push(duplicatedStep); + } + + await workflowVersionRepository.update(draftWorkflowVersion.id, { + steps: newWorkflowVersionSteps, + trigger: newWorkflowVersionTrigger, + }); + + return { + ...draftWorkflowVersion, + name: draftWorkflowVersion.name ?? '', + steps: newWorkflowVersionSteps, + trigger: newWorkflowVersionTrigger, + }; + }, + ); } async duplicateWorkflow({ @@ -141,159 +151,167 @@ export class WorkflowVersionWorkspaceService { workflowIdToDuplicate: string; workflowVersionIdToCopy: string; }) { - const workflowRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'workflow', - { shouldBypassPermissionChecks: true }, - ); + const authContext = buildSystemAuthContext(workspaceId); - const workflowVersionRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'workflowVersion', - { shouldBypassPermissionChecks: true }, - ); + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workflowRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'workflow', + { shouldBypassPermissionChecks: true }, + ); - const sourceWorkflow = await workflowRepository.findOne({ - where: { - id: workflowIdToDuplicate, - }, - }); + const workflowVersionRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'workflowVersion', + { shouldBypassPermissionChecks: true }, + ); - if (!isDefined(sourceWorkflow)) { - throw new WorkflowVersionStepException( - 'Source workflow not found', - WorkflowVersionStepExceptionCode.NOT_FOUND, - ); - } - - const sourceVersion = await workflowVersionRepository.findOne({ - where: { - id: workflowVersionIdToCopy, - workflowId: workflowIdToDuplicate, - }, - }); - - if (!isDefined(sourceVersion)) { - throw new WorkflowVersionStepException( - 'WorkflowVersion to copy not found', - WorkflowVersionStepExceptionCode.NOT_FOUND, - ); - } - - assertWorkflowVersionTriggerIsDefined(sourceVersion); - assertWorkflowVersionHasSteps(sourceVersion); - - const workflowPosition = - await this.recordPositionService.buildRecordPosition({ - value: 'first', - objectMetadata: { - isCustom: false, - nameSingular: 'workflow', - }, - workspaceId, - }); - - const insertWorkflowResult = await workflowRepository.insert({ - name: `${sourceWorkflow.name} (Duplicate)`, - statuses: [WorkflowStatus.DRAFT], - position: workflowPosition, - }); - - const newWorkflowId = ( - insertWorkflowResult.generatedMaps[0] as WorkflowWorkspaceEntity - ).id; - - const versionPosition = - await this.recordPositionService.buildRecordPosition({ - value: 'first', - objectMetadata: { - isCustom: false, - nameSingular: 'workflowVersion', - }, - workspaceId, - }); - - const insertVersionResult = await workflowVersionRepository.insert({ - workflowId: newWorkflowId, - name: 'v1', - status: WorkflowVersionStatus.DRAFT, - position: versionPosition, - }); - - const newDraftVersion = insertVersionResult - .generatedMaps[0] as WorkflowVersionWorkspaceEntity; - - const newTrigger = sourceVersion.trigger; - const sourceToClonedPairs: Array<{ - source: WorkflowAction; - duplicated: WorkflowAction; - }> = []; - const oldToNewIdMap = new Map(); - - for (const step of sourceVersion.steps ?? []) { - const clonedStep = - await this.workflowVersionStepOperationsWorkspaceService.cloneStep({ - step, - workspaceId, + const sourceWorkflow = await workflowRepository.findOne({ + where: { + id: workflowIdToDuplicate, + }, }); - sourceToClonedPairs.push({ - source: step, - duplicated: clonedStep, - }); - oldToNewIdMap.set(step.id, clonedStep.id); - } - - const remappedTrigger = isDefined(newTrigger) - ? { - ...newTrigger, - nextStepIds: (newTrigger.nextStepIds ?? []).map( - (oldId) => oldToNewIdMap.get(oldId) ?? oldId, - ), + if (!isDefined(sourceWorkflow)) { + throw new WorkflowVersionStepException( + 'Source workflow not found', + WorkflowVersionStepExceptionCode.NOT_FOUND, + ); } - : undefined; - const remappedSteps: WorkflowAction[] = sourceToClonedPairs.map( - ({ source, duplicated }) => { - const remappedStep = { - ...duplicated, - nextStepIds: (source.nextStepIds ?? []).map( - (oldId) => oldToNewIdMap.get(oldId) ?? oldId, - ), - }; + const sourceVersion = await workflowVersionRepository.findOne({ + where: { + id: workflowVersionIdToCopy, + workflowId: workflowIdToDuplicate, + }, + }); - if ( - source.type === WorkflowActionType.ITERATOR && - isDefined(source.settings?.input?.initialLoopStepIds) - ) { - remappedStep.settings = { - ...remappedStep.settings, - input: { - ...remappedStep.settings.input, - initialLoopStepIds: source.settings.input.initialLoopStepIds.map( + if (!isDefined(sourceVersion)) { + throw new WorkflowVersionStepException( + 'WorkflowVersion to copy not found', + WorkflowVersionStepExceptionCode.NOT_FOUND, + ); + } + + assertWorkflowVersionTriggerIsDefined(sourceVersion); + assertWorkflowVersionHasSteps(sourceVersion); + + const workflowPosition = + await this.recordPositionService.buildRecordPosition({ + value: 'first', + objectMetadata: { + isCustom: false, + nameSingular: 'workflow', + }, + workspaceId, + }); + + const insertWorkflowResult = await workflowRepository.insert({ + name: `${sourceWorkflow.name} (Duplicate)`, + statuses: [WorkflowStatus.DRAFT], + position: workflowPosition, + }); + + const newWorkflowId = ( + insertWorkflowResult.generatedMaps[0] as WorkflowWorkspaceEntity + ).id; + + const versionPosition = + await this.recordPositionService.buildRecordPosition({ + value: 'first', + objectMetadata: { + isCustom: false, + nameSingular: 'workflowVersion', + }, + workspaceId, + }); + + const insertVersionResult = await workflowVersionRepository.insert({ + workflowId: newWorkflowId, + name: 'v1', + status: WorkflowVersionStatus.DRAFT, + position: versionPosition, + }); + + const newDraftVersion = insertVersionResult + .generatedMaps[0] as WorkflowVersionWorkspaceEntity; + + const newTrigger = sourceVersion.trigger; + const sourceToClonedPairs: Array<{ + source: WorkflowAction; + duplicated: WorkflowAction; + }> = []; + const oldToNewIdMap = new Map(); + + for (const step of sourceVersion.steps ?? []) { + const clonedStep = + await this.workflowVersionStepOperationsWorkspaceService.cloneStep({ + step, + workspaceId, + }); + + sourceToClonedPairs.push({ + source: step, + duplicated: clonedStep, + }); + oldToNewIdMap.set(step.id, clonedStep.id); + } + + const remappedTrigger = isDefined(newTrigger) + ? { + ...newTrigger, + nextStepIds: (newTrigger.nextStepIds ?? []).map( (oldId) => oldToNewIdMap.get(oldId) ?? oldId, ), - }, - }; - } + } + : undefined; - return remappedStep; + const remappedSteps: WorkflowAction[] = sourceToClonedPairs.map( + ({ source, duplicated }) => { + const remappedStep = { + ...duplicated, + nextStepIds: (source.nextStepIds ?? []).map( + (oldId) => oldToNewIdMap.get(oldId) ?? oldId, + ), + }; + + if ( + source.type === WorkflowActionType.ITERATOR && + isDefined(source.settings?.input?.initialLoopStepIds) + ) { + remappedStep.settings = { + ...remappedStep.settings, + input: { + ...remappedStep.settings.input, + initialLoopStepIds: + source.settings.input.initialLoopStepIds.map( + (oldId) => oldToNewIdMap.get(oldId) ?? oldId, + ), + }, + }; + } + + return remappedStep; + }, + ); + + await workflowVersionRepository.update(newDraftVersion.id, { + steps: remappedSteps, + trigger: remappedTrigger, + }); + + return { + ...newDraftVersion, + name: newDraftVersion.name ?? '', + steps: remappedSteps, + trigger: remappedTrigger ?? null, + }; }, ); - - await workflowVersionRepository.update(newDraftVersion.id, { - steps: remappedSteps, - trigger: remappedTrigger, - }); - - return { - ...newDraftVersion, - name: newDraftVersion.name ?? '', - steps: remappedSteps, - trigger: remappedTrigger ?? null, - }; } async updateWorkflowVersionPositions({ @@ -305,51 +323,63 @@ export class WorkflowVersionWorkspaceService { positions: WorkflowStepPositionUpdateInput[]; workspaceId: string; }) { - const workflowVersionRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'workflowVersion', - { shouldBypassPermissionChecks: true }, - ); + const authContext = buildSystemAuthContext(workspaceId); - const workflowVersion = await workflowVersionRepository.findOneOrFail({ - where: { - id: workflowVersionId, - }, - }); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workflowVersionRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'workflowVersion', + { shouldBypassPermissionChecks: true }, + ); - assertWorkflowVersionIsDraft(workflowVersion); + const workflowVersion = await workflowVersionRepository.findOneOrFail({ + where: { + id: workflowVersionId, + }, + }); - const triggerPosition = positions.find( - (position) => position.id === TRIGGER_STEP_ID, - ); + assertWorkflowVersionIsDraft(workflowVersion); - const updatedTrigger = - isDefined(triggerPosition) && isDefined(workflowVersion.trigger) - ? { - ...workflowVersion.trigger, - position: triggerPosition.position, + const triggerPosition = positions.find( + (position) => position.id === TRIGGER_STEP_ID, + ); + + const updatedTrigger = + isDefined(triggerPosition) && isDefined(workflowVersion.trigger) + ? { + ...workflowVersion.trigger, + position: triggerPosition.position, + } + : undefined; + + const updatedSteps = workflowVersion.steps?.map((step) => { + const updatedStep = positions.find( + (position) => position.id === step.id, + ); + + if (updatedStep) { + return { + ...step, + position: updatedStep.position, + }; } - : undefined; - const updatedSteps = workflowVersion.steps?.map((step) => { - const updatedStep = positions.find((position) => position.id === step.id); + return step; + }); - if (updatedStep) { - return { - ...step, - position: updatedStep.position, + const updatePayload = { + ...(!isDefined(updatedTrigger) ? {} : { trigger: updatedTrigger }), + ...(!isDefined(updatedSteps) ? {} : { steps: updatedSteps }), }; - } - return step; - }); - - const updatePayload = { - ...(!isDefined(updatedTrigger) ? {} : { trigger: updatedTrigger }), - ...(!isDefined(updatedSteps) ? {} : { steps: updatedSteps }), - }; - - await workflowVersionRepository.update(workflowVersionId, updatePayload); + await workflowVersionRepository.update( + workflowVersionId, + updatePayload, + ); + }, + ); } } diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/delay/jobs/resume-delayed-workflow.job.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/delay/jobs/resume-delayed-workflow.job.ts index 512d2f6bee2..25fb5769916 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/delay/jobs/resume-delayed-workflow.job.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/delay/jobs/resume-delayed-workflow.job.ts @@ -7,6 +7,8 @@ import { Process } from 'src/engine/core-modules/message-queue/decorators/proces import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { WorkflowRunStatus } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity'; import { RESUME_DELAYED_WORKFLOW_JOB_NAME } from 'src/modules/workflow/workflow-executor/workflow-actions/delay/contants/resume-delayed-workflow-job-name'; import { isWorkflowDelayAction } from 'src/modules/workflow/workflow-executor/workflow-actions/delay/guards/is-workflow-delay-action.guard'; @@ -28,6 +30,7 @@ export class ResumeDelayedWorkflowJob { @InjectMessageQueue(MessageQueue.workflowQueue) private readonly messageQueueService: MessageQueueService, private readonly workflowRunWorkspaceService: WorkflowRunWorkspaceService, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, ) {} @Process(RESUME_DELAYED_WORKFLOW_JOB_NAME) @@ -36,67 +39,74 @@ export class ResumeDelayedWorkflowJob { workflowRunId, stepId, }: ResumeDelayedWorkflowJobData): Promise { - try { - const workflowRun = - await this.workflowRunWorkspaceService.getWorkflowRunOrFail({ - workflowRunId, - workspaceId, - }); + const authContext = buildSystemAuthContext(workspaceId); - if (workflowRun.status !== WorkflowRunStatus.RUNNING) { - return; - } + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + try { + const workflowRun = + await this.workflowRunWorkspaceService.getWorkflowRunOrFail({ + workflowRunId, + workspaceId, + }); - const step = workflowRun.state?.flow?.steps?.find( - (step) => step.id === stepId, - ); + if (workflowRun.status !== WorkflowRunStatus.RUNNING) { + return; + } - const stepInfo = workflowRun.state?.stepInfos[stepId]; + const step = workflowRun.state?.flow?.steps?.find( + (step) => step.id === stepId, + ); - if (!step || !isWorkflowDelayAction(step)) { - throw new WorkflowRunException( - 'Step not found or is not a delay action', - WorkflowRunExceptionCode.INVALID_INPUT, - ); - } + const stepInfo = workflowRun.state?.stepInfos[stepId]; - if (stepInfo?.status !== StepStatus.PENDING) { - throw new WorkflowRunException( - 'Step is not pending', - WorkflowRunExceptionCode.INVALID_INPUT, - ); - } + if (!step || !isWorkflowDelayAction(step)) { + throw new WorkflowRunException( + 'Step not found or is not a delay action', + WorkflowRunExceptionCode.INVALID_INPUT, + ); + } - await this.workflowRunWorkspaceService.updateWorkflowRunStepInfo({ - stepId, - stepInfo: { - status: StepStatus.SUCCESS, - result: { - success: true, - }, - }, - workspaceId, - workflowRunId, - }); + if (stepInfo?.status !== StepStatus.PENDING) { + throw new WorkflowRunException( + 'Step is not pending', + WorkflowRunExceptionCode.INVALID_INPUT, + ); + } - await this.messageQueueService.add( - RunWorkflowJob.name, - { - workspaceId, - workflowRunId, - lastExecutedStepId: stepId, - }, - ); - } catch (error) { - await this.workflowRunWorkspaceService.endWorkflowRun({ - workflowRunId, - workspaceId, - status: WorkflowRunStatus.FAILED, - error: - error instanceof Error - ? error.message - : 'Unknown error during delay resume', - }); - } + await this.workflowRunWorkspaceService.updateWorkflowRunStepInfo({ + stepId, + stepInfo: { + status: StepStatus.SUCCESS, + result: { + success: true, + }, + }, + workspaceId, + workflowRunId, + }); + + await this.messageQueueService.add( + RunWorkflowJob.name, + { + workspaceId, + workflowRunId, + lastExecutedStepId: stepId, + }, + ); + } catch (error) { + await this.workflowRunWorkspaceService.endWorkflowRun({ + workflowRunId, + workspaceId, + status: WorkflowRunStatus.FAILED, + error: + error instanceof Error + ? error.message + : 'Unknown error during delay resume', + }); + } + }, + ); } } diff --git a/packages/twenty-server/src/modules/workflow/workflow-runner/jobs/run-workflow.job.ts b/packages/twenty-server/src/modules/workflow/workflow-runner/jobs/run-workflow.job.ts index d0c414c030b..7217b1c7d77 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-runner/jobs/run-workflow.job.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-runner/jobs/run-workflow.job.ts @@ -7,6 +7,8 @@ import { Processor } from 'src/engine/core-modules/message-queue/decorators/proc import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service'; import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { WorkflowRunStatus } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity'; import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; import { WorkflowExecutorWorkspaceService } from 'src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service'; @@ -26,6 +28,7 @@ export class RunWorkflowJob { private readonly workflowExecutorWorkspaceService: WorkflowExecutorWorkspaceService, private readonly workflowRunWorkspaceService: WorkflowRunWorkspaceService, private readonly metricsService: MetricsService, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, ) {} @Process(RUN_WORKFLOW_JOB_NAME) @@ -34,27 +37,34 @@ export class RunWorkflowJob { lastExecutedStepId, workspaceId, }: RunWorkflowJobData): Promise { - try { - if (lastExecutedStepId) { - await this.resumeWorkflowExecution({ - workspaceId, - workflowRunId, - lastExecutedStepId, - }); - } else { - await this.startWorkflowExecution({ - workflowRunId, - workspaceId, - }); - } - } catch (error) { - await this.workflowRunWorkspaceService.endWorkflowRun({ - workspaceId, - workflowRunId, - status: WorkflowRunStatus.FAILED, - error: error.message, - }); - } + const authContext = buildSystemAuthContext(workspaceId); + + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + try { + if (lastExecutedStepId) { + await this.resumeWorkflowExecution({ + workspaceId, + workflowRunId, + lastExecutedStepId, + }); + } else { + await this.startWorkflowExecution({ + workflowRunId, + workspaceId, + }); + } + } catch (error) { + await this.workflowRunWorkspaceService.endWorkflowRun({ + workspaceId, + workflowRunId, + status: WorkflowRunStatus.FAILED, + error: error.message, + }); + } + }, + ); } private async startWorkflowExecution({ diff --git a/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-run-queue/cron/jobs/workflow-clean-workflow-runs.cron.job.ts b/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-run-queue/cron/jobs/workflow-clean-workflow-runs.cron.job.ts index aebd80ab96a..e98f279cdf0 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-run-queue/cron/jobs/workflow-clean-workflow-runs.cron.job.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-run-queue/cron/jobs/workflow-clean-workflow-runs.cron.job.ts @@ -9,7 +9,8 @@ import { Process } from 'src/engine/core-modules/message-queue/decorators/proces import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util'; import { WorkflowRunStatus, @@ -27,7 +28,7 @@ export class WorkflowCleanWorkflowRunsJob { constructor( @InjectRepository(WorkspaceEntity) private readonly workspaceRepository: Repository, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, @InjectDataSource() private readonly coreDataSource: DataSource, ) {} @@ -47,37 +48,44 @@ export class WorkflowCleanWorkflowRunsJob { for (const activeWorkspace of activeWorkspaces) { const schemaName = getWorkspaceSchemaName(activeWorkspace.id); - const workflowRunsToDelete = await this.coreDataSource.query( - ` - WITH ranked_runs AS ( - SELECT id, - ROW_NUMBER() OVER ( - PARTITION BY "workflowId" - ORDER BY "createdAt" DESC - ) AS rn, - "createdAt" - FROM ${schemaName}."workflowRun" - WHERE status IN ('${WorkflowRunStatus.COMPLETED}', '${WorkflowRunStatus.FAILED}') - ) - SELECT id, rn FROM ranked_runs - WHERE rn > ${NUMBER_OF_WORKFLOW_RUNS_TO_KEEP} - OR "createdAt" < NOW() - INTERVAL '14 days'; - `, - ); + const authContext = buildSystemAuthContext(activeWorkspace.id); - const workflowRunRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - activeWorkspace.id, - WorkflowRunWorkspaceEntity, - { shouldBypassPermissionChecks: true }, - ); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workflowRunsToDelete = await this.coreDataSource.query( + ` + WITH ranked_runs AS ( + SELECT id, + ROW_NUMBER() OVER ( + PARTITION BY "workflowId" + ORDER BY "createdAt" DESC + ) AS rn, + "createdAt" + FROM ${schemaName}."workflowRun" + WHERE status IN ('${WorkflowRunStatus.COMPLETED}', '${WorkflowRunStatus.FAILED}') + ) + SELECT id, rn FROM ranked_runs + WHERE rn > ${NUMBER_OF_WORKFLOW_RUNS_TO_KEEP} + OR "createdAt" < NOW() - INTERVAL '14 days'; + `, + ); - for (const workflowRunToDelete of workflowRunsToDelete) { - await workflowRunRepository.delete(workflowRunToDelete.id); - } + const workflowRunRepository = + await this.globalWorkspaceOrmManager.getRepository( + activeWorkspace.id, + WorkflowRunWorkspaceEntity, + { shouldBypassPermissionChecks: true }, + ); - this.logger.log( - `Deleted ${workflowRunsToDelete.length} workflow runs for workspace ${activeWorkspace.id} (schema ${schemaName})`, + for (const workflowRunToDelete of workflowRunsToDelete) { + await workflowRunRepository.delete(workflowRunToDelete.id); + } + + this.logger.log( + `Deleted ${workflowRunsToDelete.length} workflow runs for workspace ${activeWorkspace.id} (schema ${schemaName})`, + ); + }, ); } } diff --git a/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-run-queue/workspace-services/workflow-handle-staled-runs.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-run-queue/workspace-services/workflow-handle-staled-runs.workspace-service.ts index aa539e30c08..78bbc7a5aa1 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-run-queue/workspace-services/workflow-handle-staled-runs.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-run-queue/workspace-services/workflow-handle-staled-runs.workspace-service.ts @@ -2,7 +2,8 @@ import { Injectable, Logger } from '@nestjs/common'; import { IsNull, LessThan, Or } from 'typeorm'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { WorkflowRunStatus, WorkflowRunWorkspaceEntity, @@ -15,43 +16,50 @@ export class WorkflowHandleStaledRunsWorkspaceService { WorkflowHandleStaledRunsWorkspaceService.name, ); constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly workflowThrottlingWorkspaceService: WorkflowThrottlingWorkspaceService, ) {} async handleStaledRuns({ workspaceIds }: { workspaceIds: string[] }) { for (const workspaceId of workspaceIds) { try { - const workflowRunRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - WorkflowRunWorkspaceEntity, - { shouldBypassPermissionChecks: true }, - ); + const authContext = buildSystemAuthContext(workspaceId); - const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workflowRunRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + WorkflowRunWorkspaceEntity, + { shouldBypassPermissionChecks: true }, + ); - const staledWorkflowRuns = await workflowRunRepository.find({ - where: { - status: WorkflowRunStatus.ENQUEUED, - enqueuedAt: Or(LessThan(oneHourAgo), IsNull()), + const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000); + + const staledWorkflowRuns = await workflowRunRepository.find({ + where: { + status: WorkflowRunStatus.ENQUEUED, + enqueuedAt: Or(LessThan(oneHourAgo), IsNull()), + }, + }); + + if (staledWorkflowRuns.length <= 0) { + return; + } + + await workflowRunRepository.update( + staledWorkflowRuns.map((workflowRun) => workflowRun.id), + { + enqueuedAt: null, + status: WorkflowRunStatus.NOT_STARTED, + }, + ); + + await this.workflowThrottlingWorkspaceService.recomputeWorkflowRunNotStartedCount( + workspaceId, + ); }, - }); - - if (staledWorkflowRuns.length <= 0) { - continue; - } - - await workflowRunRepository.update( - staledWorkflowRuns.map((workflowRun) => workflowRun.id), - { - enqueuedAt: null, - status: WorkflowRunStatus.NOT_STARTED, - }, - ); - - await this.workflowThrottlingWorkspaceService.recomputeWorkflowRunNotStartedCount( - workspaceId, ); } catch (error) { this.logger.error( diff --git a/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-run-queue/workspace-services/workflow-run-enqueue.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-run-queue/workspace-services/workflow-run-enqueue.workspace-service.ts index aad71144556..9af9eb9e376 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-run-queue/workspace-services/workflow-run-enqueue.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-run-queue/workspace-services/workflow-run-enqueue.workspace-service.ts @@ -7,7 +7,8 @@ import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queu import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service'; import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { WorkflowRunStatus, WorkflowRunWorkspaceEntity, @@ -21,7 +22,7 @@ export class WorkflowRunEnqueueWorkspaceService { private readonly logger = new Logger(WorkflowRunEnqueueWorkspaceService.name); constructor( private readonly workflowThrottlingWorkspaceService: WorkflowThrottlingWorkspaceService, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, @InjectMessageQueue(MessageQueue.workflowQueue) private readonly messageQueueService: MessageQueueService, private readonly metricsService: MetricsService, @@ -56,100 +57,107 @@ export class WorkflowRunEnqueueWorkspaceService { } try { - const workflowRunRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - WorkflowRunWorkspaceEntity, - { shouldBypassPermissionChecks: true }, - ); + const authContext = buildSystemAuthContext(workspaceId); - const notStartedRunsCount = isCacheMode - ? await this.workflowThrottlingWorkspaceService.getNotStartedRunsCountFromCache( - workspaceId, - ) - : await this.workflowThrottlingWorkspaceService.getNotStartedRunsCountFromDatabase( + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workflowRunRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + WorkflowRunWorkspaceEntity, + { shouldBypassPermissionChecks: true }, + ); + + const notStartedRunsCount = isCacheMode + ? await this.workflowThrottlingWorkspaceService.getNotStartedRunsCountFromCache( + workspaceId, + ) + : await this.workflowThrottlingWorkspaceService.getNotStartedRunsCountFromDatabase( + workspaceId, + ); + + if (notStartedRunsCount <= 0) { + await this.workflowThrottlingWorkspaceService.recomputeWorkflowRunNotStartedCount( + workspaceId, + ); + + return; + } + + let remainingWorkflowRunToEnqueueCount = + await this.workflowThrottlingWorkspaceService.getRemainingRunsToEnqueueCount( + workspaceId, + ); + + const workflowRunIdsToEnqueue: string[] = []; + + if (remainingWorkflowRunToEnqueueCount > 0) { + const additionalRunsToEnqueue = await workflowRunRepository.find({ + where: { + status: WorkflowRunStatus.NOT_STARTED, + ...(workflowRunIdsToEnqueue.length > 0 + ? { id: Not(workflowRunIdsToEnqueue[0]) } + : {}), + }, + select: { + id: true, + }, + order: { + createdAt: 'ASC', + }, + take: remainingWorkflowRunToEnqueueCount, + }); + + workflowRunIdsToEnqueue.push( + ...additionalRunsToEnqueue.map( + (workflowRun: WorkflowRunWorkspaceEntity) => workflowRun.id, + ), + ); + } + + if (workflowRunIdsToEnqueue.length <= 0) { + if (!isCacheMode) { + await this.workflowThrottlingWorkspaceService.recomputeWorkflowRunNotStartedCount( + workspaceId, + ); + } + + return; + } + + await workflowRunRepository.update(workflowRunIdsToEnqueue, { + enqueuedAt: new Date().toISOString(), + status: WorkflowRunStatus.ENQUEUED, + }); + + await this.workflowThrottlingWorkspaceService.consumeRemainingRunsToEnqueueCount( workspaceId, + workflowRunIdsToEnqueue.length, ); - if (notStartedRunsCount <= 0) { - await this.workflowThrottlingWorkspaceService.recomputeWorkflowRunNotStartedCount( - workspaceId, - ); + for (const workflowRunId of workflowRunIdsToEnqueue) { + await this.messageQueueService.add( + RunWorkflowJob.name, + { + workflowRunId, + workspaceId, + }, + ); + } - return; - } - - let remainingWorkflowRunToEnqueueCount = - await this.workflowThrottlingWorkspaceService.getRemainingRunsToEnqueueCount( - workspaceId, - ); - - const workflowRunIdsToEnqueue: string[] = []; - - if (remainingWorkflowRunToEnqueueCount > 0) { - const additionalRunsToEnqueue = await workflowRunRepository.find({ - where: { - status: WorkflowRunStatus.NOT_STARTED, - ...(workflowRunIdsToEnqueue.length > 0 - ? { id: Not(workflowRunIdsToEnqueue[0]) } - : {}), - }, - select: { - id: true, - }, - order: { - createdAt: 'ASC', - }, - take: remainingWorkflowRunToEnqueueCount, - }); - - workflowRunIdsToEnqueue.push( - ...additionalRunsToEnqueue.map( - (workflowRun: WorkflowRunWorkspaceEntity) => workflowRun.id, - ), - ); - } - - if (workflowRunIdsToEnqueue.length <= 0) { - if (!isCacheMode) { - await this.workflowThrottlingWorkspaceService.recomputeWorkflowRunNotStartedCount( - workspaceId, - ); - } - - return; - } - - await workflowRunRepository.update(workflowRunIdsToEnqueue, { - enqueuedAt: new Date().toISOString(), - status: WorkflowRunStatus.ENQUEUED, - }); - - await this.workflowThrottlingWorkspaceService.consumeRemainingRunsToEnqueueCount( - workspaceId, - workflowRunIdsToEnqueue.length, + if (isCacheMode) { + await this.workflowThrottlingWorkspaceService.decreaseWorkflowRunNotStartedCount( + workspaceId, + workflowRunIdsToEnqueue.length, + ); + } else { + await this.workflowThrottlingWorkspaceService.recomputeWorkflowRunNotStartedCount( + workspaceId, + ); + } + }, ); - - for (const workflowRunId of workflowRunIdsToEnqueue) { - await this.messageQueueService.add( - RunWorkflowJob.name, - { - workflowRunId, - workspaceId, - }, - ); - } - - if (isCacheMode) { - await this.workflowThrottlingWorkspaceService.decreaseWorkflowRunNotStartedCount( - workspaceId, - workflowRunIdsToEnqueue.length, - ); - } else { - await this.workflowThrottlingWorkspaceService.recomputeWorkflowRunNotStartedCount( - workspaceId, - ); - } } catch (error) { this.metricsService.incrementCounter({ key: MetricsKeys.WorkflowRunFailedToEnqueue, diff --git a/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-run-queue/workspace-services/workflow-throttling.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-run-queue/workspace-services/workflow-throttling.workspace-service.ts index 2baaaad2e32..62c62e51eda 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-run-queue/workspace-services/workflow-throttling.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-run-queue/workspace-services/workflow-throttling.workspace-service.ts @@ -7,7 +7,8 @@ import { CacheStorageService } from 'src/engine/core-modules/cache-storage/servi import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; import { ThrottlerService } from 'src/engine/core-modules/throttler/throttler.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { WorkflowRunStatus, WorkflowRunWorkspaceEntity, @@ -18,7 +19,7 @@ export class WorkflowThrottlingWorkspaceService { constructor( @InjectCacheStorage(CacheStorageNamespace.ModuleWorkflow) private readonly cacheStorage: CacheStorageService, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly throttlerService: ThrottlerService, private readonly twentyConfigService: TwentyConfigService, ) {} @@ -75,19 +76,26 @@ export class WorkflowThrottlingWorkspaceService { async recomputeWorkflowRunNotStartedCount( workspaceId: string, ): Promise { - const workflowRunRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - WorkflowRunWorkspaceEntity, - { shouldBypassPermissionChecks: true }, - ); + const authContext = buildSystemAuthContext(workspaceId); const currentlyNotStartedWorkflowRunCount = - await workflowRunRepository.count({ - where: { - status: In([WorkflowRunStatus.NOT_STARTED]), + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workflowRunRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + WorkflowRunWorkspaceEntity, + { shouldBypassPermissionChecks: true }, + ); + + return workflowRunRepository.count({ + where: { + status: In([WorkflowRunStatus.NOT_STARTED]), + }, + }); }, - }); + ); await this.setWorkflowRunNotStartedCount( workspaceId, @@ -102,18 +110,25 @@ export class WorkflowThrottlingWorkspaceService { async getNotStartedRunsCountFromDatabase( workspaceId: string, ): Promise { - const workflowRunRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - WorkflowRunWorkspaceEntity, - { shouldBypassPermissionChecks: true }, - ); + const authContext = buildSystemAuthContext(workspaceId); - return workflowRunRepository.count({ - where: { - status: In([WorkflowRunStatus.NOT_STARTED]), + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workflowRunRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + WorkflowRunWorkspaceEntity, + { shouldBypassPermissionChecks: true }, + ); + + return workflowRunRepository.count({ + where: { + status: In([WorkflowRunStatus.NOT_STARTED]), + }, + }); }, - }); + ); } async acquireWorkflowEnqueueLock( diff --git a/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-run/command/delete-workflow-runs.command.ts b/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-run/command/delete-workflow-runs.command.ts index e0b753d1428..b7cce596ca1 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-run/command/delete-workflow-runs.command.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-run/command/delete-workflow-runs.command.ts @@ -7,7 +7,8 @@ import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/ import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { type WorkflowRunWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity'; @Command({ @@ -20,10 +21,10 @@ export class DeleteWorkflowRunsCommand extends ActiveOrSuspendedWorkspacesMigrat constructor( @InjectRepository(WorkspaceEntity) protected readonly workspaceRepository: Repository, - protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, protected readonly dataSourceService: DataSourceService, ) { - super(workspaceRepository, twentyORMGlobalManager, dataSourceService); + super(workspaceRepository, globalWorkspaceOrmManager, dataSourceService); } @Option({ @@ -50,31 +51,40 @@ export class DeleteWorkflowRunsCommand extends ActiveOrSuspendedWorkspacesMigrat workspaceId, options, }: RunOnWorkspaceArgs): Promise { - try { - const workflowRunRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'workflowRun', - { shouldBypassPermissionChecks: true }, - ); + const authContext = buildSystemAuthContext(workspaceId); - const createdAtCondition = { - createdAt: LessThan(this.createdBeforeDate || new Date().toISOString()), - }; + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + try { + const workflowRunRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'workflowRun', + { shouldBypassPermissionChecks: true }, + ); - const workflowRunCount = await workflowRunRepository.count({ - where: createdAtCondition, - }); + const createdAtCondition = { + createdAt: LessThan( + this.createdBeforeDate || new Date().toISOString(), + ), + }; - if (!options.dryRun && workflowRunCount > 0) { - await workflowRunRepository.delete(createdAtCondition); - } + const workflowRunCount = await workflowRunRepository.count({ + where: createdAtCondition, + }); - this.logger.log( - `${options.dryRun ? ' (DRY RUN): ' : ''}Deleted ${workflowRunCount} workflow runs`, - ); - } catch (error) { - this.logger.error('Error while deleting workflowRun', error); - } + if (!options.dryRun && workflowRunCount > 0) { + await workflowRunRepository.delete(createdAtCondition); + } + + this.logger.log( + `${options.dryRun ? ' (DRY RUN): ' : ''}Deleted ${workflowRunCount} workflow runs`, + ); + } catch (error) { + this.logger.error('Error while deleting workflowRun', error); + } + }, + ); } } diff --git a/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service.ts index c6e9d67be52..5b111b17a5b 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service.ts @@ -10,7 +10,8 @@ import { WithLock } from 'src/engine/core-modules/cache-lock/with-lock.decorator import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service'; import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type'; import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { WorkflowRunStatus, type WorkflowRunState, @@ -27,7 +28,7 @@ import { @Injectable() export class WorkflowRunWorkspaceService { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService, private readonly recordPositionService: RecordPositionService, private readonly metricsService: MetricsService, @@ -53,78 +54,89 @@ export class WorkflowRunWorkspaceService { error?: string; workspaceId: string; }) { - const workflowRunRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'workflowRun', - { shouldBypassPermissionChecks: true }, - ); + const authContext = buildSystemAuthContext(workspaceId); - const workflowVersion = - await this.workflowCommonWorkspaceService.getWorkflowVersionOrFail({ - workspaceId, - workflowVersionId, - }); + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workflowRunRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'workflowRun', + { shouldBypassPermissionChecks: true }, + ); - const workflowRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'workflow', - { shouldBypassPermissionChecks: true }, - ); + const workflowVersion = + await this.workflowCommonWorkspaceService.getWorkflowVersionOrFail({ + workspaceId, + workflowVersionId, + }); - const workflow = await workflowRepository.findOne({ - where: { - id: workflowVersion.workflowId, + const workflowRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'workflow', + { shouldBypassPermissionChecks: true }, + ); + + const workflow = await workflowRepository.findOne({ + where: { + id: workflowVersion.workflowId, + }, + }); + + if (!workflow) { + throw new WorkflowRunException( + 'Workflow id is invalid', + WorkflowRunExceptionCode.WORKFLOW_RUN_INVALID, + ); + } + + const position = await this.recordPositionService.buildRecordPosition({ + value: 'first', + objectMetadata: { + isCustom: false, + nameSingular: 'workflowRun', + }, + workspaceId, + }); + + const initState = this.getInitState( + workflowVersion, + triggerPayload, + error, + ); + + const lastWorkflowRun = await workflowRunRepository.findOne({ + where: { + workflowId: workflow.id, + }, + order: { createdAt: 'desc' }, + }); + + const workflowRunCountMatch = lastWorkflowRun?.name?.match(/#(\d+)/); + + const workflowRunCount = workflowRunCountMatch + ? parseInt(workflowRunCountMatch[1], 10) + : 0; + + const workflowRun = { + id: workflowRunId ?? v4(), + name: `#${workflowRunCount + 1} - ${workflow.name}`, + workflowVersionId, + createdBy, + workflowId: workflow.id, + status, + position, + state: initState, + enqueuedAt: status === WorkflowRunStatus.ENQUEUED ? new Date() : null, + }; + + await workflowRunRepository.insert(workflowRun); + + return workflowRun.id; }, - }); - - if (!workflow) { - throw new WorkflowRunException( - 'Workflow id is invalid', - WorkflowRunExceptionCode.WORKFLOW_RUN_INVALID, - ); - } - - const position = await this.recordPositionService.buildRecordPosition({ - value: 'first', - objectMetadata: { - isCustom: false, - nameSingular: 'workflowRun', - }, - workspaceId, - }); - - const initState = this.getInitState(workflowVersion, triggerPayload, error); - - const lastWorkflowRun = await workflowRunRepository.findOne({ - where: { - workflowId: workflow.id, - }, - order: { createdAt: 'desc' }, - }); - - const workflowRunCountMatch = lastWorkflowRun?.name?.match(/#(\d+)/); - - const workflowRunCount = workflowRunCountMatch - ? parseInt(workflowRunCountMatch[1], 10) - : 0; - - const workflowRun = { - id: workflowRunId ?? v4(), - name: `#${workflowRunCount + 1} - ${workflow.name}`, - workflowVersionId, - createdBy, - workflowId: workflow.id, - status, - position, - state: initState, - enqueuedAt: status === WorkflowRunStatus.ENQUEUED ? new Date() : null, - }; - - await workflowRunRepository.insert(workflowRun); - - return workflowRun.id; + ); } @WithLock('workflowRunId') @@ -328,16 +340,23 @@ export class WorkflowRunWorkspaceService { workflowRunId: string; workspaceId: string; }): Promise { - const workflowRunRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'workflowRun', - { shouldBypassPermissionChecks: true }, - ); + const authContext = buildSystemAuthContext(workspaceId); - return await workflowRunRepository.findOne({ - where: { id: workflowRunId }, - }); + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workflowRunRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'workflowRun', + { shouldBypassPermissionChecks: true }, + ); + + return await workflowRunRepository.findOne({ + where: { id: workflowRunId }, + }); + }, + ); } async getWorkflowRunOrFail({ @@ -371,29 +390,36 @@ export class WorkflowRunWorkspaceService { workspaceId: string; partialUpdate: QueryDeepPartialEntity; }) { - const workflowRunRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'workflowRun', - { shouldBypassPermissionChecks: true }, - ); + const authContext = buildSystemAuthContext(workspaceId); - const workflowRunToUpdate = await workflowRunRepository.findOneBy({ - id: workflowRunId, - }); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workflowRunRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'workflowRun', + { shouldBypassPermissionChecks: true }, + ); - if (!workflowRunToUpdate) { - throw new WorkflowRunException( - `workflowRun ${workflowRunId} not found`, - WorkflowRunExceptionCode.WORKFLOW_RUN_NOT_FOUND, - ); - } + const workflowRunToUpdate = await workflowRunRepository.findOneBy({ + id: workflowRunId, + }); - await workflowRunRepository.update( - workflowRunToUpdate.id, - partialUpdate, - undefined, - ['id'], + if (!workflowRunToUpdate) { + throw new WorkflowRunException( + `workflowRun ${workflowRunId} not found`, + WorkflowRunExceptionCode.WORKFLOW_RUN_NOT_FOUND, + ); + } + + await workflowRunRepository.update( + workflowRunToUpdate.id, + partialUpdate, + undefined, + ['id'], + ); + }, ); } diff --git a/packages/twenty-server/src/modules/workflow/workflow-status/jobs/__tests__/workflow-statuses-update.job.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-status/jobs/__tests__/workflow-statuses-update.job.spec.ts index 44a4c00fd3a..a7facf6c639 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-status/jobs/__tests__/workflow-statuses-update.job.spec.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-status/jobs/__tests__/workflow-statuses-update.job.spec.ts @@ -4,7 +4,7 @@ import { getRepositoryToken } from '@nestjs/typeorm'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { WorkflowVersionStatus } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity'; import { WorkflowStatus } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity'; import { @@ -27,8 +27,8 @@ describe('WorkflowStatusesUpdate', () => { update: jest.fn(), }; - const mockTwentyORMGlobalManager = { - getRepositoryForWorkspace: jest + const mockGlobalWorkspaceOrmManager = { + getRepository: jest .fn() .mockImplementation((_workspaceId, entity, options) => { if (!options?.shouldBypassPermissionChecks) { @@ -46,6 +46,10 @@ describe('WorkflowStatusesUpdate', () => { return Promise.resolve(null); }), + executeInWorkspaceContext: jest + .fn() + + .mockImplementation((_authContext: any, fn: () => any) => fn()), }; const mockServerlessFunctionService = { @@ -58,8 +62,8 @@ describe('WorkflowStatusesUpdate', () => { providers: [ WorkflowStatusesUpdateJob, { - provide: TwentyORMGlobalManager, - useValue: mockTwentyORMGlobalManager, + provide: GlobalWorkspaceOrmManager, + useValue: mockGlobalWorkspaceOrmManager, }, { provide: ServerlessFunctionService, diff --git a/packages/twenty-server/src/modules/workflow/workflow-status/jobs/workflow-statuses-update.job.ts b/packages/twenty-server/src/modules/workflow/workflow-status/jobs/workflow-statuses-update.job.ts index 1db714fcb6b..a428640c3ac 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-status/jobs/workflow-statuses-update.job.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-status/jobs/workflow-statuses-update.job.ts @@ -8,8 +8,9 @@ import { Process } from 'src/engine/core-modules/message-queue/decorators/proces import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { WorkflowVersionStepException, WorkflowVersionStepExceptionCode, @@ -69,37 +70,44 @@ export class WorkflowStatusesUpdateJob { protected readonly logger = new Logger(WorkflowStatusesUpdateJob.name); constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly serverlessFunctionService: ServerlessFunctionService, ) {} @Process(WorkflowStatusesUpdateJob.name) async handle(event: WorkflowVersionBatchEvent): Promise { - switch (event.type) { - case WorkflowVersionEventType.CREATE: - case WorkflowVersionEventType.DELETE: - await Promise.all( - event.workflowIds.map((workflowId) => - this.handleWorkflowVersionCreatedOrDeleted({ - workflowId, - workspaceId: event.workspaceId, - }), - ), - ); - break; - case WorkflowVersionEventType.STATUS_UPDATE: - await Promise.all( - event.statusUpdates.map((statusUpdate) => - this.handleWorkflowVersionStatusUpdated({ - statusUpdate, - workspaceId: event.workspaceId, - }), - ), - ); - break; - default: - break; - } + const authContext = buildSystemAuthContext(event.workspaceId); + + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + switch (event.type) { + case WorkflowVersionEventType.CREATE: + case WorkflowVersionEventType.DELETE: + await Promise.all( + event.workflowIds.map((workflowId) => + this.handleWorkflowVersionCreatedOrDeleted({ + workflowId, + workspaceId: event.workspaceId, + }), + ), + ); + break; + case WorkflowVersionEventType.STATUS_UPDATE: + await Promise.all( + event.statusUpdates.map((statusUpdate) => + this.handleWorkflowVersionStatusUpdated({ + statusUpdate, + workspaceId: event.workspaceId, + }), + ), + ); + break; + default: + break; + } + }, + ); } private async handleWorkflowVersionCreatedOrDeleted({ @@ -110,14 +118,14 @@ export class WorkflowStatusesUpdateJob { workspaceId: string; }): Promise { const workflowRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( + await this.globalWorkspaceOrmManager.getRepository( workspaceId, 'workflow', { shouldBypassPermissionChecks: true }, ); const workflowVersionRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( + await this.globalWorkspaceOrmManager.getRepository( workspaceId, 'workflowVersion', { shouldBypassPermissionChecks: true }, @@ -212,14 +220,14 @@ export class WorkflowStatusesUpdateJob { workspaceId: string; }): Promise { const workflowRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( + await this.globalWorkspaceOrmManager.getRepository( workspaceId, 'workflow', { shouldBypassPermissionChecks: true }, ); const workflowVersionRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( + await this.globalWorkspaceOrmManager.getRepository( workspaceId, 'workflowVersion', { shouldBypassPermissionChecks: true }, diff --git a/packages/twenty-server/src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service.ts index 97c7ada9cc9..2cde634eb9d 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service.ts @@ -4,7 +4,7 @@ import { type ToolSet } from 'ai'; import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service'; import { PerObjectToolGeneratorService } from 'src/engine/core-modules/tool-generator/services/per-object-tool-generator.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; import { WorkflowSchemaWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service'; import { WorkflowVersionEdgeWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-edge/workflow-version-edge.workspace-service'; @@ -39,7 +39,7 @@ export class WorkflowToolWorkspaceService { workflowVersionService: WorkflowVersionWorkspaceService, workflowTriggerService: WorkflowTriggerWorkspaceService, workflowSchemaService: WorkflowSchemaWorkspaceService, - twentyORMGlobalManager: TwentyORMGlobalManager, + globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, recordPositionService: RecordPositionService, private readonly perObjectToolGenerator: PerObjectToolGeneratorService, ) { @@ -49,7 +49,7 @@ export class WorkflowToolWorkspaceService { workflowVersionService, workflowTriggerService, workflowSchemaService, - twentyORMGlobalManager, + globalWorkspaceOrmManager, recordPositionService, }; diff --git a/packages/twenty-server/src/modules/workflow/workflow-tools/tools/create-complete-workflow.tool.ts b/packages/twenty-server/src/modules/workflow/workflow-tools/tools/create-complete-workflow.tool.ts index cc2a846f36e..0c87e8bda84 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-tools/tools/create-complete-workflow.tool.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-tools/tools/create-complete-workflow.tool.ts @@ -6,6 +6,7 @@ import { v4 as uuidv4 } from 'uuid'; import { z } from 'zod'; import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { WorkflowVersionStatus } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity'; import { WorkflowStatus } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity'; import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type'; @@ -63,7 +64,7 @@ type CreateCompleteWorkflowToolDeps = Pick< | 'workflowVersionService' | 'workflowVersionEdgeService' | 'workflowTriggerService' - | 'twentyORMGlobalManager' + | 'globalWorkspaceOrmManager' | 'recordPositionService' >; @@ -196,34 +197,40 @@ const createWorkflow = async ({ context: CreateCompleteWorkflowToolContext; name: string; }): Promise => { - const workflowRepository = - await deps.twentyORMGlobalManager.getRepositoryForWorkspace( - context.workspaceId, - 'workflow', - context.rolePermissionConfig, - ); + const authContext = buildSystemAuthContext(context.workspaceId); - const workflowPosition = await deps.recordPositionService.buildRecordPosition( - { - value: 'first', - objectMetadata: { - isCustom: false, - nameSingular: 'workflow', - }, - workspaceId: context.workspaceId, + return deps.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workflowRepository = + await deps.globalWorkspaceOrmManager.getRepository( + context.workspaceId, + 'workflow', + context.rolePermissionConfig, + ); + + const workflowPosition = + await deps.recordPositionService.buildRecordPosition({ + value: 'first', + objectMetadata: { + isCustom: false, + nameSingular: 'workflow', + }, + workspaceId: context.workspaceId, + }); + + const workflow = { + id: uuidv4(), + name, + statuses: [WorkflowStatus.DRAFT], + position: workflowPosition, + }; + + await workflowRepository.insert(workflow); + + return workflow.id; }, ); - - const workflow = { - id: uuidv4(), - name, - statuses: [WorkflowStatus.DRAFT], - position: workflowPosition, - }; - - await workflowRepository.insert(workflow); - - return workflow.id; }; const createWorkflowVersion = async ({ @@ -239,35 +246,43 @@ const createWorkflowVersion = async ({ trigger: WorkflowTrigger; steps: WorkflowAction[]; }): Promise => { - const workflowVersionRepository = - await deps.twentyORMGlobalManager.getRepositoryForWorkspace( - context.workspaceId, - 'workflowVersion', - context.rolePermissionConfig, - ); + const authContext = buildSystemAuthContext(context.workspaceId); - const versionPosition = await deps.recordPositionService.buildRecordPosition({ - value: 'first', - objectMetadata: { - isCustom: false, - nameSingular: 'workflowVersion', + return deps.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workflowVersionRepository = + await deps.globalWorkspaceOrmManager.getRepository( + context.workspaceId, + 'workflowVersion', + context.rolePermissionConfig, + ); + + const versionPosition = + await deps.recordPositionService.buildRecordPosition({ + value: 'first', + objectMetadata: { + isCustom: false, + nameSingular: 'workflowVersion', + }, + workspaceId: context.workspaceId, + }); + + const workflowVersion = { + id: uuidv4(), + workflowId, + name: 'v1', + status: WorkflowVersionStatus.DRAFT, + trigger, + steps, + position: versionPosition, + }; + + await workflowVersionRepository.insert(workflowVersion); + + return workflowVersion.id; }, - workspaceId: context.workspaceId, - }); - - const workflowVersion = { - id: uuidv4(), - workflowId, - name: 'v1', - status: WorkflowVersionStatus.DRAFT, - trigger, - steps, - position: versionPosition, - }; - - await workflowVersionRepository.insert(workflowVersion); - - return workflowVersion.id; + ); }; const updateWorkflowStatus = async ({ @@ -281,15 +296,22 @@ const updateWorkflowStatus = async ({ workflowId: string; workflowVersionId: string; }) => { - const workflowRepository = - await deps.twentyORMGlobalManager.getRepositoryForWorkspace( - context.workspaceId, - 'workflow', - context.rolePermissionConfig, - ); + const authContext = buildSystemAuthContext(context.workspaceId); - await workflowRepository.update(workflowId, { - statuses: [WorkflowStatus.ACTIVE], - lastPublishedVersionId: workflowVersionId, - }); + await deps.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workflowRepository = + await deps.globalWorkspaceOrmManager.getRepository( + context.workspaceId, + 'workflow', + context.rolePermissionConfig, + ); + + await workflowRepository.update(workflowId, { + statuses: [WorkflowStatus.ACTIVE], + lastPublishedVersionId: workflowVersionId, + }); + }, + ); }; diff --git a/packages/twenty-server/src/modules/workflow/workflow-tools/types/workflow-tool-dependencies.type.ts b/packages/twenty-server/src/modules/workflow/workflow-tools/types/workflow-tool-dependencies.type.ts index 2dae74f14b2..37e5ac2a8bb 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-tools/types/workflow-tool-dependencies.type.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-tools/types/workflow-tool-dependencies.type.ts @@ -1,5 +1,5 @@ import type { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service'; -import type { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import type { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import type { WorkflowSchemaWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service'; import type { WorkflowVersionEdgeWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-edge/workflow-version-edge.workspace-service'; import type { WorkflowVersionStepWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step.workspace-service'; @@ -12,7 +12,7 @@ export type WorkflowToolDependencies = { workflowVersionService: WorkflowVersionWorkspaceService; workflowTriggerService: WorkflowTriggerWorkspaceService; workflowSchemaService: WorkflowSchemaWorkspaceService; - twentyORMGlobalManager: TwentyORMGlobalManager; + globalWorkspaceOrmManager: GlobalWorkspaceOrmManager; recordPositionService: RecordPositionService; }; diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/automated-trigger.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/automated-trigger.workspace-service.ts index c7e2493483a..2673512be8a 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/automated-trigger.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/automated-trigger.workspace-service.ts @@ -1,6 +1,7 @@ import { Injectable } from '@nestjs/common'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { type AutomatedTriggerType, type WorkflowAutomatedTriggerWorkspaceEntity, @@ -10,7 +11,7 @@ import { type AutomatedTriggerSettings } from 'src/modules/workflow/workflow-tri @Injectable() export class AutomatedTriggerWorkspaceService { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, ) {} async addAutomatedTrigger({ @@ -24,17 +25,24 @@ export class AutomatedTriggerWorkspaceService { settings: AutomatedTriggerSettings; workspaceId: string; }) { - const workflowAutomatedTriggerRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'workflowAutomatedTrigger', - ); + const authContext = buildSystemAuthContext(workspaceId); - await workflowAutomatedTriggerRepository.insert({ - type, - settings, - workflowId, - }); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workflowAutomatedTriggerRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'workflowAutomatedTrigger', + ); + + await workflowAutomatedTriggerRepository.insert({ + type, + settings, + workflowId, + }); + }, + ); } async deleteAutomatedTrigger({ @@ -44,12 +52,19 @@ export class AutomatedTriggerWorkspaceService { workflowId: string; workspaceId: string; }) { - const workflowAutomatedTriggerRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'workflowAutomatedTrigger', - ); + const authContext = buildSystemAuthContext(workspaceId); - await workflowAutomatedTriggerRepository.delete({ workflowId }); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workflowAutomatedTriggerRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'workflowAutomatedTrigger', + ); + + await workflowAutomatedTriggerRepository.delete({ workflowId }); + }, + ); } } diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/listeners/__tests__/workflow-database-event-trigger.listener.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/listeners/__tests__/workflow-database-event-trigger.listener.spec.ts index c23a1adf4ce..a241d5d4d0f 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/listeners/__tests__/workflow-database-event-trigger.listener.spec.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/listeners/__tests__/workflow-database-event-trigger.listener.spec.ts @@ -2,7 +2,7 @@ import { Test, type TestingModule } from '@nestjs/testing'; import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { type WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type'; import { AutomatedTriggerType } from 'src/modules/workflow/common/standard-objects/workflow-automated-trigger.workspace-entity'; import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; @@ -11,7 +11,7 @@ import { WorkflowTriggerJob } from 'src/modules/workflow/workflow-trigger/jobs/w describe('WorkflowDatabaseEventTriggerListener', () => { let listener: WorkflowDatabaseEventTriggerListener; - let twentyORMGlobalManager: jest.Mocked; + let globalWorkspaceOrmManager: jest.Mocked; let messageQueueService: jest.Mocked; const mockRepository = { @@ -48,8 +48,11 @@ describe('WorkflowDatabaseEventTriggerListener', () => { }) as FlatObjectMetadata; beforeEach(async () => { - twentyORMGlobalManager = { - getRepositoryForWorkspace: jest.fn().mockResolvedValue(mockRepository), + globalWorkspaceOrmManager = { + getRepository: jest.fn().mockResolvedValue(mockRepository), + executeInWorkspaceContext: jest + .fn() + .mockImplementation((_authContext: any, fn: () => any) => fn()), } as any; messageQueueService = { @@ -60,8 +63,8 @@ describe('WorkflowDatabaseEventTriggerListener', () => { providers: [ WorkflowDatabaseEventTriggerListener, { - provide: TwentyORMGlobalManager, - useValue: twentyORMGlobalManager, + provide: GlobalWorkspaceOrmManager, + useValue: globalWorkspaceOrmManager, }, { provide: MessageQueueService, diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/listeners/workflow-database-event-trigger.listener.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/listeners/workflow-database-event-trigger.listener.ts index 92ebc4b9129..bc277f30f6c 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/listeners/workflow-database-event-trigger.listener.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/listeners/workflow-database-event-trigger.listener.ts @@ -20,7 +20,8 @@ import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-m import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type'; import { AutomatedTriggerType, @@ -43,7 +44,7 @@ export class WorkflowDatabaseEventTriggerListener { ); constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, @InjectMessageQueue(MessageQueue.workflowQueue) private readonly messageQueueService: MessageQueueService, private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService, @@ -242,57 +243,66 @@ export class WorkflowDatabaseEventTriggerListener { flatObjectMetadataMaps: FlatEntityMaps; flatFieldMetadataMaps: FlatEntityMaps; }) { - const { fieldIdByJoinColumnName } = buildFieldMapsFromFlatObjectMetadata( - flatFieldMetadataMaps, - flatObjectMetadata, + const authContext = buildSystemAuthContext(workspaceId); + + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const { fieldIdByJoinColumnName } = + buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + flatObjectMetadata, + ); + + for (const [joinColumnName, joinFieldId] of Object.entries( + fieldIdByJoinColumnName, + )) { + const joinField = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityMaps: flatFieldMetadataMaps, + flatEntityId: joinFieldId, + }); + + const joinRecordIds = records + .map((record) => record[joinColumnName]) + .filter(isDefined); + + if (joinRecordIds.length === 0) { + continue; + } + + const relatedObjectMetadataId = + joinField.relationTargetObjectMetadataId; + + if (!isDefined(relatedObjectMetadataId)) { + continue; + } + + const relatedObjectMetadataNameSingular = + flatObjectMetadataMaps.byId[relatedObjectMetadataId]?.nameSingular; + + if (!isDefined(relatedObjectMetadataNameSingular)) { + continue; + } + + const relatedObjectRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + relatedObjectMetadataNameSingular, + { shouldBypassPermissionChecks: true }, + ); + + const relatedRecords = await relatedObjectRepository.find({ + where: { id: In(joinRecordIds) }, + }); + + for (const record of records) { + record[joinField.name] = relatedRecords.find( + (relatedRecord) => relatedRecord.id === record[joinColumnName], + ); + } + } + }, ); - - for (const [joinColumnName, joinFieldId] of Object.entries( - fieldIdByJoinColumnName, - )) { - const joinField = findFlatEntityByIdInFlatEntityMapsOrThrow({ - flatEntityMaps: flatFieldMetadataMaps, - flatEntityId: joinFieldId, - }); - - const joinRecordIds = records - .map((record) => record[joinColumnName]) - .filter(isDefined); - - if (joinRecordIds.length === 0) { - continue; - } - - const relatedObjectMetadataId = joinField.relationTargetObjectMetadataId; - - if (!isDefined(relatedObjectMetadataId)) { - continue; - } - - const relatedObjectMetadataNameSingular = - flatObjectMetadataMaps.byId[relatedObjectMetadataId]?.nameSingular; - - if (!isDefined(relatedObjectMetadataNameSingular)) { - continue; - } - - const relatedObjectRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - relatedObjectMetadataNameSingular, - { shouldBypassPermissionChecks: true }, - ); - - const relatedRecords = await relatedObjectRepository.find({ - where: { id: In(joinRecordIds) }, - }); - - for (const record of records) { - record[joinField.name] = relatedRecords.find( - (relatedRecord) => relatedRecord.id === record[joinColumnName], - ); - } - } } private async shouldIgnoreEvent( @@ -325,45 +335,52 @@ export class WorkflowDatabaseEventTriggerListener { const databaseEventName = payload.name; const automatedTriggerTableName = 'workflowAutomatedTrigger'; - const workflowAutomatedTriggerRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - automatedTriggerTableName, - { shouldBypassPermissionChecks: true }, - ); + const authContext = buildSystemAuthContext(workspaceId); - const eventListeners = await workflowAutomatedTriggerRepository.find({ - where: { - type: AutomatedTriggerType.DATABASE_EVENT, - settings: Raw( - () => - `"${automatedTriggerTableName}"."settings"->>'eventName' = :eventName`, - { eventName: databaseEventName }, - ), - }, - }); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workflowAutomatedTriggerRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + automatedTriggerTableName, + { shouldBypassPermissionChecks: true }, + ); - for (const eventListener of eventListeners) { - for (const eventPayload of payload.events) { - const shouldTriggerJob = this.shouldTriggerJob({ - eventPayload, - eventListener, - action, + const eventListeners = await workflowAutomatedTriggerRepository.find({ + where: { + type: AutomatedTriggerType.DATABASE_EVENT, + settings: Raw( + () => + `"${automatedTriggerTableName}"."settings"->>'eventName' = :eventName`, + { eventName: databaseEventName }, + ), + }, }); - if (shouldTriggerJob) { - await this.messageQueueService.add( - WorkflowTriggerJob.name, - { - workspaceId, - workflowId: eventListener.workflowId, - payload: eventPayload, - }, - { retryLimit: 3 }, - ); + for (const eventListener of eventListeners) { + for (const eventPayload of payload.events) { + const shouldTriggerJob = this.shouldTriggerJob({ + eventPayload, + eventListener, + action, + }); + + if (shouldTriggerJob) { + await this.messageQueueService.add( + WorkflowTriggerJob.name, + { + workspaceId, + workflowId: eventListener.workflowId, + payload: eventPayload, + }, + { retryLimit: 3 }, + ); + } + } } - } - } + }, + ); } private shouldTriggerJob({ diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/jobs/workflow-trigger.job.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/jobs/workflow-trigger.job.ts index 54231583e5b..253d36386e7 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/jobs/workflow-trigger.job.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/jobs/workflow-trigger.job.ts @@ -10,7 +10,8 @@ import { Processor } from 'src/engine/core-modules/message-queue/decorators/proc import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; import { handleWorkflowTriggerException } from 'src/engine/core-modules/workflow/filters/workflow-trigger-graphql-api-exception.filter'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { WorkflowVersionStatus, type WorkflowVersionWorkspaceEntity, @@ -33,7 +34,7 @@ const DEFAULT_WORKFLOW_NAME = 'Workflow'; @Processor({ queueName: MessageQueue.workflowQueue, scope: Scope.REQUEST }) export class WorkflowTriggerJob { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly workflowRunnerWorkspaceService: WorkflowRunnerWorkspaceService, @InjectMessageQueue(MessageQueue.workflowQueue) private readonly messageQueueService: MessageQueueService, @@ -41,77 +42,83 @@ export class WorkflowTriggerJob { @Process(WorkflowTriggerJob.name) async handle(data: WorkflowTriggerJobData): Promise { - try { - const workflowRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - data.workspaceId, - 'workflow', - { shouldBypassPermissionChecks: true }, - ); + const authContext = buildSystemAuthContext(data.workspaceId); - const workflow = await workflowRepository.findOneBy({ - id: data.workflowId, - }); + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + try { + const workflowRepository = + await this.globalWorkspaceOrmManager.getRepository( + data.workspaceId, + 'workflow', + { shouldBypassPermissionChecks: true }, + ); - if (!workflow) { - throw new WorkflowTriggerException( - `Workflow ${data.workflowId} not found in workspace ${data.workspaceId}`, - WorkflowTriggerExceptionCode.NOT_FOUND, - ); - } + const workflow = await workflowRepository.findOneBy({ + id: data.workflowId, + }); - if (!workflow.lastPublishedVersionId) { - throw new WorkflowTriggerException( - `Workflow ${data.workflowId} has no published version in workspace ${data.workspaceId}`, - WorkflowTriggerExceptionCode.INTERNAL_ERROR, - ); - } + if (!workflow) { + throw new WorkflowTriggerException( + `Workflow ${data.workflowId} not found in workspace ${data.workspaceId}`, + WorkflowTriggerExceptionCode.NOT_FOUND, + ); + } - const workflowVersionRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - data.workspaceId, - 'workflowVersion', - { shouldBypassPermissionChecks: true }, - ); + if (!workflow.lastPublishedVersionId) { + throw new WorkflowTriggerException( + `Workflow ${data.workflowId} has no published version in workspace ${data.workspaceId}`, + WorkflowTriggerExceptionCode.INTERNAL_ERROR, + ); + } - const workflowVersion = await workflowVersionRepository.findOneBy({ - id: workflow.lastPublishedVersionId, - }); + const workflowVersionRepository = + await this.globalWorkspaceOrmManager.getRepository( + data.workspaceId, + 'workflowVersion', + { shouldBypassPermissionChecks: true }, + ); - if (!workflowVersion) { - throw new WorkflowTriggerException( - `Workflow version ${workflow.lastPublishedVersionId} not found in workspace ${data.workspaceId}`, - WorkflowTriggerExceptionCode.NOT_FOUND, - ); - } - if (workflowVersion.status !== WorkflowVersionStatus.ACTIVE) { - throw new WorkflowTriggerException( - `Workflow version ${workflowVersion.id} is not active in workspace ${data.workspaceId}`, - WorkflowTriggerExceptionCode.INTERNAL_ERROR, - ); - } + const workflowVersion = await workflowVersionRepository.findOneBy({ + id: workflow.lastPublishedVersionId, + }); - await this.workflowRunnerWorkspaceService.run({ - workspaceId: data.workspaceId, - workflowVersionId: workflow.lastPublishedVersionId, - payload: data.payload, - source: { - source: FieldActorSource.WORKFLOW, - name: - isDefined(workflow.name) && !isEmpty(workflow.name) - ? workflow.name - : DEFAULT_WORKFLOW_NAME, - context: {}, - workspaceMemberId: null, - }, - }); - } catch (e) { - // We remove cron if it exists when no valid workflowVersion exists - await this.messageQueueService.removeCron({ - jobName: WorkflowTriggerJob.name, - jobId: data.workflowId, - }); - handleWorkflowTriggerException(e); - } + if (!workflowVersion) { + throw new WorkflowTriggerException( + `Workflow version ${workflow.lastPublishedVersionId} not found in workspace ${data.workspaceId}`, + WorkflowTriggerExceptionCode.NOT_FOUND, + ); + } + if (workflowVersion.status !== WorkflowVersionStatus.ACTIVE) { + throw new WorkflowTriggerException( + `Workflow version ${workflowVersion.id} is not active in workspace ${data.workspaceId}`, + WorkflowTriggerExceptionCode.INTERNAL_ERROR, + ); + } + + await this.workflowRunnerWorkspaceService.run({ + workspaceId: data.workspaceId, + workflowVersionId: workflow.lastPublishedVersionId, + payload: data.payload, + source: { + source: FieldActorSource.WORKFLOW, + name: + isDefined(workflow.name) && !isEmpty(workflow.name) + ? workflow.name + : DEFAULT_WORKFLOW_NAME, + context: {}, + workspaceMemberId: null, + }, + }); + } catch (e) { + await this.messageQueueService.removeCron({ + jobName: WorkflowTriggerJob.name, + jobId: data.workflowId, + }); + handleWorkflowTriggerException(e); + } + }, + ); } } diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts index f2a132cabaa..33d786214f0 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts @@ -3,8 +3,9 @@ import { Injectable } from '@nestjs/common'; import { msg } from '@lingui/core/macro'; import { type ActorMetadata } from 'twenty-shared/types'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util'; import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; import { AutomatedTriggerType } from 'src/modules/workflow/common/standard-objects/workflow-automated-trigger.workspace-entity'; import { @@ -31,7 +32,7 @@ import { assertNever } from 'src/utils/assert'; @Injectable() export class WorkflowTriggerWorkspaceService { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService, private readonly workflowRunnerWorkspaceService: WorkflowRunnerWorkspaceService, private readonly automatedTriggerWorkspaceService: AutomatedTriggerWorkspaceService, @@ -69,71 +70,87 @@ export class WorkflowTriggerWorkspaceService { workflowVersionId: string, workspaceId: string, ) { - const workflowVersionRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'workflowVersion', - { shouldBypassPermissionChecks: true }, // settings permissions are checked at resolver-level - ); + const authContext = buildSystemAuthContext(workspaceId); - const workflowVersionNullable = await workflowVersionRepository.findOne({ - where: { id: workflowVersionId }, - }); + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workflowVersionRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'workflowVersion', + { shouldBypassPermissionChecks: true }, + ); - const workflowVersion = - await this.workflowCommonWorkspaceService.getValidWorkflowVersionOrFail( - workflowVersionNullable, - ); + const workflowVersionNullable = await workflowVersionRepository.findOne( + { + where: { id: workflowVersionId }, + }, + ); - const workflowRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'workflow', - { shouldBypassPermissionChecks: true }, // settings permissions are checked at resolver-level - ); + const workflowVersion = + await this.workflowCommonWorkspaceService.getValidWorkflowVersionOrFail( + workflowVersionNullable, + ); - const workflow = await workflowRepository.findOne({ - where: { id: workflowVersion.workflowId }, - }); + const workflowRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'workflow', + { shouldBypassPermissionChecks: true }, + ); - if (!workflow) { - throw new WorkflowTriggerException( - 'No workflow found', - WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION, - ); - } + const workflow = await workflowRepository.findOne({ + where: { id: workflowVersion.workflowId }, + }); - assertVersionCanBeActivated(workflowVersion, workflow); + if (!workflow) { + throw new WorkflowTriggerException( + 'No workflow found', + WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION, + ); + } - await this.performActivationSteps( - workflow, - workflowVersion, - workflowRepository, - workflowVersionRepository, - workspaceId, + assertVersionCanBeActivated(workflowVersion, workflow); + + await this.performActivationSteps( + workflow, + workflowVersion, + workflowRepository, + workflowVersionRepository, + workspaceId, + ); + + return true; + }, ); - - return true; } async deactivateWorkflowVersion( workflowVersionId: string, workspaceId: string, ) { - const workflowVersionRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'workflowVersion', - { shouldBypassPermissionChecks: true }, - ); + const authContext = buildSystemAuthContext(workspaceId); - await this.performDeactivationSteps( - workflowVersionId, - workflowVersionRepository, - workspaceId, + return this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext, + async () => { + const workflowVersionRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'workflowVersion', + { shouldBypassPermissionChecks: true }, + ); + + await this.performDeactivationSteps( + workflowVersionId, + workflowVersionRepository, + workspaceId, + ); + + return true; + }, ); - - return true; } async stopWorkflowRun(workflowRunId: string, workspaceId: string) { diff --git a/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-delete-one.post-query.hook.ts b/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-delete-one.post-query.hook.ts index 528b3430802..78bc1d7b42f 100644 --- a/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-delete-one.post-query.hook.ts +++ b/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-delete-one.post-query.hook.ts @@ -4,6 +4,7 @@ import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils'; import { Repository } from 'typeorm'; import { type WorkspacePostQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface'; +import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface'; import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator'; import { WorkspaceQueryHookType } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type'; @@ -15,7 +16,7 @@ import { PermissionsException, PermissionsExceptionCode, } from 'src/engine/metadata-modules/permissions/permissions.exception'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { WorkspaceMemberPreQueryHookService } from 'src/modules/workspace-member/query-hooks/workspace-member-pre-query-hook.service'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; @@ -27,7 +28,7 @@ export class WorkspaceMemberDeleteOnePostQueryHook implements WorkspacePostQueryHookInstance { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, @InjectRepository(UserWorkspaceEntity) private readonly userWorkspaceRepository: Repository, private readonly workspaceMemberPreQueryHookService: WorkspaceMemberPreQueryHookService, @@ -60,18 +61,24 @@ export class WorkspaceMemberDeleteOnePostQueryHook }, ); - const workspaceMemberRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspace.id, - 'workspaceMember', - ); + const workspaceMember = + await this.globalWorkspaceOrmManager.executeInWorkspaceContext( + authContext as WorkspaceAuthContext, + async () => { + const workspaceMemberRepository = + await this.globalWorkspaceOrmManager.getRepository( + workspace.id, + 'workspaceMember', + ); - const workspaceMember = await workspaceMemberRepository.findOne({ - where: { - id: targettedWorkspaceMemberId, - }, - withDeleted: true, - }); + return workspaceMemberRepository.findOne({ + where: { + id: targettedWorkspaceMemberId, + }, + withDeleted: true, + }); + }, + ); if (!isDefined(workspaceMember)) { throw new PermissionsException( diff --git a/packages/twenty-server/src/queue-worker/queue-worker.module.ts b/packages/twenty-server/src/queue-worker/queue-worker.module.ts index 3b8589080a1..72c5f7c62e2 100644 --- a/packages/twenty-server/src/queue-worker/queue-worker.module.ts +++ b/packages/twenty-server/src/queue-worker/queue-worker.module.ts @@ -1,10 +1,11 @@ import { Module } from '@nestjs/common'; +import { CoreEngineModule } from 'src/engine/core-modules/core-engine.module'; import { JobsModule } from 'src/engine/core-modules/message-queue/jobs.module'; import { MessageQueueModule } from 'src/engine/core-modules/message-queue/message-queue.module'; +import { GlobalWorkspaceDataSourceModule } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource.module'; import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; import { WorkspaceEventEmitterModule } from 'src/engine/workspace-event-emitter/workspace-event-emitter.module'; -import { CoreEngineModule } from 'src/engine/core-modules/core-engine.module'; @Module({ imports: [ @@ -13,6 +14,7 @@ import { CoreEngineModule } from 'src/engine/core-modules/core-engine.module'; WorkspaceEventEmitterModule, JobsModule, TwentyORMModule, + GlobalWorkspaceDataSourceModule, ], }) export class QueueWorkerModule {}