File - Migrate core pictures (workspace and member logo) + workflow attachments (#17924)

- Create a common file-by-id download controller
- Create core picture module with resolver and logic to handle
workspaceLogo and workspaceMemberProfilePicture update
- Create workflow file module (same)
- Data migration
This commit is contained in:
Etienne
2026-02-17 15:42:50 +00:00
committed by GitHub
parent 963f2de864
commit 163c1175cb
85 changed files with 2212 additions and 335 deletions
@@ -18,7 +18,7 @@ import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/featu
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
import { FilesFieldService } from 'src/engine/core-modules/file/files-field/files-field.service';
import { FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service';
import { extractFileIdFromUrl } from 'src/engine/core-modules/file/files-field/utils/extract-file-id-from-url.util';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
@@ -48,7 +48,7 @@ export class MigrateActivityRichTextAttachmentFileIdsCommand extends ActiveOrSus
private readonly fileStorageService: FileStorageService,
private readonly workspaceCacheService: WorkspaceCacheService,
private readonly applicationService: ApplicationService,
private readonly filesFieldService: FilesFieldService,
private readonly fileUrlService: FileUrlService,
@InjectDataSource()
private readonly coreDataSource: DataSource,
) {
@@ -237,7 +237,10 @@ export class MigrateActivityRichTextAttachmentFileIdsCommand extends ActiveOrSus
const props = (block.props as Record<string, unknown>) || {};
const url = props.url as string | undefined;
return isDefined(url) && !isDefined(extractFileIdFromUrl(url));
return (
isDefined(url) &&
!isDefined(extractFileIdFromUrl(url, FileFolder.FilesField))
);
});
if (!needsMigration) {
@@ -259,7 +262,7 @@ export class MigrateActivityRichTextAttachmentFileIdsCommand extends ActiveOrSus
if (
!isDefined(url) ||
isDefined(extractFileIdFromUrl(url)) ||
isDefined(extractFileIdFromUrl(url, FileFolder.FilesField)) ||
!url.includes('/files/attachment/')
) {
enrichedBlocknote.push(block);
@@ -292,9 +295,10 @@ export class MigrateActivityRichTextAttachmentFileIdsCommand extends ActiveOrSus
);
hasChanges = true;
const signedUrl = this.filesFieldService.signFileUrl({
const signedUrl = this.fileUrlService.signFileByIdUrl({
fileId,
workspaceId,
fileFolder: FileFolder.FilesField,
});
enrichedBlocknote.push({
@@ -0,0 +1,214 @@
import { Logger } from '@nestjs/common';
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { isNonEmptyString } from '@sniptt/guards';
import { Command } from 'nest-commander';
import { FileFolder } from 'twenty-shared/types';
import { isDefined, isNonEmptyArray } from 'twenty-shared/utils';
import { DataSource, In, 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 { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import {
WorkflowVersionStatus,
WorkflowVersionWorkspaceEntity,
} from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
import { WorkflowActionType } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action-type.enum';
type WorkflowFile = {
id: string;
path?: string;
name: string;
size: number;
type: string;
createdAt: string;
};
type SendEmailStep = {
id: string;
type: WorkflowActionType.SEND_EMAIL;
settings: {
input: {
files?: WorkflowFile[];
[key: string]: unknown;
};
[key: string]: unknown;
};
[key: string]: unknown;
};
@Command({
name: 'upgrade:1-18:migrate-workflow-send-email-attachments',
description:
'Migrate workflow send email attachments to FileFolder.Workflow and update payload paths',
})
export class MigrateWorkflowSendEmailAttachmentsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
protected readonly logger = new Logger(
MigrateWorkflowSendEmailAttachmentsCommand.name,
);
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
private readonly featureFlagService: FeatureFlagService,
private readonly fileStorageService: FileStorageService,
private readonly applicationService: ApplicationService,
@InjectDataSource()
private readonly coreDataSource: DataSource,
) {
super(workspaceRepository, globalWorkspaceOrmManager, dataSourceService);
}
override async runOnWorkspace({
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
const isDryRun = options.dryRun ?? false;
const isMigrated = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_OTHER_FILE_MIGRATED,
workspaceId,
);
if (isMigrated) {
this.logger.log(
`Workflow attachments migration already completed for workspace ${workspaceId}, skipping`,
);
return;
}
this.logger.log(
`${isDryRun ? '[DRY RUN] ' : ''}Starting workflow send email attachments migration for workspace ${workspaceId}`,
);
const workflowVersionRepository =
await this.globalWorkspaceOrmManager.getRepository<WorkflowVersionWorkspaceEntity>(
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
);
const workflowVersions = await workflowVersionRepository.find({
select: ['id', 'steps'],
where: {
status: In([
WorkflowVersionStatus.DRAFT,
WorkflowVersionStatus.ACTIVE,
WorkflowVersionStatus.DEACTIVATED,
]),
},
});
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
const fileRepository = this.coreDataSource.getRepository(FileEntity);
for (const workflowVersion of workflowVersions) {
const steps = workflowVersion.steps;
if (!isNonEmptyArray(steps)) {
continue;
}
for (const step of steps) {
if (step.type !== WorkflowActionType.SEND_EMAIL) {
continue;
}
const sendEmailStep = step as SendEmailStep;
const files = sendEmailStep.settings?.input?.files;
if (!isNonEmptyArray(files)) {
continue;
}
for (const file of files) {
const fileEntity = await fileRepository.findOne({
where: {
id: file.id,
workspaceId,
},
});
if (!isDefined(fileEntity)) {
this.logger.warn(
`File ${file.id} not found for workflow version ${workflowVersion.id}, skipping`,
);
continue;
}
if (fileEntity.path.startsWith(FileFolder.Workflow)) {
this.logger.log(
`File ${file.id} already in Workflow folder, skipping copy`,
);
continue;
}
const newResourcePath = `${fileEntity.id}${isNonEmptyString(file.type) ? `.${file.type}` : ''}`;
const newPath = `${FileFolder.Workflow}/${newResourcePath}`;
if (!isDryRun) {
try {
await this.fileStorageService.copyLegacy({
from: {
folderPath: `workspace-${workspaceId}`,
filename: fileEntity.path,
},
to: {
folderPath: `${workspaceId}/${workspaceCustomFlatApplication.universalIdentifier}`,
filename: newPath,
},
});
} catch (error) {
this.logger.error(
`Failed to migrate file ${fileEntity.id} in workspace ${workspaceId}: ${error.message}`,
);
continue;
}
} else {
this.logger.log(
`[DRY RUN] Would migrate file ${fileEntity.id} from ${fileEntity.path} to ${newPath}`,
);
}
await fileRepository.update(
{ id: fileEntity.id },
{
path: newPath,
applicationId: workspaceCustomFlatApplication.id,
settings: {
isTemporaryFile: true,
toDelete: false,
},
},
);
}
}
}
if (!isDryRun) {
await this.featureFlagService.enableFeatureFlags(
[FeatureFlagKey.IS_OTHER_FILE_MIGRATED],
workspaceId,
);
}
this.logger.log(
`${isDryRun ? '[DRY RUN] ' : ''}Completed workflow send email attachments migration for workspace ${workspaceId}`,
);
}
}
@@ -0,0 +1,327 @@
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { isNonEmptyString } from '@sniptt/guards';
import { Command } from 'nest-commander';
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import { FileFolder } from 'twenty-shared/types';
import {
extractFolderPathFilenameAndTypeOrThrow,
isDefined,
} from 'twenty-shared/utils';
import { And, DataSource, IsNull, Like, Not, Repository } from 'typeorm';
import { v4 } from 'uuid';
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 { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
import { FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@Command({
name: 'upgrade:1-18:migrate-workspace-pictures',
description:
'Migrate workspace logos and workspace member avatars to file records',
})
export class MigrateWorkspacePicturesCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
private readonly featureFlagService: FeatureFlagService,
private readonly fileStorageService: FileStorageService,
private readonly workspaceCacheService: WorkspaceCacheService,
private readonly applicationService: ApplicationService,
private readonly fileUrlService: FileUrlService,
@InjectDataSource()
private readonly coreDataSource: DataSource,
) {
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
override async runOnWorkspace({
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
const isDryRun = options.dryRun ?? false;
const isMigrated = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_CORE_PICTURE_MIGRATED,
workspaceId,
);
if (isMigrated) {
this.logger.log(
`Workspace pictures migration already completed for workspace ${workspaceId}, skipping`,
);
return;
}
this.logger.log(
`${isDryRun ? '[DRY RUN] ' : ''}Starting workspace pictures migration for workspace ${workspaceId}`,
);
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{
workspaceId,
},
);
const fileRepository = this.coreDataSource.getRepository(FileEntity);
await this.migrateWorkspaceLogo({
workspaceId,
isDryRun,
workspaceCustomFlatApplication,
fileRepository,
});
await this.migrateWorkspaceMemberAvatars({
workspaceId,
isDryRun,
workspaceCustomFlatApplication,
fileRepository,
});
if (!isDryRun) {
await this.featureFlagService.enableFeatureFlags(
[FeatureFlagKey.IS_CORE_PICTURE_MIGRATED],
workspaceId,
);
}
this.logger.log(
`${isDryRun ? '[DRY RUN] ' : ''}Completed workspace pictures migration for workspace ${workspaceId}`,
);
}
private async migrateWorkspaceLogo({
workspaceId,
isDryRun,
workspaceCustomFlatApplication,
fileRepository,
}: {
workspaceId: string;
isDryRun: boolean;
workspaceCustomFlatApplication: FlatApplication;
fileRepository: Repository<FileEntity>;
}): Promise<void> {
const workspace = await this.workspaceRepository.findOne({
where: {
id: workspaceId,
logo: Not(IsNull()),
logoFileId: IsNull(),
},
});
if (!workspace || !isNonEmptyString(workspace.logo)) {
this.logger.log(
`No workspace logo to migrate for workspace ${workspaceId}`,
);
return;
}
this.logger.log(
`Migrating workspace logo for workspace ${workspaceId}: ${workspace.logo}`,
);
try {
const { type: fileExtension } = extractFolderPathFilenameAndTypeOrThrow(
workspace.logo,
);
const fileId = v4();
const newFilename = `${fileId}${isNonEmptyString(fileExtension) ? `.${fileExtension}` : ''}`;
const newResourcePath = `${FileFolder.CorePicture}/${newFilename}`;
if (!isDryRun) {
await this.fileStorageService.copyLegacy({
from: {
folderPath: `workspace-${workspaceId}`,
filename: workspace.logo,
},
to: {
folderPath: `${workspaceId}/${workspaceCustomFlatApplication.universalIdentifier}`,
filename: newResourcePath,
},
});
const fileEntity = fileRepository.create({
id: fileId,
path: newResourcePath,
workspaceId,
applicationId: workspaceCustomFlatApplication.id,
size: -1,
settings: {
isTemporaryFile: false,
toDelete: false,
},
});
await fileRepository.save(fileEntity);
await this.workspaceRepository.update(
{ id: workspaceId },
{ logoFileId: fileId },
);
}
this.logger.log(
`Migrated workspace logo for workspace ${workspaceId} (${workspace.logo} -> ${newResourcePath})`,
);
} catch (error) {
this.logger.error(
`Failed to migrate workspace logo for workspace ${workspaceId}: ${error.message}`,
);
throw error;
}
}
private async migrateWorkspaceMemberAvatars({
workspaceId,
isDryRun,
workspaceCustomFlatApplication,
fileRepository,
}: {
workspaceId: string;
isDryRun: boolean;
workspaceCustomFlatApplication: FlatApplication;
fileRepository: Repository<FileEntity>;
}): Promise<void> {
const { flatObjectMetadataMaps } =
await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatObjectMetadataMaps',
]);
const workspaceMemberObjectMetadata =
findFlatEntityByUniversalIdentifier<FlatObjectMetadata>({
flatEntityMaps: flatObjectMetadataMaps,
universalIdentifier:
STANDARD_OBJECTS.workspaceMember.universalIdentifier,
});
if (!isDefined(workspaceMemberObjectMetadata)) {
this.logger.warn(
`Workspace member object metadata not found for workspace ${workspaceId}, skipping member avatar migration`,
);
return;
}
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepository<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
{ shouldBypassPermissionChecks: true },
);
const workspaceMembers = await workspaceMemberRepository.find({
where: {
avatarUrl: And(Not(IsNull()), Not(Like(`%${FileFolder.CorePicture}%`))),
},
select: ['id', 'avatarUrl'],
});
if (workspaceMembers.length === 0) {
this.logger.log(
`No workspace member avatars to migrate for workspace ${workspaceId}`,
);
return;
}
this.logger.log(
`Found ${workspaceMembers.length} workspace member avatar(s) to migrate in workspace ${workspaceId}`,
);
for (const workspaceMember of workspaceMembers) {
if (!isNonEmptyString(workspaceMember.avatarUrl)) {
this.logger.warn(
`Skipping workspace member ${workspaceMember.id} - invalid avatarUrl`,
);
continue;
}
try {
const { type: fileExtension } = extractFolderPathFilenameAndTypeOrThrow(
workspaceMember.avatarUrl,
);
const fileId = v4();
const newFilename = `${fileId}${isNonEmptyString(fileExtension) ? `.${fileExtension}` : ''}`;
const newResourcePath = `${FileFolder.CorePicture}/${newFilename}`;
if (!isDryRun) {
await this.fileStorageService.copyLegacy({
from: {
folderPath: `workspace-${workspaceId}`,
filename: workspaceMember.avatarUrl,
},
to: {
folderPath: `${workspaceId}/${workspaceCustomFlatApplication.universalIdentifier}`,
filename: newResourcePath,
},
});
const fileEntity = fileRepository.create({
id: fileId,
path: newResourcePath,
workspaceId,
applicationId: workspaceCustomFlatApplication.id,
size: -1,
settings: {
isTemporaryFile: false,
toDelete: false,
},
});
await fileRepository.save(fileEntity);
const signedUrl = this.fileUrlService.signFileByIdUrl({
fileId,
workspaceId,
fileFolder: FileFolder.CorePicture,
});
await workspaceMemberRepository.update(
{ id: workspaceMember.id },
{
avatarUrl: signedUrl,
},
);
}
this.logger.log(
`Migrated workspace member avatar ${workspaceMember.id}`,
);
} catch (error) {
this.logger.error(
`Failed to migrate workspace member avatar ${workspaceMember.id} in workspace ${workspaceId}: ${error.message}`,
);
throw error;
}
}
if (!isDryRun) {
await this.featureFlagService.enableFeatureFlags(
[FeatureFlagKey.IS_CORE_PICTURE_MIGRATED],
workspaceId,
);
}
}
}
@@ -8,12 +8,13 @@ import { MigrateActivityRichTextAttachmentFileIdsCommand } from 'src/database/co
import { MigrateAttachmentFilesCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-migrate-attachment-files.command';
import { MigrateFavoritesToNavigationMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-migrate-favorites-to-navigation-menu-items.command';
import { MigratePersonAvatarFilesCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-migrate-person-avatar-files.command';
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 { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { FileStorageModule } from 'src/engine/core-modules/file-storage/file-storage.module';
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
import { FilesFieldModule } from 'src/engine/core-modules/file/files-field/files-field.module';
import { FileModule } from 'src/engine/core-modules/file/file.module';
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
@@ -21,11 +22,12 @@ import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/
import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity';
import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@Module({
imports: [
@@ -35,18 +37,18 @@ import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/perso
PersonWorkspaceEntity,
FileEntity,
AttachmentWorkspaceEntity,
WorkspaceMemberWorkspaceEntity,
ObjectMetadataEntity,
FieldMetadataEntity,
]),
DataSourceModule,
FeatureFlagModule,
FileStorageModule.forRoot(),
WorkspaceCacheModule,
WorkspaceCacheStorageModule,
WorkspaceMetadataVersionModule,
FieldMetadataModule,
ApplicationModule,
FilesFieldModule,
FileModule,
UserWorkspaceModule,
WorkspaceMigrationModule,
],
@@ -55,18 +57,22 @@ import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/perso
MigrateFavoritesToNavigationMenuItemsCommand,
MigrateAttachmentFilesCommand,
BackfillFileSizeAndMimeTypeCommand,
MigrateWorkspacePicturesCommand,
MigrateActivityRichTextAttachmentFileIdsCommand,
BackfillMessageChannelThrottleRetryAfterCommand,
BackfillStandardViewsAndFieldMetadataCommand,
MigrateWorkflowSendEmailAttachmentsCommand,
],
exports: [
MigratePersonAvatarFilesCommand,
MigrateFavoritesToNavigationMenuItemsCommand,
MigrateAttachmentFilesCommand,
BackfillFileSizeAndMimeTypeCommand,
MigrateActivityRichTextAttachmentFileIdsCommand,
BackfillMessageChannelThrottleRetryAfterCommand,
BackfillStandardViewsAndFieldMetadataCommand,
MigrateWorkspacePicturesCommand,
BackfillFileSizeAndMimeTypeCommand,
MigrateWorkflowSendEmailAttachmentsCommand,
],
})
export class V1_18_UpgradeVersionCommandModule {}
@@ -25,6 +25,8 @@ import { MigrateActivityRichTextAttachmentFileIdsCommand } from 'src/database/co
import { MigrateAttachmentFilesCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-migrate-attachment-files.command';
import { MigrateFavoritesToNavigationMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-migrate-favorites-to-navigation-menu-items.command';
import { MigratePersonAvatarFilesCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-migrate-person-avatar-files.command';
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 { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
@@ -63,6 +65,8 @@ export class UpgradeCommand extends UpgradeCommandRunner {
protected readonly migrateActivityRichTextAttachmentFileIdsCommand: MigrateActivityRichTextAttachmentFileIdsCommand,
protected readonly backfillMessageChannelThrottleRetryAfterCommand: BackfillMessageChannelThrottleRetryAfterCommand,
protected readonly backfillStandardViewsAndFieldMetadataCommand: BackfillStandardViewsAndFieldMetadataCommand,
protected readonly migrateWorkspacePicturesCommand: MigrateWorkspacePicturesCommand,
protected readonly migrateWorkflowSendEmailAttachmentsCommand: MigrateWorkflowSendEmailAttachmentsCommand,
) {
super(
workspaceRepository,
@@ -92,6 +96,8 @@ export class UpgradeCommand extends UpgradeCommandRunner {
this.migratePersonAvatarFilesCommand,
this.migrateAttachmentFilesCommand,
this.migrateActivityRichTextAttachmentFileIdsCommand,
this.migrateWorkspacePicturesCommand,
this.migrateWorkflowSendEmailAttachmentsCommand,
this.backfillFileSizeAndMimeTypeCommand,
this.backfillMessageChannelThrottleRetryAfterCommand,
this.backfillStandardViewsAndFieldMetadataCommand,
@@ -0,0 +1,31 @@
import { type MigrationInterface, type QueryRunner } from 'typeorm';
export class AddLogoFileIdColumnOnWorkspaceTable1771323022170
implements MigrationInterface
{
name = 'AddLogoFileIdColumnOnWorkspaceTable1771323022170';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."workspace" ADD "logoFileId" uuid`,
);
await queryRunner.query(
`ALTER TABLE "core"."workspace" ADD CONSTRAINT "UQ_282123b2f32e927b6003311e33a" UNIQUE ("logoFileId")`,
);
await queryRunner.query(
`ALTER TABLE "core"."workspace" ADD CONSTRAINT "FK_282123b2f32e927b6003311e33a" FOREIGN KEY ("logoFileId") REFERENCES "core"."file"("id") ON DELETE SET NULL ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."workspace" DROP CONSTRAINT "FK_282123b2f32e927b6003311e33a"`,
);
await queryRunner.query(
`ALTER TABLE "core"."workspace" DROP CONSTRAINT "UQ_282123b2f32e927b6003311e33a"`,
);
await queryRunner.query(
`ALTER TABLE "core"."workspace" DROP COLUMN "logoFileId"`,
);
}
}