File storage cleaning (#18381)

- Remove feature flag
- Remove legacy methods in file-upload and file-service
- Migrate AI Chat to new file management

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Etienne
2026-03-04 23:46:03 +01:00
committed by GitHub
parent c41a8e2b23
commit 26f0a416a1
117 changed files with 859 additions and 3239 deletions
@@ -1,531 +0,0 @@
import { Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { Repository } from 'typeorm';
import { v4 as uuidv4 } 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 { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import {
LogicFunctionEntity,
LogicFunctionRuntime,
} from 'src/engine/metadata-modules/logic-function/logic-function.entity';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { WorkflowVersionStatus } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
import { WorkflowStatus } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity';
import { WorkflowActionType } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action-type.enum';
import { WorkflowTriggerType } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type';
import {
DEFAULT_BUILT_HANDLER_PATH,
DEFAULT_HANDLER_NAME,
DEFAULT_SOURCE_HANDLER_PATH,
} from 'src/engine/metadata-modules/logic-function/constants/handler.contant';
const OLD_BUILT_FOLDER = 'built-function';
const OLD_SOURCE_FOLDER = 'serverless-function';
const SEED_VERSION_DRAFT = 'draft';
const SEED_VERSION_PUBLISHED = '1';
const OUTPUT_SCHEMA_LINK = {
link: {
tab: 'test',
icon: 'IconVariable',
label: 'Generate Function Output',
isLeaf: true,
},
_outputSchemaType: 'LINK',
} as const;
const OUTPUT_SCHEMA_MESSAGE = {
message: {
type: 'string',
label: 'message',
value: 'Hello, input: null and null',
isLeaf: true,
},
} as const;
@Command({
name: 'upgrade:1-17:seed-workflow-v1-16',
description:
'[Temporary] Clean existing workflow runs, workflow versions, workflows, logic functions and old file storage, then seed 3 scenarios: (1) draft+active with LINK outputSchema, (2) draft-only with message outputSchema, (3) draft+active with mixed outputSchema (message on draft, LINK on active). For testing the 1-17 migrate-workflow-code-steps command.',
})
export class SeedWorkflowV1_16Command extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
protected readonly logger = new Logger(SeedWorkflowV1_16Command.name);
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
@InjectRepository(LogicFunctionEntity)
private readonly logicFunctionRepository: Repository<LogicFunctionEntity>,
protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
private readonly applicationService: ApplicationService,
private readonly fileStorageService: FileStorageService,
private readonly recordPositionService: RecordPositionService,
) {
super(workspaceRepository, globalWorkspaceOrmManager, dataSourceService);
}
override async runOnWorkspace({
workspaceId,
}: RunOnWorkspaceArgs): Promise<void> {
this.logger.log(`Seeding workflow v1.16 data for workspace ${workspaceId}`);
await this.cleanWorkflowsAndOldFileStorage(workspaceId);
const workflowRepository =
await this.globalWorkspaceOrmManager.getRepository(
workspaceId,
'workflow',
{
shouldBypassPermissionChecks: true,
},
);
const workflowVersionRepository =
await this.globalWorkspaceOrmManager.getRepository(
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
);
await this.seedScenarioDraftAndActiveLink(
workspaceId,
workflowRepository,
workflowVersionRepository,
);
await this.seedScenarioDraftOnlyMessage(
workspaceId,
workflowRepository,
workflowVersionRepository,
);
await this.seedScenarioDraftAndActiveMixedOutputSchema(
workspaceId,
workflowRepository,
workflowVersionRepository,
);
this.logger.log(
`Seeded 3 workflows (draft+active LINK, draft-only message, draft+active mixed outputSchema) in workspace ${workspaceId}. Run upgrade:1-17:migrate-workflow-code-steps to test migration.`,
);
}
private buildCodeStep(
logicFunctionId: string,
version: string,
outputSchema: object,
stepName: string,
) {
return {
id: uuidv4(),
name: stepName,
type: WorkflowActionType.CODE,
settings: {
input: {
serverlessFunctionId: logicFunctionId,
serverlessFunctionInput: {},
serverlessFunctionVersion: version,
},
outputSchema,
},
valid: true,
};
}
private buildTrigger(): {
name: string;
type: string;
settings: { outputSchema: object };
} {
return {
name: 'trigger',
type: WorkflowTriggerType.MANUAL,
settings: { outputSchema: {} },
};
}
private async seedScenarioDraftAndActiveLink(
workspaceId: string,
workflowRepository: Awaited<
ReturnType<GlobalWorkspaceOrmManager['getRepository']>
>,
workflowVersionRepository: Awaited<
ReturnType<GlobalWorkspaceOrmManager['getRepository']>
>,
): Promise<void> {
const logicFunctionId = await this.insertLogicFunctionRow(
workspaceId,
'Seed (draft+active LINK)',
);
await this.writeOldFormatFiles(logicFunctionId, SEED_VERSION_DRAFT);
await this.writeOldFormatFiles(logicFunctionId, SEED_VERSION_PUBLISHED);
const workflowId = uuidv4();
const draftVersionId = uuidv4();
const activeVersionId = uuidv4();
const workflowPosition =
await this.recordPositionService.buildRecordPosition({
value: 'first',
objectMetadata: {
isCustom: false,
nameSingular: 'workflow',
},
workspaceId,
});
const draftVersionPosition =
await this.recordPositionService.buildRecordPosition({
value: 'first',
objectMetadata: {
isCustom: false,
nameSingular: 'workflowVersion',
},
workspaceId,
});
const activeVersionPosition =
await this.recordPositionService.buildRecordPosition({
value: 'last',
objectMetadata: {
isCustom: false,
nameSingular: 'workflowVersion',
},
workspaceId,
});
await workflowRepository.insert({
id: workflowId,
name: 'Seed draft+active (LINK)',
statuses: [WorkflowStatus.DRAFT],
position: workflowPosition,
});
const trigger = this.buildTrigger();
const draftSteps = [
this.buildCodeStep(
logicFunctionId,
SEED_VERSION_DRAFT,
OUTPUT_SCHEMA_LINK,
'Code step (draft)',
),
];
const activeSteps = [
this.buildCodeStep(
logicFunctionId,
SEED_VERSION_PUBLISHED,
OUTPUT_SCHEMA_LINK,
'Code step (v1)',
),
];
await workflowVersionRepository.insert({
id: draftVersionId,
workflowId,
name: 'v1',
status: WorkflowVersionStatus.DRAFT,
trigger,
steps: draftSteps,
position: draftVersionPosition,
});
await workflowVersionRepository.insert({
id: activeVersionId,
workflowId,
name: 'v2',
status: WorkflowVersionStatus.ACTIVE,
trigger,
steps: activeSteps,
position: activeVersionPosition,
});
await workflowRepository.update(workflowId, {
lastPublishedVersionId: activeVersionId,
statuses: [WorkflowStatus.ACTIVE],
});
}
private async seedScenarioDraftOnlyMessage(
workspaceId: string,
workflowRepository: Awaited<
ReturnType<GlobalWorkspaceOrmManager['getRepository']>
>,
workflowVersionRepository: Awaited<
ReturnType<GlobalWorkspaceOrmManager['getRepository']>
>,
): Promise<void> {
const logicFunctionId = await this.insertLogicFunctionRow(
workspaceId,
'Seed (draft-only message)',
);
await this.writeOldFormatFiles(logicFunctionId, SEED_VERSION_DRAFT);
const workflowId = uuidv4();
const draftVersionId = uuidv4();
const workflowPosition =
await this.recordPositionService.buildRecordPosition({
value: 'first',
objectMetadata: {
isCustom: false,
nameSingular: 'workflow',
},
workspaceId,
});
const draftVersionPosition =
await this.recordPositionService.buildRecordPosition({
value: 'first',
objectMetadata: {
isCustom: false,
nameSingular: 'workflowVersion',
},
workspaceId,
});
await workflowRepository.insert({
id: workflowId,
name: 'Seed draft-only (message)',
statuses: [WorkflowStatus.DRAFT],
position: workflowPosition,
});
const trigger = this.buildTrigger();
const draftSteps = [
this.buildCodeStep(
logicFunctionId,
SEED_VERSION_DRAFT,
OUTPUT_SCHEMA_MESSAGE,
'Code step (draft)',
),
];
await workflowVersionRepository.insert({
id: draftVersionId,
workflowId,
name: 'v1',
status: WorkflowVersionStatus.DRAFT,
trigger,
steps: draftSteps,
position: draftVersionPosition,
});
}
private async seedScenarioDraftAndActiveMixedOutputSchema(
workspaceId: string,
workflowRepository: Awaited<
ReturnType<GlobalWorkspaceOrmManager['getRepository']>
>,
workflowVersionRepository: Awaited<
ReturnType<GlobalWorkspaceOrmManager['getRepository']>
>,
): Promise<void> {
const logicFunctionId = await this.insertLogicFunctionRow(
workspaceId,
'Seed (draft+active mixed)',
);
await this.writeOldFormatFiles(logicFunctionId, SEED_VERSION_DRAFT);
await this.writeOldFormatFiles(logicFunctionId, SEED_VERSION_PUBLISHED);
const workflowId = uuidv4();
const draftVersionId = uuidv4();
const activeVersionId = uuidv4();
const workflowPosition =
await this.recordPositionService.buildRecordPosition({
value: 'first',
objectMetadata: {
isCustom: false,
nameSingular: 'workflow',
},
workspaceId,
});
const draftVersionPosition =
await this.recordPositionService.buildRecordPosition({
value: 'first',
objectMetadata: {
isCustom: false,
nameSingular: 'workflowVersion',
},
workspaceId,
});
const activeVersionPosition =
await this.recordPositionService.buildRecordPosition({
value: 'last',
objectMetadata: {
isCustom: false,
nameSingular: 'workflowVersion',
},
workspaceId,
});
await workflowRepository.insert({
id: workflowId,
name: 'Seed draft+active (mixed)',
statuses: [WorkflowStatus.DRAFT],
position: workflowPosition,
});
const trigger = this.buildTrigger();
const draftSteps = [
this.buildCodeStep(
logicFunctionId,
SEED_VERSION_DRAFT,
OUTPUT_SCHEMA_MESSAGE,
'Code step (draft, message)',
),
];
const activeSteps = [
this.buildCodeStep(
logicFunctionId,
SEED_VERSION_PUBLISHED,
OUTPUT_SCHEMA_LINK,
'Code step (v1, LINK)',
),
];
await workflowVersionRepository.insert({
id: draftVersionId,
workflowId,
name: 'v1',
status: WorkflowVersionStatus.DRAFT,
trigger,
steps: draftSteps,
position: draftVersionPosition,
});
await workflowVersionRepository.insert({
id: activeVersionId,
workflowId,
name: 'v2',
status: WorkflowVersionStatus.ACTIVE,
trigger,
steps: activeSteps,
position: activeVersionPosition,
});
await workflowRepository.update(workflowId, {
lastPublishedVersionId: activeVersionId,
statuses: [WorkflowStatus.ACTIVE],
});
}
private async insertLogicFunctionRow(
workspaceId: string,
name: string = 'Seed code step (v1.16)',
): Promise<string> {
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
const applicationId = workspaceCustomFlatApplication.id;
const id = uuidv4();
const universalIdentifier = uuidv4();
const now = new Date();
await this.logicFunctionRepository.insert({
id,
workspaceId,
universalIdentifier,
applicationId,
name,
description: 'Temporary logic function for 1.17 migration testing',
sourceHandlerPath: DEFAULT_SOURCE_HANDLER_PATH,
builtHandlerPath: DEFAULT_BUILT_HANDLER_PATH,
handlerName: DEFAULT_HANDLER_NAME,
runtime: LogicFunctionRuntime.NODE22,
timeoutSeconds: 300,
checksum: null,
toolInputSchema: null,
isTool: false,
cronTriggerSettings: null,
databaseEventTriggerSettings: null,
httpRouteTriggerSettings: null,
createdAt: now,
updatedAt: now,
deletedAt: null,
});
return id;
}
private async cleanWorkflowsAndOldFileStorage(
workspaceId: string,
): Promise<void> {
const workflowRunRepository =
await this.globalWorkspaceOrmManager.getRepository(
workspaceId,
'workflowRun',
{ shouldBypassPermissionChecks: true },
);
const workflowVersionRepository =
await this.globalWorkspaceOrmManager.getRepository(
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
);
const workflowRepository =
await this.globalWorkspaceOrmManager.getRepository(
workspaceId,
'workflow',
{
shouldBypassPermissionChecks: true,
},
);
const deletedRuns = await workflowRunRepository.delete({});
const deletedVersions = await workflowVersionRepository.delete({});
const deletedWorkflows = await workflowRepository.delete({});
const deletedLogicFunctions = await this.logicFunctionRepository.delete({
workspaceId,
});
this.logger.log(
`Cleaned workspace ${workspaceId}: ${deletedRuns.affected ?? 0} workflow run(s), ${deletedVersions.affected ?? 0} workflow version(s), ${deletedWorkflows.affected ?? 0} workflow(s), ${deletedLogicFunctions.affected ?? 0} logic function(s)`,
);
try {
await this.fileStorageService.deleteLegacy({
folderPath: OLD_BUILT_FOLDER,
});
await this.fileStorageService.deleteLegacy({
folderPath: OLD_SOURCE_FOLDER,
});
this.logger.log(
`Cleaned old file storage: ${OLD_BUILT_FOLDER}, ${OLD_SOURCE_FOLDER}`,
);
} catch (error) {
this.logger.warn(
`Old file storage cleanup skipped (folders may not exist): ${error instanceof Error ? error.message : String(error)}`,
);
}
}
private async writeOldFormatFiles(
logicFunctionId: string,
version: string,
): Promise<void> {
const builtFolder = `${OLD_BUILT_FOLDER}/${logicFunctionId}/${version}`;
const sourceFolder = `${OLD_SOURCE_FOLDER}/${logicFunctionId}/${version}`;
const builtSources = {
'index.mjs':
'export default async function main() { return { message: "ok" }; }\n',
};
const sourceSources = {
src: {
'index.ts':
'export default async function main(): Promise<{ message: string }> {\n return { message: "ok" };\n}\n',
},
};
await this.fileStorageService.writeFolderLegacy(builtSources, builtFolder);
await this.fileStorageService.writeFolderLegacy(
sourceSources,
sourceFolder,
);
}
}
@@ -11,7 +11,6 @@ import { MigrateDateTimeIsFilterValuesCommand } from 'src/database/commands/upgr
import { MigrateNoteTargetToMorphRelationsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-migrate-note-target-to-morph-relations.command';
import { MigrateSendEmailRecipientsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-migrate-send-email-recipients.command';
import { MigrateTaskTargetToMorphRelationsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-migrate-task-target-to-morph-relations.command';
import { SeedWorkflowV1_16Command } from 'src/database/commands/upgrade-version-command/1-17/1-17-seed-workflow-v1-16.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';
@@ -76,7 +75,6 @@ import { TaskTargetWorkspaceEntity } from 'src/modules/task/standard-objects/tas
DeleteFileRecordsAndUpdateTableCommand,
MigrateSendEmailRecipientsCommand,
MigrateDateTimeIsFilterValuesCommand,
SeedWorkflowV1_16Command,
BackfillApplicationPackageFilesCommand,
],
exports: [
@@ -89,7 +87,6 @@ import { TaskTargetWorkspaceEntity } from 'src/modules/task/standard-objects/tas
MigrateSendEmailRecipientsCommand,
MigrateDateTimeIsFilterValuesCommand,
DeleteFileRecordsAndUpdateTableCommand,
SeedWorkflowV1_16Command,
BackfillApplicationPackageFilesCommand,
],
})
@@ -3,7 +3,7 @@ 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, FeatureFlagKey } from 'twenty-shared/types';
import { FileFolder } from 'twenty-shared/types';
import {
extractFolderPathFilenameAndTypeOrThrow,
isDefined,
@@ -14,7 +14,6 @@ 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 { 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';
@@ -36,7 +35,7 @@ type RichTextBlock = Record<string, unknown>;
@Command({
name: 'upgrade:1-18:migrate-activity-rich-text-attachment-file-ids',
description:
'Migrate activity rich text blocks to include attachmentFileId from attachment.file field',
'[DEPRECATED] Migrate activity rich text blocks - this migration is now complete and no longer needed',
})
export class MigrateActivityRichTextAttachmentFileIdsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@@ -44,7 +43,6 @@ export class MigrateActivityRichTextAttachmentFileIdsCommand extends ActiveOrSus
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,
@@ -57,23 +55,20 @@ export class MigrateActivityRichTextAttachmentFileIdsCommand extends ActiveOrSus
override async runOnWorkspace({
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
const isDryRun = options.dryRun ?? false;
const isFilesFieldMigrated = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_FILES_FIELD_MIGRATED,
workspaceId,
this.logger.log(
`[DEPRECATED] Activity rich text attachment file IDs migration is no longer needed for workspace ${workspaceId}. ` +
`The IS_FILES_FIELD_MIGRATED feature flag has been removed as all workspaces are now migrated.`,
);
}
if (isFilesFieldMigrated) {
this.logger.log(
`Files field already migrated for workspace ${workspaceId}, skipping`,
);
return;
}
private _deprecatedMigrationLogic = async ({
workspaceId,
isDryRun,
}: {
workspaceId: string;
isDryRun: boolean;
}) => {
this.logger.log(
`${isDryRun ? '[DRY RUN] ' : ''}Starting activity rich text attachment file IDs migration for workspace ${workspaceId}`,
);
@@ -171,18 +166,11 @@ export class MigrateActivityRichTextAttachmentFileIdsCommand extends ActiveOrSus
isDryRun,
});
if (!isDryRun) {
await this.featureFlagService.enableFeatureFlags(
[FeatureFlagKey.IS_FILES_FIELD_MIGRATED],
workspaceId,
);
}
this.logger.log(
`${isDryRun ? '[DRY RUN] ' : ''}Completed activity rich text attachment file IDs migration for workspace ${workspaceId}`,
);
}, systemAuthContext);
}
};
private async migrateActivityTable({
activityRepository,
@@ -3,11 +3,7 @@ import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { isNonEmptyString } from '@sniptt/guards';
import { Command } from 'nest-commander';
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import {
FieldMetadataType,
FileFolder,
FeatureFlagKey,
} from 'twenty-shared/types';
import { FieldMetadataType, FileFolder } from 'twenty-shared/types';
import {
extractFolderPathFilenameAndTypeOrThrow,
isDefined,
@@ -19,7 +15,6 @@ import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util';
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
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';
@@ -35,7 +30,7 @@ import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objec
@Command({
name: 'upgrade:1-18:migrate-attachment-files',
description:
'Migrate attachment files to file field: copy files and create file records',
'[DEPRECATED] Migrate attachment files to file field - this migration is now complete and no longer needed',
})
export class MigrateAttachmentFilesCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@@ -43,7 +38,6 @@ export class MigrateAttachmentFilesCommand extends ActiveOrSuspendedWorkspacesMi
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 fieldMetadataService: FieldMetadataService,
@@ -56,23 +50,20 @@ export class MigrateAttachmentFilesCommand extends ActiveOrSuspendedWorkspacesMi
override async runOnWorkspace({
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
const isDryRun = options.dryRun ?? false;
const isMigrated = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_FILES_FIELD_MIGRATED,
workspaceId,
this.logger.log(
`[DEPRECATED] Attachment files migration is no longer needed for workspace ${workspaceId}. ` +
`The IS_FILES_FIELD_MIGRATED feature flag has been removed as all workspaces are now migrated.`,
);
}
if (isMigrated) {
this.logger.log(
`Attachment files migration already completed for workspace ${workspaceId}, skipping`,
);
return;
}
private _deprecatedMigrationLogic = async ({
workspaceId,
isDryRun,
}: {
workspaceId: string;
isDryRun: boolean;
}) => {
this.logger.log(
`${isDryRun ? '[DRY RUN] ' : ''}Starting attachment files migration for workspace ${workspaceId}`,
);
@@ -302,5 +293,5 @@ export class MigrateAttachmentFilesCommand extends ActiveOrSuspendedWorkspacesMi
`${isDryRun ? '[DRY RUN] ' : ''}Completed attachment files migration for workspace ${workspaceId}`,
);
}, systemAuthContext);
}
};
}
@@ -3,11 +3,7 @@ import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { isNonEmptyString } from '@sniptt/guards';
import { Command } from 'nest-commander';
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import {
FieldMetadataType,
FileFolder,
FeatureFlagKey,
} from 'twenty-shared/types';
import { FieldMetadataType, FileFolder } from 'twenty-shared/types';
import {
assertIsDefinedOrThrow,
extractFolderPathFilenameAndTypeOrThrow,
@@ -20,7 +16,6 @@ import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util';
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
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';
@@ -36,7 +31,7 @@ import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/perso
@Command({
name: 'upgrade:1-18:migrate-person-avatar-files',
description:
'Migrate person avatarUrl files to file field: copy files and create file records',
'[DEPRECATED] Migrate person avatarUrl files to file field - this migration is now complete and no longer needed',
})
export class MigratePersonAvatarFilesCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@@ -44,7 +39,6 @@ export class MigratePersonAvatarFilesCommand extends ActiveOrSuspendedWorkspaces
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 fieldMetadataService: FieldMetadataService,
@@ -57,23 +51,20 @@ export class MigratePersonAvatarFilesCommand extends ActiveOrSuspendedWorkspaces
override async runOnWorkspace({
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
const isDryRun = options.dryRun ?? false;
const isMigrated = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_FILES_FIELD_MIGRATED,
workspaceId,
this.logger.log(
`[DEPRECATED] Person avatar files migration is no longer needed for workspace ${workspaceId}. ` +
`The IS_FILES_FIELD_MIGRATED feature flag has been removed as all workspaces are now migrated.`,
);
}
if (isMigrated) {
this.logger.log(
`Person avatar files migration already completed for workspace ${workspaceId}, skipping`,
);
return;
}
private _deprecatedMigrationLogic = async ({
workspaceId,
isDryRun,
}: {
workspaceId: string;
isDryRun: boolean;
}) => {
this.logger.log(
`${
isDryRun ? '[DRY RUN] ' : ''
@@ -304,5 +295,5 @@ export class MigratePersonAvatarFilesCommand extends ActiveOrSuspendedWorkspaces
`${isDryRun ? '[DRY RUN] ' : ''}Completed person avatar files migration for workspace ${workspaceId}`,
);
}, systemAuthContext);
}
};
}
@@ -3,14 +3,13 @@ import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { isNonEmptyString } from '@sniptt/guards';
import { Command } from 'nest-commander';
import { FileFolder, FeatureFlagKey } from 'twenty-shared/types';
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 { 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';
@@ -47,7 +46,7 @@ type SendEmailStep = {
@Command({
name: 'upgrade:1-18:migrate-workflow-send-email-attachments',
description:
'Migrate workflow send email attachments to FileFolder.Workflow and update payload paths',
'[DEPRECATED] Migrate workflow send email attachments - this migration is now complete and no longer needed',
})
export class MigrateWorkflowSendEmailAttachmentsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
protected readonly logger = new Logger(
@@ -59,7 +58,6 @@ export class MigrateWorkflowSendEmailAttachmentsCommand extends ActiveOrSuspende
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()
@@ -70,14 +68,21 @@ export class MigrateWorkflowSendEmailAttachmentsCommand extends ActiveOrSuspende
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,
this.logger.log(
`[DEPRECATED] Workflow send email attachments migration is no longer needed for workspace ${workspaceId}. ` +
`The IS_OTHER_FILE_MIGRATED feature flag has been removed as all workspaces are now migrated.`,
);
}
private _deprecatedMigrationLogic = async ({
workspaceId,
isDryRun,
}: {
workspaceId: string;
isDryRun: boolean;
}) => {
const isMigrated = false;
if (isMigrated) {
this.logger.log(
@@ -199,15 +204,8 @@ export class MigrateWorkflowSendEmailAttachmentsCommand extends ActiveOrSuspende
}
}
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}`,
);
}
};
}
@@ -4,7 +4,7 @@ import { isNonEmptyString } from '@sniptt/guards';
import FileType from 'file-type';
import { Command } from 'nest-commander';
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import { FileFolder, FeatureFlagKey } from 'twenty-shared/types';
import { FileFolder } from 'twenty-shared/types';
import {
extractFolderPathFilenameAndTypeOrThrow,
isDefined,
@@ -16,7 +16,6 @@ import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/
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 { 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';
@@ -33,7 +32,7 @@ import { getImageBufferFromUrl } from 'src/utils/image';
@Command({
name: 'upgrade:1-18:migrate-workspace-pictures',
description:
'Migrate workspace logos and workspace member avatars to file records',
'[DEPRECATED] Migrate workspace logos and workspace member avatars to file records - this migration is now complete and no longer needed',
})
export class MigrateWorkspacePicturesCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@@ -41,7 +40,6 @@ export class MigrateWorkspacePicturesCommand extends ActiveOrSuspendedWorkspaces
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,
@@ -55,59 +53,10 @@ export class MigrateWorkspacePicturesCommand extends ActiveOrSuspendedWorkspaces
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}`,
`[DEPRECATED] Workspace pictures migration is no longer needed for workspace ${workspaceId}. ` +
`The IS_CORE_PICTURE_MIGRATED feature flag has been removed as all workspaces are now migrated.`,
);
}
@@ -426,12 +375,5 @@ export class MigrateWorkspacePicturesCommand extends ActiveOrSuspendedWorkspaces
throw error;
}
}
if (!isDryRun) {
await this.featureFlagService.enableFeatureFlags(
[FeatureFlagKey.IS_CORE_PICTURE_MIGRATED],
workspaceId,
);
}
}
}