From d93024fd02c025f2c09762e2891990b22c804946 Mon Sep 17 00:00:00 2001 From: Guillim Date: Sat, 17 May 2025 13:47:01 +0200 Subject: [PATCH] Refactoring the reconnect service (#12089) following qrqc #3 : refactoring the reconnect service Fixes https://github.com/twentyhq/twenty/issues/12064 --- .../engine/core-modules/auth/auth.module.ts | 24 +- .../create-calendar-channel.service.ts | 85 +++++ .../create-connected-account.service.ts | 90 ++++++ .../create-message-channel.service.ts | 89 ++++++ .../services/create-message-folder.service.ts | 53 ++++ .../auth/services/google-apis.service.spec.ts | 251 +++++++++++++++ .../auth/services/google-apis.service.ts | 250 +++------------ .../services/microsoft-apis.service.spec.ts | 272 ++++++++++++++++ .../auth/services/microsoft-apis.service.ts | 290 ++++-------------- .../reset-calendar-channel.service.ts | 84 +++++ .../services/reset-message-channel.service.ts | 78 +++++ .../services/reset-message-folder.service.ts | 69 +++++ ...-connected-account-on-reconnect.service.ts | 88 ++++++ 13 files changed, 1286 insertions(+), 437 deletions(-) create mode 100644 packages/twenty-server/src/engine/core-modules/auth/services/create-calendar-channel.service.ts create mode 100644 packages/twenty-server/src/engine/core-modules/auth/services/create-connected-account.service.ts create mode 100644 packages/twenty-server/src/engine/core-modules/auth/services/create-message-channel.service.ts create mode 100644 packages/twenty-server/src/engine/core-modules/auth/services/create-message-folder.service.ts create mode 100644 packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.spec.ts create mode 100644 packages/twenty-server/src/engine/core-modules/auth/services/microsoft-apis.service.spec.ts create mode 100644 packages/twenty-server/src/engine/core-modules/auth/services/reset-calendar-channel.service.ts create mode 100644 packages/twenty-server/src/engine/core-modules/auth/services/reset-message-channel.service.ts create mode 100644 packages/twenty-server/src/engine/core-modules/auth/services/reset-message-folder.service.ts create mode 100644 packages/twenty-server/src/engine/core-modules/auth/services/update-connected-account-on-reconnect.service.ts diff --git a/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts b/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts index 3941f30b5..223e4aea8 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts @@ -12,12 +12,19 @@ import { MicrosoftAPIsAuthController } from 'src/engine/core-modules/auth/contro import { MicrosoftAuthController } from 'src/engine/core-modules/auth/controllers/microsoft-auth.controller'; import { SSOAuthController } from 'src/engine/core-modules/auth/controllers/sso-auth.controller'; import { ApiKeyService } from 'src/engine/core-modules/auth/services/api-key.service'; +import { AuthSsoService } from 'src/engine/core-modules/auth/services/auth-sso.service'; +import { CreateCalendarChannelService } from 'src/engine/core-modules/auth/services/create-calendar-channel.service'; +import { CreateConnectedAccountService } from 'src/engine/core-modules/auth/services/create-connected-account.service'; +import { CreateMessageChannelService } from 'src/engine/core-modules/auth/services/create-message-channel.service'; +import { CreateMessageFolderService } from 'src/engine/core-modules/auth/services/create-message-folder.service'; import { GoogleAPIsService } from 'src/engine/core-modules/auth/services/google-apis.service'; import { MicrosoftAPIsService } from 'src/engine/core-modules/auth/services/microsoft-apis.service'; -// import { OAuthService } from 'src/engine/core-modules/auth/services/oauth.service'; -import { AuthSsoService } from 'src/engine/core-modules/auth/services/auth-sso.service'; +import { ResetCalendarChannelService } from 'src/engine/core-modules/auth/services/reset-calendar-channel.service'; +import { ResetMessageChannelService } from 'src/engine/core-modules/auth/services/reset-message-channel.service'; +import { ResetMessageFolderService } from 'src/engine/core-modules/auth/services/reset-message-folder.service'; import { ResetPasswordService } from 'src/engine/core-modules/auth/services/reset-password.service'; import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service'; +import { UpdateConnectedAccountOnReconnectService } from 'src/engine/core-modules/auth/services/update-connected-account-on-reconnect.service'; import { SamlAuthStrategy } from 'src/engine/core-modules/auth/strategies/saml.auth.strategy'; import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service'; import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service'; @@ -114,11 +121,20 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy'; RefreshTokenService, LoginTokenService, ResetPasswordService, + // So far, it's not possible to have controllers in business modules + // which forces us to have these services in the auth module + // TODO: Move these calendar, message, and connected account services to the business modules once possible + ResetMessageChannelService, + ResetCalendarChannelService, + ResetMessageFolderService, + CreateMessageChannelService, + CreateCalendarChannelService, + CreateMessageFolderService, + CreateConnectedAccountService, + UpdateConnectedAccountOnReconnectService, TransientTokenService, ApiKeyService, AuthSsoService, - // reenable when working on: https://github.com/twentyhq/twenty/issues/9143 - // OAuthService, ], exports: [AccessTokenService, LoginTokenService, RefreshTokenService], }) 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 new file mode 100644 index 000000000..f1d66e2e4 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/auth/services/create-calendar-channel.service.ts @@ -0,0 +1,85 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { Repository } from 'typeorm'; +import { v4 } from 'uuid'; + +import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; +import { + CalendarChannelVisibility, + CalendarChannelWorkspaceEntity, +} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; + +export type CreateCalendarChannelInput = { + workspaceId: string; + connectedAccountId: string; + handle: string; + calendarVisibility?: CalendarChannelVisibility; + manager: WorkspaceEntityManager; +}; + +@Injectable() +export class CreateCalendarChannelService { + constructor( + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly workspaceEventEmitter: WorkspaceEventEmitter, + @InjectRepository(ObjectMetadataEntity, 'metadata') + private readonly objectMetadataRepository: Repository, + ) {} + + async createCalendarChannel( + input: CreateCalendarChannelInput, + ): Promise { + const { + workspaceId, + connectedAccountId, + handle, + calendarVisibility, + manager, + } = input; + + const calendarChannelRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + 'calendarChannel', + ); + + const newCalendarChannel = await calendarChannelRepository.save( + { + id: v4(), + connectedAccountId, + handle, + visibility: + calendarVisibility || CalendarChannelVisibility.SHARE_EVERYTHING, + }, + {}, + manager, + ); + + const calendarChannelMetadata = + await this.objectMetadataRepository.findOneOrFail({ + where: { nameSingular: 'calendarChannel', workspaceId }, + }); + + this.workspaceEventEmitter.emitDatabaseBatchEvent({ + objectMetadataNameSingular: 'calendarChannel', + action: DatabaseEventAction.CREATED, + events: [ + { + recordId: newCalendarChannel.id, + objectMetadata: calendarChannelMetadata, + properties: { + after: newCalendarChannel, + }, + }, + ], + workspaceId, + }); + + 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 new file mode 100644 index 000000000..29a8c2693 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/auth/services/create-connected-account.service.ts @@ -0,0 +1,90 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { ConnectedAccountProvider } from 'twenty-shared/types'; +import { Repository } from 'typeorm'; + +import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; +import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; + +export type CreateConnectedAccountInput = { + workspaceId: string; + connectedAccountId: string; + handle: string; + provider: ConnectedAccountProvider; + accessToken: string; + refreshToken: string; + accountOwnerId: string; + scopes: string[]; + manager: WorkspaceEntityManager; +}; + +@Injectable() +export class CreateConnectedAccountService { + constructor( + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly workspaceEventEmitter: WorkspaceEventEmitter, + @InjectRepository(ObjectMetadataEntity, 'metadata') + private readonly objectMetadataRepository: Repository, + ) {} + + async createConnectedAccount( + input: CreateConnectedAccountInput, + ): Promise { + const { + workspaceId, + connectedAccountId, + handle, + provider, + accessToken, + refreshToken, + accountOwnerId, + scopes, + manager, + } = input; + + const connectedAccountRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + 'connectedAccount', + ); + + const newConnectedAccount = await connectedAccountRepository.save( + { + id: connectedAccountId, + handle, + provider, + accessToken, + refreshToken, + accountOwnerId, + scopes, + }, + {}, + manager, + ); + + const connectedAccountMetadata = + await this.objectMetadataRepository.findOneOrFail({ + where: { nameSingular: 'connectedAccount', workspaceId }, + }); + + this.workspaceEventEmitter.emitDatabaseBatchEvent({ + objectMetadataNameSingular: 'connectedAccount', + action: DatabaseEventAction.CREATED, + events: [ + { + recordId: newConnectedAccount.id, + objectMetadata: connectedAccountMetadata, + properties: { + after: newConnectedAccount, + }, + }, + ], + workspaceId, + }); + } +} 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 new file mode 100644 index 000000000..d5b15c455 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/auth/services/create-message-channel.service.ts @@ -0,0 +1,89 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { Repository } from 'typeorm'; +import { v4 } from 'uuid'; + +import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; +import { + MessageChannelSyncStatus, + MessageChannelType, + MessageChannelVisibility, + MessageChannelWorkspaceEntity, +} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; + +export type CreateMessageChannelInput = { + workspaceId: string; + connectedAccountId: string; + handle: string; + messageVisibility?: MessageChannelVisibility; + manager: WorkspaceEntityManager; +}; + +@Injectable() +export class CreateMessageChannelService { + constructor( + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly workspaceEventEmitter: WorkspaceEventEmitter, + @InjectRepository(ObjectMetadataEntity, 'metadata') + private readonly objectMetadataRepository: Repository, + ) {} + + async createMessageChannel( + input: CreateMessageChannelInput, + ): Promise { + const { + workspaceId, + connectedAccountId, + handle, + messageVisibility, + manager, + } = input; + + const messageChannelRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + 'messageChannel', + ); + + const newMessageChannel = await messageChannelRepository.save( + { + id: v4(), + connectedAccountId, + type: MessageChannelType.EMAIL, + handle, + visibility: + messageVisibility || MessageChannelVisibility.SHARE_EVERYTHING, + syncStatus: MessageChannelSyncStatus.ONGOING, + }, + {}, + manager, + ); + + const messageChannelMetadata = + await this.objectMetadataRepository.findOneOrFail({ + where: { nameSingular: 'messageChannel', workspaceId }, + }); + + this.workspaceEventEmitter.emitDatabaseBatchEvent({ + objectMetadataNameSingular: 'messageChannel', + action: DatabaseEventAction.CREATED, + events: [ + { + recordId: newMessageChannel.id, + objectMetadata: messageChannelMetadata, + properties: { + after: newMessageChannel, + }, + }, + ], + workspaceId, + }); + + return newMessageChannel.id; + } +} diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/create-message-folder.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/create-message-folder.service.ts new file mode 100644 index 000000000..73fa19f81 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/auth/services/create-message-folder.service.ts @@ -0,0 +1,53 @@ +import { Injectable } from '@nestjs/common'; + +import { v4 } from 'uuid'; + +import { WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { MessageFolderWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity'; +import { MessageFolderName } from 'src/modules/messaging/message-import-manager/drivers/microsoft/types/folders'; + +export type CreateMessageFoldersInput = { + workspaceId: string; + messageChannelId: string; + manager: WorkspaceEntityManager; +}; + +@Injectable() +export class CreateMessageFolderService { + constructor( + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + ) {} + + async createMessageFolders(input: CreateMessageFoldersInput): Promise { + const { workspaceId, messageChannelId, manager } = input; + + const messageFolderRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + 'messageFolder', + ); + + await messageFolderRepository.save( + { + id: v4(), + messageChannelId, + name: MessageFolderName.INBOX, + syncCursor: '', + }, + {}, + manager, + ); + + await messageFolderRepository.save( + { + id: v4(), + messageChannelId, + name: MessageFolderName.SENT_ITEMS, + syncCursor: '', + }, + {}, + manager, + ); + } +} 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 new file mode 100644 index 000000000..8414f0c5b --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.spec.ts @@ -0,0 +1,251 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; + +import { ConnectedAccountProvider } from 'twenty-shared/types'; + +import { CreateCalendarChannelService } from 'src/engine/core-modules/auth/services/create-calendar-channel.service'; +import { CreateConnectedAccountService } from 'src/engine/core-modules/auth/services/create-connected-account.service'; +import { CreateMessageChannelService } from 'src/engine/core-modules/auth/services/create-message-channel.service'; +import { GoogleAPIsService } from 'src/engine/core-modules/auth/services/google-apis.service'; +import { ResetCalendarChannelService } from 'src/engine/core-modules/auth/services/reset-calendar-channel.service'; +import { ResetMessageChannelService } from 'src/engine/core-modules/auth/services/reset-message-channel.service'; +import { UpdateConnectedAccountOnReconnectService } from 'src/engine/core-modules/auth/services/update-connected-account-on-reconnect.service'; +import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; +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 { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; +import { + CalendarChannelSyncStage, + CalendarChannelVisibility, +} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; +import { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service'; +import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; +import { MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; + +jest.mock('uuid', () => ({ + v4: jest.fn(() => 'mocked-uuid'), +})); + +describe('GoogleAPIsService', () => { + let service: GoogleAPIsService; + let resetCalendarChannelService: ResetCalendarChannelService; + let resetMessageChannelService: ResetMessageChannelService; + let createMessageChannelService: CreateMessageChannelService; + + const mockConnectedAccountRepository = { + findOne: jest.fn(), + find: jest.fn(), + update: jest.fn(), + }; + + const mockCalendarChannelRepository = { + findOne: jest.fn(), + find: jest.fn(), + update: jest.fn(), + }; + + const mockMessageChannelRepository = { + findOne: jest.fn(), + find: jest.fn(), + update: jest.fn(), + }; + + const mockWorkspaceMemberRepository = { + findOneOrFail: jest.fn(), + }; + + const mockWorkspaceDataSource = { + transaction: jest.fn((callback) => callback({})), + }; + + const mockTwentyConfigService = { + get: jest.fn(), + }; + + const mockMessageQueueService = { + add: jest.fn(), + }; + + const mockCalendarQueueService = { + add: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + GoogleAPIsService, + { + provide: TwentyORMGlobalManager, + useValue: { + getRepositoryForWorkspace: jest + .fn() + .mockImplementation((workspaceId, entity) => { + if (entity === 'connectedAccount') + return mockConnectedAccountRepository; + if (entity === 'calendarChannel') + return mockCalendarChannelRepository; + if (entity === 'messageChannel') + return mockMessageChannelRepository; + if (entity === 'workspaceMember') + return mockWorkspaceMemberRepository; + + return {}; + }), + getDataSourceForWorkspace: jest + .fn() + .mockImplementation(() => mockWorkspaceDataSource), + }, + }, + { + provide: getRepositoryToken(ObjectMetadataEntity, 'metadata'), + useValue: { + findOneOrFail: jest.fn(), + }, + }, + { + provide: TwentyConfigService, + useValue: mockTwentyConfigService, + }, + { + provide: ResetCalendarChannelService, + useValue: { + resetCalendarChannels: jest.fn(), + }, + }, + { + provide: ResetMessageChannelService, + useValue: { + resetMessageChannels: jest.fn(), + }, + }, + { + provide: CreateConnectedAccountService, + useValue: { + createConnectedAccount: jest.fn(), + }, + }, + { + provide: CreateMessageChannelService, + useValue: { + createMessageChannel: jest.fn(), + }, + }, + { + provide: CreateCalendarChannelService, + useValue: { + createCalendarChannel: jest.fn(), + }, + }, + { + provide: UpdateConnectedAccountOnReconnectService, + useValue: { + updateConnectedAccountOnReconnect: jest.fn(), + }, + }, + { + provide: AccountsToReconnectService, + useValue: { + removeAccountToReconnect: jest.fn(), + }, + }, + { + provide: WorkspaceEventEmitter, + useValue: { + emitDatabaseBatchEvent: jest.fn(), + }, + }, + { + provide: getQueueToken(MessageQueue.messagingQueue), + useValue: mockMessageQueueService, + }, + { + provide: getQueueToken(MessageQueue.calendarQueue), + useValue: mockCalendarQueueService, + }, + ], + }).compile(); + + service = module.get(GoogleAPIsService); + resetCalendarChannelService = module.get( + ResetCalendarChannelService, + ); + resetMessageChannelService = module.get( + ResetMessageChannelService, + ); + createMessageChannelService = module.get( + CreateMessageChannelService, + ); + }); + + describe('refreshGoogleRefreshToken', () => { + it('should reset calendar channels with FAILED_UNKNOWN syncStatus and FAILED syncStage', async () => { + mockTwentyConfigService.get.mockImplementation((key) => { + if (key === 'CALENDAR_PROVIDER_GOOGLE_ENABLED') return true; + if (key === 'MESSAGING_PROVIDER_GMAIL_ENABLED') return true; + + return false; + }); + + const existingConnectedAccount = { + id: 'existing-account-id', + handle: 'test@example.com', + accountOwnerId: 'workspace-member-id', + provider: ConnectedAccountProvider.GOOGLE, + } as ConnectedAccountWorkspaceEntity; + + mockConnectedAccountRepository.findOne.mockResolvedValue( + existingConnectedAccount, + ); + + mockWorkspaceMemberRepository.findOneOrFail.mockResolvedValue({ + id: 'workspace-member-id', + userId: 'user-id', + }); + + const failedCalendarChannel = { + id: 'calendar-channel-id', + connectedAccountId: 'existing-account-id', + syncStatus: 'FAILED_UNKNOWN', + syncStage: CalendarChannelSyncStage.FAILED, + }; + + mockCalendarChannelRepository.find.mockResolvedValue([ + failedCalendarChannel, + ]); + + mockMessageChannelRepository.find.mockResolvedValue([]); + + await service.refreshGoogleRefreshToken({ + handle: 'test@example.com', + workspaceMemberId: 'workspace-member-id', + workspaceId: 'workspace-id', + accessToken: 'new-access-token', + refreshToken: 'new-refresh-token', + calendarVisibility: CalendarChannelVisibility.SHARE_EVERYTHING, + messageVisibility: MessageChannelVisibility.SHARE_EVERYTHING, + }); + + expect( + resetCalendarChannelService.resetCalendarChannels, + ).toHaveBeenCalledWith({ + workspaceId: 'workspace-id', + connectedAccountId: 'existing-account-id', + manager: expect.any(Object), + }); + + expect( + resetMessageChannelService.resetMessageChannels, + ).toHaveBeenCalledWith({ + workspaceId: 'workspace-id', + connectedAccountId: 'existing-account-id', + manager: expect.any(Object), + }); + + expect( + createMessageChannelService.createMessageChannel, + ).not.toHaveBeenCalled(); + }); + }); +}); 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 742e77dca..731104d4d 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 @@ -5,7 +5,12 @@ import { ConnectedAccountProvider } from 'twenty-shared/types'; import { Repository } from 'typeorm'; import { v4 } from 'uuid'; -import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; +import { CreateCalendarChannelService } from 'src/engine/core-modules/auth/services/create-calendar-channel.service'; +import { CreateConnectedAccountService } from 'src/engine/core-modules/auth/services/create-connected-account.service'; +import { CreateMessageChannelService } from 'src/engine/core-modules/auth/services/create-message-channel.service'; +import { ResetCalendarChannelService } from 'src/engine/core-modules/auth/services/reset-calendar-channel.service'; +import { ResetMessageChannelService } from 'src/engine/core-modules/auth/services/reset-message-channel.service'; +import { UpdateConnectedAccountOnReconnectService } from 'src/engine/core-modules/auth/services/update-connected-account-on-reconnect.service'; import { getGoogleApisOauthScopes } from 'src/engine/core-modules/auth/utils/get-google-apis-oauth-scopes'; 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'; @@ -20,16 +25,12 @@ import { CalendarEventListFetchJobData, } from 'src/modules/calendar/calendar-event-import-manager/jobs/calendar-event-list-fetch.job'; import { - CalendarChannelSyncStage, CalendarChannelVisibility, CalendarChannelWorkspaceEntity, } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; import { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service'; import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { - MessageChannelSyncStage, - MessageChannelSyncStatus, - MessageChannelType, MessageChannelVisibility, MessageChannelWorkspaceEntity, } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; @@ -49,6 +50,12 @@ export class GoogleAPIsService { private readonly calendarQueueService: MessageQueueService, private readonly twentyConfigService: TwentyConfigService, private readonly accountsToReconnectService: AccountsToReconnectService, + private readonly resetMessageChannelService: ResetMessageChannelService, + private readonly resetCalendarChannelService: ResetCalendarChannelService, + private readonly createMessageChannelService: CreateMessageChannelService, + private readonly createCalendarChannelService: CreateCalendarChannelService, + private readonly createConnectedAccountService: CreateConnectedAccountService, + private readonly updateConnectedAccountOnReconnectService: UpdateConnectedAccountOnReconnectService, private readonly workspaceEventEmitter: WorkspaceEventEmitter, @InjectRepository(ObjectMetadataEntity, 'metadata') private readonly objectMetadataRepository: Repository, @@ -110,145 +117,47 @@ export class GoogleAPIsService { await workspaceDataSource.transaction( async (manager: WorkspaceEntityManager) => { if (!existingAccountId) { - const newConnectedAccount = await connectedAccountRepository.save( - { - id: newOrExistingConnectedAccountId, - handle, - provider: ConnectedAccountProvider.GOOGLE, - accessToken: input.accessToken, - refreshToken: input.refreshToken, - accountOwnerId: workspaceMemberId, - scopes, - }, - {}, - manager, - ); - - const connectedAccountMetadata = - await this.objectMetadataRepository.findOneOrFail({ - where: { nameSingular: 'connectedAccount', workspaceId }, - }); - - this.workspaceEventEmitter.emitDatabaseBatchEvent({ - objectMetadataNameSingular: 'connectedAccount', - action: DatabaseEventAction.CREATED, - events: [ - { - recordId: newConnectedAccount.id, - objectMetadata: connectedAccountMetadata, - properties: { - after: newConnectedAccount, - }, - }, - ], + await this.createConnectedAccountService.createConnectedAccount({ workspaceId, + connectedAccountId: newOrExistingConnectedAccountId, + handle, + provider: ConnectedAccountProvider.GOOGLE, + accessToken: input.accessToken, + refreshToken: input.refreshToken, + accountOwnerId: workspaceMemberId, + scopes, + manager, }); - const newMessageChannel = await messageChannelRepository.save( - { - id: v4(), - connectedAccountId: newOrExistingConnectedAccountId, - type: MessageChannelType.EMAIL, - handle, - visibility: - messageVisibility || MessageChannelVisibility.SHARE_EVERYTHING, - syncStatus: MessageChannelSyncStatus.ONGOING, - }, - {}, - manager, - ); - - const messageChannelMetadata = - await this.objectMetadataRepository.findOneOrFail({ - where: { nameSingular: 'messageChannel', workspaceId }, - }); - - this.workspaceEventEmitter.emitDatabaseBatchEvent({ - objectMetadataNameSingular: 'messageChannel', - action: DatabaseEventAction.CREATED, - events: [ - { - recordId: newMessageChannel.id, - objectMetadata: messageChannelMetadata, - properties: { - after: newMessageChannel, - }, - }, - ], + await this.createMessageChannelService.createMessageChannel({ workspaceId, + connectedAccountId: newOrExistingConnectedAccountId, + handle, + messageVisibility, + manager, }); if (isCalendarEnabled) { - const newCalendarChannel = await calendarChannelRepository.save( - { - id: v4(), - connectedAccountId: newOrExistingConnectedAccountId, - handle, - visibility: - calendarVisibility || - CalendarChannelVisibility.SHARE_EVERYTHING, - }, - {}, - manager, - ); - - const calendarChannelMetadata = - await this.objectMetadataRepository.findOneOrFail({ - where: { nameSingular: 'calendarChannel', workspaceId }, - }); - - this.workspaceEventEmitter.emitDatabaseBatchEvent({ - objectMetadataNameSingular: 'calendarChannel', - action: DatabaseEventAction.CREATED, - events: [ - { - recordId: newCalendarChannel.id, - objectMetadata: calendarChannelMetadata, - properties: { - after: newCalendarChannel, - }, - }, - ], + await this.createCalendarChannelService.createCalendarChannel({ workspaceId, + connectedAccountId: newOrExistingConnectedAccountId, + handle, + calendarVisibility, + manager, }); } } else { - const updatedConnectedAccount = - await connectedAccountRepository.update( - { - id: newOrExistingConnectedAccountId, - }, - { - accessToken: input.accessToken, - refreshToken: input.refreshToken, - scopes, - }, + await this.updateConnectedAccountOnReconnectService.updateConnectedAccountOnReconnect( + { + workspaceId, + connectedAccountId: newOrExistingConnectedAccountId, + accessToken: input.accessToken, + refreshToken: input.refreshToken, + scopes, + connectedAccount, manager, - ); - - const connectedAccountMetadata = - await this.objectMetadataRepository.findOneOrFail({ - where: { nameSingular: 'connectedAccount', workspaceId }, - }); - - this.workspaceEventEmitter.emitDatabaseBatchEvent({ - objectMetadataNameSingular: 'connectedAccount', - action: DatabaseEventAction.UPDATED, - events: [ - { - recordId: newOrExistingConnectedAccountId, - objectMetadata: connectedAccountMetadata, - properties: { - before: connectedAccount, - after: { - ...connectedAccount, - ...updatedConnectedAccount.raw[0], - }, - }, - }, - ], - workspaceId, - }); + }, + ); const workspaceMemberRepository = await this.twentyORMGlobalManager.getRepositoryForWorkspace( @@ -270,81 +179,16 @@ export class GoogleAPIsService { newOrExistingConnectedAccountId, ); - const messageChannels = await messageChannelRepository.find({ - where: { connectedAccountId: newOrExistingConnectedAccountId }, - }); - - const messageChannelUpdates = await messageChannelRepository.update( - { - connectedAccountId: newOrExistingConnectedAccountId, - }, - { - syncStage: - MessageChannelSyncStage.FULL_MESSAGE_LIST_FETCH_PENDING, - syncStatus: null, - syncCursor: '', - syncStageStartedAt: null, - }, - manager, - ); - - const messageChannelMetadata = - await this.objectMetadataRepository.findOneOrFail({ - where: { nameSingular: 'messageChannel', workspaceId }, - }); - - this.workspaceEventEmitter.emitDatabaseBatchEvent({ - objectMetadataNameSingular: 'messageChannel', - action: DatabaseEventAction.UPDATED, - events: messageChannels.map((messageChannel) => ({ - recordId: messageChannel.id, - objectMetadata: messageChannelMetadata, - properties: { - before: messageChannel, - after: { ...messageChannel, ...messageChannelUpdates.raw[0] }, - }, - })), + await this.resetMessageChannelService.resetMessageChannels({ workspaceId, - }); - - const calendarChannels = await calendarChannelRepository.find({ - where: { connectedAccountId: newOrExistingConnectedAccountId }, - }); - - const calendarChannelUpdates = await calendarChannelRepository.update( - { - connectedAccountId: newOrExistingConnectedAccountId, - }, - { - syncStage: - CalendarChannelSyncStage.FULL_CALENDAR_EVENT_LIST_FETCH_PENDING, - syncStatus: null, - syncCursor: '', - syncStageStartedAt: null, - }, + connectedAccountId: newOrExistingConnectedAccountId, manager, - ); + }); - const calendarChannelMetadata = - await this.objectMetadataRepository.findOneOrFail({ - where: { nameSingular: 'calendarChannel', workspaceId }, - }); - - this.workspaceEventEmitter.emitDatabaseBatchEvent({ - objectMetadataNameSingular: 'calendarChannel', - action: DatabaseEventAction.UPDATED, - events: calendarChannels.map((calendarChannel) => ({ - recordId: calendarChannel.id, - objectMetadata: calendarChannelMetadata, - properties: { - before: calendarChannel, - after: { - ...calendarChannel, - ...calendarChannelUpdates.raw[0], - }, - }, - })), + await this.resetCalendarChannelService.resetCalendarChannels({ workspaceId, + connectedAccountId: newOrExistingConnectedAccountId, + manager, }); } }, 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 new file mode 100644 index 000000000..7b5301100 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/auth/services/microsoft-apis.service.spec.ts @@ -0,0 +1,272 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; + +import { ConnectedAccountProvider } from 'twenty-shared/types'; + +import { CreateCalendarChannelService } from 'src/engine/core-modules/auth/services/create-calendar-channel.service'; +import { CreateConnectedAccountService } from 'src/engine/core-modules/auth/services/create-connected-account.service'; +import { CreateMessageChannelService } from 'src/engine/core-modules/auth/services/create-message-channel.service'; +import { CreateMessageFolderService } from 'src/engine/core-modules/auth/services/create-message-folder.service'; +import { MicrosoftAPIsService } from 'src/engine/core-modules/auth/services/microsoft-apis.service'; +import { ResetCalendarChannelService } from 'src/engine/core-modules/auth/services/reset-calendar-channel.service'; +import { ResetMessageChannelService } from 'src/engine/core-modules/auth/services/reset-message-channel.service'; +import { ResetMessageFolderService } from 'src/engine/core-modules/auth/services/reset-message-folder.service'; +import { UpdateConnectedAccountOnReconnectService } from 'src/engine/core-modules/auth/services/update-connected-account-on-reconnect.service'; +import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; +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 { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; +import { + CalendarChannelSyncStage, + CalendarChannelVisibility, +} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; +import { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service'; +import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; +import { MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; + +jest.mock('uuid', () => ({ + v4: jest.fn(() => 'mocked-uuid'), +})); + +describe('MicrosoftAPIsService', () => { + let service: MicrosoftAPIsService; + let resetCalendarChannelService: ResetCalendarChannelService; + let resetMessageChannelService: ResetMessageChannelService; + let createMessageChannelService: CreateMessageChannelService; + + const mockConnectedAccountRepository = { + findOne: jest.fn(), + find: jest.fn(), + update: jest.fn(), + }; + + const mockCalendarChannelRepository = { + findOne: jest.fn(), + find: jest.fn(), + update: jest.fn(), + }; + + const mockMessageChannelRepository = { + findOne: jest.fn(), + find: jest.fn(), + update: jest.fn(), + }; + + const mockWorkspaceMemberRepository = { + findOneOrFail: jest.fn(), + }; + + const mockWorkspaceDataSource = { + transaction: jest.fn((callback) => callback({})), + }; + + const mockTwentyConfigService = { + get: jest.fn(), + }; + + const mockMessageQueueService = { + add: jest.fn(), + }; + + const mockCalendarQueueService = { + add: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + MicrosoftAPIsService, + { + provide: TwentyORMGlobalManager, + useValue: { + getRepositoryForWorkspace: jest + .fn() + .mockImplementation((workspaceId, entity) => { + if (entity === 'connectedAccount') + return mockConnectedAccountRepository; + if (entity === 'calendarChannel') + return mockCalendarChannelRepository; + if (entity === 'messageChannel') + return mockMessageChannelRepository; + if (entity === 'workspaceMember') + return mockWorkspaceMemberRepository; + + return {}; + }), + getDataSourceForWorkspace: jest + .fn() + .mockImplementation(() => mockWorkspaceDataSource), + }, + }, + { + provide: getRepositoryToken(ObjectMetadataEntity, 'metadata'), + useValue: { + findOneOrFail: jest.fn(), + }, + }, + { + provide: TwentyConfigService, + useValue: mockTwentyConfigService, + }, + { + provide: ResetCalendarChannelService, + useValue: { + resetCalendarChannels: jest.fn(), + }, + }, + { + provide: ResetMessageChannelService, + useValue: { + resetMessageChannels: jest.fn(), + }, + }, + { + provide: ResetMessageFolderService, + useValue: { + resetMessageFolders: jest.fn(), + }, + }, + { + provide: CreateConnectedAccountService, + useValue: { + createConnectedAccount: jest.fn(), + }, + }, + { + provide: CreateMessageChannelService, + useValue: { + createMessageChannel: jest + .fn() + .mockResolvedValue('message-channel-id'), + }, + }, + { + provide: CreateMessageFolderService, + useValue: { + createMessageFolders: jest.fn(), + }, + }, + { + provide: CreateCalendarChannelService, + useValue: { + createCalendarChannel: jest.fn(), + }, + }, + { + provide: UpdateConnectedAccountOnReconnectService, + useValue: { + updateConnectedAccountOnReconnect: jest.fn(), + }, + }, + { + provide: AccountsToReconnectService, + useValue: { + removeAccountToReconnect: jest.fn(), + }, + }, + { + provide: WorkspaceEventEmitter, + useValue: { + emitDatabaseBatchEvent: jest.fn(), + }, + }, + { + provide: getQueueToken(MessageQueue.messagingQueue), + useValue: mockMessageQueueService, + }, + { + provide: getQueueToken(MessageQueue.calendarQueue), + useValue: mockCalendarQueueService, + }, + ], + }).compile(); + + service = module.get(MicrosoftAPIsService); + resetCalendarChannelService = module.get( + ResetCalendarChannelService, + ); + resetMessageChannelService = module.get( + ResetMessageChannelService, + ); + createMessageChannelService = module.get( + CreateMessageChannelService, + ); + }); + + describe('refreshMicrosoftRefreshToken', () => { + it('should reset calendar channels and message channels', async () => { + mockTwentyConfigService.get.mockImplementation((key) => { + if (key === 'CALENDAR_PROVIDER_MICROSOFT_ENABLED') return true; + if (key === 'MESSAGING_PROVIDER_MICROSOFT_ENABLED') return true; + + return false; + }); + + const existingConnectedAccount = { + id: 'existing-account-id', + handle: 'test@example.com', + accountOwnerId: 'workspace-member-id', + provider: ConnectedAccountProvider.MICROSOFT, + } as ConnectedAccountWorkspaceEntity; + + mockConnectedAccountRepository.findOne.mockResolvedValue( + existingConnectedAccount, + ); + + mockWorkspaceMemberRepository.findOneOrFail.mockResolvedValue({ + id: 'workspace-member-id', + userId: 'user-id', + }); + + const failedCalendarChannel = { + id: 'calendar-channel-id', + connectedAccountId: 'existing-account-id', + syncStatus: 'FAILED_UNKNOWN', + syncStage: CalendarChannelSyncStage.FAILED, + }; + + mockCalendarChannelRepository.find.mockResolvedValue([ + failedCalendarChannel, + ]); + + mockMessageChannelRepository.find.mockResolvedValue([ + { + id: 'message-channel-id', + connectedAccountId: 'existing-account-id', + }, + ]); + + await service.refreshMicrosoftRefreshToken({ + handle: 'test@example.com', + workspaceMemberId: 'workspace-member-id', + workspaceId: 'workspace-id', + accessToken: 'new-access-token', + refreshToken: 'new-refresh-token', + calendarVisibility: CalendarChannelVisibility.SHARE_EVERYTHING, + messageVisibility: MessageChannelVisibility.SHARE_EVERYTHING, + }); + + expect( + resetCalendarChannelService.resetCalendarChannels, + ).toHaveBeenCalledWith({ + workspaceId: 'workspace-id', + connectedAccountId: 'existing-account-id', + manager: expect.any(Object), + }); + + expect( + resetMessageChannelService.resetMessageChannels, + ).toHaveBeenCalledWith({ + workspaceId: 'workspace-id', + connectedAccountId: 'existing-account-id', + manager: expect.any(Object), + }); + + expect( + createMessageChannelService.createMessageChannel, + ).not.toHaveBeenCalled(); + }); + }); +}); 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 1e908595c..22e7c515a 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 @@ -5,7 +5,14 @@ import { ConnectedAccountProvider } from 'twenty-shared/types'; import { Repository } from 'typeorm'; import { v4 } from 'uuid'; -import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; +import { CreateCalendarChannelService } from 'src/engine/core-modules/auth/services/create-calendar-channel.service'; +import { CreateConnectedAccountService } from 'src/engine/core-modules/auth/services/create-connected-account.service'; +import { CreateMessageChannelService } from 'src/engine/core-modules/auth/services/create-message-channel.service'; +import { CreateMessageFolderService } from 'src/engine/core-modules/auth/services/create-message-folder.service'; +import { ResetCalendarChannelService } from 'src/engine/core-modules/auth/services/reset-calendar-channel.service'; +import { ResetMessageChannelService } from 'src/engine/core-modules/auth/services/reset-message-channel.service'; +import { ResetMessageFolderService } from 'src/engine/core-modules/auth/services/reset-message-folder.service'; +import { UpdateConnectedAccountOnReconnectService } from 'src/engine/core-modules/auth/services/update-connected-account-on-reconnect.service'; import { getMicrosoftApisOauthScopes } from 'src/engine/core-modules/auth/utils/get-microsoft-apis-oauth-scopes'; 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'; @@ -20,21 +27,15 @@ import { CalendarEventListFetchJobData, } from 'src/modules/calendar/calendar-event-import-manager/jobs/calendar-event-list-fetch.job'; import { - CalendarChannelSyncStage, CalendarChannelVisibility, CalendarChannelWorkspaceEntity, } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; import { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service'; import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { - MessageChannelSyncStage, - MessageChannelSyncStatus, - MessageChannelType, MessageChannelVisibility, MessageChannelWorkspaceEntity, } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; -import { MessageFolderWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity'; -import { MessageFolderName } from 'src/modules/messaging/message-import-manager/drivers/microsoft/types/folders'; import { MessagingMessageListFetchJob, MessagingMessageListFetchJobData, @@ -50,6 +51,14 @@ export class MicrosoftAPIsService { @InjectMessageQueue(MessageQueue.calendarQueue) private readonly calendarQueueService: MessageQueueService, private readonly accountsToReconnectService: AccountsToReconnectService, + private readonly resetMessageChannelService: ResetMessageChannelService, + private readonly resetMessageFolderService: ResetMessageFolderService, + private readonly resetCalendarChannelService: ResetCalendarChannelService, + private readonly createMessageChannelService: CreateMessageChannelService, + private readonly createCalendarChannelService: CreateCalendarChannelService, + private readonly createMessageFolderService: CreateMessageFolderService, + private readonly createConnectedAccountService: CreateConnectedAccountService, + private readonly updateConnectedAccountOnReconnectService: UpdateConnectedAccountOnReconnectService, private readonly workspaceEventEmitter: WorkspaceEventEmitter, @InjectRepository(ObjectMetadataEntity, 'metadata') private readonly objectMetadataRepository: Repository, @@ -98,12 +107,6 @@ export class MicrosoftAPIsService { 'messageChannel', ); - const messageFolderRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'messageFolder', - ); - const workspaceDataSource = await this.twentyORMGlobalManager.getDataSourceForWorkspace({ workspaceId, @@ -114,169 +117,56 @@ export class MicrosoftAPIsService { await workspaceDataSource.transaction( async (manager: WorkspaceEntityManager) => { if (!existingAccountId) { - const newConnectedAccount = await connectedAccountRepository.save( - { - id: newOrExistingConnectedAccountId, - handle, - provider: ConnectedAccountProvider.MICROSOFT, - accessToken: input.accessToken, - refreshToken: input.refreshToken, - accountOwnerId: workspaceMemberId, - scopes, - }, - {}, - manager, - ); - - const connectedAccountMetadata = - await this.objectMetadataRepository.findOneOrFail({ - where: { nameSingular: 'connectedAccount', workspaceId }, - }); - - this.workspaceEventEmitter.emitDatabaseBatchEvent({ - objectMetadataNameSingular: 'connectedAccount', - action: DatabaseEventAction.CREATED, - events: [ - { - recordId: newConnectedAccount.id, - objectMetadata: connectedAccountMetadata, - properties: { - after: newConnectedAccount, - }, - }, - ], + await this.createConnectedAccountService.createConnectedAccount({ workspaceId, + connectedAccountId: newOrExistingConnectedAccountId, + handle, + provider: ConnectedAccountProvider.MICROSOFT, + accessToken: input.accessToken, + refreshToken: input.refreshToken, + accountOwnerId: workspaceMemberId, + scopes, + manager, }); - const newMessageChannel = await messageChannelRepository.save( - { - id: v4(), + const newMessageChannelId = + await this.createMessageChannelService.createMessageChannel({ + workspaceId, connectedAccountId: newOrExistingConnectedAccountId, - type: MessageChannelType.EMAIL, handle, - visibility: - messageVisibility || MessageChannelVisibility.SHARE_EVERYTHING, - syncStatus: MessageChannelSyncStatus.ONGOING, - }, - {}, - manager, - ); - - await messageFolderRepository.save( - { - id: v4(), - messageChannelId: newMessageChannel.id, - name: MessageFolderName.INBOX, - syncCursor: '', - }, - {}, - manager, - ); - - await messageFolderRepository.save( - { - id: v4(), - messageChannelId: newMessageChannel.id, - name: MessageFolderName.SENT_ITEMS, - syncCursor: '', - }, - {}, - manager, - ); - - const messageChannelMetadata = - await this.objectMetadataRepository.findOneOrFail({ - where: { nameSingular: 'messageChannel', workspaceId }, + messageVisibility, + manager, }); - this.workspaceEventEmitter.emitDatabaseBatchEvent({ - objectMetadataNameSingular: 'messageChannel', - action: DatabaseEventAction.CREATED, - events: [ - { - recordId: newMessageChannel.id, - objectMetadata: messageChannelMetadata, - properties: { - after: newMessageChannel, - }, - }, - ], + await this.createMessageFolderService.createMessageFolders({ workspaceId, + messageChannelId: newMessageChannelId, + manager, }); if ( this.twentyConfigService.get('CALENDAR_PROVIDER_MICROSOFT_ENABLED') ) { - const newCalendarChannel = await calendarChannelRepository.save( - { - id: v4(), - connectedAccountId: newOrExistingConnectedAccountId, - handle, - visibility: - calendarVisibility || - CalendarChannelVisibility.SHARE_EVERYTHING, - }, - {}, - manager, - ); - - const calendarChannelMetadata = - await this.objectMetadataRepository.findOneOrFail({ - where: { nameSingular: 'calendarChannel', workspaceId }, - }); - - this.workspaceEventEmitter.emitDatabaseBatchEvent({ - objectMetadataNameSingular: 'calendarChannel', - action: DatabaseEventAction.CREATED, - events: [ - { - recordId: newCalendarChannel.id, - objectMetadata: calendarChannelMetadata, - properties: { - after: newCalendarChannel, - }, - }, - ], + await this.createCalendarChannelService.createCalendarChannel({ workspaceId, + connectedAccountId: newOrExistingConnectedAccountId, + handle, + calendarVisibility, + manager, }); } } else { - const updatedConnectedAccount = - await connectedAccountRepository.update( - { - id: newOrExistingConnectedAccountId, - }, - { - accessToken: input.accessToken, - refreshToken: input.refreshToken, - scopes, - }, + await this.updateConnectedAccountOnReconnectService.updateConnectedAccountOnReconnect( + { + workspaceId, + connectedAccountId: newOrExistingConnectedAccountId, + accessToken: input.accessToken, + refreshToken: input.refreshToken, + scopes, + connectedAccount, manager, - ); - - const connectedAccountMetadata = - await this.objectMetadataRepository.findOneOrFail({ - where: { nameSingular: 'connectedAccount', workspaceId }, - }); - - this.workspaceEventEmitter.emitDatabaseBatchEvent({ - objectMetadataNameSingular: 'connectedAccount', - action: DatabaseEventAction.UPDATED, - events: [ - { - recordId: newOrExistingConnectedAccountId, - objectMetadata: connectedAccountMetadata, - properties: { - before: connectedAccount, - after: { - ...connectedAccount, - ...updatedConnectedAccount.raw[0], - }, - }, - }, - ], - workspaceId, - }); + }, + ); const workspaceMemberRepository = await this.twentyORMGlobalManager.getRepositoryForWorkspace( @@ -298,82 +188,22 @@ export class MicrosoftAPIsService { newOrExistingConnectedAccountId, ); - const messageChannels = await messageChannelRepository.find({ - where: { connectedAccountId: newOrExistingConnectedAccountId }, - }); - - const messageChannelUpdates = await messageChannelRepository.update( - { - connectedAccountId: newOrExistingConnectedAccountId, - }, - { - syncStage: - MessageChannelSyncStage.FULL_MESSAGE_LIST_FETCH_PENDING, - syncStatus: MessageChannelSyncStatus.ONGOING, - syncCursor: '', - syncStageStartedAt: null, - }, - manager, - ); - - const messageChannelMetadata = - await this.objectMetadataRepository.findOneOrFail({ - where: { nameSingular: 'messageChannel', workspaceId }, - }); - - this.workspaceEventEmitter.emitDatabaseBatchEvent({ - objectMetadataNameSingular: 'messageChannel', - action: DatabaseEventAction.UPDATED, - events: messageChannels.map((messageChannel) => ({ - recordId: messageChannel.id, - objectMetadata: messageChannelMetadata, - properties: { - before: messageChannel, - after: { ...messageChannel, ...messageChannelUpdates.raw[0] }, - }, - })), + await this.resetMessageChannelService.resetMessageChannels({ workspaceId, - }); - - // now for the calendar channels - const calendarChannels = await calendarChannelRepository.find({ - where: { connectedAccountId: newOrExistingConnectedAccountId }, - }); - - const calendarChannelUpdates = await calendarChannelRepository.update( - { - connectedAccountId: newOrExistingConnectedAccountId, - }, - { - syncStage: - CalendarChannelSyncStage.FULL_CALENDAR_EVENT_LIST_FETCH_PENDING, - syncStatus: null, - syncCursor: '', - syncStageStartedAt: null, - }, + connectedAccountId: newOrExistingConnectedAccountId, manager, - ); + }); - const calendarChannelMetadata = - await this.objectMetadataRepository.findOneOrFail({ - where: { nameSingular: 'calendarChannel', workspaceId }, - }); - - this.workspaceEventEmitter.emitDatabaseBatchEvent({ - objectMetadataNameSingular: 'calendarChannel', - action: DatabaseEventAction.UPDATED, - events: calendarChannels.map((calendarChannel) => ({ - recordId: calendarChannel.id, - objectMetadata: calendarChannelMetadata, - properties: { - before: calendarChannel, - after: { - ...calendarChannel, - ...calendarChannelUpdates.raw[0], - }, - }, - })), + await this.resetMessageFolderService.resetMessageFolders({ workspaceId, + connectedAccountId: newOrExistingConnectedAccountId, + manager, + }); + + await this.resetCalendarChannelService.resetCalendarChannels({ + workspaceId, + connectedAccountId: newOrExistingConnectedAccountId, + manager, }); } }, diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/reset-calendar-channel.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/reset-calendar-channel.service.ts new file mode 100644 index 000000000..fc4c8b1b8 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/auth/services/reset-calendar-channel.service.ts @@ -0,0 +1,84 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { Repository } from 'typeorm'; + +import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; +import { + CalendarChannelSyncStage, + CalendarChannelWorkspaceEntity, +} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; + +export type ResetCalendarChannelsInput = { + workspaceId: string; + connectedAccountId: string; + manager: WorkspaceEntityManager; +}; + +@Injectable() +export class ResetCalendarChannelService { + constructor( + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly workspaceEventEmitter: WorkspaceEventEmitter, + @InjectRepository(ObjectMetadataEntity, 'metadata') + private readonly objectMetadataRepository: Repository, + ) {} + + async resetCalendarChannels( + input: ResetCalendarChannelsInput, + ): Promise { + const { workspaceId, connectedAccountId, manager } = input; + + const calendarChannelRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + 'calendarChannel', + ); + + const calendarChannels = await calendarChannelRepository.find({ + where: { connectedAccountId }, + }); + + const calendarChannelUpdates = await calendarChannelRepository.update( + { + connectedAccountId, + }, + { + syncStage: + CalendarChannelSyncStage.FULL_CALENDAR_EVENT_LIST_FETCH_PENDING, + syncStatus: null, + syncCursor: '', + syncStageStartedAt: null, + }, + manager, + ); + + const calendarChannelMetadata = + await this.objectMetadataRepository.findOneOrFail({ + where: { nameSingular: 'calendarChannel', workspaceId }, + }); + + this.workspaceEventEmitter.emitDatabaseBatchEvent({ + objectMetadataNameSingular: 'calendarChannel', + action: DatabaseEventAction.UPDATED, + events: calendarChannels.map((calendarChannel) => ({ + recordId: calendarChannel.id, + objectMetadata: calendarChannelMetadata, + properties: { + before: calendarChannel, + after: { + ...calendarChannel, + ...calendarChannelUpdates.raw[0], + }, + }, + })), + workspaceId, + }); + + return; + } +} diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/reset-message-channel.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/reset-message-channel.service.ts new file mode 100644 index 000000000..f35317980 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/auth/services/reset-message-channel.service.ts @@ -0,0 +1,78 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { Repository } from 'typeorm'; + +import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; +import { + MessageChannelSyncStage, + MessageChannelWorkspaceEntity, +} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; + +export type ResetMessageChannelsInput = { + workspaceId: string; + connectedAccountId: string; + manager: WorkspaceEntityManager; +}; + +@Injectable() +export class ResetMessageChannelService { + constructor( + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly workspaceEventEmitter: WorkspaceEventEmitter, + @InjectRepository(ObjectMetadataEntity, 'metadata') + private readonly objectMetadataRepository: Repository, + ) {} + + async resetMessageChannels(input: ResetMessageChannelsInput): Promise { + const { workspaceId, connectedAccountId, manager } = input; + + const messageChannelRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + 'messageChannel', + ); + + const messageChannels = await messageChannelRepository.find({ + where: { connectedAccountId }, + }); + + const messageChannelUpdates = await messageChannelRepository.update( + { + connectedAccountId, + }, + { + syncStage: MessageChannelSyncStage.FULL_MESSAGE_LIST_FETCH_PENDING, + syncStatus: null, + syncCursor: '', + syncStageStartedAt: null, + }, + manager, + ); + + const messageChannelMetadata = + await this.objectMetadataRepository.findOneOrFail({ + where: { nameSingular: 'messageChannel', workspaceId }, + }); + + this.workspaceEventEmitter.emitDatabaseBatchEvent({ + objectMetadataNameSingular: 'messageChannel', + action: DatabaseEventAction.UPDATED, + events: messageChannels.map((messageChannel) => ({ + recordId: messageChannel.id, + objectMetadata: messageChannelMetadata, + properties: { + before: messageChannel, + after: { ...messageChannel, ...messageChannelUpdates.raw[0] }, + }, + })), + workspaceId, + }); + + return; + } +} diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/reset-message-folder.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/reset-message-folder.service.ts new file mode 100644 index 000000000..d37266403 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/auth/services/reset-message-folder.service.ts @@ -0,0 +1,69 @@ +import { Injectable } from '@nestjs/common'; + +import { In } from 'typeorm'; + +import { WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; +import { MessageFolderWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity'; + +export type ResetMessageFoldersInput = { + workspaceId: string; + connectedAccountId: string; + manager: WorkspaceEntityManager; +}; + +@Injectable() +export class ResetMessageFolderService { + constructor( + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + ) {} + + async resetMessageFolders(input: ResetMessageFoldersInput): Promise { + const { workspaceId, connectedAccountId, manager } = input; + + const messageChannelRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + 'messageChannel', + ); + + const messageChannels = await messageChannelRepository.find({ + where: { connectedAccountId }, + }); + + const messageChannelIds = messageChannels.map((channel) => channel.id); + + if (messageChannelIds.length === 0) { + return; + } + + const messageFolderRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + 'messageFolder', + ); + + const messageFolders = await messageFolderRepository.find({ + where: { + messageChannelId: In(messageChannelIds), + }, + }); + + if (messageFolders.length === 0) { + return; + } + + await messageFolderRepository.update( + { + messageChannelId: In(messageChannelIds), + }, + { + syncCursor: '', + }, + manager, + ); + + return; + } +} 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 new file mode 100644 index 000000000..3ecc1a064 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/auth/services/update-connected-account-on-reconnect.service.ts @@ -0,0 +1,88 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { Repository } from 'typeorm'; + +import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; +import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; + +export type UpdateConnectedAccountOnReconnectInput = { + workspaceId: string; + connectedAccountId: string; + accessToken: string; + refreshToken: string; + scopes: string[]; + connectedAccount: ConnectedAccountWorkspaceEntity; + manager: WorkspaceEntityManager; +}; + +@Injectable() +export class UpdateConnectedAccountOnReconnectService { + constructor( + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly workspaceEventEmitter: WorkspaceEventEmitter, + @InjectRepository(ObjectMetadataEntity, 'metadata') + private readonly objectMetadataRepository: Repository, + ) {} + + async updateConnectedAccountOnReconnect( + input: UpdateConnectedAccountOnReconnectInput, + ): Promise { + const { + workspaceId, + connectedAccountId, + accessToken, + refreshToken, + scopes, + connectedAccount, + manager, + } = input; + + const connectedAccountRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + 'connectedAccount', + ); + + const updatedConnectedAccount = await connectedAccountRepository.update( + { + id: connectedAccountId, + }, + { + accessToken, + refreshToken, + scopes, + authFailedAt: null, + }, + manager, + ); + + const connectedAccountMetadata = + await this.objectMetadataRepository.findOneOrFail({ + where: { nameSingular: 'connectedAccount', workspaceId }, + }); + + this.workspaceEventEmitter.emitDatabaseBatchEvent({ + objectMetadataNameSingular: 'connectedAccount', + action: DatabaseEventAction.UPDATED, + events: [ + { + recordId: connectedAccountId, + objectMetadata: connectedAccountMetadata, + properties: { + before: connectedAccount, + after: { + ...connectedAccount, + ...updatedConnectedAccount.raw[0], + }, + }, + }, + ], + workspaceId, + }); + } +}