refactor imap gql resolver (#16375)

This commit is contained in:
neo773
2025-12-08 19:24:24 +05:30
committed by GitHub
parent 51c0a8dd86
commit 5f4c7e016c
5 changed files with 124 additions and 273 deletions
@@ -5574,7 +5574,7 @@ export type GetConnectedImapSmtpCaldavAccountQueryVariables = Exact<{
}>;
export type GetConnectedImapSmtpCaldavAccountQuery = { __typename?: 'Query', getConnectedImapSmtpCaldavAccount: { __typename?: 'ConnectedImapSmtpCaldavAccount', id: string, handle: string, provider: string, accountOwnerId: string, connectionParameters?: { __typename?: 'ImapSmtpCaldavConnectionParameters', IMAP?: { __typename?: 'ConnectionParametersOutput', host: string, port: number, secure?: boolean | null, password: string } | null, SMTP?: { __typename?: 'ConnectionParametersOutput', host: string, username?: string | null, port: number, secure?: boolean | null, password: string } | null, CALDAV?: { __typename?: 'ConnectionParametersOutput', host: string, username?: string | null, password: string } | null } | null } };
export type GetConnectedImapSmtpCaldavAccountQuery = { __typename?: 'Query', getConnectedImapSmtpCaldavAccount: { __typename?: 'ConnectedImapSmtpCaldavAccount', id: string, handle: string, provider: string, accountOwnerId: string, connectionParameters?: { __typename?: 'ImapSmtpCaldavConnectionParameters', IMAP?: { __typename?: 'ConnectionParametersOutput', host: string, port: number, secure?: boolean | null, username?: string | null, password: string } | null, SMTP?: { __typename?: 'ConnectionParametersOutput', host: string, username?: string | null, port: number, secure?: boolean | null, password: string } | null, CALDAV?: { __typename?: 'ConnectionParametersOutput', host: string, username?: string | null, password: string } | null } | null } };
export type CreateDatabaseConfigVariableMutationVariables = Exact<{
key: Scalars['String'];
@@ -10202,6 +10202,7 @@ export const GetConnectedImapSmtpCaldavAccountDocument = gql`
host
port
secure
username
password
}
SMTP {
@@ -12,6 +12,7 @@ export const GET_CONNECTED_IMAP_SMTP_CALDAV_ACCOUNT = gql`
host
port
secure
username
password
}
SMTP {
@@ -150,6 +150,12 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
TransientTokenService,
AuthSsoService,
],
exports: [AccessTokenService, LoginTokenService, RefreshTokenService],
exports: [
AccessTokenService,
LoginTokenService,
RefreshTokenService,
CreateMessageChannelService,
CreateCalendarChannelService,
],
})
export class AuthModule {}
@@ -2,20 +2,14 @@ import { Test, type TestingModule } from '@nestjs/testing';
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 { 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 { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.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';
import {
MessageChannelPendingGroupEmailsAction,
MessageChannelSyncStage,
MessageChannelSyncStatus,
MessageChannelType,
type MessageChannelWorkspaceEntity,
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
jest.mock('uuid', () => ({
v4: jest.fn(() => 'mocked-uuid'),
@@ -43,12 +37,12 @@ describe('ImapSmtpCalDavAPIService', () => {
transaction: jest.fn((callback) => callback({})),
};
const mockMessageQueueService = {
add: jest.fn(),
const mockCreateMessageChannelService = {
createMessageChannel: jest.fn().mockResolvedValue('mocked-uuid'),
};
const mockCalendarQueueService = {
add: jest.fn(),
const mockCreateCalendarChannelService = {
createCalendarChannel: jest.fn().mockResolvedValue('mocked-uuid'),
};
beforeEach(async () => {
@@ -76,12 +70,12 @@ describe('ImapSmtpCalDavAPIService', () => {
},
},
{
provide: getQueueToken(MessageQueue.messagingQueue),
useValue: mockMessageQueueService,
provide: CreateMessageChannelService,
useValue: mockCreateMessageChannelService,
},
{
provide: getQueueToken(MessageQueue.calendarQueue),
useValue: mockCalendarQueueService,
provide: CreateCalendarChannelService,
useValue: mockCreateCalendarChannelService,
},
],
}).compile();
@@ -113,28 +107,11 @@ describe('ImapSmtpCalDavAPIService', () => {
} as EmailAccountConnectionParameters,
};
it('should create new account with message and calendar channels when account does not exist', async () => {
it('should create new account with message channel when account does not exist and IMAP is configured', async () => {
mockConnectedAccountRepository.findOne.mockResolvedValue(null);
mockMessageChannelRepository.findOne.mockResolvedValue(null);
mockCalendarChannelRepository.findOne.mockResolvedValue(null);
const expectedMessageChannel = {
id: 'mocked-uuid',
connectedAccountId: 'mocked-uuid',
type: MessageChannelType.EMAIL,
handle: 'test@example.com',
isSyncEnabled: true,
syncStatus: MessageChannelSyncStatus.NOT_SYNCED,
syncStage: MessageChannelSyncStage.PENDING_CONFIGURATION,
pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction.NONE,
syncCursor: '',
syncStageStartedAt: null,
};
mockMessageChannelRepository.save.mockResolvedValue(
expectedMessageChannel,
);
await service.processAccount(baseInput);
expect(mockConnectedAccountRepository.save).toHaveBeenCalledWith(
@@ -146,25 +123,21 @@ describe('ImapSmtpCalDavAPIService', () => {
accountOwnerId: 'workspace-member-id',
},
{},
);
expect(mockMessageChannelRepository.save).toHaveBeenCalledWith(
{
id: 'mocked-uuid',
connectedAccountId: 'mocked-uuid',
type: MessageChannelType.EMAIL,
handle: 'test@example.com',
isSyncEnabled: true,
syncStatus: MessageChannelSyncStatus.NOT_SYNCED,
syncStage: MessageChannelSyncStage.PENDING_CONFIGURATION,
pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction.NONE,
syncCursor: '',
syncStageStartedAt: null,
},
{},
);
expect(mockMessageQueueService.add).not.toHaveBeenCalled();
expect(
mockCreateMessageChannelService.createMessageChannel,
).toHaveBeenCalledWith({
workspaceId: 'workspace-id',
connectedAccountId: 'mocked-uuid',
handle: 'test@example.com',
manager: {},
});
expect(
mockCreateCalendarChannelService.createCalendarChannel,
).not.toHaveBeenCalled();
});
it('should preserve existing channels when updating account credentials', async () => {
@@ -178,10 +151,6 @@ describe('ImapSmtpCalDavAPIService', () => {
const existingMessageChannel = {
id: 'existing-message-channel-id',
connectedAccountId: 'existing-account-id',
type: MessageChannelType.EMAIL,
handle: 'test@example.com',
isSyncEnabled: true,
syncStatus: MessageChannelSyncStatus.ONGOING,
} as MessageChannelWorkspaceEntity;
const existingCalendarChannel = {
@@ -213,12 +182,15 @@ describe('ImapSmtpCalDavAPIService', () => {
accountOwnerId: 'workspace-member-id',
},
{},
{},
);
expect(mockMessageChannelRepository.save).not.toHaveBeenCalled();
expect(mockCalendarChannelRepository.save).not.toHaveBeenCalled();
expect(mockMessageQueueService.add).not.toHaveBeenCalled();
expect(
mockCreateMessageChannelService.createMessageChannel,
).not.toHaveBeenCalled();
expect(
mockCreateCalendarChannelService.createCalendarChannel,
).not.toHaveBeenCalled();
});
it('should only create message channel when only IMAP is configured', async () => {
@@ -238,27 +210,14 @@ describe('ImapSmtpCalDavAPIService', () => {
mockMessageChannelRepository.findOne.mockResolvedValue(null);
mockCalendarChannelRepository.findOne.mockResolvedValue(null);
const expectedMessageChannel = {
id: 'mocked-uuid',
connectedAccountId: 'mocked-uuid',
type: MessageChannelType.EMAIL,
handle: 'test@example.com',
isSyncEnabled: true,
syncStatus: MessageChannelSyncStatus.NOT_SYNCED,
syncStage: MessageChannelSyncStage.PENDING_CONFIGURATION,
};
mockMessageChannelRepository.save.mockResolvedValue(
expectedMessageChannel,
);
await service.processAccount(imapOnlyInput);
expect(mockMessageChannelRepository.save).toHaveBeenCalled();
expect(mockCalendarChannelRepository.save).not.toHaveBeenCalled();
expect(mockMessageQueueService.add).not.toHaveBeenCalled();
expect(mockCalendarQueueService.add).not.toHaveBeenCalled();
expect(
mockCreateMessageChannelService.createMessageChannel,
).toHaveBeenCalled();
expect(
mockCreateCalendarChannelService.createCalendarChannel,
).not.toHaveBeenCalled();
});
it('should only create calendar channel when only CALDAV is configured', async () => {
@@ -279,23 +238,14 @@ describe('ImapSmtpCalDavAPIService', () => {
mockMessageChannelRepository.findOne.mockResolvedValue(null);
mockCalendarChannelRepository.findOne.mockResolvedValue(null);
const expectedCalendarChannel = {
id: 'mocked-uuid',
connectedAccountId: 'mocked-uuid',
handle: 'test@example.com',
};
mockCalendarChannelRepository.save.mockResolvedValue(
expectedCalendarChannel,
);
await service.processAccount(caldavOnlyInput);
expect(mockMessageChannelRepository.save).not.toHaveBeenCalled();
expect(mockCalendarChannelRepository.save).toHaveBeenCalled();
expect(mockMessageQueueService.add).not.toHaveBeenCalled();
expect(mockCalendarQueueService.add).not.toHaveBeenCalled();
expect(
mockCreateMessageChannelService.createMessageChannel,
).not.toHaveBeenCalled();
expect(
mockCreateCalendarChannelService.createCalendarChannel,
).toHaveBeenCalled();
});
it('should handle IMAP + SMTP configuration without CALDAV', async () => {
@@ -322,26 +272,14 @@ describe('ImapSmtpCalDavAPIService', () => {
mockMessageChannelRepository.findOne.mockResolvedValue(null);
mockCalendarChannelRepository.findOne.mockResolvedValue(null);
const expectedMessageChannel = {
id: 'mocked-uuid',
connectedAccountId: 'mocked-uuid',
type: MessageChannelType.EMAIL,
handle: 'test@example.com',
isSyncEnabled: true,
syncStatus: MessageChannelSyncStatus.NOT_SYNCED,
syncStage: MessageChannelSyncStage.PENDING_CONFIGURATION,
};
mockMessageChannelRepository.save.mockResolvedValue(
expectedMessageChannel,
);
await service.processAccount(imapSmtpInput);
expect(mockMessageChannelRepository.save).toHaveBeenCalled();
expect(mockCalendarChannelRepository.save).not.toHaveBeenCalled();
expect(mockMessageQueueService.add).not.toHaveBeenCalled();
expect(mockCalendarQueueService.add).not.toHaveBeenCalled();
expect(
mockCreateMessageChannelService.createMessageChannel,
).toHaveBeenCalled();
expect(
mockCreateCalendarChannelService.createCalendarChannel,
).not.toHaveBeenCalled();
});
it('should handle full IMAP + SMTP + CALDAV configuration', async () => {
@@ -375,35 +313,14 @@ describe('ImapSmtpCalDavAPIService', () => {
mockMessageChannelRepository.findOne.mockResolvedValue(null);
mockCalendarChannelRepository.findOne.mockResolvedValue(null);
const expectedMessageChannel = {
id: 'mocked-uuid',
connectedAccountId: 'mocked-uuid',
type: MessageChannelType.EMAIL,
handle: 'test@example.com',
isSyncEnabled: true,
syncStatus: MessageChannelSyncStatus.NOT_SYNCED,
syncStage: MessageChannelSyncStage.PENDING_CONFIGURATION,
};
const expectedCalendarChannel = {
id: 'mocked-uuid',
connectedAccountId: 'mocked-uuid',
handle: 'test@example.com',
};
mockMessageChannelRepository.save.mockResolvedValue(
expectedMessageChannel,
);
mockCalendarChannelRepository.save.mockResolvedValue(
expectedCalendarChannel,
);
await service.processAccount(fullConfigInput);
expect(mockMessageChannelRepository.save).toHaveBeenCalled();
expect(mockCalendarChannelRepository.save).toHaveBeenCalled();
expect(mockMessageQueueService.add).not.toHaveBeenCalled();
expect(mockCalendarQueueService.add).not.toHaveBeenCalled();
expect(
mockCreateMessageChannelService.createMessageChannel,
).toHaveBeenCalled();
expect(
mockCreateCalendarChannelService.createCalendarChannel,
).toHaveBeenCalled();
});
it('should handle account found by handle when connectedAccountId is not provided', async () => {
@@ -439,11 +356,12 @@ describe('ImapSmtpCalDavAPIService', () => {
accountOwnerId: 'workspace-member-id',
},
{},
{},
);
});
it('should not enqueue sync jobs when channels are disabled', async () => {
const disabledInput = {
it('should not create channels when neither IMAP nor CALDAV is configured', async () => {
const smtpOnlyInput = {
...baseInput,
connectionParameters: {
SMTP: {
@@ -460,20 +378,20 @@ describe('ImapSmtpCalDavAPIService', () => {
mockMessageChannelRepository.findOne.mockResolvedValue(null);
mockCalendarChannelRepository.findOne.mockResolvedValue(null);
await service.processAccount(disabledInput);
await service.processAccount(smtpOnlyInput);
expect(mockMessageQueueService.add).not.toHaveBeenCalled();
expect(mockCalendarQueueService.add).not.toHaveBeenCalled();
expect(
mockCreateMessageChannelService.createMessageChannel,
).not.toHaveBeenCalled();
expect(
mockCreateCalendarChannelService.createCalendarChannel,
).not.toHaveBeenCalled();
});
it('should handle transaction correctly', async () => {
mockConnectedAccountRepository.findOne.mockResolvedValue(null);
mockMessageChannelRepository.findOne.mockResolvedValue(null);
mockCalendarChannelRepository.findOne.mockResolvedValue(null);
mockMessageChannelRepository.save.mockResolvedValue({
id: 'mocked-uuid',
connectedAccountId: 'mocked-uuid',
});
await service.processAccount(baseInput);
@@ -1,29 +1,24 @@
import { Injectable } from '@nestjs/common';
import { ConnectedAccountProvider } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
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 { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
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 {
CalendarChannelSyncStage,
CalendarChannelSyncStatus,
type CalendarChannelWorkspaceEntity,
} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
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 {
MessageChannelPendingGroupEmailsAction,
MessageChannelSyncStage,
MessageChannelSyncStatus,
MessageChannelType,
type MessageChannelWorkspaceEntity,
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
@Injectable()
export class ImapSmtpCalDavAPIService {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly createMessageChannelService: CreateMessageChannelService,
private readonly createCalendarChannelService: CreateCalendarChannelService,
) {}
async processAccount(input: {
@@ -62,138 +57,68 @@ export class ImapSmtpCalDavAPIService {
where: { handle, accountOwnerId: workspaceMemberId },
});
const accountId = existingAccount?.id ?? connectedAccountId ?? v4();
const newOrExistingAccountId =
existingAccount?.id ?? connectedAccountId ?? v4();
const workspaceDataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace({
workspaceId,
});
let messageChannel: MessageChannelWorkspaceEntity | null = existingAccount
const existingMessageChannel = existingAccount
? await messageChannelRepository.findOne({
where: { connectedAccountId: existingAccount.id },
})
: null;
let calendarChannel: CalendarChannelWorkspaceEntity | null = existingAccount
const existingCalendarChannel = existingAccount
? await calendarChannelRepository.findOne({
where: { connectedAccountId: existingAccount.id },
})
: null;
await workspaceDataSource.transaction(async () => {
await this.upsertConnectedAccount(
input,
accountId,
connectedAccountRepository,
);
const shouldCreateMessageChannel =
!isDefined(existingMessageChannel) &&
Boolean(input.connectionParameters.IMAP);
if (!messageChannel) {
messageChannel = await this.setupMessageChannels(
input,
accountId,
messageChannelRepository,
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 (!calendarChannel) {
calendarChannel = await this.setupCalendarChannels(
input,
accountId,
calendarChannelRepository,
);
}
});
if (shouldCreateMessageChannel) {
await this.createMessageChannelService.createMessageChannel({
workspaceId,
connectedAccountId: newOrExistingAccountId,
handle,
manager,
});
}
return accountId;
}
private async upsertConnectedAccount(
input: {
handle: string;
workspaceMemberId: string;
workspaceId: string;
connectionParameters: EmailAccountConnectionParameters;
},
accountId: string,
connectedAccountRepository: WorkspaceRepository<ConnectedAccountWorkspaceEntity>,
) {
const accountData = {
id: accountId,
handle: input.handle,
provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV,
connectionParameters: input.connectionParameters,
accountOwnerId: input.workspaceMemberId,
};
await connectedAccountRepository.save(accountData, {});
}
private async setupMessageChannels(
input: {
handle: string;
workspaceId: string;
connectionParameters: EmailAccountConnectionParameters;
},
accountId: string,
messageChannelRepository: WorkspaceRepository<MessageChannelWorkspaceEntity>,
): Promise<MessageChannelWorkspaceEntity | null> {
const shouldCreateMessageChannel = Boolean(input.connectionParameters.IMAP);
if (shouldCreateMessageChannel) {
const newMessageChannel = await messageChannelRepository.save(
{
id: v4(),
connectedAccountId: accountId,
type: MessageChannelType.EMAIL,
handle: input.handle,
isSyncEnabled: true,
syncStatus: MessageChannelSyncStatus.NOT_SYNCED,
syncStage: MessageChannelSyncStage.PENDING_CONFIGURATION,
pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction.NONE,
syncCursor: '',
syncStageStartedAt: null,
},
{},
);
return newMessageChannel;
}
return null;
}
private async setupCalendarChannels(
input: {
handle: string;
workspaceId: string;
connectionParameters: EmailAccountConnectionParameters;
},
accountId: string,
calendarChannelRepository: WorkspaceRepository<CalendarChannelWorkspaceEntity>,
): Promise<CalendarChannelWorkspaceEntity | null> {
const shouldCreateCalendarChannel = Boolean(
input.connectionParameters.CALDAV,
if (shouldCreateCalendarChannel) {
await this.createCalendarChannelService.createCalendarChannel({
workspaceId,
connectedAccountId: newOrExistingAccountId,
handle,
manager,
});
}
},
);
if (shouldCreateCalendarChannel) {
const newCalendarChannel = await calendarChannelRepository.save(
{
id: v4(),
connectedAccountId: accountId,
handle: input.handle,
isSyncEnabled: shouldCreateCalendarChannel,
syncStatus: CalendarChannelSyncStatus.NOT_SYNCED,
syncStage: CalendarChannelSyncStage.PENDING_CONFIGURATION,
syncCursor: '',
syncStageStartedAt: null,
},
{},
);
return newCalendarChannel;
}
return null;
return newOrExistingAccountId;
}
}