Fix author attachment field (#15065)
# Migrate Attachment Author to CreatedBy Field **Twill Task**: https://twill.ai/twentyhq/ENG/tasks/7 ## Summary This PR implements a migration to transition the `Attachment` object from using an `author` relation field to using the standard `createdBy` field, addressing issue https://github.com/twentyhq/core-team-issues/issues/1594. ## Changes - **Added migration command** (`1-8-migrate-attachment-author-to-created-by.command.ts`): - Migrates existing attachment data to use `createdBy` instead of `author` - Ensures data integrity during the transition to the standard field pattern - **Updated Attachment workspace entity**: - Added `createdBy` relation field to the `Attachment` standard object - Registered new field ID in `standard-field-ids.ts` constants - **Integrated migration into upgrade pipeline**: - Added migration module for version 1.8 - Registered in the main upgrade version command module This change aligns the `Attachment` object with Twenty's standard field conventions by using the built-in `createdBy` field instead of a custom `author` field. --- Fixes https://github.com/twentyhq/core-team-issues/issues/1594 --------- Co-authored-by: Twill <agent@twill.ai> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Félix Malfait <felix.malfait@gmail.com> Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
+108
@@ -0,0 +1,108 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IsNull, Not, Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
||||
type RunOnWorkspaceArgs,
|
||||
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { type AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity';
|
||||
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-10:migrate-attachment-author-to-created-by',
|
||||
description:
|
||||
'Migrate attachment author field data to createdBy composite field',
|
||||
})
|
||||
export class MigrateAttachmentAuthorToCreatedByCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(Workspace)
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
this.logger.log(
|
||||
`Migrating attachment author to createdBy for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const attachmentRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<AttachmentWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'attachment',
|
||||
);
|
||||
|
||||
const workspaceMemberRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workspaceMember',
|
||||
);
|
||||
|
||||
const attachments = await attachmentRepository.find({
|
||||
where: {
|
||||
authorId: Not(IsNull()),
|
||||
createdBy: {
|
||||
workspaceMemberId: IsNull(),
|
||||
},
|
||||
},
|
||||
select: ['id', 'authorId'],
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`Found ${attachments.length} attachments to migrate for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
let migratedCount = 0;
|
||||
|
||||
for (const attachment of attachments) {
|
||||
const { id, authorId } = attachment;
|
||||
|
||||
if (!isDefined(authorId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const workspaceMember = await workspaceMemberRepository.findOne({
|
||||
where: { id: authorId },
|
||||
});
|
||||
|
||||
if (!isDefined(workspaceMember)) {
|
||||
this.logger.warn(
|
||||
`Workspace member ${authorId} not found for attachment ${id}, skipping`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const firstName = workspaceMember.name?.firstName || '';
|
||||
const lastName = workspaceMember.name?.lastName || '';
|
||||
const displayName =
|
||||
firstName || lastName ? `${firstName} ${lastName}`.trim() : 'Unknown';
|
||||
|
||||
await attachmentRepository.update(
|
||||
{ id },
|
||||
{
|
||||
createdBy: {
|
||||
source: FieldActorSource.MANUAL,
|
||||
workspaceMemberId: workspaceMember.id,
|
||||
name: displayName,
|
||||
context: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
migratedCount++;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Successfully migrated ${migratedCount} attachments for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
||||
type RunOnWorkspaceArgs,
|
||||
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { type AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity';
|
||||
|
||||
const TYPE_TO_FILE_CATEGORY_MAPPING: Record<string, string> = {
|
||||
Archive: 'ARCHIVE',
|
||||
Audio: 'AUDIO',
|
||||
Image: 'IMAGE',
|
||||
Presentation: 'PRESENTATION',
|
||||
Spreadsheet: 'SPREADSHEET',
|
||||
TextDocument: 'TEXT_DOCUMENT',
|
||||
Video: 'VIDEO',
|
||||
Other: 'OTHER',
|
||||
};
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-10:migrate-attachment-type-to-file-category',
|
||||
description:
|
||||
'Migrate attachment type field data to fileCategory SELECT field',
|
||||
})
|
||||
export class MigrateAttachmentTypeToFileCategoryCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(Workspace)
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
this.logger.log(
|
||||
`Migrating attachment type to fileCategory for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const attachmentRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<AttachmentWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'attachment',
|
||||
);
|
||||
|
||||
const attachments = await attachmentRepository.find({
|
||||
select: ['id', 'type'],
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`Found ${attachments.length} attachments to migrate for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
let migratedCount = 0;
|
||||
|
||||
for (const attachment of attachments) {
|
||||
const { id, type } = attachment;
|
||||
|
||||
const fileCategory =
|
||||
TYPE_TO_FILE_CATEGORY_MAPPING[type] ||
|
||||
TYPE_TO_FILE_CATEGORY_MAPPING.Other;
|
||||
|
||||
await attachmentRepository.update(
|
||||
{ id },
|
||||
{
|
||||
fileCategory,
|
||||
},
|
||||
);
|
||||
|
||||
migratedCount++;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Successfully migrated ${migratedCount} attachments for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { MigrateAttachmentAuthorToCreatedByCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-author-to-created-by.command';
|
||||
import { MigrateAttachmentTypeToFileCategoryCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-type-to-file-category.command';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Workspace]),
|
||||
WorkspaceSchemaManagerModule,
|
||||
],
|
||||
providers: [
|
||||
MigrateAttachmentAuthorToCreatedByCommand,
|
||||
MigrateAttachmentTypeToFileCategoryCommand,
|
||||
],
|
||||
exports: [
|
||||
MigrateAttachmentAuthorToCreatedByCommand,
|
||||
MigrateAttachmentTypeToFileCategoryCommand,
|
||||
],
|
||||
})
|
||||
export class V1_10_UpgradeVersionCommandModule {}
|
||||
+3
-3
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { DeduplicateUniqueFieldsCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-deduplicate-unique-fields.command';
|
||||
import { FillNullServerlessFunctionLayerIdCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-fill-null-serverless-function-layer-id.command';
|
||||
import { MigrateChannelSyncStagesCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-migrate-channel-sync-stages.command';
|
||||
import { MigrateWorkflowStepFilterOperandValueCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-migrate-workflow-step-filter-operand-value';
|
||||
import { RegeneratePersonSearchVectorWithPhonesCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-regenerate-person-search-vector-with-phones.command';
|
||||
@@ -10,12 +11,11 @@ import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/
|
||||
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { IndexMetadataModule } from 'src/engine/metadata-modules/index-metadata/index-metadata.module';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ServerlessFunctionLayerEntity } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.entity';
|
||||
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
||||
import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module';
|
||||
import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.module';
|
||||
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module';
|
||||
import { FillNullServerlessFunctionLayerIdCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-fill-null-serverless-function-layer-id.command';
|
||||
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
||||
import { ServerlessFunctionLayerEntity } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
||||
+2
@@ -10,6 +10,7 @@ import { V1_5_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-
|
||||
import { V1_6_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-6/1-6-upgrade-version-command.module';
|
||||
import { V1_7_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-7/1-7-upgrade-version-command.module';
|
||||
import { V1_8_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-8/1-8-upgrade-version-command.module';
|
||||
import { V1_10_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-10/1-10-upgrade-version-command.module';
|
||||
import { UpgradeCommand } from 'src/database/commands/upgrade-version-command/upgrade.command';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module';
|
||||
@@ -26,6 +27,7 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp
|
||||
V1_6_UpgradeVersionCommandModule,
|
||||
V1_7_UpgradeVersionCommandModule,
|
||||
V1_8_UpgradeVersionCommandModule,
|
||||
V1_10_UpgradeVersionCommandModule,
|
||||
WorkspaceSyncMetadataModule,
|
||||
],
|
||||
providers: [UpgradeCommand],
|
||||
|
||||
+17
-2
@@ -19,6 +19,8 @@ import { AddEnqueuedStatusToWorkflowRunCommand } from 'src/database/commands/upg
|
||||
import { FixSchemaArrayTypeCommand } from 'src/database/commands/upgrade-version-command/1-1/1-1-fix-schema-array-type.command';
|
||||
import { FixUpdateStandardFieldsIsLabelSyncedWithName } from 'src/database/commands/upgrade-version-command/1-1/1-1-fix-update-standard-field-is-label-synced-with-name.command';
|
||||
import { MigrateWorkflowRunStatesCommand } from 'src/database/commands/upgrade-version-command/1-1/1-1-migrate-workflow-run-state.command';
|
||||
import { MigrateAttachmentAuthorToCreatedByCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-author-to-created-by.command';
|
||||
import { MigrateAttachmentTypeToFileCategoryCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-type-to-file-category.command';
|
||||
import { AddEnqueuedStatusToWorkflowRunV2Command } from 'src/database/commands/upgrade-version-command/1-2/1-2-add-enqueued-status-to-workflow-run-v2.command';
|
||||
import { AddNextStepIdsToWorkflowVersionTriggers } from 'src/database/commands/upgrade-version-command/1-2/1-2-add-next-step-ids-to-workflow-version-triggers.command';
|
||||
import { RemoveWorkflowRunsWithoutState } from 'src/database/commands/upgrade-version-command/1-2/1-2-remove-workflow-runs-without-state.command';
|
||||
@@ -30,6 +32,7 @@ import { RemoveFavoriteViewRelationCommand } from 'src/database/commands/upgrade
|
||||
import { FixLabelIdentifierPositionAndVisibilityCommand } from 'src/database/commands/upgrade-version-command/1-6/1-6-fix-label-identifier-position-and-visibility.command';
|
||||
import { BackfillWorkflowManualTriggerAvailabilityCommand } from 'src/database/commands/upgrade-version-command/1-7/1-7-backfill-workflow-manual-trigger-availability.command';
|
||||
import { DeduplicateUniqueFieldsCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-deduplicate-unique-fields.command';
|
||||
import { FillNullServerlessFunctionLayerIdCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-fill-null-serverless-function-layer-id.command';
|
||||
import { MigrateChannelSyncStagesCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-migrate-channel-sync-stages.command';
|
||||
import { MigrateWorkflowStepFilterOperandValueCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-migrate-workflow-step-filter-operand-value';
|
||||
import { RegeneratePersonSearchVectorWithPhonesCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-regenerate-person-search-vector-with-phones.command';
|
||||
@@ -37,7 +40,6 @@ import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twent
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command';
|
||||
import { FillNullServerlessFunctionLayerIdCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-fill-null-serverless-function-layer-id.command';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade',
|
||||
@@ -90,11 +92,15 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
protected readonly backfillWorkflowManualTriggerAvailabilityCommand: BackfillWorkflowManualTriggerAvailabilityCommand,
|
||||
|
||||
// 1.8 Commands
|
||||
protected readonly fillNullServerlessFunctionLayerIdCommand: FillNullServerlessFunctionLayerIdCommand,
|
||||
protected readonly migrateWorkflowStepFilterOperandValueCommand: MigrateWorkflowStepFilterOperandValueCommand,
|
||||
protected readonly deduplicateUniqueFieldsCommand: DeduplicateUniqueFieldsCommand,
|
||||
protected readonly regeneratePersonSearchVectorWithPhonesCommand: RegeneratePersonSearchVectorWithPhonesCommand,
|
||||
protected readonly migrateChannelSyncStagesCommand: MigrateChannelSyncStagesCommand,
|
||||
protected readonly fillNullServerlessFunctionLayerIdCommand: FillNullServerlessFunctionLayerIdCommand,
|
||||
|
||||
// 1.10 Commands
|
||||
protected readonly migrateAttachmentAuthorToCreatedByCommand: MigrateAttachmentAuthorToCreatedByCommand,
|
||||
protected readonly migrateAttachmentTypeToFileCategoryCommand: MigrateAttachmentTypeToFileCategoryCommand,
|
||||
) {
|
||||
super(
|
||||
workspaceRepository,
|
||||
@@ -199,6 +205,14 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
afterSyncMetadata: [],
|
||||
};
|
||||
|
||||
const commands_1100: VersionCommands = {
|
||||
beforeSyncMetadata: [],
|
||||
afterSyncMetadata: [
|
||||
this.migrateAttachmentAuthorToCreatedByCommand,
|
||||
this.migrateAttachmentTypeToFileCategoryCommand,
|
||||
],
|
||||
};
|
||||
|
||||
this.allCommands = {
|
||||
'0.53.0': commands_053,
|
||||
'0.54.0': commands_054,
|
||||
@@ -213,6 +227,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
'1.6.0': commands_160,
|
||||
'1.7.0': commands_170,
|
||||
'1.8.0': commands_180,
|
||||
'1.10.0': commands_1100,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user