Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 00d629d7ce | |||
| 9362a0d8c7 |
+218
@@ -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}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+3
@@ -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,
|
||||
],
|
||||
|
||||
+3
@@ -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,
|
||||
];
|
||||
|
||||
+7
@@ -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,
|
||||
|
||||
+140
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
+106
@@ -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);
|
||||
});
|
||||
});
|
||||
+63
@@ -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;
|
||||
};
|
||||
+2
@@ -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 {}
|
||||
|
||||
+1
@@ -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',
|
||||
}
|
||||
|
||||
|
||||
+2
@@ -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,
|
||||
|
||||
+59
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+26
-13
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user