From 9bd8f94b3aa73a65bb63ed9a33418b6ccc15bf4a Mon Sep 17 00:00:00 2001 From: Weiko Date: Wed, 10 Dec 2025 17:17:33 +0100 Subject: [PATCH] Refactor global datasource part 3 (#16447) ## Context Following https://github.com/twentyhq/twenty/pull/16399 Now using the new global orm manager everywhere and returning a GlobalDatasource/WorkspaceDatasource based on a feature flag. This means we now need to wrap all our ORM calls within executeInWorkspaceContext callback (at least for now) so the global datasource can dynamically hydrate its context via the new store (the global datasource does not store anything related to workspaces as it is now a unique singleton). If feature flag is off it still uses local data stored in the workspace datasource. --- packages/twenty-server/src/app.module.ts | 2 + .../__tests__/upgrade.command-runner.spec.ts | 25 +- ...ded-workspaces-migration.command-runner.ts | 6 +- .../command-runners/upgrade.command-runner.ts | 6 +- .../workspaces-migration.command-runner.ts | 52 +- ...-add-workflow-run-stop-statuses.command.ts | 6 +- ...ate-operation-field-metadata-id.command.ts | 4 +- ...tadata-id-foreign-key-migration.command.ts | 4 +- .../1-10/1-10-flush-cache.command.ts | 4 +- ...sure-dashboard-naming-available.command.ts | 4 +- ...attachment-author-to-created-by.command.ts | 8 +- ...ttachment-type-to-file-category.command.ts | 6 +- ...hannel-partial-full-sync-stages.command.ts | 4 +- .../1-10-regenerate-search-vectors.command.ts | 4 +- .../1-10/1-10-seed-dashboard-view.command.ts | 4 +- ...-11-clean-orphaned-role-targets.command.ts | 4 +- ...-clean-orphaned-user-workspaces.command.ts | 6 +- ...ate-twenty-standard-application.command.ts | 4 +- ...nts-import-scheduled-sync-stage.command.ts | 4 +- ...ges-import-scheduled-sync-stage.command.ts | 4 +- .../1-12/1-12-clean-null-equivalent-values.ts | 4 +- ...te-workspace-custom-application.command.ts | 4 +- ...d-application-not-uninstallable.command.ts | 4 +- ...ation-id-non-nullable-migration.command.ts | 4 +- ...ge-layout-universal-identifiers.command.ts | 4 +- ...main-group-by-field-metadata-id.command.ts | 4 +- ...mpty-string-null-in-text-fields.command.ts | 13 +- .../1-13-deduplicate-role-targets.command.ts | 4 +- ...grate-standard-invalid-entities.command.ts | 4 +- .../1-13/1-13-rename-unique-index.command.ts | 4 +- ...ets-unique-constraint-migration.command.ts | 4 +- ...ntifier-position-and-visibility.command.ts | 4 +- ...low-manual-trigger-availability.command.ts | 4 +- .../1-8-deduplicate-unique-fields.command.ts | 13 +- ...ll-serverless-function-layer-id.command.ts | 4 +- ...1-8-migrate-channel-sync-stages.command.ts | 4 +- ...rate-workflow-step-filter-operand-value.ts | 6 +- ...erson-search-vector-with-phones.command.ts | 4 +- .../upgrade.command.ts | 6 +- .../common-base-query-runner.service.ts | 5 +- .../api/common/core-common-api.module.ts | 2 - .../core/handlers/rest-api-base.handler.ts | 3 - ...eated-by-from-auth-context.service.spec.ts | 18 +- .../created-by-from-auth-context.service.ts | 43 +- .../approved-access-domain.resolver.ts | 35 +- .../create-calendar-channel.service.ts | 48 +- .../create-connected-account.service.ts | 44 +- .../create-message-channel.service.ts | 91 +-- .../auth/services/google-apis.service.spec.ts | 9 +- .../auth/services/google-apis.service.ts | 307 +++++----- .../services/microsoft-apis.service.spec.ts | 9 +- .../auth/services/microsoft-apis.service.ts | 322 ++++++----- ...-connected-account-on-reconnect.service.ts | 42 +- .../services/access-token.service.spec.ts | 31 +- .../token/services/access-token.service.ts | 57 +- .../billing-sync-customer-data.command.ts | 6 +- ...lling-update-subscription-price.command.ts | 6 +- .../jobs/update-subscription-quantity.job.ts | 64 ++- .../timeline-calendar-event.service.spec.ts | 13 +- .../timeline-calendar-event.service.ts | 381 ++++++------ .../imap-smtp-caldav-connection.resolver.ts | 16 +- .../imap-smtp-caldav-connection.service.ts | 36 +- .../services/get-messages.service.ts | 133 +++-- .../services/timeline-messaging.service.ts | 380 ++++++------ .../services/create-record.service.ts | 209 +++---- .../services/delete-record.service.ts | 155 ++--- .../services/find-records.service.ts | 139 ++--- .../services/update-record.service.ts | 229 ++++---- .../services/upsert-record.service.ts | 303 +++++----- .../__tests__/record-position.service.spec.ts | 17 +- .../services/record-position.service.ts | 111 ++-- .../search/__tests__/search.service.spec.ts | 4 +- .../search/services/search.service.ts | 50 +- .../tools/send-email-tool/send-email-tool.ts | 91 +-- .../user-workspace.service.spec.ts | 39 +- .../user-workspace/user-workspace.service.ts | 117 ++-- .../jobs/update-workspace-member-email.job.ts | 29 +- .../user/services/user.service.spec.ts | 46 +- .../user/services/user.service.ts | 119 ++-- .../engine/core-modules/user/user.resolver.ts | 37 +- .../workflow-trigger.controller.ts | 119 ++-- .../resolvers/workflow-trigger.resolver.ts | 35 +- .../workspace-invitation.resolver.ts | 65 ++- .../services/agent-actor-context.service.ts | 35 +- .../services/chat-execution.service.ts | 196 ++++--- .../object-metadata.service.ts | 32 +- .../services/page-layout.service.ts | 40 +- .../user-role/user-role.service.ts | 36 +- .../object-metadata-repository.module.ts | 8 +- .../__tests__/trash-cleanup.service.spec.ts | 25 +- .../services/trash-cleanup.service.ts | 67 ++- .../datasource/workspace.datasource.ts | 7 +- .../workspace-entity-manager.ts | 43 +- .../global-workspace-datasource.module.ts | 2 + .../global-workspace-datasource.ts | 25 +- .../global-workspace-orm.manager.ts | 91 ++- .../workspace-datasource.interface.ts | 49 ++ .../__tests__/workspace.repository.spec.ts | 4 +- .../repository/workspace.repository.ts | 14 +- .../storage/orm-workspace-context.storage.ts | 1 + .../utils/build-system-auth-context.util.ts | 15 + .../workspace-manager.service.spec.ts | 4 +- .../services/cleaner.workspace-service.ts | 6 +- .../workspace-manager.service.ts | 4 +- .../sync-workspace-metadata.command.ts | 8 +- .../services/blocklist-validation.service.ts | 52 +- .../repositories/blocklist.repository.ts | 59 +- ...ocklist-item-delete-calendar-events.job.ts | 216 +++---- .../blocklist-reimport-calendar-events.job.ts | 60 +- .../calendar-event-cleaner.service.ts | 57 +- .../jobs/calendar-event-list-fetch.job.ts | 96 ++-- .../jobs/calendar-events-import.job.ts | 96 ++-- .../jobs/calendar-ongoing-stale.job.ts | 104 ++-- ...ar-relaunch-failed-calendar-channel.job.ts | 69 ++- ...-event-import-exception-handler.service.ts | 36 +- .../calendar-events-import.service.ts | 219 +++---- .../services/calendar-fetch-events.service.ts | 219 +++---- .../services/calendar-save-events.service.ts | 440 +++++++------- .../calendar-event-participant.service.ts | 235 ++++---- ...ts-visibility-restrictions.service.spec.ts | 33 +- ...-events-visibility-restrictions.service.ts | 172 +++--- .../calendar-channel-sync-status.service.ts | 287 +++++---- .../services/channel-sync.service.ts | 120 ++-- .../email-alias-manager.service.spec.ts | 9 +- .../services/email-alias-manager.service.ts | 30 +- ...workspace-member-connected-accounts.job.ts | 28 +- .../listeners/connected-account.listener.ts | 56 +- ...ected-account-delete-one.pre-query.hook.ts | 27 +- ...ted-account-refresh-tokens.service.spec.ts | 26 +- ...onnected-account-refresh-tokens.service.ts | 32 +- .../imap-smtp-caldav-apis.service.spec.ts | 10 +- .../services/imap-smtp-caldav-apis.service.ts | 182 +++--- ...create-company-and-contact.service.spec.ts | 4 +- .../__tests__/create-company.service.spec.ts | 11 +- .../create-company-and-contact.service.ts | 212 +++---- .../services/create-company.service.ts | 187 +++--- .../services/create-person.service.ts | 97 ++-- .../listeners/favorite-folder.listener.ts | 35 +- .../services/favorite-deletion.service.ts | 121 ++-- .../match-participant.service.ts | 160 +++--- ...ging-blocklist-item-delete-messages.job.ts | 230 ++++---- ...ssaging-blocklist-reimport-messages.job.ts | 58 +- ...es-visibility-restrictions.service.spec.ts | 33 +- ...essages-visibility-restrictions.service.ts | 191 +++--- ...ssage-channel-update-one.pre-query.hook.ts | 173 +++--- .../message-channel-sync-status.service.ts | 355 +++++++----- ...message-clearner-remove-orphans.command.ts | 6 +- .../messaging-reset-channel.command.ts | 84 +-- .../messaging-message-cleaner.service.ts | 313 +++++----- ...essage-folder-update-one.pre-query.hook.ts | 122 ++-- .../services/sync-message-folders.service.ts | 34 +- .../jobs/messaging-message-list-fetch.job.ts | 152 ++--- .../jobs/messaging-messages-import.job.ts | 115 ++-- .../jobs/messaging-ongoing-stale.job.ts | 104 ++-- ...ing-relaunch-failed-message-channel.job.ts | 68 ++- ...ssaging-message-list-fetch.service.spec.ts | 44 +- .../messaging-messages-import.service.spec.ts | 9 +- .../services/messaging-cursor.service.ts | 100 ++-- ...ing-delete-group-email-messages.service.ts | 166 +++--- ...saging-import-exception-handler.service.ts | 34 +- .../messaging-message-list-fetch.service.ts | 477 +++++++-------- .../services/messaging-message.service.ts | 337 +++++------ .../messaging-messages-import.service.ts | 285 ++++----- ...essaging-process-folder-actions.service.ts | 68 ++- ...ing-process-group-email-actions.service.ts | 135 +++-- ...d-enqueue-contact-creation.service.spec.ts | 7 +- ...es-and-enqueue-contact-creation.service.ts | 149 ++--- .../messaging-message-participant.service.ts | 104 ++-- ...channel-sync-status-monitoring.cron.job.ts | 56 +- ...meline-activity-from-internal-event.job.ts | 58 +- .../timeline-activity.repository.ts | 117 ++-- .../services/timeline-activity.service.ts | 87 +-- .../workflow-create-many.post-query.hook.ts | 60 +- .../workflow-create-one.post-query.hook.ts | 50 +- .../workflow-common.workspace-service.ts | 175 +++--- ...ow-version-validation.workspace-service.ts | 113 ++-- ...low-version-edge.workspace-service.spec.ts | 17 +- ...workflow-version-edge.workspace-service.ts | 183 +++--- ...-step-operations.workspace-service.spec.ts | 15 +- ...low-version-step.workspace-service.spec.ts | 26 +- ...-version-step-helpers.workspace-service.ts | 40 +- ...rsion-step-operations.workspace-service.ts | 216 +++---- .../workflow-version.workspace-service.ts | 544 +++++++++--------- .../delay/jobs/resume-delayed-workflow.job.ts | 120 ++-- .../workflow-runner/jobs/run-workflow.job.ts | 52 +- .../workflow-clean-workflow-runs.cron.job.ts | 68 ++- ...ow-handle-staled-runs.workspace-service.ts | 66 ++- .../workflow-run-enqueue.workspace-service.ts | 188 +++--- .../workflow-throttling.workspace-service.ts | 59 +- .../command/delete-workflow-runs.command.ts | 60 +- .../workflow-run.workspace-service.ts | 224 ++++---- .../workflow-statuses-update.job.spec.ts | 14 +- .../jobs/workflow-statuses-update.job.ts | 70 ++- .../workflow-tool.workspace-service.ts | 6 +- .../tools/create-complete-workflow.tool.ts | 148 +++-- .../types/workflow-tool-dependencies.type.ts | 4 +- .../automated-trigger.workspace-service.ts | 51 +- ...ow-database-event-trigger.listener.spec.ts | 15 +- ...orkflow-database-event-trigger.listener.ts | 189 +++--- .../jobs/workflow-trigger.job.ts | 141 ++--- .../workflow-trigger.workspace-service.ts | 119 ++-- ...space-member-delete-one.post-query.hook.ts | 33 +- .../src/queue-worker/queue-worker.module.ts | 4 +- 203 files changed, 8887 insertions(+), 7237 deletions(-) create mode 100644 packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-datasource.interface.ts create mode 100644 packages/twenty-server/src/engine/twenty-orm/utils/build-system-auth-context.util.ts 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 {}