Compare commits

...

2 Commits

Author SHA1 Message Date
neo773 00d629d7ce changes 2026-02-25 11:41:05 +05:30
neo773 9362a0d8c7 gmail backfill historical messages on folder toggle 2026-02-25 11:29:49 +05:30
12 changed files with 630 additions and 13 deletions
@@ -0,0 +1,218 @@
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import { type FieldMetadataComplexOption } from 'twenty-shared/types';
import { DataSource, Repository } from 'typeorm';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
import { getMetadataRelatedMetadataNames } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-related-metadata-names.util';
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { type WorkspaceCacheKeyName } from 'src/engine/workspace-cache/types/workspace-cache-key.type';
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
import { computeTwentyStandardApplicationAllFlatEntityMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant';
import {
escapeIdentifier,
escapeLiteral,
} from 'src/engine/workspace-manager/workspace-migration/utils/remove-sql-injection.util';
const MESSAGE_FOLDER_PENDING_SYNC_ACTION_ENUM_NAME =
'messageFolder_pendingSyncAction_enum';
const FOLDER_IMPORT_ENUM_VALUE = 'FOLDER_IMPORT';
const MESSAGE_FOLDER_PENDING_SYNC_ACTION_FIELD_UNIVERSAL_IDENTIFIER =
STANDARD_OBJECTS.messageFolder.fields.pendingSyncAction.universalIdentifier;
@Command({
name: 'upgrade:1-19:add-folder-import-to-message-folder-pending-sync-action',
description:
'Add FOLDER_IMPORT to messageFolder.pendingSyncAction enum and metadata options',
})
export class AddFolderImportToMessageFolderPendingSyncActionCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
@InjectDataSource()
private readonly coreDataSource: DataSource,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
private readonly applicationService: ApplicationService,
private readonly workspaceCacheService: WorkspaceCacheService,
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
) {
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
override async runOnWorkspace({
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
const dryRun = options?.dryRun ?? false;
this.logger.log(
`${dryRun ? '[DRY RUN] ' : ''}Adding FOLDER_IMPORT to messageFolder.pendingSyncAction in workspace ${workspaceId}`,
);
if (dryRun) {
this.logger.log(
`[DRY RUN] Would add FOLDER_IMPORT enum value and field metadata option in workspace ${workspaceId}. Skipping.`,
);
return;
}
const queryRunner = this.coreDataSource.createQueryRunner();
await queryRunner.connect();
try {
const schemaName = getWorkspaceSchemaName(workspaceId);
const rows = await queryRunner.query(
`SELECT "id", "options"
FROM core."fieldMetadata"
WHERE "workspaceId" = $1
AND "universalIdentifier" = $2
LIMIT 1`,
[
workspaceId,
MESSAGE_FOLDER_PENDING_SYNC_ACTION_FIELD_UNIVERSAL_IDENTIFIER,
],
);
const fieldMetadata = rows[0];
if (!fieldMetadata) {
this.logger.warn(
`pendingSyncAction field metadata not found for workspace ${workspaceId}, skipping upgrade`,
);
return;
}
const currentOptions: FieldMetadataComplexOption[] = Array.isArray(
fieldMetadata.options,
)
? fieldMetadata.options
: [];
const alreadyHasFolderImport = currentOptions.some(
(option) => option?.value === FOLDER_IMPORT_ENUM_VALUE,
);
if (alreadyHasFolderImport) {
this.logger.log(
`FOLDER_IMPORT already present for workspace ${workspaceId}`,
);
return;
}
await queryRunner.query(
`ALTER TYPE ${escapeIdentifier(schemaName)}.${escapeIdentifier(MESSAGE_FOLDER_PENDING_SYNC_ACTION_ENUM_NAME)} ADD VALUE IF NOT EXISTS ${escapeLiteral(FOLDER_IMPORT_ENUM_VALUE)}`,
);
const folderImportOption =
await this.getFolderImportOptionFromStandardMetadata(workspaceId);
const maxPosition = currentOptions.reduce(
(max, option) =>
Math.max(
max,
typeof option?.position === 'number' ? option.position : 0,
),
0,
);
const updatedOptions: FieldMetadataComplexOption[] = [
...currentOptions,
{ ...folderImportOption, position: maxPosition + 1 },
];
await queryRunner.query(
`UPDATE core."fieldMetadata"
SET "options" = $1::jsonb
WHERE "id" = $2`,
[JSON.stringify(updatedOptions), fieldMetadata.id],
);
this.logger.log(
`Added FOLDER_IMPORT option to field metadata for workspace ${workspaceId}`,
);
await this.invalidateCaches(workspaceId);
} finally {
await queryRunner.release();
}
}
private async getFolderImportOptionFromStandardMetadata(
workspaceId: string,
): Promise<FieldMetadataComplexOption> {
const { twentyStandardFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
const { allFlatEntityMaps: standardAllFlatEntityMaps } =
computeTwentyStandardApplicationAllFlatEntityMaps({
now: new Date().toISOString(),
workspaceId,
twentyStandardApplicationId: twentyStandardFlatApplication.id,
});
const standardField = findFlatEntityByUniversalIdentifierOrThrow({
flatEntityMaps: standardAllFlatEntityMaps.flatFieldMetadataMaps,
universalIdentifier:
MESSAGE_FOLDER_PENDING_SYNC_ACTION_FIELD_UNIVERSAL_IDENTIFIER,
});
const folderImportOption = (
standardField.options as FieldMetadataComplexOption[]
)?.find((option) => option.value === FOLDER_IMPORT_ENUM_VALUE);
if (!folderImportOption) {
throw new Error(
`FOLDER_IMPORT option not found in standard metadata for workspace ${workspaceId}`,
);
}
return folderImportOption;
}
private async invalidateCaches(workspaceId: string): Promise<void> {
const modifiedMetadataNames = ['fieldMetadata'] as const;
const cacheKeysToInvalidate: WorkspaceCacheKeyName[] = [
...new Set(
modifiedMetadataNames
.flatMap((name) => [name, ...getMetadataRelatedMetadataNames(name)])
.map(getMetadataFlatEntityMapsKey),
),
'ORMEntityMetadatas',
];
await this.workspaceCacheService.invalidateAndRecompute(
workspaceId,
cacheKeysToInvalidate,
);
await this.workspaceMetadataVersionService.incrementMetadataVersion(
workspaceId,
);
await this.workspaceCacheStorageService.flush(workspaceId);
this.logger.log(
`Cache invalidated and metadata version incremented for workspace ${workspaceId}`,
);
}
}
@@ -1,6 +1,7 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AddFolderImportToMessageFolderPendingSyncActionCommand } from 'src/database/commands/upgrade-version-command/1-19/1-19-add-folder-import-to-message-folder-pending-sync-action.command';
import { AddMissingSystemFieldsToStandardObjectsCommand } from 'src/database/commands/upgrade-version-command/1-19/1-19-add-missing-system-fields-to-standard-objects.command';
import { BackfillMessageChannelMessageAssociationMessageFolderCommand } from 'src/database/commands/upgrade-version-command/1-19/1-19-backfill-message-channel-message-association-message-folder.command';
import { BackfillPageLayoutsCommand } from 'src/database/commands/upgrade-version-command/1-19/1-19-backfill-page-layouts.command';
@@ -30,12 +31,14 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
providers: [
BackfillSystemFieldsIsSystemCommand,
AddMissingSystemFieldsToStandardObjectsCommand,
AddFolderImportToMessageFolderPendingSyncActionCommand,
BackfillMessageChannelMessageAssociationMessageFolderCommand,
BackfillPageLayoutsCommand,
],
exports: [
BackfillSystemFieldsIsSystemCommand,
AddMissingSystemFieldsToStandardObjectsCommand,
AddFolderImportToMessageFolderPendingSyncActionCommand,
BackfillMessageChannelMessageAssociationMessageFolderCommand,
BackfillPageLayoutsCommand,
],
@@ -27,6 +27,7 @@ import { MigratePersonAvatarFilesCommand } from 'src/database/commands/upgrade-v
import { MigrateWorkflowSendEmailAttachmentsCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-migrate-workflow-send-email-attachments.command';
import { MigrateWorkspacePicturesCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-migrate-workspace-pictures.command';
import { AddMissingSystemFieldsToStandardObjectsCommand } from 'src/database/commands/upgrade-version-command/1-19/1-19-add-missing-system-fields-to-standard-objects.command';
import { AddFolderImportToMessageFolderPendingSyncActionCommand } from 'src/database/commands/upgrade-version-command/1-19/1-19-add-folder-import-to-message-folder-pending-sync-action.command';
import { BackfillMessageChannelMessageAssociationMessageFolderCommand } from 'src/database/commands/upgrade-version-command/1-19/1-19-backfill-message-channel-message-association-message-folder.command';
import { BackfillPageLayoutsCommand } from 'src/database/commands/upgrade-version-command/1-19/1-19-backfill-page-layouts.command';
import { BackfillSystemFieldsIsSystemCommand } from 'src/database/commands/upgrade-version-command/1-19/1-19-backfill-system-fields-is-system.command';
@@ -73,6 +74,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
// 1.19 Commands
protected readonly backfillSystemFieldsIsSystemCommand: BackfillSystemFieldsIsSystemCommand,
protected readonly addMissingSystemFieldsToStandardObjectsCommand: AddMissingSystemFieldsToStandardObjectsCommand,
protected readonly addFolderImportToMessageFolderPendingSyncActionCommand: AddFolderImportToMessageFolderPendingSyncActionCommand,
protected readonly backfillMessageChannelMessageAssociationMessageFolderCommand: BackfillMessageChannelMessageAssociationMessageFolderCommand,
protected readonly backfillPageLayoutsCommand: BackfillPageLayoutsCommand,
) {
@@ -113,6 +115,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
const commands_1190: VersionCommands = [
this.backfillSystemFieldsIsSystemCommand,
this.addMissingSystemFieldsToStandardObjectsCommand,
this.addFolderImportToMessageFolderPendingSyncActionCommand,
this.backfillMessageChannelMessageAssociationMessageFolderCommand,
this.backfillPageLayoutsCommand,
];
@@ -322,6 +322,13 @@ export const buildMessageFolderStandardFlatFieldMetadatas = ({
position: 1,
color: 'blue',
},
{
id: '20202020-5fb5-4a92-8dc0-1251676cad57',
value: 'FOLDER_IMPORT',
label: 'Folder import',
position: 2,
color: 'green',
},
],
},
standardObjectMetadataRelatedEntityIds,
@@ -0,0 +1,140 @@
import { Logger } from '@nestjs/common';
import { msg } from '@lingui/core/macro';
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
import { In } from 'typeorm';
import { type WorkspacePreQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { type UpdateManyResolverArgs } 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 AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
import { computePendingSyncActionForFolderUpdate } from 'src/modules/messaging/common/query-hooks/message/utils/compute-message-folder-update-payload.util';
import {
MessageChannelSyncStage,
MessageFolderImportPolicy,
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import {
type MessageFolderWorkspaceEntity,
MessageFolderPendingSyncAction,
} from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
@WorkspaceQueryHook(`messageFolder.updateMany`)
export class MessageFolderUpdateManyPreQueryHook
implements WorkspacePreQueryHookInstance
{
private readonly logger = new Logger(
MessageFolderUpdateManyPreQueryHook.name,
);
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
) {}
async execute(
authContext: AuthContext,
_objectName: string,
payload: UpdateManyResolverArgs<MessageFolderWorkspaceEntity>,
): Promise<UpdateManyResolverArgs<MessageFolderWorkspaceEntity>> {
if (!isDefined(payload.data.isSynced)) {
return payload;
}
const folderIds = payload.filter?.id?.in;
if (!Array.isArray(folderIds) || folderIds.length === 0) {
return payload;
}
const workspace = authContext.workspace;
assertIsDefinedOrThrow(workspace, WorkspaceNotFoundDefaultError);
const systemAuthContext = buildSystemAuthContext(workspace.id);
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
async () => {
const messageFolderRepository =
await this.globalWorkspaceOrmManager.getRepository<MessageFolderWorkspaceEntity>(
workspace.id,
'messageFolder',
);
const messageFolders = await messageFolderRepository.find({
where: { id: In(folderIds) },
relations: ['messageChannel'],
});
const foldersPendingActions = new Map<
string,
MessageFolderPendingSyncAction
>();
for (const folder of messageFolders) {
if (
folder.messageChannel.syncStage ===
MessageChannelSyncStage.PENDING_CONFIGURATION
) {
continue;
}
if (payload.data.isSynced === folder.isSynced) {
continue;
}
if (
folder.messageChannel.messageFolderImportPolicy ===
MessageFolderImportPolicy.ALL_FOLDERS
) {
throw new WorkspaceQueryRunnerException(
'Cannot toggle folder sync when import policy is ALL_FOLDERS',
WorkspaceQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
{
userFriendlyMessage: msg`Cannot toggle individual folder sync when all folders are synced.`,
},
);
}
const action = computePendingSyncActionForFolderUpdate(
folder,
payload.data.isSynced,
);
foldersPendingActions.set(folder.id, action);
}
if (foldersPendingActions.size > 0) {
const uniqueActions = new Set(foldersPendingActions.values());
if (uniqueActions.size > 1) {
throw new WorkspaceQueryRunnerException(
'Cannot update multiple folders with different pending sync actions in a single operation',
WorkspaceQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
{
userFriendlyMessage: msg`Cannot update folders with conflicting sync states in a single operation.`,
},
);
}
const [pendingSyncAction] = uniqueActions;
this.logger.log(
`Setting pendingSyncAction to ${pendingSyncAction} for ${foldersPendingActions.size} folders`,
);
payload.data.pendingSyncAction = pendingSyncAction;
}
return payload;
},
systemAuthContext,
);
}
}
@@ -0,0 +1,106 @@
import { WorkspaceQueryRunnerException } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception';
import { computePendingSyncActionForFolderUpdate } from 'src/modules/messaging/common/query-hooks/message/utils/compute-message-folder-update-payload.util';
import { MessageChannelSyncStage } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { MessageFolderPendingSyncAction } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
const buildFolder = (
overrides: {
isSynced?: boolean;
pendingSyncAction?: MessageFolderPendingSyncAction;
syncStage?: MessageChannelSyncStage;
} = {},
) => ({
isSynced: overrides.isSynced ?? false,
pendingSyncAction:
overrides.pendingSyncAction ?? MessageFolderPendingSyncAction.NONE,
messageChannel: {
syncStage:
overrides.syncStage ?? MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING,
},
});
describe('computePendingSyncActionForFolderUpdate', () => {
it('should return FOLDER_IMPORT when enabling sync', () => {
expect(computePendingSyncActionForFolderUpdate(buildFolder(), true)).toBe(
MessageFolderPendingSyncAction.FOLDER_IMPORT,
);
});
it('should throw when enabling with pending FOLDER_DELETION', () => {
expect(() =>
computePendingSyncActionForFolderUpdate(
buildFolder({
pendingSyncAction: MessageFolderPendingSyncAction.FOLDER_DELETION,
}),
true,
),
).toThrow(WorkspaceQueryRunnerException);
});
it('should cancel pending FOLDER_IMPORT when disabling sync', () => {
expect(
computePendingSyncActionForFolderUpdate(
buildFolder({
isSynced: true,
pendingSyncAction: MessageFolderPendingSyncAction.FOLDER_IMPORT,
}),
false,
),
).toBe(MessageFolderPendingSyncAction.NONE);
});
it('should return FOLDER_IMPORT idempotently when enabling with existing FOLDER_IMPORT', () => {
expect(
computePendingSyncActionForFolderUpdate(
buildFolder({
pendingSyncAction: MessageFolderPendingSyncAction.FOLDER_IMPORT,
}),
true,
),
).toBe(MessageFolderPendingSyncAction.FOLDER_IMPORT);
});
it('should preserve current pendingSyncAction when disabling with no pending import', () => {
expect(
computePendingSyncActionForFolderUpdate(
buildFolder({ isSynced: true }),
false,
),
).toBe(MessageFolderPendingSyncAction.NONE);
});
it('should preserve FOLDER_DELETION when disabling sync on a folder with pending deletion', () => {
expect(
computePendingSyncActionForFolderUpdate(
buildFolder({
isSynced: true,
pendingSyncAction: MessageFolderPendingSyncAction.FOLDER_DELETION,
}),
false,
),
).toBe(MessageFolderPendingSyncAction.FOLDER_DELETION);
});
it('should throw when toggling during ongoing sync with pending action', () => {
expect(() =>
computePendingSyncActionForFolderUpdate(
buildFolder({
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_ONGOING,
pendingSyncAction: MessageFolderPendingSyncAction.FOLDER_DELETION,
}),
true,
),
).toThrow(WorkspaceQueryRunnerException);
expect(() =>
computePendingSyncActionForFolderUpdate(
buildFolder({
isSynced: true,
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_ONGOING,
pendingSyncAction: MessageFolderPendingSyncAction.FOLDER_IMPORT,
}),
false,
),
).toThrow(WorkspaceQueryRunnerException);
});
});
@@ -0,0 +1,63 @@
import { msg } from '@lingui/core/macro';
import {
WorkspaceQueryRunnerException,
WorkspaceQueryRunnerExceptionCode,
} from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception';
import {
MessageChannelSyncStage,
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';
export const computePendingSyncActionForFolderUpdate = (
messageFolderWithMessageChannel: Pick<
MessageFolderWorkspaceEntity,
'isSynced' | 'pendingSyncAction'
> & {
messageChannel: Pick<MessageChannelWorkspaceEntity, 'syncStage'>;
},
isSyncEnabled: boolean,
): MessageFolderPendingSyncAction => {
const { pendingSyncAction, messageChannel } = messageFolderWithMessageChannel;
const isSyncOngoing =
messageChannel.syncStage ===
MessageChannelSyncStage.MESSAGE_LIST_FETCH_ONGOING;
const hasPendingAction =
pendingSyncAction !== MessageFolderPendingSyncAction.NONE;
if (isSyncOngoing && hasPendingAction) {
throw new WorkspaceQueryRunnerException(
'Cannot update message folder while sync is ongoing with pending actions',
WorkspaceQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
{
userFriendlyMessage: msg`Cannot update message folder while sync is ongoing. Please wait for the sync to complete.`,
},
);
}
if (isSyncEnabled) {
if (pendingSyncAction === MessageFolderPendingSyncAction.FOLDER_DELETION) {
throw new WorkspaceQueryRunnerException(
'Cannot enable sync while a folder deletion is pending',
WorkspaceQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
{
userFriendlyMessage: msg`Cannot enable sync while folder deletion is in progress.`,
},
);
}
return MessageFolderPendingSyncAction.FOLDER_IMPORT;
}
if (pendingSyncAction === MessageFolderPendingSyncAction.FOLDER_IMPORT) {
return MessageFolderPendingSyncAction.NONE;
}
return pendingSyncAction;
};
@@ -4,6 +4,7 @@ import { ApplyMessagesVisibilityRestrictionsService } from 'src/modules/messagin
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 { MessageFolderUpdateManyPreQueryHook } from 'src/modules/messaging/common/query-hooks/message/message-folder-update-many.pre-query.hook';
import { MessagingImportManagerModule } from 'src/modules/messaging/message-import-manager/messaging-import-manager.module';
@Module({
@@ -13,6 +14,7 @@ import { MessagingImportManagerModule } from 'src/modules/messaging/message-impo
MessageFindOnePostQueryHook,
MessageFindManyPostQueryHook,
MessageChannelUpdateOnePreQueryHook,
MessageFolderUpdateManyPreQueryHook,
],
})
export class MessagingQueryHookModule {}
@@ -10,6 +10,7 @@ import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common
export enum MessageFolderPendingSyncAction {
FOLDER_DELETION = 'FOLDER_DELETION',
FOLDER_IMPORT = 'FOLDER_IMPORT',
NONE = 'NONE',
}
@@ -42,6 +42,7 @@ import { MessagingDeleteGroupEmailMessagesService } from 'src/modules/messaging/
import { MessagingGetMessageListService } from 'src/modules/messaging/message-import-manager/services/messaging-get-message-list.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 { MessagingImportFolderMessagesService } from 'src/modules/messaging/message-import-manager/services/messaging-import-folder-messages.service';
import { MessagingMessageFolderAssociationService } from 'src/modules/messaging/message-import-manager/services/messaging-message-folder-association.service';
import { MessagingMessageListFetchService } from 'src/modules/messaging/message-import-manager/services/messaging-message-list-fetch.service';
import { MessagingMessageService } from 'src/modules/messaging/message-import-manager/services/messaging-message.service';
@@ -104,6 +105,7 @@ import { MessagingMonitoringModule } from 'src/modules/messaging/monitoring/mess
MessagingCursorService,
MessagingAccountAuthenticationService,
MessagingProcessFolderActionsService,
MessagingImportFolderMessagesService,
MessagingProcessGroupEmailActionsService,
MessagingDeleteFolderMessagesService,
MessagingDeleteGroupEmailMessagesService,
@@ -0,0 +1,59 @@
import { Injectable, Logger } from '@nestjs/common';
import { ConnectedAccountProvider } from 'twenty-shared/types';
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 { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service';
import {
MessageImportDriverException,
MessageImportDriverExceptionCode,
} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception';
@Injectable()
export class MessagingImportFolderMessagesService {
private readonly logger = new Logger(
MessagingImportFolderMessagesService.name,
);
constructor(
private readonly messageChannelSyncStatusService: MessageChannelSyncStatusService,
) {}
async importFolderMessages(
workspaceId: string,
messageChannel: Pick<
MessageChannelWorkspaceEntity,
'id' | 'connectedAccount'
>,
messageFolders: Pick<MessageFolderWorkspaceEntity, 'id' | 'isSynced'>[],
): Promise<void> {
const hasFoldersToImport = messageFolders.some((folder) => folder.isSynced);
if (!hasFoldersToImport) {
return;
}
switch (messageChannel.connectedAccount.provider) {
case ConnectedAccountProvider.GOOGLE:
await this.messageChannelSyncStatusService.resetAndMarkAsMessagesListFetchPending(
[messageChannel.id],
workspaceId,
);
return;
case ConnectedAccountProvider.MICROSOFT:
case ConnectedAccountProvider.IMAP_SMTP_CALDAV:
this.logger.debug(
`WorkspaceId: ${workspaceId}, MessageChannelId: ${messageChannel.id} - Skipping folder import backfill for provider ${messageChannel.connectedAccount.provider}`,
);
return;
default:
throw new MessageImportDriverException(
`Provider ${messageChannel.connectedAccount.provider} is not supported`,
MessageImportDriverExceptionCode.PROVIDER_NOT_SUPPORTED,
);
}
}
}
@@ -12,6 +12,7 @@ import {
MessageFolderWorkspaceEntity,
} from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
import { MessagingDeleteFolderMessagesService } from 'src/modules/messaging/message-import-manager/services/messaging-delete-folder-messages.service';
import { MessagingImportFolderMessagesService } from 'src/modules/messaging/message-import-manager/services/messaging-import-folder-messages.service';
@Injectable()
export class MessagingProcessFolderActionsService {
@@ -22,6 +23,7 @@ export class MessagingProcessFolderActionsService {
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly messagingDeleteFolderMessagesService: MessagingDeleteFolderMessagesService,
private readonly messagingImportFolderMessagesService: MessagingImportFolderMessagesService,
) {}
async processFolderActions(
@@ -53,21 +55,32 @@ export class MessagingProcessFolderActionsService {
`WorkspaceId: ${workspaceId}, MessageChannelId: ${messageChannel.id}, FolderId: ${folder.id} - Processing folder action: ${folder.pendingSyncAction}`,
);
if (
folder.pendingSyncAction ===
MessageFolderPendingSyncAction.FOLDER_DELETION
) {
await this.messagingDeleteFolderMessagesService.deleteFolderMessages(
workspaceId,
messageChannel,
folder,
);
switch (folder.pendingSyncAction) {
case MessageFolderPendingSyncAction.FOLDER_DELETION:
await this.messagingDeleteFolderMessagesService.deleteFolderMessages(
workspaceId,
messageChannel,
folder,
);
folderIdsToDelete.push(folder.id);
folderIdsToDelete.push(folder.id);
this.logger.debug(
`WorkspaceId: ${workspaceId}, MessageChannelId: ${messageChannel.id}, FolderId: ${folder.id} - Completed FOLDER_DELETION action`,
);
this.logger.debug(
`WorkspaceId: ${workspaceId}, MessageChannelId: ${messageChannel.id}, FolderId: ${folder.id} - Completed FOLDER_DELETION action`,
);
break;
case MessageFolderPendingSyncAction.FOLDER_IMPORT: {
await this.messagingImportFolderMessagesService.importFolderMessages(
workspaceId,
messageChannel,
[folder],
);
this.logger.debug(
`WorkspaceId: ${workspaceId}, MessageChannelId: ${messageChannel.id}, FolderId: ${folder.id} - Completed FOLDER_IMPORT action`,
);
break;
}
}
processedFolderIds.push(folder.id);