Remove connected account feature flag (#19286)

Co-authored-by: martmull <martmull@hotmail.fr>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@twenty.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
neo773
2026-04-06 13:43:41 +05:30
committed by GitHub
parent 3a1e112b86
commit d3f0162cf5
246 changed files with 5216 additions and 6424 deletions
@@ -1,31 +1,24 @@
import { Scope } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { type ObjectRecordCreateEvent } from 'twenty-shared/database-events';
import { MessageParticipantRole } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import {
And,
Any,
type FindManyOptions,
ILike,
In,
IsNull,
Not,
Or,
} from 'typeorm';
import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { And, Any, ILike, In, Not, Or, Repository } 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 { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
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';
import { MessagingMessageCleanerService } from 'src/modules/messaging/message-cleaner/services/messaging-message-cleaner.service';
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
export type BlocklistItemDeleteMessagesJobData = WorkspaceEventBatch<
ObjectRecordCreateEvent<BlocklistWorkspaceEntity>
@@ -39,7 +32,12 @@ export class BlocklistItemDeleteMessagesJob {
constructor(
private readonly threadCleanerService: MessagingMessageCleanerService,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly messageChannelDataAccessService: MessageChannelDataAccessService,
@InjectRepository(MessageChannelEntity)
private readonly messageChannelRepository: Repository<MessageChannelEntity>,
@InjectRepository(ConnectedAccountEntity)
private readonly connectedAccountRepository: Repository<ConnectedAccountEntity>,
@InjectRepository(UserWorkspaceEntity)
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
) {}
@Process(BlocklistItemDeleteMessagesJob.name)
@@ -90,6 +88,12 @@ export class BlocklistItemDeleteMessagesJob {
'messageChannelMessageAssociation',
);
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
);
for (const workspaceMemberId of handlesToDeleteByWorkspaceMemberIdMap.keys()) {
const handles =
handlesToDeleteByWorkspaceMemberIdMap.get(workspaceMemberId);
@@ -103,23 +107,46 @@ export class BlocklistItemDeleteMessagesJob {
MessageParticipantRole.TO,
] as const;
const messageChannels =
await this.messageChannelDataAccessService.findMany(workspaceId, {
select: {
id: true,
handle: true,
connectedAccount: {
handleAliases: true,
},
const workspaceMember = await workspaceMemberRepository.findOne({
where: { id: workspaceMemberId },
});
if (!workspaceMember) {
continue;
}
const userWorkspace = await this.userWorkspaceRepository.findOne({
where: { userId: workspaceMember.userId, workspaceId },
});
if (!userWorkspace) {
continue;
}
const connectedAccounts = await this.connectedAccountRepository.find({
where: { userWorkspaceId: userWorkspace.id, workspaceId },
});
const connectedAccountIds = connectedAccounts.map((ca) => ca.id);
if (connectedAccountIds.length === 0) {
continue;
}
const messageChannels = await this.messageChannelRepository.find({
select: {
id: true,
handle: true,
connectedAccount: {
handleAliases: true,
},
where: {
connectedAccount: {
accountOwnerId: workspaceMemberId,
deletedAt: IsNull(),
},
},
relations: ['connectedAccount'],
} as FindManyOptions<MessageChannelWorkspaceEntity>);
},
where: {
connectedAccountId: In(connectedAccountIds),
workspaceId,
},
relations: { connectedAccount: true },
});
for (const messageChannel of messageChannels) {
const messageChannelHandles = [messageChannel.handle];
@@ -1,18 +1,22 @@
import { Scope } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Not } from 'typeorm';
import { In, Not, Repository } from 'typeorm';
import { type ObjectRecordDeleteEvent } from 'twenty-shared/database-events';
import { MessageChannelSyncStage } from 'twenty-shared/types';
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 { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
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';
import { MessageChannelSyncStage } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
export type BlocklistReimportMessagesJobData = WorkspaceEventBatch<
ObjectRecordDeleteEvent<BlocklistWorkspaceEntity>
@@ -25,7 +29,12 @@ export type BlocklistReimportMessagesJobData = WorkspaceEventBatch<
export class BlocklistReimportMessagesJob {
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly messageChannelDataAccessService: MessageChannelDataAccessService,
@InjectRepository(MessageChannelEntity)
private readonly messageChannelRepository: Repository<MessageChannelEntity>,
@InjectRepository(ConnectedAccountEntity)
private readonly connectedAccountRepository: Repository<ConnectedAccountEntity>,
@InjectRepository(UserWorkspaceEntity)
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
private readonly messagingChannelSyncStatusService: MessageChannelSyncStatusService,
) {}
@@ -36,19 +45,49 @@ export class BlocklistReimportMessagesJob {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
);
for (const eventPayload of data.events) {
const workspaceMemberId =
eventPayload.properties.before.workspaceMemberId;
const messageChannels = await this.messageChannelDataAccessService.find(
workspaceId,
{
connectedAccount: {
accountOwnerId: workspaceMemberId,
},
const workspaceMember = await workspaceMemberRepository.findOne({
where: { id: workspaceMemberId },
});
if (!workspaceMember) {
continue;
}
const userWorkspace = await this.userWorkspaceRepository.findOne({
where: { userId: workspaceMember.userId, workspaceId },
});
if (!userWorkspace) {
continue;
}
const connectedAccounts = await this.connectedAccountRepository.find({
where: { userWorkspaceId: userWorkspace.id, workspaceId },
});
const connectedAccountIds = connectedAccounts.map((ca) => ca.id);
if (connectedAccountIds.length === 0) {
continue;
}
const messageChannels = await this.messageChannelRepository.find({
where: {
connectedAccountId: In(connectedAccountIds),
syncStage: Not(MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING),
workspaceId,
},
);
});
await this.messagingChannelSyncStatusService.resetAndMarkAsMessagesListFetchPending(
messageChannels.map((messageChannel) => messageChannel.id),
@@ -1,6 +1,9 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { MessageChannelDataAccessModule } from 'src/engine/metadata-modules/message-channel/data-access/message-channel-data-access.module';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import { BlocklistItemDeleteMessagesJob } from 'src/modules/messaging/blocklist-manager/jobs/messaging-blocklist-item-delete-messages.job';
import { BlocklistReimportMessagesJob } from 'src/modules/messaging/blocklist-manager/jobs/messaging-blocklist-reimport-messages.job';
import { MessagingBlocklistListener } from 'src/modules/messaging/blocklist-manager/listeners/messaging-blocklist.listener';
@@ -11,7 +14,11 @@ import { MessagingMessageCleanerModule } from 'src/modules/messaging/message-cle
imports: [
MessagingCommonModule,
MessagingMessageCleanerModule,
MessageChannelDataAccessModule,
TypeOrmModule.forFeature([
MessageChannelEntity,
ConnectedAccountEntity,
UserWorkspaceEntity,
]),
],
providers: [
MessagingBlocklistListener,
@@ -1,11 +1,11 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module';
import { ConnectedAccountDataAccessModule } from 'src/engine/metadata-modules/connected-account/data-access/connected-account-data-access.module';
import { MessageChannelDataAccessModule } from 'src/engine/metadata-modules/message-channel/data-access/message-channel-data-access.module';
import { MessageFolderDataAccessModule } from 'src/engine/metadata-modules/message-folder/data-access/message-folder-data-access.module';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module';
import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service';
@@ -13,11 +13,13 @@ import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/se
@Module({
imports: [
WorkspaceDataSourceModule,
TypeOrmModule.forFeature([FeatureFlagEntity]),
TypeOrmModule.forFeature([
MessageChannelEntity,
MessageFolderEntity,
ConnectedAccountEntity,
UserWorkspaceEntity,
]),
ConnectedAccountModule,
ConnectedAccountDataAccessModule,
MessageChannelDataAccessModule,
MessageFolderDataAccessModule,
MetricsModule,
],
providers: [MessageChannelSyncStatusService],
@@ -1,10 +1,13 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants';
import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service';
import { MessageChannelVisibility } from 'twenty-shared/types';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
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';
import { ApplyMessagesVisibilityRestrictionsService } from './apply-messages-visibility-restrictions.service';
@@ -39,7 +42,15 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => {
findOneByOrFail: jest.fn(),
};
const mockConnectedAccountDataAccessService = {
const mockConnectedAccountRepository = {
find: jest.fn(),
};
const mockUserWorkspaceRepository = {
findOne: jest.fn(),
};
const mockMessageChannelRepository = {
find: jest.fn(),
};
@@ -66,8 +77,16 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => {
useValue: mockGlobalWorkspaceOrmManager,
},
{
provide: ConnectedAccountDataAccessService,
useValue: mockConnectedAccountDataAccessService,
provide: getRepositoryToken(ConnectedAccountEntity),
useValue: mockConnectedAccountRepository,
},
{
provide: getRepositoryToken(UserWorkspaceEntity),
useValue: mockUserWorkspaceRepository,
},
{
provide: getRepositoryToken(MessageChannelEntity),
useValue: mockMessageChannelRepository,
},
],
}).compile();
@@ -87,10 +106,14 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => {
mockMessageChannelMessageAssociationRepository.find.mockResolvedValue([
{
messageId: 'messageId',
messageChannel: {
id: 'messageChannelId',
visibility: MessageChannelVisibility.SHARE_EVERYTHING,
},
messageChannelId: 'messageChannelId',
},
]);
mockMessageChannelRepository.find.mockResolvedValue([
{
id: 'messageChannelId',
visibility: MessageChannelVisibility.SHARE_EVERYTHING,
},
]);
@@ -107,7 +130,7 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => {
item.subject === 'Test Subject' && item.text === 'Test Message',
),
).toBe(true);
expect(mockConnectedAccountDataAccessService.find).not.toHaveBeenCalled();
expect(mockConnectedAccountRepository.find).not.toHaveBeenCalled();
});
it('should return message without obfuscated subject and with obfuscated text if the visibility is SUBJECT', async () => {
@@ -118,17 +141,27 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => {
mockMessageChannelMessageAssociationRepository.find.mockResolvedValue([
{
messageId: 'messageId',
messageChannel: {
id: 'messageChannelId',
visibility: MessageChannelVisibility.SUBJECT,
},
messageChannelId: 'messageChannelId',
},
]);
mockConnectedAccountDataAccessService.find.mockResolvedValue([]);
mockMessageChannelRepository.find.mockResolvedValue([
{
id: 'messageChannelId',
visibility: MessageChannelVisibility.SUBJECT,
},
]);
mockConnectedAccountRepository.find.mockResolvedValue([]);
mockWorkspaceMemberRepository.findOneByOrFail.mockResolvedValue({
id: 'workspace-member-id',
userId: 'user-id',
});
mockUserWorkspaceRepository.findOne.mockResolvedValue({
id: 'user-workspace-id',
userId: 'user-id',
});
const result = await service.applyMessagesVisibilityRestrictions(
@@ -153,17 +186,27 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => {
mockMessageChannelMessageAssociationRepository.find.mockResolvedValue([
{
messageId: 'messageId',
messageChannel: {
id: 'messageChannelId',
visibility: MessageChannelVisibility.METADATA,
},
messageChannelId: 'messageChannelId',
},
]);
mockConnectedAccountDataAccessService.find.mockResolvedValue([]);
mockMessageChannelRepository.find.mockResolvedValue([
{
id: 'messageChannelId',
visibility: MessageChannelVisibility.METADATA,
},
]);
mockConnectedAccountRepository.find.mockResolvedValue([]);
mockWorkspaceMemberRepository.findOneByOrFail.mockResolvedValue({
id: 'workspace-member-id',
userId: 'user-id',
});
mockUserWorkspaceRepository.findOne.mockResolvedValue({
id: 'user-workspace-id',
userId: 'user-id',
});
const result = await service.applyMessagesVisibilityRestrictions(
@@ -189,18 +232,28 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => {
mockMessageChannelMessageAssociationRepository.find.mockResolvedValue([
{
messageId: 'messageId',
messageChannel: {
id: 'messageChannelId',
visibility: MessageChannelVisibility.METADATA,
},
messageChannelId: 'messageChannelId',
},
]);
mockMessageChannelRepository.find.mockResolvedValue([
{
id: 'messageChannelId',
visibility: MessageChannelVisibility.METADATA,
},
]);
mockWorkspaceMemberRepository.findOneByOrFail.mockResolvedValue({
id: 'workspace-member-account-owner-id',
userId: 'user-id',
});
mockConnectedAccountDataAccessService.find.mockResolvedValue([{ id: '1' }]);
mockUserWorkspaceRepository.findOne.mockResolvedValue({
id: 'user-workspace-id',
userId: 'user-id',
});
mockConnectedAccountRepository.find.mockResolvedValue([{ id: '1' }]);
const result = await service.applyMessagesVisibilityRestrictions(
messages,
@@ -225,17 +278,27 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => {
mockMessageChannelMessageAssociationRepository.find.mockResolvedValue([
{
messageId: 'messageId',
messageChannel: {
id: 'messageChannelId',
},
messageChannelId: 'messageChannelId',
},
]);
mockMessageChannelRepository.find.mockResolvedValue([
{
id: 'messageChannelId',
},
]);
mockWorkspaceMemberRepository.findOneByOrFail.mockResolvedValue({
id: 'workspace-member-not-account-owner-id',
userId: 'user-id',
});
mockConnectedAccountDataAccessService.find.mockResolvedValue([]);
mockUserWorkspaceRepository.findOne.mockResolvedValue({
id: 'user-workspace-id',
userId: 'user-id',
});
mockConnectedAccountRepository.find.mockResolvedValue([]);
const result = await service.applyMessagesVisibilityRestrictions(
messages,
@@ -254,34 +317,28 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => {
];
mockMessageChannelMessageAssociationRepository.find.mockResolvedValue([
{
messageId: '1',
messageChannel: {
id: '1',
visibility: MessageChannelVisibility.SHARE_EVERYTHING,
},
},
{
messageId: '2',
messageChannel: {
id: '2',
visibility: MessageChannelVisibility.SUBJECT,
},
},
{
messageId: '3',
messageChannel: {
id: '3',
visibility: MessageChannelVisibility.METADATA,
},
},
{ messageId: '1', messageChannelId: '1' },
{ messageId: '2', messageChannelId: '2' },
{ messageId: '3', messageChannelId: '3' },
]);
mockMessageChannelRepository.find.mockResolvedValue([
{ id: '1', visibility: MessageChannelVisibility.SHARE_EVERYTHING },
{ id: '2', visibility: MessageChannelVisibility.SUBJECT },
{ id: '3', visibility: MessageChannelVisibility.METADATA },
]);
mockWorkspaceMemberRepository.findOneByOrFail.mockResolvedValue({
id: 'workspace-member-id',
userId: 'user-id',
});
mockConnectedAccountDataAccessService.find
mockUserWorkspaceRepository.findOne.mockResolvedValue({
id: 'user-workspace-id',
userId: 'user-id',
});
mockConnectedAccountRepository.find
.mockResolvedValueOnce([]) // request for message 3
.mockResolvedValueOnce([]); // request for message 2
@@ -313,34 +370,22 @@ describe('ApplyMessagesVisibilityRestrictionsService', () => {
];
mockMessageChannelMessageAssociationRepository.find.mockResolvedValue([
{
messageId: '1',
messageChannel: {
id: '1',
visibility: MessageChannelVisibility.SHARE_EVERYTHING,
},
},
{
messageId: '2',
messageChannel: {
id: '2',
visibility: MessageChannelVisibility.SUBJECT,
},
},
{
messageId: '3',
messageChannel: {
id: '3',
visibility: MessageChannelVisibility.METADATA,
},
},
{ messageId: '1', messageChannelId: '1' },
{ messageId: '2', messageChannelId: '2' },
{ messageId: '3', messageChannelId: '3' },
]);
mockMessageChannelRepository.find.mockResolvedValue([
{ id: '1', visibility: MessageChannelVisibility.SHARE_EVERYTHING },
{ id: '2', visibility: MessageChannelVisibility.SUBJECT },
{ id: '3', visibility: MessageChannelVisibility.METADATA },
]);
mockWorkspaceMemberRepository.findOneByOrFail.mockResolvedValue({
id: 'workspace-member-id',
});
mockConnectedAccountDataAccessService.find
mockConnectedAccountRepository.find
.mockResolvedValueOnce([]) // request for message 3
.mockResolvedValueOnce([]); // request for message 2
@@ -1,16 +1,19 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import groupBy from 'lodash.groupby';
import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants';
import { MessageChannelVisibility } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { In } from 'typeorm';
import { In, Repository } from 'typeorm';
import { NotFoundError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
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 { 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';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@@ -18,7 +21,12 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
export class ApplyMessagesVisibilityRestrictionsService {
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService,
@InjectRepository(ConnectedAccountEntity)
private readonly connectedAccountRepository: Repository<ConnectedAccountEntity>,
@InjectRepository(UserWorkspaceEntity)
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
@InjectRepository(MessageChannelEntity)
private readonly messageChannelRepository: Repository<MessageChannelEntity>,
) {}
public async applyMessagesVisibilityRestrictions(
@@ -41,9 +49,28 @@ export class ApplyMessagesVisibilityRestrictionsService {
where: {
messageId: In(messages.map((message) => message.id)),
},
relations: ['messageChannel'],
});
const messageChannelIds = [
...new Set(
messageChannelMessagesAssociations.map((a) => a.messageChannelId),
),
];
const messageChannelsFromCore =
messageChannelIds.length > 0
? await this.messageChannelRepository.find({
where: {
id: In(messageChannelIds),
workspaceId,
},
})
: [];
const messageChannelMap = new Map(
messageChannelsFromCore.map((ch) => [ch.id, ch]),
);
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspaceId,
@@ -51,17 +78,15 @@ export class ApplyMessagesVisibilityRestrictionsService {
);
for (let i = messages.length - 1; i >= 0; i--) {
const messageChannelMessageAssociations =
messageChannelMessagesAssociations.filter(
(association) => association.messageId === messages[i].id,
);
const associations = messageChannelMessagesAssociations.filter(
(association) => association.messageId === messages[i].id,
);
const messageChannels = messageChannelMessageAssociations
.map((association) => association.messageChannel)
.filter(
(channel): channel is NonNullable<typeof channel> =>
channel !== null,
);
const messageChannels = associations
.map((association) =>
messageChannelMap.get(association.messageChannelId),
)
.filter(isDefined);
if (messageChannels.length === 0) {
throw new NotFoundError('Associated message channels not found');
@@ -86,16 +111,26 @@ export class ApplyMessagesVisibilityRestrictionsService {
userId,
});
const connectedAccounts =
await this.connectedAccountDataAccessService.find(workspaceId, {
messageChannels: {
id: In(messageChannels.map((channel) => channel.id)),
},
accountOwnerId: workspaceMember.id,
});
const userWorkspace = await this.userWorkspaceRepository.findOne({
where: { userId: workspaceMember.userId, workspaceId },
});
if (connectedAccounts.length > 0) {
continue;
if (userWorkspace) {
const connectedAccounts =
await this.connectedAccountRepository.find({
where: {
userWorkspaceId: userWorkspace.id,
workspaceId,
messageChannels: {
id: In(messageChannels.map((channel) => channel.id)),
},
},
relations: { messageChannels: true },
});
if (connectedAccounts.length > 0) {
continue;
}
}
}
@@ -1,144 +0,0 @@
import { Logger } from '@nestjs/common';
import { msg } from '@lingui/core/macro';
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
import { Not } from 'typeorm';
import { type WorkspacePreQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { type UpdateOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
import {
WorkspaceQueryRunnerException,
WorkspaceQueryRunnerExceptionCode,
} from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception';
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service';
import { MessageFolderDataAccessService } from 'src/engine/metadata-modules/message-folder/data-access/services/message-folder-data-access.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 {
MessageChannelPendingGroupEmailsAction,
MessageChannelSyncStage,
type MessageChannelWorkspaceEntity,
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { MessageFolderPendingSyncAction } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
import { MessagingProcessGroupEmailActionsService } from 'src/modules/messaging/message-import-manager/services/messaging-process-group-email-actions.service';
const ONGOING_SYNC_STAGES = [
MessageChannelSyncStage.MESSAGE_LIST_FETCH_ONGOING,
];
@WorkspaceQueryHook(`messageChannel.updateOne`)
export class MessageChannelUpdateOnePreQueryHook
implements WorkspacePreQueryHookInstance
{
private readonly logger = new Logger(
MessageChannelUpdateOnePreQueryHook.name,
);
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly messageChannelDataAccessService: MessageChannelDataAccessService,
private readonly messageFolderDataAccessService: MessageFolderDataAccessService,
private readonly messagingProcessGroupEmailActionsService: MessagingProcessGroupEmailActionsService,
) {}
async execute(
authContext: WorkspaceAuthContext,
_objectName: string,
payload: UpdateOneResolverArgs<MessageChannelWorkspaceEntity>,
): Promise<UpdateOneResolverArgs<MessageChannelWorkspaceEntity>> {
const workspace = authContext.workspace;
assertIsDefinedOrThrow(workspace, WorkspaceNotFoundDefaultError);
const systemAuthContext = buildSystemAuthContext(workspace.id);
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
async () => {
const messageChannel =
await this.messageChannelDataAccessService.findOne(workspace.id, {
where: { id: payload.id },
});
if (!isDefined(messageChannel)) {
throw new WorkspaceQueryRunnerException(
'Message channel not found',
WorkspaceQueryRunnerExceptionCode.DATA_NOT_FOUND,
{
userFriendlyMessage: msg`Message channel not found`,
},
);
}
const messageChannelWorkspace =
messageChannel as unknown as MessageChannelWorkspaceEntity;
const isSyncOngoing = ONGOING_SYNC_STAGES.includes(
messageChannelWorkspace.syncStage,
);
const messageFoldersWithPendingAction =
await this.messageFolderDataAccessService.find(workspace.id, {
messageChannelId: messageChannel.id,
pendingSyncAction: Not(MessageFolderPendingSyncAction.NONE),
});
const messageFoldersWithPendingActionCount =
messageFoldersWithPendingAction.length;
const hasPendingFolderActions =
messageFoldersWithPendingActionCount > 0;
const hasPendingGroupEmailsAction =
messageChannelWorkspace.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 =
messageChannelWorkspace.syncStage !==
MessageChannelSyncStage.PENDING_CONFIGURATION;
if (!hasCompletedConfiguration) {
this.logger.log(
`MessageChannelId: ${messageChannelWorkspace.id} - Skipping pending action for message channel in PENDING_CONFIGURATION state`,
);
return payload;
}
const excludeGroupEmailsChanged =
isDefined(payload.data.excludeGroupEmails) &&
payload.data.excludeGroupEmails !==
messageChannelWorkspace.excludeGroupEmails;
if (excludeGroupEmailsChanged) {
await this.messagingProcessGroupEmailActionsService.markMessageChannelAsPendingGroupEmailsAction(
messageChannelWorkspace,
workspace.id,
payload.data.excludeGroupEmails
? MessageChannelPendingGroupEmailsAction.GROUP_EMAILS_DELETION
: MessageChannelPendingGroupEmailsAction.GROUP_EMAILS_IMPORT,
);
}
return payload;
},
systemAuthContext,
);
}
}
@@ -1,10 +1,11 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConnectedAccountDataAccessModule } from 'src/engine/metadata-modules/connected-account/data-access/connected-account-data-access.module';
import { MessageChannelDataAccessModule } from 'src/engine/metadata-modules/message-channel/data-access/message-channel-data-access.module';
import { MessageFolderDataAccessModule } from 'src/engine/metadata-modules/message-folder/data-access/message-folder-data-access.module';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
import { ApplyMessagesVisibilityRestrictionsService } from 'src/modules/messaging/common/query-hooks/message/apply-messages-visibility-restrictions.service';
import { MessageChannelUpdateOnePreQueryHook } from 'src/modules/messaging/common/query-hooks/message/message-channel-update-one.pre-query.hook';
import { MessageFindManyPostQueryHook } from 'src/modules/messaging/common/query-hooks/message/message-find-many.post-query.hook';
import { MessageFindOnePostQueryHook } from 'src/modules/messaging/common/query-hooks/message/message-find-one.post-query.hook';
import { MessagingImportManagerModule } from 'src/modules/messaging/message-import-manager/messaging-import-manager.module';
@@ -12,15 +13,17 @@ import { MessagingImportManagerModule } from 'src/modules/messaging/message-impo
@Module({
imports: [
MessagingImportManagerModule,
ConnectedAccountDataAccessModule,
MessageChannelDataAccessModule,
MessageFolderDataAccessModule,
TypeOrmModule.forFeature([
ConnectedAccountEntity,
MessageChannelEntity,
MessageFolderEntity,
UserWorkspaceEntity,
]),
],
providers: [
ApplyMessagesVisibilityRestrictionsService,
MessageFindOnePostQueryHook,
MessageFindManyPostQueryHook,
MessageChannelUpdateOnePreQueryHook,
],
})
export class MessagingQueryHookModule {}
@@ -1,25 +1,27 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Any, In } from 'typeorm';
import { Any, In, Repository } from 'typeorm';
import {
MessageChannelPendingGroupEmailsAction,
MessageChannelSyncStage,
MessageChannelSyncStatus,
MessageFolderPendingSyncAction,
} from 'twenty-shared/types';
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 { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type';
import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service';
import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service';
import { MessageFolderDataAccessService } from 'src/engine/metadata-modules/message-folder/data-access/services/message-folder-data-access.service';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
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 { AccountsToReconnectKeys } from 'src/modules/connected-account/types/accounts-to-reconnect-key-value.type';
import {
MessageChannelPendingGroupEmailsAction,
MessageChannelSyncStage,
MessageChannelSyncStatus,
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { MessageFolderPendingSyncAction } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@Injectable()
@@ -28,9 +30,14 @@ export class MessageChannelSyncStatusService {
@InjectCacheStorage(CacheStorageNamespace.ModuleMessaging)
private readonly cacheStorage: CacheStorageService,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly messageChannelDataAccessService: MessageChannelDataAccessService,
private readonly messageFolderDataAccessService: MessageFolderDataAccessService,
private readonly connectedAccountDataAccessService: ConnectedAccountDataAccessService,
@InjectRepository(MessageChannelEntity)
private readonly messageChannelRepository: Repository<MessageChannelEntity>,
@InjectRepository(MessageFolderEntity)
private readonly messageFolderRepository: Repository<MessageFolderEntity>,
@InjectRepository(ConnectedAccountEntity)
private readonly connectedAccountRepository: Repository<ConnectedAccountEntity>,
@InjectRepository(UserWorkspaceEntity)
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
private readonly accountsToReconnectService: AccountsToReconnectService,
private readonly metricsService: MetricsService,
) {}
@@ -47,9 +54,8 @@ export class MessageChannelSyncStatusService {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
await this.messageChannelDataAccessService.update(
workspaceId,
{ id: In(messageChannelIds) },
await this.messageChannelRepository.update(
{ id: In(messageChannelIds), workspaceId },
{
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING,
...(!preserveSyncStageStartedAt ? { syncStageStartedAt: null } : {}),
@@ -70,9 +76,8 @@ export class MessageChannelSyncStatusService {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
await this.messageChannelDataAccessService.update(
workspaceId,
{ id: In(messageChannelIds) },
await this.messageChannelRepository.update(
{ id: In(messageChannelIds), workspaceId },
{
syncStage: MessageChannelSyncStage.MESSAGES_IMPORT_PENDING,
...(!preserveSyncStageStartedAt ? { syncStageStartedAt: null } : {}),
@@ -98,9 +103,8 @@ export class MessageChannelSyncStatusService {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
await this.messageChannelDataAccessService.update(
workspaceId,
{ id: In(messageChannelIds) },
await this.messageChannelRepository.update(
{ id: In(messageChannelIds), workspaceId },
{
syncCursor: '',
syncStageStartedAt: null,
@@ -110,9 +114,8 @@ export class MessageChannelSyncStatusService {
},
);
await this.messageFolderDataAccessService.update(
workspaceId,
{ messageChannelId: In(messageChannelIds) },
await this.messageFolderRepository.update(
{ messageChannelId: In(messageChannelIds), workspaceId },
{
syncCursor: '',
pendingSyncAction: MessageFolderPendingSyncAction.NONE,
@@ -134,9 +137,8 @@ export class MessageChannelSyncStatusService {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
await this.messageChannelDataAccessService.update(
workspaceId,
{ id: In(messageChannelIds) },
await this.messageChannelRepository.update(
{ id: In(messageChannelIds), workspaceId },
{ syncStageStartedAt: null },
);
}, authContext);
@@ -153,9 +155,8 @@ export class MessageChannelSyncStatusService {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
await this.messageChannelDataAccessService.update(
workspaceId,
{ id: In(messageChannelIds) },
await this.messageChannelRepository.update(
{ id: In(messageChannelIds), workspaceId },
{
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_SCHEDULED,
syncStatus: MessageChannelSyncStatus.ONGOING,
@@ -176,9 +177,8 @@ export class MessageChannelSyncStatusService {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
await this.messageChannelDataAccessService.update(
workspaceId,
{ id: In(messageChannelIds) },
await this.messageChannelRepository.update(
{ id: In(messageChannelIds), workspaceId },
{
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_ONGOING,
syncStatus: MessageChannelSyncStatus.ONGOING,
@@ -199,9 +199,8 @@ export class MessageChannelSyncStatusService {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
await this.messageChannelDataAccessService.update(
workspaceId,
{ id: In(messageChannelIds) },
await this.messageChannelRepository.update(
{ id: In(messageChannelIds), workspaceId },
{
syncStatus: MessageChannelSyncStatus.ACTIVE,
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING,
@@ -230,9 +229,8 @@ export class MessageChannelSyncStatusService {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
await this.messageChannelDataAccessService.update(
workspaceId,
{ id: In(messageChannelIds) },
await this.messageChannelRepository.update(
{ id: In(messageChannelIds), workspaceId },
{
syncStage: MessageChannelSyncStage.MESSAGES_IMPORT_SCHEDULED,
},
@@ -251,9 +249,8 @@ export class MessageChannelSyncStatusService {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
await this.messageChannelDataAccessService.update(
workspaceId,
{ id: In(messageChannelIds) },
await this.messageChannelRepository.update(
{ id: In(messageChannelIds), workspaceId },
{
syncStage: MessageChannelSyncStage.MESSAGES_IMPORT_ONGOING,
syncStatus: MessageChannelSyncStatus.ONGOING,
@@ -277,9 +274,8 @@ export class MessageChannelSyncStatusService {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
await this.messageChannelDataAccessService.update(
workspaceId,
{ id: In(messageChannelIds) },
await this.messageChannelRepository.update(
{ id: In(messageChannelIds), workspaceId },
{
syncStage: MessageChannelSyncStage.FAILED,
syncStatus: syncStatus,
@@ -300,18 +296,16 @@ export class MessageChannelSyncStatusService {
if (
syncStatus === MessageChannelSyncStatus.FAILED_INSUFFICIENT_PERMISSIONS
) {
const messageChannels = await this.messageChannelDataAccessService.find(
workspaceId,
{ id: In(messageChannelIds) },
);
const messageChannels = await this.messageChannelRepository.find({
where: { id: In(messageChannelIds), workspaceId },
});
const connectedAccountIds = messageChannels.map(
(messageChannel) => messageChannel.connectedAccountId,
);
await this.connectedAccountDataAccessService.update(
workspaceId,
{ id: Any(connectedAccountIds) },
await this.connectedAccountRepository.update(
{ id: Any(connectedAccountIds), workspaceId },
{
authFailedAt: new Date(),
},
@@ -333,12 +327,9 @@ export class MessageChannelSyncStatusService {
return;
}
const messageChannels = await this.messageChannelDataAccessService.findMany(
workspaceId,
{
where: { id: In(messageChannelIds) },
},
);
const messageChannels = await this.messageChannelRepository.find({
where: { id: In(messageChannelIds), workspaceId },
});
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
@@ -348,17 +339,24 @@ export class MessageChannelSyncStatusService {
);
for (const messageChannel of messageChannels) {
const connectedAccount =
await this.connectedAccountDataAccessService.findOne(workspaceId, {
where: { id: messageChannel.connectedAccountId },
});
const connectedAccount = await this.connectedAccountRepository.findOne({
where: { id: messageChannel.connectedAccountId, workspaceId },
});
if (!connectedAccount) {
continue;
}
const userWorkspace = await this.userWorkspaceRepository.findOne({
where: { id: connectedAccount.userWorkspaceId },
});
if (!userWorkspace) {
continue;
}
const workspaceMember = await workspaceMemberRepository.findOne({
where: { id: connectedAccount.accountOwnerId },
where: { userId: userWorkspace.userId },
});
if (!workspaceMember) {
@@ -5,7 +5,6 @@ import { type FieldTypeAndNameMetadata } from 'src/engine/workspace-manager/util
import { type EntityRelation } from 'src/engine/workspace-manager/workspace-migration/types/entity-relation.interface';
import { type MessageDirection } from 'src/modules/messaging/common/enums/message-direction.enum';
import { type MessageChannelMessageAssociationMessageFolderWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association-message-folder.workspace-entity';
import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { type MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message.workspace-entity';
const MESSAGE_EXTERNAL_ID_FIELD_NAME = 'messageExternalId';
@@ -17,7 +16,6 @@ export class MessageChannelMessageAssociationWorkspaceEntity extends BaseWorkspa
messageExternalId: string | null;
messageThreadExternalId: string | null;
direction: MessageDirection;
messageChannel: EntityRelation<MessageChannelWorkspaceEntity> | null;
messageChannelId: string;
message: EntityRelation<MessageWorkspaceEntity> | null;
messageId: string;
@@ -9,7 +9,6 @@ import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity
import { type FieldTypeAndNameMetadata } from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util';
import { type EntityRelation } from 'src/engine/workspace-manager/workspace-migration/types/entity-relation.interface';
import { type MessageChannelMessageAssociationMessageFolderWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association-message-folder.workspace-entity';
import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
export { MessageFolderPendingSyncAction };
@@ -25,7 +24,6 @@ export const SEARCH_FIELDS_FOR_MESSAGE_FOLDER: FieldTypeAndNameMetadata[] = [
export class MessageFolderWorkspaceEntity extends BaseWorkspaceEntity {
name: string | null;
messageChannel: EntityRelation<MessageChannelWorkspaceEntity>;
syncCursor: string | null;
isSentFolder: boolean;
isSynced: boolean;
@@ -1,9 +1,11 @@
import { Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Command, CommandRunner, Option } from 'nest-commander';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
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';
@@ -24,7 +26,8 @@ export class MessagingResetChannelCommand extends CommandRunner {
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly messageChannelDataAccessService: MessageChannelDataAccessService,
@InjectRepository(MessageChannelEntity)
private readonly messageChannelRepository: Repository<MessageChannelEntity>,
private readonly messagingChannelSyncStatusService: MessageChannelSyncStatusService,
private readonly messagingMessageCleanerService: MessagingMessageCleanerService,
) {
@@ -44,10 +47,12 @@ export class MessagingResetChannelCommand extends CommandRunner {
`No message channel ID provided, resetting all message channels in workspace ${workspaceId}`,
);
const messageChannels = await this.messageChannelDataAccessService.find(
workspaceId,
isDefined(messageChannelId) ? { id: messageChannelId } : {},
);
const messageChannels = await this.messageChannelRepository.find({
where: {
...(isDefined(messageChannelId) ? { id: messageChannelId } : {}),
workspaceId,
},
});
if (messageChannels.length === 0) {
this.logger.log(
@@ -1,8 +1,5 @@
import { Logger, Scope } from '@nestjs/common';
import { FeatureFlagKey } from 'twenty-shared/types';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
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';
@@ -24,22 +21,12 @@ export class MessagingMessageChannelDeletionCleanupJob {
constructor(
private readonly messageCleanerService: MessagingMessageCleanerService,
private readonly featureFlagService: FeatureFlagService,
) {}
@Process(MessagingMessageChannelDeletionCleanupJob.name)
async handle(
data: MessagingMessageChannelDeletionCleanupJobData,
): Promise<void> {
const isMigrated = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED,
data.workspaceId,
);
if (!isMigrated) {
return;
}
this.logger.debug(
`WorkspaceId: ${data.workspaceId} Cleaning up message channel message associations for channel ${data.messageChannelId}`,
);
@@ -6,7 +6,7 @@ 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 { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import {
MessagingConnectedAccountDeletionCleanupJob,
type MessagingConnectedAccountDeletionCleanupJobData,
@@ -24,7 +24,7 @@ export class MessagingMessageCleanerConnectedAccountListener {
@OnDatabaseBatchEvent('connectedAccount', DatabaseEventAction.DESTROYED)
async handleDestroyedEvent(
payload: WorkspaceEventBatch<
ObjectRecordDeleteEvent<ConnectedAccountWorkspaceEntity>
ObjectRecordDeleteEvent<ConnectedAccountEntity>
>,
) {
await Promise.all(
@@ -6,7 +6,7 @@ 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 { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type';
import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { type MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import {
MessagingMessageChannelDeletionCleanupJob,
type MessagingMessageChannelDeletionCleanupJobData,
@@ -23,9 +23,7 @@ export class MessagingMessageCleanerMessageChannelListener {
@OnDatabaseBatchEvent('messageChannel', DatabaseEventAction.DESTROYED)
async handleDestroyedEvent(
payload: WorkspaceEventBatch<
ObjectRecordDeleteEvent<MessageChannelWorkspaceEntity>
>,
payload: WorkspaceEventBatch<ObjectRecordDeleteEvent<MessageChannelEntity>>,
) {
await Promise.all(
payload.events.map((eventPayload) =>
@@ -5,7 +5,7 @@ import { WorkspaceIteratorModule } from 'src/database/commands/command-runners/w
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { MessageChannelDataAccessModule } from 'src/engine/metadata-modules/message-channel/data-access/message-channel-data-access.module';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import { MessagingCommonModule } from 'src/modules/messaging/common/messaging-common.module';
import { MessagingMessageCleanerRemoveOrphansCommand } from 'src/modules/messaging/message-cleaner/commands/messaging-message-clearner-remove-orphans.command';
import { MessagingResetChannelCommand } from 'src/modules/messaging/message-cleaner/commands/messaging-reset-channel.command';
@@ -13,6 +13,7 @@ import { MessagingConnectedAccountDeletionCleanupJob } from 'src/modules/messagi
import { MessagingMessageChannelDeletionCleanupJob } from 'src/modules/messaging/message-cleaner/jobs/messaging-message-channel-deletion-cleanup.job';
import { MessagingMessageCleanerConnectedAccountListener } from 'src/modules/messaging/message-cleaner/listeners/messaging-message-cleaner-connected-account.listener';
import { MessagingMessageCleanerMessageChannelListener } from 'src/modules/messaging/message-cleaner/listeners/messaging-message-cleaner-message-channel.listener';
import { MessagingMessageCleanerService } from 'src/modules/messaging/message-cleaner/services/messaging-message-cleaner.service';
@Module({
@@ -21,7 +22,7 @@ import { MessagingMessageCleanerService } from 'src/modules/messaging/message-cl
DataSourceModule,
FeatureFlagModule,
MessagingCommonModule,
MessageChannelDataAccessModule,
TypeOrmModule.forFeature([MessageChannelEntity]),
WorkspaceIteratorModule,
],
providers: [
@@ -7,9 +7,9 @@ import {
MessageFolderDriver,
} from 'src/modules/messaging/message-folder-manager/interfaces/message-folder-driver.interface';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { GmailFoldersErrorHandlerService } from 'src/modules/messaging/message-folder-manager/drivers/gmail/services/gmail-folders-error-handler.service';
import { extractGmailFolderName } from 'src/modules/messaging/message-folder-manager/drivers/gmail/utils/extract-gmail-folder-name.util';
import { getGmailFolderParentId } from 'src/modules/messaging/message-folder-manager/drivers/gmail/utils/get-gmail-folder-parent-id.util';
@@ -27,13 +27,10 @@ export class GmailGetAllFoldersService implements MessageFolderDriver {
async getAllMessageFolders(
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
ConnectedAccountEntity,
'provider' | 'refreshToken' | 'accessToken' | 'id' | 'handle'
>,
messageChannel: Pick<
MessageChannelWorkspaceEntity,
'messageFolderImportPolicy'
>,
messageChannel: Pick<MessageChannelEntity, 'messageFolderImportPolicy'>,
): Promise<DiscoveredMessageFolder[]> {
try {
const oAuth2Client =
@@ -8,8 +8,8 @@ import {
MessageFolderDriver,
} from 'src/modules/messaging/message-folder-manager/interfaces/message-folder-driver.interface';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { shouldCreateFolderByDefault } from 'src/modules/messaging/message-folder-manager/utils/should-create-folder-by-default.util';
import { shouldSyncFolderByDefault } from 'src/modules/messaging/message-folder-manager/utils/should-sync-folder-by-default.util';
import { ImapClientProvider } from 'src/modules/messaging/message-import-manager/drivers/imap/providers/imap-client.provider';
@@ -27,13 +27,10 @@ export class ImapGetAllFoldersService implements MessageFolderDriver {
public async getAllMessageFolders(
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
ConnectedAccountEntity,
'id' | 'provider' | 'connectionParameters' | 'handle'
>,
messageChannel: Pick<
MessageChannelWorkspaceEntity,
'messageFolderImportPolicy'
>,
messageChannel: Pick<MessageChannelEntity, 'messageFolderImportPolicy'>,
): Promise<DiscoveredMessageFolder[]> {
try {
const client = await this.imapClientProvider.getClient(connectedAccount);
@@ -62,10 +59,7 @@ export class ImapGetAllFoldersService implements MessageFolderDriver {
private async filterAndMapFolders(
client: ImapFlow,
mailboxList: ListResponse[],
messageChannel: Pick<
MessageChannelWorkspaceEntity,
'messageFolderImportPolicy'
>,
messageChannel: Pick<MessageChannelEntity, 'messageFolderImportPolicy'>,
): Promise<DiscoveredMessageFolder[]> {
const folders: DiscoveredMessageFolder[] = [];
const pathToExternalIdMap = new Map<string, string>();
@@ -7,9 +7,9 @@ import {
MessageFolderDriver,
} from 'src/modules/messaging/message-folder-manager/interfaces/message-folder-driver.interface';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { shouldCreateFolderByDefault } from 'src/modules/messaging/message-folder-manager/utils/should-create-folder-by-default.util';
import { shouldSyncFolderByDefault } from 'src/modules/messaging/message-folder-manager/utils/should-sync-folder-by-default.util';
import { MicrosoftMessageListFetchErrorHandler } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-message-list-fetch-error-handler.service';
@@ -37,13 +37,10 @@ export class MicrosoftGetAllFoldersService implements MessageFolderDriver {
async getAllMessageFolders(
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
ConnectedAccountEntity,
'accessToken' | 'refreshToken' | 'id' | 'handle' | 'provider'
>,
messageChannel: Pick<
MessageChannelWorkspaceEntity,
'messageFolderImportPolicy'
>,
messageChannel: Pick<MessageChannelEntity, 'messageFolderImportPolicy'>,
): Promise<DiscoveredMessageFolder[]> {
try {
const microsoftClient =
@@ -1,14 +1,14 @@
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';
import { type MessageFolderWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
import { type MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { type MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
export type DiscoveredMessageFolder = Pick<
MessageFolderWorkspaceEntity,
MessageFolderEntity,
'name' | 'isSynced' | 'isSentFolder' | 'externalId' | 'parentFolderId'
>;
export type MessageFolder = Pick<
MessageFolderWorkspaceEntity,
MessageFolderEntity,
| 'name'
| 'isSynced'
| 'isSentFolder'
@@ -22,7 +22,7 @@ export type MessageFolder = Pick<
export type MessageFolderDriver = {
getAllMessageFolders(
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
ConnectedAccountEntity,
| 'provider'
| 'accessToken'
| 'refreshToken'
@@ -30,9 +30,6 @@ export type MessageFolderDriver = {
| 'handle'
| 'connectionParameters'
>,
messageChannel: Pick<
MessageChannelWorkspaceEntity,
'messageFolderImportPolicy'
>,
messageChannel: Pick<MessageChannelEntity, 'messageFolderImportPolicy'>,
): Promise<DiscoveredMessageFolder[]>;
};
@@ -4,7 +4,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { MessageFolderDataAccessModule } from 'src/engine/metadata-modules/message-folder/data-access/message-folder-data-access.module';
import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { OAuth2ClientManagerModule } from 'src/modules/connected-account/oauth2-client-manager/oauth2-client-manager.module';
import { GmailFoldersErrorHandlerService } from 'src/modules/messaging/message-folder-manager/drivers/gmail/services/gmail-folders-error-handler.service';
@@ -21,8 +21,7 @@ import { MessagingMicrosoftDriverModule } from 'src/modules/messaging/message-im
FeatureFlagModule,
WorkspaceDataSourceModule,
DataSourceModule,
MessageFolderDataAccessModule,
TypeOrmModule.forFeature([WorkspaceEntity]),
TypeOrmModule.forFeature([WorkspaceEntity, MessageFolderEntity]),
OAuth2ClientManagerModule,
MessagingGmailDriverModule,
MessagingMicrosoftDriverModule,
@@ -1,29 +1,27 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { ConnectedAccountProvider } from 'twenty-shared/types';
import { In } from 'typeorm';
import { type DiscoveredMessageFolder } from 'src/modules/messaging/message-folder-manager/interfaces/message-folder-driver.interface';
import { MessageFolderDataAccessService } from 'src/engine/metadata-modules/message-folder/data-access/services/message-folder-data-access.service';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import {
ConnectedAccountProvider,
MessageChannelContactAutoCreationPolicy,
MessageChannelType,
MessageChannelVisibility,
MessageFolderImportPolicy,
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import {
MessageFolderPendingSyncAction,
type MessageFolderWorkspaceEntity,
} from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
} from 'twenty-shared/types';
import { In } from 'typeorm';
import { type DiscoveredMessageFolder } from 'src/modules/messaging/message-folder-manager/interfaces/message-folder-driver.interface';
import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { GmailGetAllFoldersService } from 'src/modules/messaging/message-folder-manager/drivers/gmail/services/gmail-get-all-folders.service';
import { ImapGetAllFoldersService } from 'src/modules/messaging/message-folder-manager/drivers/imap/services/imap-get-all-folders.service';
import { MicrosoftGetAllFoldersService } from 'src/modules/messaging/message-folder-manager/drivers/microsoft/services/microsoft-get-all-folders.service';
import { SyncMessageFoldersService } from 'src/modules/messaging/message-folder-manager/services/sync-message-folders.service';
type SyncedMessageFolder = Pick<
MessageFolderWorkspaceEntity,
MessageFolderEntity,
| 'id'
| 'name'
| 'isSynced'
@@ -111,7 +109,7 @@ describe('SyncMessageFoldersService', () => {
let service: SyncMessageFoldersService;
let gmailGetAllFoldersService: jest.Mocked<GmailGetAllFoldersService>;
let mockMessageFolderDataAccessService: {
let mockMessageFolderRepository: {
delete: jest.Mock;
update: jest.Mock;
save: jest.Mock;
@@ -119,7 +117,7 @@ describe('SyncMessageFoldersService', () => {
};
let createdFolderRecords: Array<
Partial<MessageFolderWorkspaceEntity> & {
Partial<MessageFolderEntity> & {
id: string;
externalId: string;
}
@@ -128,10 +126,10 @@ describe('SyncMessageFoldersService', () => {
beforeEach(async () => {
createdFolderRecords = [];
mockMessageFolderDataAccessService = {
mockMessageFolderRepository = {
delete: jest.fn(),
update: jest.fn().mockResolvedValue(undefined),
save: jest.fn().mockImplementation(async (_workspaceId, folder) => {
save: jest.fn().mockImplementation(async (folder) => {
createdFolderRecords.push({
...folder,
id: `new-folder-${createdFolderRecords.length}-${Math.random().toString(36).substring(7)}`,
@@ -141,7 +139,7 @@ describe('SyncMessageFoldersService', () => {
externalId: folder.externalId as string,
});
}),
find: jest.fn().mockImplementation(async (_workspaceId, where) => {
find: jest.fn().mockImplementation(async ({ where }) => {
if (!where?.externalId) {
return [];
}
@@ -171,8 +169,8 @@ describe('SyncMessageFoldersService', () => {
},
},
{
provide: MessageFolderDataAccessService,
useValue: mockMessageFolderDataAccessService,
provide: getRepositoryToken(MessageFolderEntity),
useValue: mockMessageFolderRepository,
},
{
provide: GmailGetAllFoldersService,
@@ -228,18 +226,18 @@ describe('SyncMessageFoldersService', () => {
workspaceId,
});
expect(mockMessageFolderDataAccessService.save).toHaveBeenCalledWith(
workspaceId,
expect(mockMessageFolderRepository.save).toHaveBeenCalledWith(
expect.objectContaining({
workspaceId,
name: 'INBOX',
externalId: 'inbox-ext',
messageChannelId: 'channel-123',
isSentFolder: false,
}),
);
expect(mockMessageFolderDataAccessService.save).toHaveBeenCalledWith(
workspaceId,
expect(mockMessageFolderRepository.save).toHaveBeenCalledWith(
expect.objectContaining({
workspaceId,
name: 'Sent',
externalId: 'sent-ext',
messageChannelId: 'channel-123',
@@ -275,9 +273,9 @@ describe('SyncMessageFoldersService', () => {
workspaceId,
});
expect(mockMessageFolderDataAccessService.save).toHaveBeenCalledWith(
workspaceId,
expect(mockMessageFolderRepository.save).toHaveBeenCalledWith(
expect.objectContaining({
workspaceId,
name: 'Projects',
parentFolderId: 'parent-folder-id',
}),
@@ -311,9 +309,8 @@ describe('SyncMessageFoldersService', () => {
workspaceId,
});
expect(mockMessageFolderDataAccessService.update).toHaveBeenCalledWith(
workspaceId,
{ id: 'folder-1', messageChannelId: 'channel-123' },
expect(mockMessageFolderRepository.update).toHaveBeenCalledWith(
{ id: 'folder-1', messageChannelId: 'channel-123', workspaceId },
expect.objectContaining({ name: 'Primary Inbox' }),
);
expect(result).toContainEqual(
@@ -325,11 +322,16 @@ describe('SyncMessageFoldersService', () => {
});
it('should update folder when parent folder changes', async () => {
const newParentFolder = createMockExistingFolder({
id: 'new-parent-uuid',
externalId: 'new-parent-id',
name: 'New Parent',
});
const existingFolder = createMockExistingFolder({
id: 'folder-1',
externalId: 'child-ext',
name: 'Projects',
parentFolderId: 'old-parent-id',
parentFolderId: 'old-parent-uuid',
});
const discoveredFolders = [
createMockDiscoveredFolder({
@@ -337,9 +339,13 @@ describe('SyncMessageFoldersService', () => {
name: 'Projects',
parentFolderId: 'new-parent-id',
}),
createMockDiscoveredFolder({
externalId: 'new-parent-id',
name: 'New Parent',
}),
];
const messageChannel = createMockMessageChannel({
messageFolders: [existingFolder],
messageFolders: [existingFolder, newParentFolder],
});
gmailGetAllFoldersService.getAllMessageFolders.mockResolvedValue(
@@ -351,11 +357,10 @@ describe('SyncMessageFoldersService', () => {
workspaceId,
});
expect(mockMessageFolderDataAccessService.update).toHaveBeenCalledWith(
workspaceId,
{ id: 'folder-1', messageChannelId: 'channel-123' },
expect(mockMessageFolderRepository.update).toHaveBeenCalledWith(
{ id: 'folder-1', messageChannelId: 'channel-123', workspaceId },
expect.objectContaining({
parentFolderId: 'new-parent-id',
parentFolderId: 'new-parent-uuid',
}),
);
});
@@ -389,9 +394,7 @@ describe('SyncMessageFoldersService', () => {
workspaceId,
});
expect(
mockMessageFolderDataAccessService.update,
).not.toHaveBeenCalled();
expect(mockMessageFolderRepository.update).not.toHaveBeenCalled();
});
});
@@ -428,9 +431,8 @@ describe('SyncMessageFoldersService', () => {
workspaceId,
});
expect(mockMessageFolderDataAccessService.update).toHaveBeenCalledWith(
workspaceId,
{ id: In(['folder-2']) },
expect(mockMessageFolderRepository.update).toHaveBeenCalledWith(
{ id: In(['folder-2']), workspaceId },
expect.objectContaining({
pendingSyncAction: 'FOLDER_DELETION',
}),
@@ -491,21 +493,25 @@ describe('SyncMessageFoldersService', () => {
workspaceId,
});
expect(mockMessageFolderDataAccessService.update).toHaveBeenCalledWith(
workspaceId,
{ id: In(['folder-to-delete']) },
expect(mockMessageFolderRepository.update).toHaveBeenCalledWith(
{ id: In(['folder-to-delete']), workspaceId },
expect.objectContaining({
pendingSyncAction: 'FOLDER_DELETION',
}),
);
expect(mockMessageFolderDataAccessService.update).toHaveBeenCalledWith(
workspaceId,
{ id: 'folder-to-update', messageChannelId: 'channel-123' },
expect(mockMessageFolderRepository.update).toHaveBeenCalledWith(
{
id: 'folder-to-update',
messageChannelId: 'channel-123',
workspaceId,
},
expect.objectContaining({ name: 'New Name' }),
);
expect(mockMessageFolderDataAccessService.save).toHaveBeenCalledWith(
workspaceId,
expect.objectContaining({ externalId: 'new-ext' }),
expect(mockMessageFolderRepository.save).toHaveBeenCalledWith(
expect.objectContaining({
workspaceId,
externalId: 'new-ext',
}),
);
expect(result).toHaveLength(4);
expect(result).toContainEqual(
@@ -1,20 +1,23 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { ConnectedAccountProvider } from 'twenty-shared/types';
import {
ConnectedAccountProvider,
MessageFolderPendingSyncAction,
} from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { In } from 'typeorm';
import { In, Repository } from 'typeorm';
import {
DiscoveredMessageFolder,
MessageFolder,
} from 'src/modules/messaging/message-folder-manager/interfaces/message-folder-driver.interface';
import { MessageFolderDataAccessService } from 'src/engine/metadata-modules/message-folder/data-access/services/message-folder-data-access.service';
import { type MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
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 { 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';
import { MessageFolderPendingSyncAction } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { GmailGetAllFoldersService } from 'src/modules/messaging/message-folder-manager/drivers/gmail/services/gmail-get-all-folders.service';
import { ImapGetAllFoldersService } from 'src/modules/messaging/message-folder-manager/drivers/imap/services/imap-get-all-folders.service';
import { MicrosoftGetAllFoldersService } from 'src/modules/messaging/message-folder-manager/drivers/microsoft/services/microsoft-get-all-folders.service';
@@ -27,7 +30,8 @@ import { computeUpdatedFolders } from 'src/modules/messaging/message-folder-mana
export class SyncMessageFoldersService {
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly messageFolderDataAccessService: MessageFolderDataAccessService,
@InjectRepository(MessageFolderEntity)
private readonly messageFolderRepository: Repository<MessageFolderEntity>,
private readonly gmailGetAllFoldersService: GmailGetAllFoldersService,
private readonly microsoftGetAllFoldersService: MicrosoftGetAllFoldersService,
private readonly imapGetAllFoldersService: ImapGetAllFoldersService,
@@ -38,11 +42,11 @@ export class SyncMessageFoldersService {
workspaceId,
}: {
messageChannel: Pick<
MessageChannelWorkspaceEntity,
MessageChannelEntity,
'id' | 'messageFolderImportPolicy'
> & {
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
ConnectedAccountEntity,
| 'provider'
| 'accessToken'
| 'refreshToken'
@@ -72,7 +76,7 @@ export class SyncMessageFoldersService {
async discoverAllFolders(
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
ConnectedAccountEntity,
| 'accessToken'
| 'refreshToken'
| 'id'
@@ -80,10 +84,7 @@ export class SyncMessageFoldersService {
| 'provider'
| 'connectionParameters'
>,
messageChannel: Pick<
MessageChannelWorkspaceEntity,
'messageFolderImportPolicy'
>,
messageChannel: Pick<MessageChannelEntity, 'messageFolderImportPolicy'>,
): Promise<DiscoveredMessageFolder[]> {
switch (connectedAccount.provider) {
case ConnectedAccountProvider.GOOGLE:
@@ -114,6 +115,12 @@ export class SyncMessageFoldersService {
messageChannelId: string,
workspaceId: string,
): Promise<MessageFolder[]> {
const externalIdToUuidMap = new Map(
existingFolders
.filter((folder) => isDefined(folder.externalId))
.map((folder) => [folder.externalId as string, folder.id]),
);
const foldersToCreate = computeFoldersToCreate({
discoveredFolders,
existingFolders,
@@ -123,6 +130,7 @@ export class SyncMessageFoldersService {
const foldersToUpdate = computeFoldersToUpdate({
discoveredFolders,
existingFolders,
externalIdToUuidMap,
});
const folderIdsToDelete = computeFolderIdsToDelete({
@@ -132,16 +140,11 @@ export class SyncMessageFoldersService {
const authContext = buildSystemAuthContext(workspaceId);
// TODO: Restore transaction wrapper once migration is complete — folder
// sync operations (create/update/delete) are no longer atomic since
// the data access layer routes writes across workspace and core schemas.
// Acceptable during transition as sync is idempotent and self-corrects.
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
async () => {
if (folderIdsToDelete.length > 0) {
await this.messageFolderDataAccessService.update(
workspaceId,
{ id: In(folderIdsToDelete) },
await this.messageFolderRepository.update(
{ id: In(folderIdsToDelete), workspaceId },
{
pendingSyncAction: MessageFolderPendingSyncAction.FOLDER_DELETION,
},
@@ -150,32 +153,34 @@ export class SyncMessageFoldersService {
if (foldersToUpdate.size > 0) {
for (const [id, data] of foldersToUpdate.entries()) {
await this.messageFolderDataAccessService.update(
workspaceId,
{ id, messageChannelId },
data,
await this.messageFolderRepository.update(
{ id, messageChannelId, workspaceId },
data as Record<string, unknown>,
);
}
}
if (foldersToCreate.length > 0) {
for (const folderToCreate of foldersToCreate) {
await this.messageFolderDataAccessService.save(
await this.messageFolderRepository.save({
...folderToCreate,
workspaceId,
folderToCreate,
);
});
}
}
const createdFolders =
foldersToCreate.length > 0
? await this.messageFolderDataAccessService.find(workspaceId, {
messageChannelId,
externalId: In(
foldersToCreate
.map((folder) => folder.externalId)
.filter(isDefined),
),
? await this.messageFolderRepository.find({
where: {
messageChannelId,
externalId: In(
foldersToCreate
.map((folder) => folder.externalId)
.filter(isDefined),
),
workspaceId,
},
})
: [];
@@ -185,10 +190,7 @@ export class SyncMessageFoldersService {
folderIdsToDelete,
});
return [
...updatedExistingFolders,
...(createdFolders as MessageFolder[]),
];
return [...updatedExistingFolders, ...createdFolders];
},
authContext,
);
@@ -1,4 +1,4 @@
import { MessageFolderPendingSyncAction } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
import { MessageFolderPendingSyncAction } from 'twenty-shared/types';
import { computeFolderIdsToDelete } from 'src/modules/messaging/message-folder-manager/utils/compute-folder-ids-to-delete.util';
describe('computeFolderIdsToDelete', () => {
@@ -1,4 +1,4 @@
import { MessageFolderPendingSyncAction } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
import { MessageFolderPendingSyncAction } from 'twenty-shared/types';
import { computeFoldersToCreate } from 'src/modules/messaging/message-folder-manager/utils/compute-folders-to-create.util';
describe('computeFoldersToCreate', () => {
@@ -1,7 +1,9 @@
import { MessageFolderPendingSyncAction } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
import { MessageFolderPendingSyncAction } from 'twenty-shared/types';
import { computeFoldersToUpdate } from 'src/modules/messaging/message-folder-manager/utils/compute-folders-to-update.util';
describe('computeFoldersToUpdate', () => {
const emptyMap = new Map<string, string>();
it('should detect folder rename from provider', () => {
const discoveredFolders = [
{
@@ -29,19 +31,22 @@ describe('computeFoldersToUpdate', () => {
const result = computeFoldersToUpdate({
discoveredFolders,
existingFolders,
externalIdToUuidMap: emptyMap,
});
expect(result.get('folder-id')?.name).toBe('Work Emails');
});
it('should detect folder moved to different parent', () => {
const existingParentUuid = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee';
const discoveredFolders = [
{
name: 'Subfolder',
externalId: 'sub-1',
isSynced: true,
isSentFolder: false,
parentFolderId: 'new-parent-id',
parentFolderId: 'new-parent-ext',
},
];
@@ -52,18 +57,23 @@ describe('computeFoldersToUpdate', () => {
externalId: 'sub-1',
isSynced: true,
isSentFolder: false,
parentFolderId: 'old-parent-id',
parentFolderId: 'old-parent-uuid',
syncCursor: 'cursor',
pendingSyncAction: MessageFolderPendingSyncAction.NONE,
},
];
const externalIdToUuidMap = new Map([
['new-parent-ext', existingParentUuid],
]);
const result = computeFoldersToUpdate({
discoveredFolders,
existingFolders,
externalIdToUuidMap,
});
expect(result.get('folder-id')?.parentFolderId).toBe('new-parent-id');
expect(result.get('folder-id')?.parentFolderId).toBe(existingParentUuid);
});
it('should not flag unchanged folders for update', () => {
@@ -88,6 +98,7 @@ describe('computeFoldersToUpdate', () => {
const result = computeFoldersToUpdate({
discoveredFolders,
existingFolders,
externalIdToUuidMap: emptyMap,
});
expect(result.size).toBe(0);
@@ -120,6 +131,7 @@ describe('computeFoldersToUpdate', () => {
const result = computeFoldersToUpdate({
discoveredFolders,
existingFolders,
externalIdToUuidMap: emptyMap,
});
expect(result.size).toBe(0);
@@ -1,4 +1,4 @@
import { MessageFolderPendingSyncAction } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
import { MessageFolderPendingSyncAction } from 'twenty-shared/types';
import { computeUpdatedFolders } from 'src/modules/messaging/message-folder-manager/utils/compute-updated-folders.util';
describe('computeUpdatedFolders', () => {
@@ -1,4 +1,4 @@
import { MessageFolderImportPolicy } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { MessageFolderImportPolicy } from 'twenty-shared/types';
import { shouldSyncFolderByDefault } from 'src/modules/messaging/message-folder-manager/utils/should-sync-folder-by-default.util';
describe('shouldSyncFolderByDefault', () => {
@@ -5,7 +5,7 @@ import {
type MessageFolder,
} from 'src/modules/messaging/message-folder-manager/interfaces/message-folder-driver.interface';
import { type MessageFolderWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
import { type MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
export const computeFoldersToCreate = ({
discoveredFolders,
@@ -15,7 +15,7 @@ export const computeFoldersToCreate = ({
discoveredFolders: DiscoveredMessageFolder[];
existingFolders: MessageFolder[];
messageChannelId: string;
}): Partial<MessageFolderWorkspaceEntity>[] => {
}): Partial<MessageFolderEntity>[] => {
const existingFoldersByExternalId = new Map(
existingFolders.map((folder) => [folder.externalId, folder]),
);
@@ -6,23 +6,22 @@ import {
type MessageFolder,
} from 'src/modules/messaging/message-folder-manager/interfaces/message-folder-driver.interface';
import { type MessageFolderWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
import { type MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
export const computeFoldersToUpdate = ({
discoveredFolders,
existingFolders,
externalIdToUuidMap,
}: {
discoveredFolders: DiscoveredMessageFolder[];
existingFolders: MessageFolder[];
}): Map<string, Partial<MessageFolderWorkspaceEntity>> => {
externalIdToUuidMap: Map<string, string>;
}): Map<string, Partial<MessageFolderEntity>> => {
const existingFoldersByExternalId = new Map(
existingFolders.map((folder) => [folder.externalId, folder]),
);
const foldersToUpdate = new Map<
string,
Partial<MessageFolderWorkspaceEntity>
>();
const foldersToUpdate = new Map<string, Partial<MessageFolderEntity>>();
for (const discoveredFolder of discoveredFolders) {
const existingFolder = existingFoldersByExternalId.get(
@@ -33,12 +32,16 @@ export const computeFoldersToUpdate = ({
continue;
}
const resolvedParentFolderId = isNonEmptyString(
discoveredFolder.parentFolderId,
)
? (externalIdToUuidMap.get(discoveredFolder.parentFolderId) ?? null)
: null;
const discoveredFolderData = {
name: discoveredFolder.name,
isSentFolder: discoveredFolder.isSentFolder,
parentFolderId: isNonEmptyString(discoveredFolder.parentFolderId)
? discoveredFolder.parentFolderId
: null,
parentFolderId: resolvedParentFolderId,
};
const existingFolderData = {
@@ -1,9 +1,8 @@
import { type MessageFolder } from 'src/modules/messaging/message-folder-manager/interfaces/message-folder-driver.interface';
import {
MessageFolderPendingSyncAction,
type MessageFolderWorkspaceEntity,
} from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
import { MessageFolderPendingSyncAction } from 'twenty-shared/types';
import { type MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
export const computeUpdatedFolders = ({
existingFolders,
@@ -11,7 +10,7 @@ export const computeUpdatedFolders = ({
folderIdsToDelete,
}: {
existingFolders: MessageFolder[];
foldersToUpdate: Map<string, Partial<MessageFolderWorkspaceEntity>>;
foldersToUpdate: Map<string, Partial<MessageFolderEntity>>;
folderIdsToDelete: string[];
}): MessageFolder[] => {
return existingFolders.map((existingFolder) => {
@@ -1,4 +1,4 @@
import { MessageFolderImportPolicy } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { MessageFolderImportPolicy } from 'twenty-shared/types';
export const shouldSyncFolderByDefault = (
messageFolderImportPolicy: MessageFolderImportPolicy,
@@ -1,18 +1,20 @@
import { Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Command, CommandRunner, Option } from 'nest-commander';
import { Repository } from 'typeorm';
import { MessageChannelSyncStage } from 'twenty-shared/types';
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 { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
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 {
MessagingMessageListFetchJob,
type MessagingMessageListFetchJobData,
} from 'src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job';
import { MessageChannelSyncStage } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
type MessagingTriggerMessageListFetchCommandOptions = {
workspaceId: string;
@@ -31,7 +33,8 @@ export class MessagingTriggerMessageListFetchCommand extends CommandRunner {
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly messageChannelDataAccessService: MessageChannelDataAccessService,
@InjectRepository(MessageChannelEntity)
private readonly messageChannelRepository: Repository<MessageChannelEntity>,
@InjectMessageQueue(MessageQueue.messagingQueue)
private readonly messageQueueService: MessageQueueService,
) {
@@ -51,14 +54,14 @@ export class MessagingTriggerMessageListFetchCommand extends CommandRunner {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
const messageChannels = await this.messageChannelDataAccessService.find(
workspaceId,
{
const messageChannels = await this.messageChannelRepository.find({
where: {
isSyncEnabled: true,
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING,
...(messageChannelId ? { id: messageChannelId } : {}),
workspaceId,
},
);
});
if (messageChannels.length === 0) {
this.logger.warn(
@@ -73,9 +76,8 @@ export class MessagingTriggerMessageListFetchCommand extends CommandRunner {
);
for (const messageChannel of messageChannels) {
await this.messageChannelDataAccessService.update(
workspaceId,
{ id: messageChannel.id },
await this.messageChannelRepository.update(
{ id: messageChannel.id, workspaceId },
{
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_SCHEDULED,
syncStageStartedAt: new Date().toISOString(),
@@ -1,20 +1,17 @@
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { FeatureFlagKey } from 'twenty-shared/types';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { DataSource, Repository } from 'typeorm';
import { MessageChannelSyncStage } from 'twenty-shared/types';
import { SentryCronMonitor } from 'src/engine/core-modules/cron/sentry-cron-monitor.decorator';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
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 { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
import { MessageChannelSyncStage } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import {
MessagingMessageListFetchJob,
type MessagingMessageListFetchJobData,
@@ -32,7 +29,6 @@ export class MessagingMessageListFetchCronJob {
@InjectDataSource()
private readonly coreDataSource: DataSource,
private readonly exceptionHandlerService: ExceptionHandlerService,
private readonly featureFlagService: FeatureFlagService,
) {}
@Process(MessagingMessageListFetchCronJob.name)
@@ -51,22 +47,11 @@ export class MessagingMessageListFetchCronJob {
try {
const now = new Date().toISOString();
// TODO: remove workspace schema branch once IS_CONNECTED_ACCOUNT_MIGRATED feature flag is removed
const isMigrated = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED,
activeWorkspace.id,
const [messageChannels] = await this.coreDataSource.query(
`UPDATE core."messageChannel" SET "syncStage" = '${MessageChannelSyncStage.MESSAGE_LIST_FETCH_SCHEDULED}', "syncStageStartedAt" = COALESCE("syncStageStartedAt", '${now}')
WHERE "workspaceId" = '${activeWorkspace.id}' AND "isSyncEnabled" = true AND "syncStage" = '${MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING}' RETURNING *`,
);
const [messageChannels] = isMigrated
? await this.coreDataSource.query(
`UPDATE core."messageChannel" SET "syncStage" = '${MessageChannelSyncStage.MESSAGE_LIST_FETCH_SCHEDULED}', "syncStageStartedAt" = COALESCE("syncStageStartedAt", '${now}')
WHERE "workspaceId" = '${activeWorkspace.id}' AND "isSyncEnabled" = true AND "syncStage" = '${MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING}' RETURNING *`,
)
: await this.coreDataSource.query(
`UPDATE ${getWorkspaceSchemaName(activeWorkspace.id)}."messageChannel" SET "syncStage" = '${MessageChannelSyncStage.MESSAGE_LIST_FETCH_SCHEDULED}', "syncStageStartedAt" = COALESCE("syncStageStartedAt", '${now}')
WHERE "isSyncEnabled" = true AND "syncStage" = '${MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING}' RETURNING *`,
);
for (const messageChannel of messageChannels) {
await this.messageQueueService.add<MessagingMessageListFetchJobData>(
MessagingMessageListFetchJob.name,
@@ -1,13 +1,12 @@
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { FeatureFlagKey } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { DataSource, Repository } from 'typeorm';
import { MessageChannelSyncStage } from 'twenty-shared/types';
import { SentryCronMonitor } from 'src/engine/core-modules/cron/sentry-cron-monitor.decorator';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
@@ -18,8 +17,6 @@ import {
DataSourceException,
DataSourceExceptionCode,
} from 'src/engine/metadata-modules/data-source/data-source.exception';
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
import { MessageChannelSyncStage } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import {
MessagingMessagesImportJob,
type MessagingMessagesImportJobData,
@@ -37,7 +34,6 @@ export class MessagingMessagesImportCronJob {
private readonly exceptionHandlerService: ExceptionHandlerService,
@InjectDataSource()
private readonly coreDataSource: DataSource,
private readonly featureFlagService: FeatureFlagService,
) {}
@Process(MessagingMessagesImportCronJob.name)
@@ -56,22 +52,11 @@ export class MessagingMessagesImportCronJob {
try {
const now = new Date().toISOString();
// TODO: remove workspace schema branch once IS_CONNECTED_ACCOUNT_MIGRATED feature flag is removed
const isMigrated = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED,
activeWorkspace.id,
const [messageChannels] = await this.coreDataSource.query(
`UPDATE core."messageChannel" SET "syncStage" = '${MessageChannelSyncStage.MESSAGES_IMPORT_SCHEDULED}', "syncStageStartedAt" = COALESCE("syncStageStartedAt", '${now}')
WHERE "workspaceId" = '${activeWorkspace.id}' AND "isSyncEnabled" = true AND "syncStage" = '${MessageChannelSyncStage.MESSAGES_IMPORT_PENDING}' RETURNING *`,
);
const [messageChannels] = isMigrated
? await this.coreDataSource.query(
`UPDATE core."messageChannel" SET "syncStage" = '${MessageChannelSyncStage.MESSAGES_IMPORT_SCHEDULED}', "syncStageStartedAt" = COALESCE("syncStageStartedAt", '${now}')
WHERE "workspaceId" = '${activeWorkspace.id}' AND "isSyncEnabled" = true AND "syncStage" = '${MessageChannelSyncStage.MESSAGES_IMPORT_PENDING}' RETURNING *`,
)
: await this.coreDataSource.query(
`UPDATE ${getWorkspaceSchemaName(activeWorkspace.id)}."messageChannel" SET "syncStage" = '${MessageChannelSyncStage.MESSAGES_IMPORT_SCHEDULED}', "syncStageStartedAt" = COALESCE("syncStageStartedAt", '${now}')
WHERE "isSyncEnabled" = true AND "syncStage" = '${MessageChannelSyncStage.MESSAGES_IMPORT_PENDING}' RETURNING *`,
);
for (const messageChannel of messageChannels) {
await this.messageQueueService.add<MessagingMessagesImportJobData>(
MessagingMessagesImportJob.name,
@@ -82,9 +67,6 @@ export class MessagingMessagesImportCronJob {
);
}
} catch (error) {
// We had issues with the workspace schema not being found, due
// to users deleting their workspaces in the middle of the cron job
// We only throw an error when the workspace is found & schema not found
if (
error.code === '42P01' &&
error.message.includes('messageChannel" does not exist')
@@ -3,6 +3,10 @@ import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { DataSource, Repository } from 'typeorm';
import {
MessageChannelSyncStage,
MessageChannelSyncStatus,
} from 'twenty-shared/types';
import { SentryCronMonitor } from 'src/engine/core-modules/cron/sentry-cron-monitor.decorator';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
@@ -12,10 +16,6 @@ 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 { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
import {
MessageChannelSyncStage,
MessageChannelSyncStatus,
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import {
MessagingRelaunchFailedMessageChannelJob,
type MessagingRelaunchFailedMessageChannelJobData,
@@ -1,14 +1,16 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { google } from 'googleapis';
import { ConnectedAccountProvider } from 'twenty-shared/types';
import {
ConnectedAccountProvider,
MessageFolderImportPolicy,
MessageFolderPendingSyncAction,
} from 'twenty-shared/types';
import { type MessageFolder } from 'src/modules/messaging/message-folder-manager/interfaces/message-folder-driver.interface';
import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { MessageFolderImportPolicy } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { MessageFolderPendingSyncAction } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { GmailGetHistoryService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-history.service';
import { GmailGetMessageListService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-message-list.service';
import { GmailMessageListFetchErrorHandler } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-message-list-fetch-error-handler.service';
@@ -31,7 +33,7 @@ describe('GmailGetMessageListService', () => {
let oAuth2ClientManagerService: OAuth2ClientManagerService;
const mockConnectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
ConnectedAccountEntity,
| 'provider'
| 'accessToken'
| 'refreshToken'
@@ -3,13 +3,11 @@ import { Injectable, Logger } from '@nestjs/common';
import { isNonEmptyString } from '@sniptt/guards';
import { google } from 'googleapis';
import { MessageFolderImportPolicy } from 'twenty-shared/types';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import {
MessageChannelWorkspaceEntity,
MessageFolderImportPolicy,
} 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';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { type MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
import {
MessageImportDriverException,
MessageImportDriverExceptionCode,
@@ -32,17 +30,14 @@ export class GmailGetMessageListService {
private async getMessageListWithoutCursor(
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
ConnectedAccountEntity,
'provider' | 'accessToken' | 'refreshToken' | 'id' | 'handle'
>,
messageFolders: Pick<
MessageFolderWorkspaceEntity,
MessageFolderEntity,
'name' | 'externalId' | 'isSynced' | 'parentFolderId'
>[],
messageChannel: Pick<
MessageChannelWorkspaceEntity,
'messageFolderImportPolicy'
>,
messageChannel: Pick<MessageChannelEntity, 'messageFolderImportPolicy'>,
): Promise<GetMessageListsResponse> {
const oAuth2Client =
await this.oAuth2ClientManagerService.getGoogleOAuth2Client(
@@ -5,12 +5,10 @@ import { isNonEmptyString } from '@sniptt/guards';
import { type gmail_v1 as gmailV1, google } from 'googleapis';
import { isDefined } from 'twenty-shared/utils';
import { MessageFolderImportPolicy } from 'twenty-shared/types';
import { type MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import {
type MessageChannelWorkspaceEntity,
MessageFolderImportPolicy,
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { GmailMessagesImportErrorHandler } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-messages-import-error-handler.service';
import { filterGmailMessagesByFolderPolicy } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/filter-gmail-messages-by-folder-policy.util';
import { parseAndFormatGmailMessage } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-and-format-gmail-message.util';
@@ -28,7 +26,7 @@ export class GmailGetMessagesService {
async getMessages(
messageIds: string[],
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
ConnectedAccountEntity,
| 'provider'
| 'accessToken'
| 'refreshToken'
@@ -37,7 +35,7 @@ export class GmailGetMessagesService {
| 'handleAliases'
>,
messageChannel: Pick<
MessageChannelWorkspaceEntity,
MessageChannelEntity,
'messageFolders' | 'messageFolderImportPolicy'
>,
): Promise<MessageWithParticipants[]> {
@@ -169,10 +167,7 @@ export class GmailGetMessagesService {
private async fetchMessages(
gmailClient: gmailV1.Gmail,
messageIds: string[],
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
'handle' | 'handleAliases'
>,
connectedAccount: Pick<ConnectedAccountEntity, 'handle' | 'handleAliases'>,
): Promise<MessageWithParticipants[]> {
const results = await Promise.all(
messageIds.map((messageId) =>
@@ -1,4 +1,4 @@
import { MessageFolderImportPolicy } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { MessageFolderImportPolicy } from 'twenty-shared/types';
import { computeGmailExcludeSearchFilter } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/compute-gmail-exclude-search-filter.util';
describe('computeGmailExcludeSearchFilter', () => {
@@ -1,5 +1,5 @@
import { MessageFolderImportPolicy } 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';
import { MessageFolderImportPolicy } from 'twenty-shared/types';
import { type MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
import { filterGmailMessagesByFolderPolicy } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/filter-gmail-messages-by-folder-policy.util';
import { type MessageWithParticipants } from 'src/modules/messaging/message-import-manager/types/message';
@@ -12,8 +12,8 @@ const createMessage = (
const createFolder = (
externalId: string,
isSynced: boolean,
): MessageFolderWorkspaceEntity =>
({ externalId, isSynced }) as MessageFolderWorkspaceEntity;
): MessageFolderEntity =>
({ externalId, isSynced }) as unknown as MessageFolderEntity;
describe('filterGmailMessagesByFolderPolicy', () => {
describe('ALL_FOLDERS policy', () => {
@@ -1,9 +1,9 @@
import { type MessageFolderWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
import { type MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
const MAXIMUM_GMAIL_FOLDER_DEPTH = 50;
type FolderInput = Pick<
MessageFolderWorkspaceEntity,
MessageFolderEntity,
'externalId' | 'name' | 'parentFolderId'
>;
@@ -1,7 +1,7 @@
import { isDefined } from 'twenty-shared/utils';
import { MessageFolderImportPolicy } 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';
import { MessageFolderImportPolicy } from 'twenty-shared/types';
import { type MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
import { MESSAGING_GMAIL_DEFAULT_EXCLUDED_LABELS } from 'src/modules/messaging/message-import-manager/drivers/gmail/constants/messaging-gmail-default-excluded-labels.constant';
import { MESSAGING_GMAIL_EXCLUDED_SYSTEM_LABELS } from 'src/modules/messaging/message-import-manager/drivers/gmail/constants/messaging-gmail-excluded-system-labels.constant';
import { MESSAGING_GMAIL_FOLDERS_WITH_CATEGORY_EXCLUSIONS } from 'src/modules/messaging/message-import-manager/drivers/gmail/constants/messaging-gmail-folders-with-category-exclusions.constant';
@@ -10,7 +10,7 @@ import { computeGmailDefaultNotSyncedLabelsSearchFilter } from 'src/modules/mess
export const computeGmailExcludeSearchFilter = (
messageFolders: Pick<
MessageFolderWorkspaceEntity,
MessageFolderEntity,
'externalId' | 'isSynced' | 'name' | 'parentFolderId'
>[],
messageFolderImportPolicy: MessageFolderImportPolicy,
@@ -1,12 +1,9 @@
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { MessageDirection } from 'src/modules/messaging/common/enums/message-direction.enum';
export const computeMessageDirection = (
fromHandle: string,
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
'handle' | 'handleAliases'
>,
connectedAccount: Pick<ConnectedAccountEntity, 'handle' | 'handleAliases'>,
): MessageDirection =>
connectedAccount.handle === fromHandle ||
connectedAccount.handleAliases?.includes(fromHandle)
@@ -1,7 +1,5 @@
import {
type MessageChannelWorkspaceEntity,
MessageFolderImportPolicy,
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { MessageFolderImportPolicy } from 'twenty-shared/types';
import { type MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import { MESSAGING_GMAIL_EXCLUDED_CATEGORY_LABELS } from 'src/modules/messaging/message-import-manager/drivers/gmail/constants/messaging-gmail-excluded-category-labels.constant';
import { MESSAGING_GMAIL_FOLDERS_WITH_CATEGORY_EXCLUSIONS } from 'src/modules/messaging/message-import-manager/drivers/gmail/constants/messaging-gmail-folders-with-category-exclusions.constant';
import { type MessageWithParticipants } from 'src/modules/messaging/message-import-manager/types/message';
@@ -9,7 +7,7 @@ import { type MessageWithParticipants } from 'src/modules/messaging/message-impo
export const filterGmailMessagesByFolderPolicy = (
messages: MessageWithParticipants[],
messageChannel: Pick<
MessageChannelWorkspaceEntity,
MessageChannelEntity,
'messageFolders' | 'messageFolderImportPolicy'
>,
): MessageWithParticipants[] => {
@@ -2,7 +2,7 @@ import { type gmail_v1 as gmailV1 } from 'googleapis';
import planer from 'planer';
import { MessageParticipantRole } from 'twenty-shared/types';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { computeMessageDirection } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/compute-message-direction.util';
import { parseGmailMessage } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-message.util';
import { type MessageWithParticipants } from 'src/modules/messaging/message-import-manager/types/message';
@@ -11,10 +11,7 @@ import { sanitizeString } from 'src/modules/messaging/message-import-manager/uti
export const parseAndFormatGmailMessage = (
message: gmailV1.Schema$Message,
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
'handle' | 'handleAliases'
>,
connectedAccount: Pick<ConnectedAccountEntity, 'handle' | 'handleAliases'>,
): MessageWithParticipants | null => {
const {
id,
@@ -6,12 +6,12 @@ import { CustomError, isDefined } from 'twenty-shared/utils';
import { type ImapSmtpCaldavParams } from 'src/engine/core-modules/imap-smtp-caldav-connection/types/imap-smtp-caldav-connection.type';
import { SecureHttpClientService } from 'src/engine/core-modules/secure-http-client/secure-http-client.service';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { MessageImportDriverExceptionCode } from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception';
import { parseImapAuthenticationError } from 'src/modules/messaging/message-import-manager/drivers/imap/utils/parse-imap-authentication-error.util';
type ConnectedAccountIdentifier = Pick<
ConnectedAccountWorkspaceEntity,
ConnectedAccountEntity,
'id' | 'provider' | 'connectionParameters' | 'handle'
>;
@@ -1,12 +1,14 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { ConnectedAccountProvider } from 'twenty-shared/types';
import {
ConnectedAccountProvider,
MessageFolderImportPolicy,
MessageFolderPendingSyncAction,
} from 'twenty-shared/types';
import { type MessageFolder } from 'src/modules/messaging/message-folder-manager/interfaces/message-folder-driver.interface';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { MessageFolderImportPolicy } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { MessageFolderPendingSyncAction } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { ImapClientProvider } from 'src/modules/messaging/message-import-manager/drivers/imap/providers/imap-client.provider';
import { ImapGetMessageListService } from 'src/modules/messaging/message-import-manager/drivers/imap/services/imap-get-message-list.service';
import { ImapMessageListFetchErrorHandler } from 'src/modules/messaging/message-import-manager/drivers/imap/services/imap-message-list-fetch-error-handler.service';
@@ -29,7 +31,7 @@ describe('ImapGetMessageListService', () => {
let imapClientProvider: ImapClientProvider;
const mockConnectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
ConnectedAccountEntity,
| 'provider'
| 'accessToken'
| 'refreshToken'
@@ -3,9 +3,9 @@ import { Injectable, Logger } from '@nestjs/common';
import { type ImapFlow } from 'imapflow';
import { isDefined } from 'twenty-shared/utils';
import { MessageFolderImportPolicy } from 'twenty-shared/types';
import { MessageFolder } from 'src/modules/messaging/message-folder-manager/interfaces/message-folder-driver.interface';
import { MessageFolderImportPolicy } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import {
MessageImportDriverException,
MessageImportDriverExceptionCode,
@@ -4,7 +4,7 @@ import { type ImapFlow } from 'imapflow';
import { Address, type Email as ParsedMail } from 'postal-mime';
import { MessageParticipantRole } from 'twenty-shared/types';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { computeMessageDirection } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/compute-message-direction.util';
import { ImapClientProvider } from 'src/modules/messaging/message-import-manager/drivers/imap/providers/imap-client.provider';
import { ImapMessageParserService } from 'src/modules/messaging/message-import-manager/drivers/imap/services/imap-message-parser.service';
@@ -17,7 +17,7 @@ import { formatAddressObjectAsParticipants } from 'src/modules/messaging/message
import { sanitizeString } from 'src/modules/messaging/message-import-manager/utils/sanitize-string.util';
type ConnectedAccount = Pick<
ConnectedAccountWorkspaceEntity,
ConnectedAccountEntity,
'id' | 'provider' | 'handle' | 'handleAliases' | 'connectionParameters'
>;
@@ -162,10 +162,7 @@ export class ImapGetMessagesService {
uid: number,
folderPath: string,
folderExternalId: string,
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
'handle' | 'handleAliases'
>,
connectedAccount: Pick<ConnectedAccountEntity, 'handle' | 'handleAliases'>,
): MessageWithParticipants {
const fromAddresses = this.extractAddresses(parsed.from);
const senderAddress = fromAddresses[0]?.address ?? '';
@@ -1,7 +1,7 @@
import { Injectable } from '@nestjs/common';
import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { type MicrosoftGraphBatchResponse } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.interface';
@Injectable()
@@ -13,7 +13,7 @@ export class MicrosoftFetchByBatchService {
async fetchAllByBatches(
messageIds: string[],
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
ConnectedAccountEntity,
'accessToken' | 'refreshToken' | 'id' | 'provider'
>,
): Promise<{
@@ -1,20 +1,18 @@
import { ConfigService } from '@nestjs/config';
import { Test, type TestingModule } from '@nestjs/testing';
import { ConnectedAccountProvider } from 'twenty-shared/types';
import {
ConnectedAccountProvider,
MessageFolderImportPolicy,
MessageFolderPendingSyncAction,
} from 'twenty-shared/types';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty-config.module';
import { MicrosoftOAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/drivers/microsoft/microsoft-oauth2-client-manager.service';
import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import {
MessageChannelWorkspaceEntity,
MessageFolderImportPolicy,
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import {
MessageFolderPendingSyncAction,
MessageFolderWorkspaceEntity,
} from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
import { microsoftGraphWithMessagesDeltaLink } from 'src/modules/messaging/message-import-manager/drivers/microsoft/mocks/microsoft-api-examples';
import { MessageFolderName } from 'src/modules/messaging/message-import-manager/drivers/microsoft/types/folders';
@@ -26,7 +24,7 @@ const accessToken = 'replace-with-your-access-token';
const refreshToken = 'replace-with-your-refresh-token';
const syncCursor = `replace-with-your-sync-cursor`;
const mockConnectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
ConnectedAccountEntity,
| 'provider'
| 'accessToken'
| 'refreshToken'
@@ -43,7 +41,7 @@ const mockConnectedAccount: Pick<
};
const mockMessageChannel: Pick<
MessageChannelWorkspaceEntity,
MessageChannelEntity,
'id' | 'syncCursor' | 'messageFolderImportPolicy'
> = {
id: 'message-channel-id',
@@ -178,7 +176,7 @@ xdescribe('Microsoft dev tests : get message list service', () => {
xdescribe('Microsoft dev tests : get message list service for folders', () => {
let service: MicrosoftGetMessageListService;
const inboxFolder = new MessageFolderWorkspaceEntity();
const inboxFolder = new MessageFolderEntity();
inboxFolder.id = 'inbox-folder-id';
inboxFolder.name = MessageFolderName.INBOX;
@@ -186,7 +184,7 @@ xdescribe('Microsoft dev tests : get message list service for folders', () => {
inboxFolder.messageChannelId = 'message-channel-1';
inboxFolder.parentFolderId = null;
const sentFolder = new MessageFolderWorkspaceEntity();
const sentFolder = new MessageFolderEntity();
sentFolder.id = 'sent-folder-id';
sentFolder.name = MessageFolderName.SENT_ITEMS;
@@ -194,7 +192,7 @@ xdescribe('Microsoft dev tests : get message list service for folders', () => {
sentFolder.messageChannelId = 'message-channel-1';
sentFolder.parentFolderId = null;
const otherFolder = new MessageFolderWorkspaceEntity();
const otherFolder = new MessageFolderEntity();
otherFolder.id = 'other-folder-id';
otherFolder.name = 'other';
@@ -202,7 +200,7 @@ xdescribe('Microsoft dev tests : get message list service for folders', () => {
otherFolder.messageChannelId = 'message-channel-2';
otherFolder.parentFolderId = null;
const messageChannelNoFolders = new MessageChannelWorkspaceEntity();
const messageChannelNoFolders = new MessageChannelEntity();
messageChannelNoFolders.id = 'message-channel-0';
messageChannelNoFolders.messageFolders = [];
@@ -210,7 +208,7 @@ xdescribe('Microsoft dev tests : get message list service for folders', () => {
messageChannelNoFolders.messageFolderImportPolicy =
MessageFolderImportPolicy.SELECTED_FOLDERS;
const messageChannelMicrosoftOneFolder = new MessageChannelWorkspaceEntity();
const messageChannelMicrosoftOneFolder = new MessageChannelEntity();
messageChannelMicrosoftOneFolder.id = 'message-channel-1';
messageChannelMicrosoftOneFolder.messageFolders = [inboxFolder];
@@ -218,7 +216,7 @@ xdescribe('Microsoft dev tests : get message list service for folders', () => {
messageChannelMicrosoftOneFolder.messageFolderImportPolicy =
MessageFolderImportPolicy.SELECTED_FOLDERS;
const messageChannelMicrosoft = new MessageChannelWorkspaceEntity();
const messageChannelMicrosoft = new MessageChannelEntity();
messageChannelMicrosoft.id = 'message-channel-2';
messageChannelMicrosoft.messageFolders = [inboxFolder, sentFolder];
@@ -1,13 +1,15 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { ConnectedAccountProvider } from 'twenty-shared/types';
import {
ConnectedAccountProvider,
MessageFolderImportPolicy,
MessageFolderPendingSyncAction,
} from 'twenty-shared/types';
import { type MessageFolder } from 'src/modules/messaging/message-folder-manager/interfaces/message-folder-driver.interface';
import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { MessageFolderImportPolicy } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { MessageFolderPendingSyncAction } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { MicrosoftGetMessageListService } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-message-list.service';
import { MicrosoftMessageListFetchErrorHandler } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-message-list-fetch-error-handler.service';
@@ -28,7 +30,7 @@ describe('MicrosoftGetMessageListService', () => {
let oAuth2ClientManagerService: OAuth2ClientManagerService;
const mockConnectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
ConnectedAccountEntity,
| 'provider'
| 'accessToken'
| 'refreshToken'
@@ -8,10 +8,10 @@ import {
import { isNonEmptyString } from '@sniptt/guards';
import pLimit from 'p-limit';
import { MessageFolderImportPolicy } from 'twenty-shared/types';
import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { MessageFolderImportPolicy } 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';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { type MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
import { MicrosoftMessageListFetchErrorHandler } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-message-list-fetch-error-handler.service';
import { type GetMessageListsArgs } from 'src/modules/messaging/message-import-manager/types/get-message-lists-args.type';
import {
@@ -72,11 +72,11 @@ export class MicrosoftGetMessageListService {
public async getMessageList(
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
ConnectedAccountEntity,
'provider' | 'accessToken' | 'id'
>,
messageFolder: Pick<
MessageFolderWorkspaceEntity,
MessageFolderEntity,
'name' | 'syncCursor' | 'externalId'
>,
): Promise<GetOneMessageListResponse> {
@@ -47,7 +47,7 @@ xdescribe('Microsoft dev tests : get messages service', () => {
id: 'connected-account-id',
provider: ConnectedAccountProvider.MICROSOFT,
handle: 'John.Walker@outlook.fr',
handleAliases: '',
handleAliases: [] as string[],
accessToken: accessToken,
refreshToken: refreshToken,
};
@@ -69,7 +69,7 @@ describe('Microsoft get messages service', () => {
accessToken: 'access-token',
refreshToken: 'refresh-token',
handle: 'John.l@outlook.fr',
handleAliases: '',
handleAliases: [] as string[],
};
const messages = service.formatBatchResponsesAsMessages(
batchResponses,
@@ -170,7 +170,7 @@ describe('Microsoft get messages service', () => {
accessToken: 'access-token',
refreshToken: 'refresh-token',
handle: 'John.l@outlook.fr',
handleAliases: '',
handleAliases: [] as string[],
};
const messages = service.formatBatchResponsesAsMessages(
batchResponses,
@@ -4,7 +4,7 @@ import { type EmailAddress } from 'addressparser';
import { MessageParticipantRole } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { MessageDirection } from 'src/modules/messaging/common/enums/message-direction.enum';
import { computeMessageDirection } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/compute-message-direction.util';
import { MicrosoftImportDriverException } from 'src/modules/messaging/message-import-manager/drivers/microsoft/exceptions/microsoft-import-driver.exception';
@@ -17,7 +17,7 @@ import { MicrosoftFetchByBatchService } from './microsoft-fetch-by-batch.service
import { MicrosoftMessagesImportErrorHandler } from './microsoft-messages-import-error-handler.service';
type ConnectedAccountType = Pick<
ConnectedAccountWorkspaceEntity,
ConnectedAccountEntity,
| 'accessToken'
| 'refreshToken'
| 'id'
@@ -6,7 +6,7 @@ import { isDefined } from 'twenty-shared/utils';
import type SMTPConnection from 'nodemailer/lib/smtp-connection';
import { SecureHttpClientService } from 'src/engine/core-modules/secure-http-client/secure-http-client.service';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
@Injectable()
export class SmtpClientProvider {
@@ -16,7 +16,7 @@ export class SmtpClientProvider {
public async getSmtpClient(
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
ConnectedAccountEntity,
'connectionParameters' | 'handle'
>,
): Promise<Transporter> {
@@ -1,14 +1,14 @@
import { type Provider } from '@nestjs/common';
import { Test, type TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import {
MessageChannelSyncStage,
MessageChannelSyncStatus,
type MessageChannelWorkspaceEntity,
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
} from 'twenty-shared/types';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { MessagingRelaunchFailedMessageChannelJob } from 'src/modules/messaging/message-import-manager/jobs/messaging-relaunch-failed-message-channel.job';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
describe('MessagingRelaunchFailedMessageChannelJob', () => {
let job: MessagingRelaunchFailedMessageChannelJob;
@@ -33,7 +33,7 @@ describe('MessagingRelaunchFailedMessageChannelJob', () => {
},
},
{
provide: MessageChannelDataAccessService,
provide: getRepositoryToken(MessageChannelEntity),
useValue: {
findOne: mockFindOne,
update: mockUpdate,
@@ -56,13 +56,12 @@ describe('MessagingRelaunchFailedMessageChannelJob', () => {
throttleFailureCount: 5,
throttleRetryAfter: '2026-03-19T06:49:34.295Z',
syncStageStartedAt: '2026-03-19T06:34:34.000Z',
} as Partial<MessageChannelWorkspaceEntity>);
} as unknown as Partial<MessageChannelEntity>);
await job.handle({ workspaceId, messageChannelId });
expect(mockUpdate).toHaveBeenCalledWith(
workspaceId,
{ id: messageChannelId },
{ id: messageChannelId, workspaceId },
{
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING,
syncStatus: MessageChannelSyncStatus.ACTIVE,
@@ -1,23 +1,23 @@
import { Scope } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { MessageChannelSyncStage } from 'twenty-shared/types';
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 { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.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 { isThrottled } from 'src/modules/connected-account/utils/is-throttled';
import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service';
import {
MessageChannelSyncStage,
type MessageChannelWorkspaceEntity,
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import {
MessageImportExceptionHandlerService,
MessageImportSyncStep,
} from 'src/modules/messaging/message-import-manager/services/messaging-import-exception-handler.service';
import { MessagingMessageListFetchService } from 'src/modules/messaging/message-import-manager/services/messaging-message-list-fetch.service';
import { MessagingMonitoringService } from 'src/modules/messaging/monitoring/services/messaging-monitoring.service';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
const toIsoStringOrNull = (
value: string | Date | null | undefined,
@@ -43,7 +43,8 @@ export class MessagingMessageListFetchJob {
private readonly messagingMessageListFetchService: MessagingMessageListFetchService,
private readonly messagingMonitoringService: MessagingMonitoringService,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly messageChannelDataAccessService: MessageChannelDataAccessService,
@InjectRepository(MessageChannelEntity)
private readonly messageChannelRepository: Repository<MessageChannelEntity>,
private readonly messageImportErrorHandlerService: MessageImportExceptionHandlerService,
private readonly messageChannelSyncStatusService: MessageChannelSyncStatusService,
) {}
@@ -61,15 +62,13 @@ export class MessagingMessageListFetchJob {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
const messageChannel = await this.messageChannelDataAccessService.findOne(
workspaceId,
{
where: {
id: messageChannelId,
},
relations: ['connectedAccount', 'messageFolders'],
const messageChannel = await this.messageChannelRepository.findOne({
where: {
id: messageChannelId,
workspaceId,
},
);
relations: { connectedAccount: true, messageFolders: true },
});
if (!messageChannel) {
await this.messagingMonitoringService.track({
@@ -113,7 +112,7 @@ export class MessagingMessageListFetchJob {
});
await this.messagingMessageListFetchService.processMessageListFetch(
messageChannel as unknown as MessageChannelWorkspaceEntity,
messageChannel,
workspaceId,
);
@@ -127,7 +126,7 @@ export class MessagingMessageListFetchJob {
await this.messageImportErrorHandlerService.handleDriverException(
error,
MessageImportSyncStep.MESSAGE_LIST_FETCH,
messageChannel as unknown as MessageChannelWorkspaceEntity,
messageChannel,
workspaceId,
);
}
@@ -1,19 +1,19 @@
import { Scope } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { MessageChannelSyncStage } from 'twenty-shared/types';
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 { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.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 { isThrottled } from 'src/modules/connected-account/utils/is-throttled';
import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service';
import {
MessageChannelSyncStage,
type MessageChannelWorkspaceEntity,
} 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';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
const toIsoStringOrNull = (
value: string | Date | null | undefined,
@@ -40,7 +40,8 @@ export class MessagingMessagesImportJob {
private readonly messagingMonitoringService: MessagingMonitoringService,
private readonly messageChannelSyncStatusService: MessageChannelSyncStatusService,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly messageChannelDataAccessService: MessageChannelDataAccessService,
@InjectRepository(MessageChannelEntity)
private readonly messageChannelRepository: Repository<MessageChannelEntity>,
) {}
@Process(MessagingMessagesImportJob.name)
@@ -56,15 +57,13 @@ export class MessagingMessagesImportJob {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
const messageChannel = await this.messageChannelDataAccessService.findOne(
workspaceId,
{
where: {
id: messageChannelId,
},
relations: ['connectedAccount', 'messageFolders'],
const messageChannel = await this.messageChannelRepository.findOne({
where: {
id: messageChannelId,
workspaceId,
},
);
relations: { connectedAccount: true, messageFolders: true },
});
if (!messageChannel) {
await this.messagingMonitoringService.track({
@@ -103,12 +102,9 @@ export class MessagingMessagesImportJob {
return;
}
const messageChannelWorkspace =
messageChannel as unknown as MessageChannelWorkspaceEntity;
await this.messagingMessagesImportService.processMessageBatchImport(
messageChannelWorkspace,
messageChannelWorkspace.connectedAccount,
messageChannel,
messageChannel.connectedAccount,
workspaceId,
);
}, authContext);
@@ -1,15 +1,16 @@
import { Logger, Scope } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { In } from 'typeorm';
import { In, Repository } from 'typeorm';
import { MessageChannelSyncStage } from 'twenty-shared/types';
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 { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
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 } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { isSyncStale } from 'src/modules/messaging/message-import-manager/utils/is-sync-stale.util';
const toIsoStringOrNull = (
@@ -34,7 +35,8 @@ export class MessagingOngoingStaleJob {
private readonly logger = new Logger(MessagingOngoingStaleJob.name);
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly messageChannelDataAccessService: MessageChannelDataAccessService,
@InjectRepository(MessageChannelEntity)
private readonly messageChannelRepository: Repository<MessageChannelEntity>,
private readonly messageChannelSyncStatusService: MessageChannelSyncStatusService,
) {}
@@ -45,17 +47,17 @@ export class MessagingOngoingStaleJob {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
const messageChannels = await this.messageChannelDataAccessService.find(
workspaceId,
{
const messageChannels = await this.messageChannelRepository.find({
where: {
syncStage: In([
MessageChannelSyncStage.MESSAGES_IMPORT_ONGOING,
MessageChannelSyncStage.MESSAGE_LIST_FETCH_ONGOING,
MessageChannelSyncStage.MESSAGES_IMPORT_SCHEDULED,
MessageChannelSyncStage.MESSAGE_LIST_FETCH_SCHEDULED,
]),
workspaceId,
},
);
});
for (const messageChannel of messageChannels) {
if (isSyncStale(toIsoStringOrNull(messageChannel.syncStageStartedAt))) {
@@ -1,15 +1,18 @@
import { Scope } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } 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 { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.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 {
MessageChannelSyncStage,
MessageChannelSyncStatus,
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
} from 'twenty-shared/types';
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 { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
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 MessagingRelaunchFailedMessageChannelJobData = {
workspaceId: string;
@@ -23,7 +26,8 @@ export type MessagingRelaunchFailedMessageChannelJobData = {
export class MessagingRelaunchFailedMessageChannelJob {
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly messageChannelDataAccessService: MessageChannelDataAccessService,
@InjectRepository(MessageChannelEntity)
private readonly messageChannelRepository: Repository<MessageChannelEntity>,
) {}
@Process(MessagingRelaunchFailedMessageChannelJob.name)
@@ -33,14 +37,12 @@ export class MessagingRelaunchFailedMessageChannelJob {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
const messageChannel = await this.messageChannelDataAccessService.findOne(
workspaceId,
{
where: {
id: messageChannelId,
},
const messageChannel = await this.messageChannelRepository.findOne({
where: {
id: messageChannelId,
workspaceId,
},
);
});
if (
!messageChannel ||
@@ -50,9 +52,8 @@ export class MessagingRelaunchFailedMessageChannelJob {
return;
}
await this.messageChannelDataAccessService.update(
workspaceId,
{ id: messageChannelId },
await this.messageChannelRepository.update(
{ id: messageChannelId, workspaceId },
{
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING,
syncStatus: MessageChannelSyncStatus.ACTIVE,
@@ -1,42 +0,0 @@
import { Injectable } from '@nestjs/common';
import { type ObjectRecordDeleteEvent } from 'twenty-shared/database-events';
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 { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type';
import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import {
MessagingCleanCacheJob,
type MessagingCleanCacheJobData,
} from 'src/modules/messaging/message-import-manager/jobs/messaging-clean-cache';
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';
@Injectable()
export class MessagingMessageImportManagerMessageChannelListener {
constructor(
@InjectMessageQueue(MessageQueue.messagingQueue)
private readonly messageQueueService: MessageQueueService,
) {}
@OnDatabaseBatchEvent('messageChannel', DatabaseEventAction.DESTROYED)
async handleDestroyedEvent(
payload: WorkspaceEventBatch<
ObjectRecordDeleteEvent<MessageChannelWorkspaceEntity>
>,
) {
await Promise.all(
payload.events.map((eventPayload) =>
this.messageQueueService.add<MessagingCleanCacheJobData>(
MessagingCleanCacheJob.name,
{
workspaceId: payload.workspaceId,
messageChannelId: eventPayload.recordId,
},
),
),
);
}
}
@@ -2,10 +2,11 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
import { MessageChannelDataAccessModule } from 'src/engine/metadata-modules/message-channel/data-access/message-channel-data-access.module';
import { MessageFolderDataAccessModule } from 'src/engine/metadata-modules/message-folder/data-access/message-folder-data-access.module';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { WorkspaceEventEmitterModule } from 'src/engine/workspace-event-emitter/workspace-event-emitter.module';
@@ -36,7 +37,6 @@ import { MessagingMessageListFetchJob } from 'src/modules/messaging/message-impo
import { MessagingMessagesImportJob } from 'src/modules/messaging/message-import-manager/jobs/messaging-messages-import.job';
import { MessagingOngoingStaleJob } from 'src/modules/messaging/message-import-manager/jobs/messaging-ongoing-stale.job';
import { MessagingRelaunchFailedMessageChannelJob } from 'src/modules/messaging/message-import-manager/jobs/messaging-relaunch-failed-message-channel.job';
import { MessagingMessageImportManagerMessageChannelListener } from 'src/modules/messaging/message-import-manager/listeners/messaging-import-manager-message-channel.listener';
import { MessagingAccountAuthenticationService } from 'src/modules/messaging/message-import-manager/services/messaging-account-authentication.service';
import { MessagingCursorService } from 'src/modules/messaging/message-import-manager/services/messaging-cursor.service';
import { MessagingDeleteFolderMessagesService } from 'src/modules/messaging/message-import-manager/services/messaging-delete-folder-messages.service';
@@ -63,12 +63,13 @@ import { MessagingMonitoringModule } from 'src/modules/messaging/monitoring/mess
MessagingIMAPDriverModule,
MessagingSmtpDriverModule,
MessagingCommonModule,
MessageChannelDataAccessModule,
MessageFolderDataAccessModule,
TypeOrmModule.forFeature([
WorkspaceEntity,
DataSourceEntity,
ObjectMetadataEntity,
MessageChannelEntity,
MessageFolderEntity,
UserWorkspaceEntity,
]),
EmailAliasManagerModule,
FeatureFlagModule,
@@ -95,7 +96,6 @@ import { MessagingMonitoringModule } from 'src/modules/messaging/monitoring/mess
MessagingOngoingStaleCronJob,
MessagingRelaunchFailedMessageChannelsCronJob,
MessagingAddSingleMessageToCacheForImportJob,
MessagingMessageImportManagerMessageChannelListener,
MessagingCleanCacheJob,
MessagingMessageService,
MessagingMessageFolderAssociationService,
@@ -1,19 +1,15 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { ConnectedAccountProvider } from 'twenty-shared/types';
import {
ConnectedAccountProvider,
MessageFolderImportPolicy,
MessageFolderPendingSyncAction,
} 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 { 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 {
MessageFolderImportPolicy,
type MessageChannelWorkspaceEntity,
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import {
MessageFolderPendingSyncAction,
type MessageFolderWorkspaceEntity,
} from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
import { MessagingMessageCleanerService } from 'src/modules/messaging/message-cleaner/services/messaging-message-cleaner.service';
import { SyncMessageFoldersService } from 'src/modules/messaging/message-folder-manager/services/sync-message-folders.service';
import { MessagingAccountAuthenticationService } from 'src/modules/messaging/message-import-manager/services/messaging-account-authentication.service';
@@ -24,7 +20,8 @@ import { MessagingMessageListFetchService } from 'src/modules/messaging/message-
import { MessagingMessagesImportService } from 'src/modules/messaging/message-import-manager/services/messaging-messages-import.service';
import { MessagingProcessFolderActionsService } from 'src/modules/messaging/message-import-manager/services/messaging-process-folder-actions.service';
import { MessagingProcessGroupEmailActionsService } from 'src/modules/messaging/message-import-manager/services/messaging-process-group-email-actions.service';
import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import { getRepositoryToken } from '@nestjs/typeorm';
describe('MessagingMessageListFetchService', () => {
let messagingMessageListFetchService: MessagingMessageListFetchService;
@@ -34,8 +31,8 @@ describe('MessagingMessageListFetchService', () => {
let globalWorkspaceOrmManager: GlobalWorkspaceOrmManager;
let messagingCursorService: MessagingCursorService;
let mockMicrosoftMessageChannel: MessageChannelWorkspaceEntity;
let mockGoogleMessageChannel: MessageChannelWorkspaceEntity;
let mockMicrosoftMessageChannel: MessageChannelEntity;
let mockGoogleMessageChannel: MessageChannelEntity;
const workspaceId = 'workspace-id';
@@ -48,7 +45,7 @@ describe('MessagingMessageListFetchService', () => {
handle: 'test@microsoft.com',
accessToken: 'old-microsoft-access-token',
refreshToken: 'microsoft-refresh-token',
handleAliases: '',
handleAliases: [] as string[],
},
messageFolders: [
{
@@ -56,10 +53,10 @@ describe('MessagingMessageListFetchService', () => {
name: 'inbox',
syncCursor: 'inbox-sync-cursor',
messageChannelId: 'microsoft-message-channel-id',
} as MessageFolderWorkspaceEntity,
},
],
messageFolderImportPolicy: MessageFolderImportPolicy.SELECTED_FOLDERS,
} as MessageChannelWorkspaceEntity;
} as unknown as MessageChannelEntity;
mockGoogleMessageChannel = {
id: 'google-message-channel-id',
@@ -69,12 +66,12 @@ describe('MessagingMessageListFetchService', () => {
handle: 'test@gmail.com',
accessToken: 'old-google-access-token',
refreshToken: 'google-refresh-token',
handleAliases: '',
handleAliases: [] as string[],
},
syncCursor: 'google-sync-cursor',
messageFolders: [],
messageFolderImportPolicy: MessageFolderImportPolicy.SELECTED_FOLDERS,
} as unknown as MessageChannelWorkspaceEntity;
} as unknown as MessageChannelEntity;
});
beforeEach(async () => {
@@ -231,7 +228,7 @@ describe('MessagingMessageListFetchService', () => {
},
},
{
provide: MessageChannelDataAccessService,
provide: getRepositoryToken(MessageChannelEntity),
useValue: {
findOne: jest.fn().mockResolvedValue(undefined),
},
@@ -1,7 +1,11 @@
import { Logger, type Provider } from '@nestjs/common';
import { Test, type TestingModule } from '@nestjs/testing';
import { ConnectedAccountProvider } from 'twenty-shared/types';
import {
ConnectedAccountProvider,
MessageChannelSyncStage,
MessageFolderImportPolicy,
} 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';
@@ -9,21 +13,19 @@ import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspac
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';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service';
import {
MessageChannelSyncStage,
MessageFolderImportPolicy,
type MessageChannelWorkspaceEntity,
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { MESSAGING_GMAIL_USERS_MESSAGES_GET_BATCH_SIZE } from 'src/modules/messaging/message-import-manager/drivers/gmail/constants/messaging-gmail-users-messages-get-batch-size.constant';
import { MessagingAccountAuthenticationService } from 'src/modules/messaging/message-import-manager/services/messaging-account-authentication.service';
import { MessagingGetMessagesService } from 'src/modules/messaging/message-import-manager/services/messaging-get-messages.service';
import { MessageImportExceptionHandlerService } from 'src/modules/messaging/message-import-manager/services/messaging-import-exception-handler.service';
import { MessagingMessagesImportService } from 'src/modules/messaging/message-import-manager/services/messaging-messages-import.service';
import { MessagingSaveMessagesAndEnqueueContactCreationService } from 'src/modules/messaging/message-import-manager/services/messaging-save-messages-and-enqueue-contact-creation.service';
import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service';
import { getRepositoryToken } from '@nestjs/typeorm';
import { MessagingMonitoringService } from 'src/modules/messaging/monitoring/services/messaging-monitoring.service';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
describe('MessagingMessagesImportService', () => {
let service: MessagingMessagesImportService;
@@ -35,7 +37,7 @@ describe('MessagingMessagesImportService', () => {
const workspaceId = 'workspace-id';
let mockMessageChannel: Pick<
MessageChannelWorkspaceEntity,
MessageChannelEntity,
| 'id'
| 'syncStage'
| 'connectedAccountId'
@@ -43,7 +45,7 @@ describe('MessagingMessagesImportService', () => {
| 'messageFolders'
| 'messageFolderImportPolicy'
>;
let mockConnectedAccount: ConnectedAccountWorkspaceEntity;
let mockConnectedAccount: ConnectedAccountEntity;
let providersBase: Provider[];
beforeEach(async () => {
@@ -53,9 +55,9 @@ describe('MessagingMessagesImportService', () => {
handle: 'test@gmail.com',
refreshToken: 'refresh-token',
accessToken: 'old-access-token',
accountOwnerId: 'account-owner-id',
handleAliases: 'alias1@gmail.com,alias2@gmail.com',
} as ConnectedAccountWorkspaceEntity;
userWorkspaceId: 'user-workspace-id',
handleAliases: ['alias1@gmail.com', 'alias2@gmail.com'],
} as ConnectedAccountEntity;
mockMessageChannel = {
id: 'message-channel-id',
@@ -114,7 +116,9 @@ describe('MessagingMessagesImportService', () => {
{
provide: EmailAliasManagerService,
useValue: {
refreshHandleAliases: jest.fn().mockResolvedValue(undefined),
refreshHandleAliases: jest
.fn()
.mockResolvedValue(['alias1@gmail.com', 'alias2@gmail.com']),
},
},
{
@@ -122,6 +126,10 @@ describe('MessagingMessagesImportService', () => {
useValue: {
getRepository: jest.fn().mockResolvedValue({
update: jest.fn().mockResolvedValue(undefined),
findOne: jest.fn().mockResolvedValue({
id: 'workspace-member-id',
userId: 'user-id',
}),
}),
executeInWorkspaceContext: jest
.fn()
@@ -129,7 +137,7 @@ describe('MessagingMessagesImportService', () => {
},
},
{
provide: MessageChannelDataAccessService,
provide: getRepositoryToken(MessageChannelEntity),
useValue: {
update: jest.fn().mockResolvedValue(undefined),
},
@@ -169,6 +177,12 @@ describe('MessagingMessagesImportService', () => {
provide: MessagingAccountAuthenticationService,
useClass: MessagingAccountAuthenticationService,
},
{
provide: getRepositoryToken(UserWorkspaceEntity),
useValue: {
findOne: jest.fn().mockResolvedValue({ userId: 'user-id' }),
},
},
];
const module: TestingModule = await Test.createTestingModule({
providers: [
@@ -219,7 +233,7 @@ describe('MessagingMessagesImportService', () => {
expect(
service.processMessageBatchImport(
mockMessageChannel as MessageChannelWorkspaceEntity,
mockMessageChannel as MessageChannelEntity,
mockConnectedAccount,
workspaceId,
),
@@ -228,7 +242,7 @@ describe('MessagingMessagesImportService', () => {
it('should process message batch import successfully', async () => {
await service.processMessageBatchImport(
mockMessageChannel as MessageChannelWorkspaceEntity,
mockMessageChannel as MessageChannelEntity,
mockConnectedAccount,
workspaceId,
);
@@ -312,7 +326,7 @@ describe('MessagingMessagesImportService', () => {
);
await service.processMessageBatchImport(
mockMessageChannel as MessageChannelWorkspaceEntity,
mockMessageChannel as MessageChannelEntity,
mockConnectedAccount,
workspaceId,
);
@@ -8,7 +8,7 @@ import {
ConnectedAccountRefreshTokensService,
type ConnectedAccountTokens,
} from 'src/modules/connected-account/refresh-tokens-manager/services/connected-account-refresh-tokens.service';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import {
MessageImportDriverException,
MessageImportDriverExceptionCode,
@@ -16,7 +16,7 @@ import {
import { MessagingMonitoringService } from 'src/modules/messaging/monitoring/services/messaging-monitoring.service';
interface ValidateAndRefreshConnectedAccountAuthenticationParams {
connectedAccount: ConnectedAccountWorkspaceEntity;
connectedAccount: ConnectedAccountEntity;
workspaceId: string;
messageChannelId: string;
}
@@ -1,21 +1,25 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service';
import { MessageFolderDataAccessService } from 'src/engine/metadata-modules/message-folder/data-access/services/message-folder-data-access.service';
import { Repository } from 'typeorm';
import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
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 { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
@Injectable()
export class MessagingCursorService {
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly messageChannelDataAccessService: MessageChannelDataAccessService,
private readonly messageFolderDataAccessService: MessageFolderDataAccessService,
@InjectRepository(MessageChannelEntity)
private readonly messageChannelRepository: Repository<MessageChannelEntity>,
@InjectRepository(MessageFolderEntity)
private readonly messageFolderRepository: Repository<MessageFolderEntity>,
) {}
public async updateCursor(
messageChannel: MessageChannelWorkspaceEntity,
messageChannel: MessageChannelEntity,
nextSyncCursor: string,
workspaceId: string,
folderId?: string,
@@ -24,11 +28,8 @@ export class MessagingCursorService {
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
if (!folderId) {
await this.messageChannelDataAccessService.update(
workspaceId,
{
id: messageChannel.id,
},
await this.messageChannelRepository.update(
{ id: messageChannel.id, workspaceId },
{
throttleFailureCount: 0,
throttleRetryAfter: null,
@@ -41,20 +42,14 @@ export class MessagingCursorService {
},
);
} else {
await this.messageFolderDataAccessService.update(
workspaceId,
{
id: folderId,
},
await this.messageFolderRepository.update(
{ id: folderId, workspaceId },
{
syncCursor: nextSyncCursor,
},
);
await this.messageChannelDataAccessService.update(
workspaceId,
{
id: messageChannel.id,
},
await this.messageChannelRepository.update(
{ id: messageChannel.id, workspaceId },
{
throttleFailureCount: 0,
throttleRetryAfter: null,
@@ -3,12 +3,12 @@ import { Injectable, Logger } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { In } from 'typeorm';
import { type MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
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 MessageChannelMessageAssociationMessageFolderWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association-message-folder.workspace-entity';
import { type MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity';
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';
import { type MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
import { MessagingMessageCleanerService } from 'src/modules/messaging/message-cleaner/services/messaging-message-cleaner.service';
const BATCH_SIZE = 200;
@@ -26,8 +26,8 @@ export class MessagingDeleteFolderMessagesService {
async deleteFolderMessages(
workspaceId: string,
messageChannel: MessageChannelWorkspaceEntity,
messageFolder: MessageFolderWorkspaceEntity,
messageChannel: MessageChannelEntity,
messageFolder: MessageFolderEntity,
): Promise<number> {
this.logger.log(
`WorkspaceId: ${workspaceId}, MessageChannelId: ${messageChannel.id}, FolderId: ${messageFolder.id} - Deleting messages from folder: ${messageFolder.name}`,
@@ -2,9 +2,9 @@ import { Injectable } from '@nestjs/common';
import { ConnectedAccountProvider } from 'twenty-shared/types';
import { type MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import { type MessageFolder } from 'src/modules/messaging/message-folder-manager/interfaces/message-folder-driver.interface';
import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import {
MessageImportDriverException,
MessageImportDriverExceptionCode,
@@ -23,7 +23,7 @@ export class MessagingGetMessageListService {
) {}
public async getMessageLists(
messageChannel: MessageChannelWorkspaceEntity,
messageChannel: MessageChannelEntity,
messageFolders: MessageFolder[],
): Promise<GetMessageListsResponse> {
switch (messageChannel.connectedAccount.provider) {
@@ -2,8 +2,8 @@ import { Injectable } from '@nestjs/common';
import { ConnectedAccountProvider } from 'twenty-shared/types';
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';
import { type MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import {
MessageImportDriverException,
MessageImportDriverExceptionCode,
@@ -26,18 +26,18 @@ export class MessagingGetMessagesService {
public async getMessages(
messageIds: string[],
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
ConnectedAccountEntity,
| 'provider'
| 'accessToken'
| 'refreshToken'
| 'id'
| 'handle'
| 'handleAliases'
| 'accountOwnerId'
| 'userWorkspaceId'
| 'connectionParameters'
>,
messageChannel: Pick<
MessageChannelWorkspaceEntity,
MessageChannelEntity,
'messageFolders' | 'messageFolderImportPolicy'
>,
): Promise<GetMessagesResponse> {
@@ -1,24 +1,23 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { MessageChannelSyncStatus } from 'twenty-shared/types';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import {
type TwentyORMException,
TwentyORMExceptionCode,
} from 'src/engine/twenty-orm/exceptions/twenty-orm.exception';
import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service';
import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service';
import {
MessageChannelSyncStatus,
type MessageChannelWorkspaceEntity,
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { MESSAGING_THROTTLE_MAX_ATTEMPTS } from 'src/modules/messaging/message-import-manager/constants/messaging-throttle-max-attempts';
import {
MessageImportDriverException,
MessageImportDriverExceptionCode,
} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception';
import { MessageNetworkExceptionCode } from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-network.exception';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
export enum MessageImportSyncStep {
MESSAGE_LIST_FETCH = 'MESSAGE_LIST_FETCH',
@@ -29,7 +28,8 @@ export enum MessageImportSyncStep {
@Injectable()
export class MessageImportExceptionHandlerService {
constructor(
private readonly messageChannelDataAccessService: MessageChannelDataAccessService,
@InjectRepository(MessageChannelEntity)
private readonly messageChannelRepository: Repository<MessageChannelEntity>,
private readonly messageChannelSyncStatusService: MessageChannelSyncStatusService,
private readonly exceptionHandlerService: ExceptionHandlerService,
) {}
@@ -37,10 +37,7 @@ export class MessageImportExceptionHandlerService {
public async handleDriverException(
exception: MessageImportDriverException | Error | TwentyORMException,
syncStep: MessageImportSyncStep,
messageChannel: Pick<
MessageChannelWorkspaceEntity,
'id' | 'throttleFailureCount'
>,
messageChannel: Pick<MessageChannelEntity, 'id' | 'throttleFailureCount'>,
workspaceId: string,
): Promise<void> {
if (exception instanceof MessageImportDriverException) {
@@ -105,7 +102,7 @@ export class MessageImportExceptionHandlerService {
}
private async handleSyncCursorErrorException(
messageChannel: Pick<MessageChannelWorkspaceEntity, 'id'>,
messageChannel: Pick<MessageChannelEntity, 'id'>,
workspaceId: string,
): Promise<void> {
await this.messageChannelSyncStatusService.resetAndMarkAsMessagesListFetchPending(
@@ -116,10 +113,7 @@ export class MessageImportExceptionHandlerService {
private async handleTemporaryException(
syncStep: MessageImportSyncStep,
messageChannel: Pick<
MessageChannelWorkspaceEntity,
'id' | 'throttleFailureCount'
>,
messageChannel: Pick<MessageChannelEntity, 'id' | 'throttleFailureCount'>,
workspaceId: string,
exception: { message: string },
): Promise<void> {
@@ -151,9 +145,8 @@ export class MessageImportExceptionHandlerService {
return;
}
await this.messageChannelDataAccessService.increment(
workspaceId,
{ id: messageChannel.id },
await this.messageChannelRepository.increment(
{ id: messageChannel.id, workspaceId },
'throttleFailureCount',
1,
);
@@ -163,9 +156,8 @@ export class MessageImportExceptionHandlerService {
? exception.throttleRetryAfter
: undefined;
await this.messageChannelDataAccessService.update(
workspaceId,
{ id: messageChannel.id },
await this.messageChannelRepository.update(
{ id: messageChannel.id, workspaceId },
{
throttleRetryAfter: isDefined(throttleRetryAfter)
? throttleRetryAfter.toISOString()
@@ -197,7 +189,7 @@ export class MessageImportExceptionHandlerService {
}
private async handleInsufficientPermissionsException(
messageChannel: Pick<MessageChannelWorkspaceEntity, 'id'>,
messageChannel: Pick<MessageChannelEntity, 'id'>,
workspaceId: string,
): Promise<void> {
await this.messageChannelSyncStatusService.markAsFailed(
@@ -209,7 +201,7 @@ export class MessageImportExceptionHandlerService {
private async handleUnknownException(
exception: Error,
messageChannel: Pick<MessageChannelWorkspaceEntity, 'id'>,
messageChannel: Pick<MessageChannelEntity, 'id'>,
workspaceId: string,
): Promise<void> {
this.exceptionHandlerService.captureExceptions([exception], {
@@ -223,7 +215,7 @@ export class MessageImportExceptionHandlerService {
}
private async handlePermanentException(
messageChannel: Pick<MessageChannelWorkspaceEntity, 'id'>,
messageChannel: Pick<MessageChannelEntity, 'id'>,
workspaceId: string,
): Promise<void> {
await this.messageChannelSyncStatusService.markAsFailed(
@@ -235,7 +227,7 @@ export class MessageImportExceptionHandlerService {
private async handleNotFoundException(
syncStep: MessageImportSyncStep,
messageChannel: Pick<MessageChannelWorkspaceEntity, 'id'>,
messageChannel: Pick<MessageChannelEntity, 'id'>,
workspaceId: string,
): Promise<void> {
if (syncStep === MessageImportSyncStep.MESSAGE_LIST_FETCH) {
@@ -1,24 +1,23 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isNonEmptyString } from '@sniptt/guards';
import chunk from 'lodash.chunk';
import { isDefined } from 'twenty-shared/utils';
import { In, MoreThanOrEqual } from 'typeorm';
import { In, MoreThanOrEqual, Repository } from 'typeorm';
import {
MessageChannelPendingGroupEmailsAction,
MessageChannelSyncStage,
MessageFolderPendingSyncAction,
} from 'twenty-shared/types';
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 { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.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 { 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 {
MessageChannelPendingGroupEmailsAction,
MessageChannelSyncStage,
MessageChannelWorkspaceEntity,
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { MessageFolderPendingSyncAction } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
import { MessagingMessageCleanerService } from 'src/modules/messaging/message-cleaner/services/messaging-message-cleaner.service';
import { SyncMessageFoldersService } from 'src/modules/messaging/message-folder-manager/services/sync-message-folders.service';
import { MessagingAccountAuthenticationService } from 'src/modules/messaging/message-import-manager/services/messaging-account-authentication.service';
@@ -31,6 +30,7 @@ import {
import { MessagingMessagesImportService } from 'src/modules/messaging/message-import-manager/services/messaging-messages-import.service';
import { MessagingProcessFolderActionsService } from 'src/modules/messaging/message-import-manager/services/messaging-process-folder-actions.service';
import { MessagingProcessGroupEmailActionsService } from 'src/modules/messaging/message-import-manager/services/messaging-process-group-email-actions.service';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
const ONE_WEEK_IN_MILLISECONDS = 7 * 24 * 60 * 60 * 1000;
@@ -42,7 +42,8 @@ export class MessagingMessageListFetchService {
private readonly cacheStorage: CacheStorageService,
private readonly messageChannelSyncStatusService: MessageChannelSyncStatusService,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly messageChannelDataAccessService: MessageChannelDataAccessService,
@InjectRepository(MessageChannelEntity)
private readonly messageChannelRepository: Repository<MessageChannelEntity>,
private readonly messagingGetMessageListService: MessagingGetMessageListService,
private readonly messageImportErrorHandlerService: MessageImportExceptionHandlerService,
private readonly messagingMessageCleanerService: MessagingMessageCleanerService,
@@ -55,7 +56,7 @@ export class MessagingMessageListFetchService {
) {}
public async processMessageListFetch(
messageChannel: MessageChannelWorkspaceEntity,
messageChannel: MessageChannelEntity,
workspaceId: string,
) {
const authContext = buildSystemAuthContext(workspaceId);
@@ -82,11 +83,12 @@ export class MessagingMessageListFetchService {
const freshMessageChannel =
pendingGroupEmailActionsProcessed || pendingFolderActionsProcessed
? await this.messageChannelDataAccessService.findOne(workspaceId, {
? await this.messageChannelRepository.findOne({
where: {
id: messageChannel.id,
workspaceId,
},
relations: ['connectedAccount', 'messageFolders'],
relations: { connectedAccount: true, messageFolders: true },
})
: messageChannel;
@@ -297,7 +299,7 @@ export class MessagingMessageListFetchService {
}
private async processPendingGroupEmailActions(
messageChannel: MessageChannelWorkspaceEntity,
messageChannel: MessageChannelEntity,
workspaceId: string,
): Promise<boolean> {
const hasPendingGroupEmailAction =
@@ -323,7 +325,7 @@ export class MessagingMessageListFetchService {
}
private async processPendingFolderActions(
messageChannel: MessageChannelWorkspaceEntity,
messageChannel: MessageChannelEntity,
workspaceId: string,
): Promise<boolean> {
const foldersWithPendingActions = messageChannel.messageFolders.filter(
@@ -350,7 +352,7 @@ export class MessagingMessageListFetchService {
}
private async computeFullSyncMessageChannelMessageAssociationsToDelete(
messageChannel: Pick<MessageChannelWorkspaceEntity, 'id'>,
messageChannel: Pick<MessageChannelEntity, 'id'>,
messageExternalIds: string[],
workspaceId: string,
) {
@@ -1,23 +1,23 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { MessageChannelSyncStage } from 'twenty-shared/types';
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 { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
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';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service';
import {
MessageChannelSyncStage,
type MessageChannelWorkspaceEntity,
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import {
MessageImportDriverException,
MessageImportDriverExceptionCode,
@@ -32,6 +32,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';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
@Injectable()
export class MessagingMessagesImportService {
@@ -47,15 +48,18 @@ export class MessagingMessagesImportService {
private readonly blocklistRepository: BlocklistRepository,
private readonly emailAliasManagerService: EmailAliasManagerService,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly messageChannelDataAccessService: MessageChannelDataAccessService,
@InjectRepository(MessageChannelEntity)
private readonly messageChannelRepository: Repository<MessageChannelEntity>,
private readonly messagingGetMessagesService: MessagingGetMessagesService,
private readonly messageImportErrorHandlerService: MessageImportExceptionHandlerService,
private readonly messagingAccountAuthenticationService: MessagingAccountAuthenticationService,
@InjectRepository(UserWorkspaceEntity)
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
) {}
async processMessageBatchImport(
messageChannel: MessageChannelWorkspaceEntity,
connectedAccount: ConnectedAccountWorkspaceEntity,
messageChannel: MessageChannelEntity,
connectedAccount: ConnectedAccountEntity,
workspaceId: string,
) {
let messageIdsToFetch: string[] = [];
@@ -98,10 +102,13 @@ export class MessagingMessagesImportService {
refreshToken,
};
await this.emailAliasManagerService.refreshHandleAliases(
connectedAccountWithFreshTokens,
workspaceId,
);
const refreshedHandleAliases =
await this.emailAliasManagerService.refreshHandleAliases(
connectedAccountWithFreshTokens,
workspaceId,
);
connectedAccountWithFreshTokens.handleAliases = refreshedHandleAliases;
messageIdsToFetch = await this.cacheStorage.setPop(
`messages-to-import:${workspaceId}:${messageChannel.id}`,
@@ -145,10 +152,30 @@ export class MessagingMessagesImportService {
.filter(isDefined);
}
const blocklist = await this.blocklistRepository.getByWorkspaceMemberId(
connectedAccountWithFreshTokens.accountOwnerId,
workspaceId,
);
const userWorkspace = await this.userWorkspaceRepository.findOne({
where: {
id: connectedAccountWithFreshTokens.userWorkspaceId,
},
});
const workspaceMemberRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
);
const workspaceMember = userWorkspace
? await workspaceMemberRepository.findOne({
where: { userId: userWorkspace.userId },
})
: null;
const blocklist = workspaceMember
? await this.blocklistRepository.getByWorkspaceMemberId(
workspaceMember.id,
workspaceId,
)
: [];
if (!isDefined(messageChannel.handle)) {
throw new MessageImportDriverException(
@@ -166,7 +193,7 @@ export class MessagingMessagesImportService {
const messagesToSave = filterEmails(
messageChannel.handle,
[...connectedAccountWithFreshTokens.handleAliases.split(',')],
[...connectedAccountWithFreshTokens.handleAliases],
allMessages,
blocklist
.map((blocklistItem) => blocklistItem.handle)
@@ -198,11 +225,8 @@ export class MessagingMessagesImportService {
);
}
await this.messageChannelDataAccessService.update(
workspaceId,
{
id: messageChannel.id,
},
await this.messageChannelRepository.update(
{ id: messageChannel.id, workspaceId },
{
throttleFailureCount: 0,
throttleRetryAfter: null,
@@ -239,7 +263,7 @@ export class MessagingMessagesImportService {
}
private async trackMessageImportCompleted(
messageChannel: MessageChannelWorkspaceEntity,
messageChannel: MessageChannelEntity,
workspaceId: string,
) {
await this.messagingMonitoringService.track({
@@ -1,16 +1,14 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'twenty-shared/utils';
import { In } from 'typeorm';
import { In, Repository } from 'typeorm';
import { MessageFolderDataAccessService } from 'src/engine/metadata-modules/message-folder/data-access/services/message-folder-data-access.service';
import { type MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
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,
MessageFolderWorkspaceEntity,
} from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
import { MessageFolderPendingSyncAction } from 'twenty-shared/types';
import { MessagingDeleteFolderMessagesService } from 'src/modules/messaging/message-import-manager/services/messaging-delete-folder-messages.service';
@Injectable()
@@ -21,13 +19,14 @@ export class MessagingProcessFolderActionsService {
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly messageFolderDataAccessService: MessageFolderDataAccessService,
@InjectRepository(MessageFolderEntity)
private readonly messageFolderRepository: Repository<MessageFolderEntity>,
private readonly messagingDeleteFolderMessagesService: MessagingDeleteFolderMessagesService,
) {}
async processFolderActions(
messageChannel: MessageChannelWorkspaceEntity,
messageFolders: MessageFolderWorkspaceEntity[],
messageChannel: MessageChannelEntity,
messageFolders: MessageFolderEntity[],
workspaceId: string,
): Promise<void> {
const foldersWithPendingActions = messageFolders.filter(
@@ -93,9 +92,8 @@ export class MessagingProcessFolderActionsService {
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
async () => {
if (processedFolderIds.length > 0) {
await this.messageFolderDataAccessService.update(
workspaceId,
{ id: In(processedFolderIds) },
await this.messageFolderRepository.update(
{ id: In(processedFolderIds), workspaceId },
{ pendingSyncAction: MessageFolderPendingSyncAction.NONE },
);
@@ -105,8 +103,9 @@ export class MessagingProcessFolderActionsService {
}
if (folderIdsToDelete.length > 0) {
await this.messageFolderDataAccessService.delete(workspaceId, {
await this.messageFolderRepository.delete({
id: In(folderIdsToDelete),
workspaceId,
});
this.logger.log(
@@ -1,17 +1,15 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service';
import { MessageFolderDataAccessService } from 'src/engine/metadata-modules/message-folder/data-access/services/message-folder-data-access.service';
import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
import { MessageChannelPendingGroupEmailsAction } from 'twenty-shared/types';
import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
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,
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { MessagingDeleteGroupEmailMessagesService } from 'src/modules/messaging/message-import-manager/services/messaging-delete-group-email-messages.service';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
@Injectable()
export class MessagingProcessGroupEmailActionsService {
@@ -21,25 +19,22 @@ export class MessagingProcessGroupEmailActionsService {
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly messageChannelDataAccessService: MessageChannelDataAccessService,
private readonly messageFolderDataAccessService: MessageFolderDataAccessService,
@InjectRepository(MessageChannelEntity)
private readonly messageChannelRepository: Repository<MessageChannelEntity>,
@InjectRepository(MessageFolderEntity)
private readonly messageFolderRepository: Repository<MessageFolderEntity>,
private readonly messagingDeleteGroupEmailMessagesService: MessagingDeleteGroupEmailMessagesService,
) {}
async markMessageChannelAsPendingGroupEmailsAction(
messageChannel: MessageChannelWorkspaceEntity,
messageChannel: MessageChannelEntity,
workspaceId: string,
pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction,
): Promise<void> {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
await this.messageChannelDataAccessService.update(
workspaceId,
{ id: messageChannel.id },
{ pendingGroupEmailsAction },
);
}, authContext);
await this.messageChannelRepository.update(
{ id: messageChannel.id, workspaceId },
{ pendingGroupEmailsAction },
);
this.logger.debug(
`WorkspaceId: ${workspaceId}, MessageChannelId: ${messageChannel.id} - Marked message channel as pending group emails action: ${pendingGroupEmailsAction}`,
@@ -47,7 +42,7 @@ export class MessagingProcessGroupEmailActionsService {
}
async processGroupEmailActions(
messageChannel: MessageChannelWorkspaceEntity,
messageChannel: MessageChannelEntity,
workspaceId: string,
): Promise<void> {
const { pendingGroupEmailsAction } = messageChannel;
@@ -65,70 +60,56 @@ export class MessagingProcessGroupEmailActionsService {
const authContext = buildSystemAuthContext(workspaceId);
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(async () => {
const workspaceDataSource =
await this.globalWorkspaceOrmManager.getGlobalWorkspaceDataSource();
await workspaceDataSource?.transaction(
async (transactionManager: WorkspaceEntityManager) => {
try {
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 this.messageChannelDataAccessService.update(
workspaceId,
{ id: messageChannel.id },
{
pendingGroupEmailsAction:
MessageChannelPendingGroupEmailsAction.NONE,
},
transactionManager,
);
this.logger.debug(
`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;
try {
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
async () => {
switch (pendingGroupEmailsAction) {
case MessageChannelPendingGroupEmailsAction.GROUP_EMAILS_DELETION:
await this.handleGroupEmailsDeletion(
workspaceId,
messageChannel.id,
);
break;
case MessageChannelPendingGroupEmailsAction.GROUP_EMAILS_IMPORT:
await this.handleGroupEmailsImport(
workspaceId,
messageChannel.id,
);
break;
}
},
authContext,
);
}, authContext);
await this.messageChannelRepository.update(
{ id: messageChannel.id, workspaceId },
{
pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction.NONE,
},
);
this.logger.debug(
`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 as Error).stack,
);
throw error;
}
}
private async handleGroupEmailsDeletion(
workspaceId: string,
messageChannelId: string,
transactionManager: WorkspaceEntityManager,
): Promise<void> {
await this.messagingDeleteGroupEmailMessagesService.deleteGroupEmailMessages(
workspaceId,
messageChannelId,
);
await this.resetCursors({
workspaceId,
messageChannelId,
transactionManager,
});
await this.resetCursors(workspaceId, messageChannelId);
this.logger.debug(
`WorkspaceId: ${workspaceId}, MessageChannelId: ${messageChannelId} - Completed GROUP_EMAILS_DELETION action`,
@@ -138,42 +119,23 @@ export class MessagingProcessGroupEmailActionsService {
private async handleGroupEmailsImport(
workspaceId: string,
messageChannelId: string,
transactionManager: WorkspaceEntityManager,
): Promise<void> {
await this.resetCursors({
workspaceId,
messageChannelId,
transactionManager,
});
await this.resetCursors(workspaceId, messageChannelId);
this.logger.debug(
`WorkspaceId: ${workspaceId}, MessageChannelId: ${messageChannelId} - Completed GROUP_EMAILS_IMPORT action`,
);
}
private async resetCursors({
workspaceId,
messageChannelId,
transactionManager,
}: {
workspaceId: string;
messageChannelId: string;
transactionManager: WorkspaceEntityManager;
}) {
await this.messageChannelDataAccessService.update(
workspaceId,
{ id: messageChannelId },
{
syncCursor: '',
},
transactionManager,
private async resetCursors(workspaceId: string, messageChannelId: string) {
await this.messageChannelRepository.update(
{ id: messageChannelId, workspaceId },
{ syncCursor: '' },
);
await this.messageFolderDataAccessService.update(
workspaceId,
{ messageChannelId },
await this.messageFolderRepository.update(
{ messageChannelId, workspaceId },
{ syncCursor: '' },
transactionManager,
);
}
}
@@ -1,20 +1,21 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { FieldActorSource, MessageParticipantRole } from 'twenty-shared/types';
import {
FieldActorSource,
MessageChannelContactAutoCreationPolicy,
MessageParticipantRole,
} from 'twenty-shared/types';
import { type MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
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 { 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 { 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 { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.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';
import {
MessageChannelContactAutoCreationPolicy,
type MessageChannelWorkspaceEntity,
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { MessagingMessageFolderAssociationService } from 'src/modules/messaging/message-import-manager/services/messaging-message-folder-association.service';
import { MessagingMessageService } from 'src/modules/messaging/message-import-manager/services/messaging-message.service';
import { MessagingSaveMessagesAndEnqueueContactCreationService } from 'src/modules/messaging/message-import-manager/services/messaging-save-messages-and-enqueue-contact-creation.service';
@@ -31,20 +32,20 @@ describe('MessagingSaveMessagesAndEnqueueContactCreationService', () => {
const workspaceId = 'workspace-id';
const mockConnectedAccount: ConnectedAccountWorkspaceEntity = {
const mockConnectedAccount: ConnectedAccountEntity = {
id: 'connected-account-id',
handle: 'test@example.com',
handleAliases: 'alias1@example.com,alias2@example.com',
} as ConnectedAccountWorkspaceEntity;
handleAliases: ['alias1@example.com', 'alias2@example.com'],
} as ConnectedAccountEntity;
const mockMessageChannel: MessageChannelWorkspaceEntity = {
const mockMessageChannel: MessageChannelEntity = {
id: 'message-channel-id',
isContactAutoCreationEnabled: true,
contactAutoCreationPolicy:
MessageChannelContactAutoCreationPolicy.SENT_AND_RECEIVED,
excludeNonProfessionalEmails: true,
excludeGroupEmails: true,
} as MessageChannelWorkspaceEntity;
} as MessageChannelEntity;
const mockMessages: MessageWithParticipants[] = [
{
@@ -1,23 +1,24 @@
import { Injectable } from '@nestjs/common';
import { FieldActorSource, MessageParticipantRole } from 'twenty-shared/types';
import {
FieldActorSource,
MessageChannelContactAutoCreationPolicy,
MessageParticipantRole,
} from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
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 { 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 { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import {
CreateCompanyAndContactJob,
type CreateCompanyAndContactJobData,
} from 'src/modules/contact-creation-manager/jobs/create-company-and-contact.job';
import {
MessageChannelContactAutoCreationPolicy,
type MessageChannelWorkspaceEntity,
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import {
type Participant,
type ParticipantWithMessageId,
@@ -44,11 +45,11 @@ export class MessagingSaveMessagesAndEnqueueContactCreationService {
async saveMessagesAndEnqueueContactCreation(
messagesToSave: MessageWithParticipants[],
messageChannel: MessageChannelWorkspaceEntity,
connectedAccount: ConnectedAccountWorkspaceEntity,
messageChannel: MessageChannelEntity,
connectedAccount: ConnectedAccountEntity,
workspaceId: string,
) {
const handleAliases = connectedAccount.handleAliases?.split(',') || [];
const handleAliases = connectedAccount.handleAliases || [];
const authContext = buildSystemAuthContext(workspaceId);
const participantsWithMessageId =
@@ -1,15 +1,15 @@
import { type MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import { type MessageFolder } from 'src/modules/messaging/message-folder-manager/interfaces/message-folder-driver.interface';
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';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
export type GetMessageListsArgs = {
messageChannel: Pick<
MessageChannelWorkspaceEntity,
MessageChannelEntity,
'syncCursor' | 'id' | 'messageFolderImportPolicy'
>;
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
ConnectedAccountEntity,
| 'provider'
| 'accessToken'
| 'refreshToken'
@@ -7,7 +7,7 @@ import { isDefined } from 'twenty-shared/utils';
import { type MessageOutboundDriver } from 'src/modules/messaging/message-outbound-manager/interfaces/message-outbound-driver.interface';
import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { mimeEncode } from 'src/modules/messaging/message-import-manager/utils/mime-encode.util';
import { type SendMessageInput } from 'src/modules/messaging/message-outbound-manager/types/send-message-input.type';
import { toMailComposerOptions } from 'src/modules/messaging/message-outbound-manager/utils/to-mail-composer-options.util';
@@ -20,7 +20,7 @@ export class GmailMessageOutboundService implements MessageOutboundDriver {
async sendMessage(
sendMessageInput: SendMessageInput,
connectedAccount: ConnectedAccountWorkspaceEntity,
connectedAccount: ConnectedAccountEntity,
): Promise<void> {
const { gmailClient, encodedMessage } = await this.composeGmailMessage(
connectedAccount,
@@ -37,7 +37,7 @@ export class GmailMessageOutboundService implements MessageOutboundDriver {
async createDraft(
sendMessageInput: SendMessageInput,
connectedAccount: ConnectedAccountWorkspaceEntity,
connectedAccount: ConnectedAccountEntity,
): Promise<void> {
const { gmailClient, encodedMessage } = await this.composeGmailMessage(
connectedAccount,
@@ -55,7 +55,7 @@ export class GmailMessageOutboundService implements MessageOutboundDriver {
}
private async composeGmailMessage(
connectedAccount: ConnectedAccountWorkspaceEntity,
connectedAccount: ConnectedAccountEntity,
sendMessageInput: SendMessageInput,
): Promise<{
gmailClient: gmail_v1.Gmail;
@@ -1,11 +1,15 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import MailComposer from 'nodemailer/lib/mail-composer';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { type MessageOutboundDriver } from 'src/modules/messaging/message-outbound-manager/interfaces/message-outbound-driver.interface';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { ImapClientProvider } from 'src/modules/messaging/message-import-manager/drivers/imap/providers/imap-client.provider';
import { ImapFindDraftsFolderService } from 'src/modules/messaging/message-import-manager/drivers/imap/services/imap-find-drafts-folder.service';
import { SmtpClientProvider } from 'src/modules/messaging/message-import-manager/drivers/smtp/providers/smtp-client.provider';
@@ -18,13 +22,17 @@ export class ImapSmtpMessageOutboundService implements MessageOutboundDriver {
private readonly smtpClientProvider: SmtpClientProvider,
private readonly imapClientProvider: ImapClientProvider,
private readonly imapFindDraftsFolderService: ImapFindDraftsFolderService,
@InjectRepository(MessageChannelEntity)
private readonly messageChannelRepository: Repository<MessageChannelEntity>,
@InjectRepository(MessageFolderEntity)
private readonly messageFolderRepository: Repository<MessageFolderEntity>,
) {}
async sendMessage(
sendMessageInput: SendMessageInput,
connectedAccount: ConnectedAccountWorkspaceEntity,
connectedAccount: ConnectedAccountEntity,
): Promise<void> {
const { handle, connectionParameters, messageChannels } = connectedAccount;
const { handle, connectionParameters } = connectedAccount;
const smtpClient =
await this.smtpClientProvider.getSmtpClient(connectedAccount);
@@ -48,13 +56,23 @@ export class ImapSmtpMessageOutboundService implements MessageOutboundDriver {
const imapClient =
await this.imapClientProvider.getClient(connectedAccount);
const messageChannel = messageChannels.find(
(channel) => channel.handle === handle,
);
const messageChannel = await this.messageChannelRepository.findOne({
where: {
connectedAccountId: connectedAccount.id,
handle: handle,
},
});
const sentFolder = messageChannel?.messageFolders.find(
(messageFolder) => messageFolder.isSentFolder,
);
let sentFolder: MessageFolderEntity | null = null;
if (isDefined(messageChannel)) {
sentFolder = await this.messageFolderRepository.findOne({
where: {
messageChannelId: messageChannel.id,
isSentFolder: true,
},
});
}
if (isDefined(sentFolder) && isDefined(sentFolder.name)) {
await imapClient.append(sentFolder.name, messageBuffer);
@@ -66,7 +84,7 @@ export class ImapSmtpMessageOutboundService implements MessageOutboundDriver {
async createDraft(
sendMessageInput: SendMessageInput,
connectedAccount: ConnectedAccountWorkspaceEntity,
connectedAccount: ConnectedAccountEntity,
): Promise<void> {
const { handle, connectionParameters } = connectedAccount;
@@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common';
import { type MessageOutboundDriver } from 'src/modules/messaging/message-outbound-manager/interfaces/message-outbound-driver.interface';
import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { toMicrosoftRecipients } from 'src/modules/messaging/message-import-manager/utils/to-microsoft-recipients.util';
import { type SendMessageInput } from 'src/modules/messaging/message-outbound-manager/types/send-message-input.type';
import { type Client as MicrosoftGraphClient } from '@microsoft/microsoft-graph-client';
@@ -17,7 +17,7 @@ export class MicrosoftMessageOutboundService implements MessageOutboundDriver {
async sendMessage(
sendMessageInput: SendMessageInput,
connectedAccount: ConnectedAccountWorkspaceEntity,
connectedAccount: ConnectedAccountEntity,
): Promise<void> {
const microsoftClient =
await this.oAuth2ClientManagerService.getMicrosoftOAuth2Client(
@@ -34,7 +34,7 @@ export class MicrosoftMessageOutboundService implements MessageOutboundDriver {
async createDraft(
sendMessageInput: SendMessageInput,
connectedAccount: ConnectedAccountWorkspaceEntity,
connectedAccount: ConnectedAccountEntity,
): Promise<void> {
const microsoftClient =
await this.oAuth2ClientManagerService.getMicrosoftOAuth2Client(
@@ -1,14 +1,14 @@
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { type SendMessageInput } from 'src/modules/messaging/message-outbound-manager/types/send-message-input.type';
export type MessageOutboundDriver = {
sendMessage(
sendMessageInput: SendMessageInput,
connectedAccount: ConnectedAccountWorkspaceEntity,
connectedAccount: ConnectedAccountEntity,
): Promise<void>;
createDraft(
sendMessageInput: SendMessageInput,
connectedAccount: ConnectedAccountWorkspaceEntity,
connectedAccount: ConnectedAccountEntity,
): Promise<void>;
};
@@ -1,5 +1,8 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
import { OAuth2ClientManagerModule } from 'src/modules/connected-account/oauth2-client-manager/oauth2-client-manager.module';
import { MessagingIMAPDriverModule } from 'src/modules/messaging/message-import-manager/drivers/imap/messaging-imap-driver.module';
import { MessagingSmtpDriverModule } from 'src/modules/messaging/message-import-manager/drivers/smtp/messaging-smtp-driver.module';
@@ -13,6 +16,7 @@ import { MessagingMessageOutboundService } from 'src/modules/messaging/message-o
OAuth2ClientManagerModule,
MessagingIMAPDriverModule,
MessagingSmtpDriverModule,
TypeOrmModule.forFeature([MessageChannelEntity, MessageFolderEntity]),
],
providers: [
GmailMessageOutboundService,
@@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common';
import { ConnectedAccountProvider } from 'twenty-shared/types';
import { assertUnreachable } from 'twenty-shared/utils';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
import { GmailMessageOutboundService } from 'src/modules/messaging/message-outbound-manager/drivers/gmail/services/gmail-message-outbound.service';
import { ImapSmtpMessageOutboundService } from 'src/modules/messaging/message-outbound-manager/drivers/imap/services/imap-smtp-message-outbound.service';
import { MicrosoftMessageOutboundService } from 'src/modules/messaging/message-outbound-manager/drivers/microsoft/services/microsoft-message-outbound.service';
@@ -19,7 +19,7 @@ export class MessagingMessageOutboundService {
public async sendMessage(
sendMessageInput: SendMessageInput,
connectedAccount: ConnectedAccountWorkspaceEntity,
connectedAccount: ConnectedAccountEntity,
): Promise<void> {
switch (connectedAccount.provider) {
case ConnectedAccountProvider.GOOGLE:
@@ -52,7 +52,7 @@ export class MessagingMessageOutboundService {
public async createDraft(
sendMessageInput: SendMessageInput,
connectedAccount: ConnectedAccountWorkspaceEntity,
connectedAccount: ConnectedAccountEntity,
): Promise<void> {
switch (connectedAccount.provider) {
case ConnectedAccountProvider.GOOGLE:
@@ -10,7 +10,7 @@ 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 { MessageChannelDataAccessService } from 'src/engine/metadata-modules/message-channel/data-access/services/message-channel-data-access.service';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
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 { MessagingMonitoringService } from 'src/modules/messaging/monitoring/services/messaging-monitoring.service';
@@ -25,7 +25,8 @@ export class MessagingMessageChannelSyncStatusMonitoringCronJob {
private readonly workspaceRepository: Repository<WorkspaceEntity>,
private readonly messagingMonitoringService: MessagingMonitoringService,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly messageChannelDataAccessService: MessageChannelDataAccessService,
@InjectRepository(MessageChannelEntity)
private readonly messageChannelRepository: Repository<MessageChannelEntity>,
private readonly exceptionHandlerService: ExceptionHandlerService,
) {}
@@ -52,13 +53,10 @@ export class MessagingMessageChannelSyncStatusMonitoringCronJob {
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
async () => {
const messageChannels =
await this.messageChannelDataAccessService.findMany(
activeWorkspace.id,
{
select: ['id', 'syncStatus', 'connectedAccountId'],
},
);
const messageChannels = await this.messageChannelRepository.find({
select: ['id', 'syncStatus', 'connectedAccountId'],
where: { workspaceId: activeWorkspace.id },
});
for (const messageChannel of messageChannels) {
if (!messageChannel.syncStatus) {
@@ -4,7 +4,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
import { MessageChannelDataAccessModule } from 'src/engine/metadata-modules/message-channel/data-access/message-channel-data-access.module';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import { MessagingCommonModule } from 'src/modules/messaging/common/messaging-common.module';
import { MessagingMessageChannelSyncStatusMonitoringCronCommand } from 'src/modules/messaging/monitoring/crons/commands/messaging-message-channel-sync-status-monitoring.cron.command';
import { MessagingMessageChannelSyncStatusMonitoringCronJob } from 'src/modules/messaging/monitoring/crons/jobs/messaging-message-channel-sync-status-monitoring.cron.job';
@@ -13,10 +13,12 @@ import { MessagingMonitoringService } from 'src/modules/messaging/monitoring/ser
@Module({
imports: [
AuditModule,
MessageChannelDataAccessModule,
MessagingCommonModule,
TypeOrmModule.forFeature([WorkspaceEntity]),
TypeOrmModule.forFeature([DataSourceEntity]),
TypeOrmModule.forFeature([
WorkspaceEntity,
DataSourceEntity,
MessageChannelEntity,
]),
],
providers: [
MessagingMessageChannelSyncStatusMonitoringCronCommand,