Migrate messageFolder parentFolderId from UUID to externalId (#19348)
This PR migrates `messageFolder`.`parentFolderId` from an internal `UUID` reference to external provider id. Eliminates unnecessary lookup and complexity --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
+2
-2
@@ -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]);
|
||||
|
||||
+3
-9
@@ -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),
|
||||
|
||||
+2
-9
@@ -11,21 +11,17 @@ export const computeMessageFolderTree = (
|
||||
folders: MessageFolder[],
|
||||
): MessageFolderTreeNode[] => {
|
||||
const folderByExternalIdMap = new Map<string, MessageFolder>();
|
||||
const folderByIdMap = new Map<string, MessageFolder>();
|
||||
const childrenMap = new Map<string, MessageFolder[]>();
|
||||
|
||||
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));
|
||||
|
||||
+98
@@ -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<MessageFolderEntity>,
|
||||
protected readonly workspaceIteratorService: WorkspaceIteratorService,
|
||||
) {
|
||||
super(workspaceIteratorService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const isDryRun = options.dryRun ?? false;
|
||||
|
||||
const folders = await this.messageFolderRepository.find({
|
||||
where: { workspaceId },
|
||||
});
|
||||
|
||||
const idToExternalIdMap = new Map<string, string>();
|
||||
const existingExternalIds = new Set<string>();
|
||||
|
||||
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}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+9
-1
@@ -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 {}
|
||||
|
||||
+3
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user