diff --git a/packages/twenty-front/src/modules/settings/accounts/components/message-folders/utils/__tests__/computeMessageFolderTree.test.ts b/packages/twenty-front/src/modules/settings/accounts/components/message-folders/utils/__tests__/computeMessageFolderTree.test.ts index 59666cfef2..d9b701e637 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/message-folders/utils/__tests__/computeMessageFolderTree.test.ts +++ b/packages/twenty-front/src/modules/settings/accounts/components/message-folders/utils/__tests__/computeMessageFolderTree.test.ts @@ -147,7 +147,7 @@ describe('computeMessageFolderTree', () => { expect(result[0].children[1].folder.name).toBe('Projects'); }); - it('should resolve parent-child when parentFolderId references parent id instead of externalId', () => { + it('should resolve parent-child when parentFolderId references parent externalId', () => { const parent = createFolder( '20202020-aaaa-bbbb-cccc-000000000001', 'custom folder', @@ -157,7 +157,7 @@ describe('computeMessageFolderTree', () => { const child = createFolder( '20202020-aaaa-bbbb-cccc-000000000002', 'child folder', - '20202020-aaaa-bbbb-cccc-000000000001', + 'Label_5900090362003645629', 'Label_7713410187110265162', ); const result = computeMessageFolderTree([parent, child]); diff --git a/packages/twenty-front/src/modules/settings/accounts/components/message-folders/utils/computeFolderIdsForSyncToggle.ts b/packages/twenty-front/src/modules/settings/accounts/components/message-folders/utils/computeFolderIdsForSyncToggle.ts index dcbf9275bf..c21f38d9a6 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/message-folders/utils/computeFolderIdsForSyncToggle.ts +++ b/packages/twenty-front/src/modules/settings/accounts/components/message-folders/utils/computeFolderIdsForSyncToggle.ts @@ -18,9 +18,7 @@ export const computeFolderIdsForSyncToggle = ({ const folder = folderById.get(id); const children = folder ? allFolders.filter( - (childFolder) => - childFolder.parentFolderId === folder.externalId || - childFolder.parentFolderId === folder.id, + (childFolder) => childFolder.parentFolderId === folder.externalId, ) : []; @@ -40,9 +38,7 @@ export const computeFolderIdsForSyncToggle = ({ break; } - const parent = - folderByExternalId.get(current.parentFolderId) ?? - folderById.get(current.parentFolderId); + const parent = folderByExternalId.get(current.parentFolderId); if (!parent) { break; @@ -67,9 +63,7 @@ export const computeFolderIdsForSyncToggle = ({ for (const parent of collectParents(folderId)) { const children = allFolders.filter( - (folder) => - folder.parentFolderId === parent.externalId || - folder.parentFolderId === parent.id, + (folder) => folder.parentFolderId === parent.externalId, ); const hasOtherSyncedChild = children.some( (child) => child.isSynced && !idsToUnsync.has(child.id), diff --git a/packages/twenty-front/src/modules/settings/accounts/components/message-folders/utils/computeMessageFolderTree.ts b/packages/twenty-front/src/modules/settings/accounts/components/message-folders/utils/computeMessageFolderTree.ts index 46847673e2..bbb7a7ea18 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/message-folders/utils/computeMessageFolderTree.ts +++ b/packages/twenty-front/src/modules/settings/accounts/components/message-folders/utils/computeMessageFolderTree.ts @@ -11,21 +11,17 @@ export const computeMessageFolderTree = ( folders: MessageFolder[], ): MessageFolderTreeNode[] => { const folderByExternalIdMap = new Map(); - const folderByIdMap = new Map(); const childrenMap = new Map(); folders.forEach((folder) => { if (isDefined(folder.externalId)) { folderByExternalIdMap.set(folder.externalId, folder); } - folderByIdMap.set(folder.id, folder); }); folders.forEach((folder) => { if (isDefined(folder.parentFolderId)) { - const parent = - folderByExternalIdMap.get(folder.parentFolderId) ?? - folderByIdMap.get(folder.parentFolderId); + const parent = folderByExternalIdMap.get(folder.parentFolderId); if (isDefined(parent)) { const siblings = childrenMap.get(parent.id) || []; @@ -51,10 +47,7 @@ export const computeMessageFolderTree = ( const rootFolders = folders.filter((folder) => { if (!folder.parentFolderId) return true; - return ( - !folderByExternalIdMap.has(folder.parentFolderId) && - !folderByIdMap.has(folder.parentFolderId) - ); + return !folderByExternalIdMap.has(folder.parentFolderId); }); rootFolders.sort((a, b) => a.name.localeCompare(b.name)); diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-migrate-message-folder-parent-id-to-external-id.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-migrate-message-folder-parent-id-to-external-id.command.ts new file mode 100644 index 0000000000..4231ba386a --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-migrate-message-folder-parent-id-to-external-id.command.ts @@ -0,0 +1,98 @@ +import { InjectRepository } from '@nestjs/typeorm'; + +import { Command } from 'nest-commander'; +import { Repository } from 'typeorm'; + +import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner'; +import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service'; +import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner'; +import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity'; + +const UUID_REGEX = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + +@Command({ + name: 'upgrade:1-21:migrate-message-folder-parent-id-to-external-id', + description: + 'Migrate messageFolder parentFolderId from internal UUID references to external IDs', +}) +export class MigrateMessageFolderParentIdToExternalIdCommand extends ActiveOrSuspendedWorkspaceCommandRunner { + constructor( + @InjectRepository(MessageFolderEntity) + private readonly messageFolderRepository: Repository, + protected readonly workspaceIteratorService: WorkspaceIteratorService, + ) { + super(workspaceIteratorService); + } + + override async runOnWorkspace({ + workspaceId, + options, + }: RunOnWorkspaceArgs): Promise { + const isDryRun = options.dryRun ?? false; + + const folders = await this.messageFolderRepository.find({ + where: { workspaceId }, + }); + + const idToExternalIdMap = new Map(); + const existingExternalIds = new Set(); + + for (const folder of folders) { + if (folder.externalId) { + idToExternalIdMap.set(folder.id, folder.externalId); + existingExternalIds.add(folder.externalId); + } + } + + let migratedCount = 0; + + for (const folder of folders) { + if (!folder.parentFolderId) { + continue; + } + + if (!UUID_REGEX.test(folder.parentFolderId)) { + continue; + } + + // Already points to a valid externalId — skip even if it looks like a UUID + if (existingExternalIds.has(folder.parentFolderId)) { + continue; + } + + const parentExternalId = idToExternalIdMap.get(folder.parentFolderId); + + if (!parentExternalId) { + this.logger.warn( + `Folder ${folder.id}: parent ${folder.parentFolderId} not found or has no externalId, setting to null`, + ); + + if (!isDryRun) { + await this.messageFolderRepository.update(folder.id, { + parentFolderId: null, + }); + } + + migratedCount++; + continue; + } + + if (isDryRun) { + this.logger.log( + `[DRY RUN] Would update folder ${folder.id}: parentFolderId ${folder.parentFolderId} -> ${parentExternalId}`, + ); + } else { + await this.messageFolderRepository.update(folder.id, { + parentFolderId: parentExternalId, + }); + } + + migratedCount++; + } + + this.logger.log( + `${isDryRun ? '[DRY RUN] ' : ''}Migrated ${migratedCount} folder(s) for workspace ${workspaceId}`, + ); + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-upgrade-version-command.module.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-upgrade-version-command.module.ts index 171663308b..66702e1800 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-upgrade-version-command.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-upgrade-version-command.module.ts @@ -8,6 +8,7 @@ import { BackfillPageLayoutsAndFieldsWidgetViewFieldsCommand } from 'src/databas import { DeduplicateEngineCommandsCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-deduplicate-engine-commands.command'; import { DropWorkspaceMessagingFksCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-drop-workspace-messaging-fks.command'; import { FixSelectAllCommandMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-fix-select-all-command-menu-items.command'; +import { MigrateMessageFolderParentIdToExternalIdCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-migrate-message-folder-parent-id-to-external-id.command'; import { MigrateAiAgentTextToJsonResponseFormatCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-migrate-ai-agent-text-to-json-response-format.command'; import { UpdateEditLayoutCommandMenuItemLabelCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-update-edit-layout-command-menu-item-label.command'; import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; @@ -15,13 +16,18 @@ import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature- import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; +import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity'; import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.module'; import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module'; @Module({ imports: [ - TypeOrmModule.forFeature([WorkspaceEntity, DataSourceEntity]), + TypeOrmModule.forFeature([ + WorkspaceEntity, + DataSourceEntity, + MessageFolderEntity, + ]), DataSourceModule, WorkspaceCacheModule, ApplicationModule, @@ -39,6 +45,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace MigrateAiAgentTextToJsonResponseFormatCommand, UpdateEditLayoutCommandMenuItemLabelCommand, DropWorkspaceMessagingFksCommand, + MigrateMessageFolderParentIdToExternalIdCommand, ], exports: [ AddGlobalKeyValuePairUniqueIndexCommand, @@ -49,6 +56,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace MigrateAiAgentTextToJsonResponseFormatCommand, UpdateEditLayoutCommandMenuItemLabelCommand, DropWorkspaceMessagingFksCommand, + MigrateMessageFolderParentIdToExternalIdCommand, ], }) export class V1_21_UpgradeVersionCommandModule {} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts index cbe8a9ac0f..33a7d58474 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts @@ -37,6 +37,7 @@ import { CoreEngineVersionService } from 'src/engine/core-engine-version/service import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { WorkspaceVersionService } from 'src/engine/workspace-manager/workspace-version/services/workspace-version.service'; import { DropWorkspaceMessagingFksCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-drop-workspace-messaging-fks.command'; +import { MigrateMessageFolderParentIdToExternalIdCommand } from 'src/database/commands/upgrade-version-command/1-21/1-21-migrate-message-folder-parent-id-to-external-id.command'; @Command({ name: 'upgrade', @@ -80,6 +81,7 @@ export class UpgradeCommand extends UpgradeCommandRunner { private readonly migrateAiAgentTextToJsonResponseFormatCommand: MigrateAiAgentTextToJsonResponseFormatCommand, private readonly updateEditLayoutCommandMenuItemLabelCommand: UpdateEditLayoutCommandMenuItemLabelCommand, private readonly dropWorkspaceMessagingFksCommand: DropWorkspaceMessagingFksCommand, + private readonly migrateMessageFolderParentIdToExternalIdCommand: MigrateMessageFolderParentIdToExternalIdCommand, ) { super( workspaceRepository, @@ -120,6 +122,7 @@ export class UpgradeCommand extends UpgradeCommandRunner { this.migrateAiAgentTextToJsonResponseFormatCommand, this.updateEditLayoutCommandMenuItemLabelCommand, this.dropWorkspaceMessagingFksCommand, + this.migrateMessageFolderParentIdToExternalIdCommand, ]; this.allCommands = {