Files command - fixes (#18016)

- Fixed "property entity not found" error when updating/creating a new
field and querying the same object repository just after
- Downgraded log type for unnecessary migration
This commit is contained in:
Etienne
2026-02-18 10:09:52 +01:00
committed by GitHub
parent b7274da8fa
commit ee15e034b5
4 changed files with 188 additions and 177 deletions
@@ -444,7 +444,7 @@ export class MigrateActivityRichTextAttachmentFileIdsCommand extends ActiveOrSus
return fileId;
} catch (error) {
this.logger.error(
this.logger.warn(
`Failed to create attachment from URL ${url}: ${error.message}`,
);
@@ -25,6 +25,7 @@ import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata
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 { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity';
@@ -195,102 +196,108 @@ export class MigrateAttachmentFilesCommand extends ActiveOrSuspendedWorkspacesMi
return;
}
const attachmentRepository =
await this.twentyORMGlobalManager.getRepository<AttachmentWorkspaceEntity>(
workspaceId,
'attachment',
{ shouldBypassPermissionChecks: true },
const systemAuthContext = buildSystemAuthContext(workspaceId);
await this.twentyORMGlobalManager.executeInWorkspaceContext(async () => {
const attachmentRepository =
await this.twentyORMGlobalManager.getRepository<AttachmentWorkspaceEntity>(
workspaceId,
'attachment',
{ shouldBypassPermissionChecks: true },
);
const attachments = await attachmentRepository.find({
where: {
fullPath: Not(IsNull()),
file: Or(IsNull(), Equal([])),
},
select: ['id', 'name', 'fullPath'],
});
if (attachments.length === 0) {
this.logger.log(
`No attachments to migrate for workspace ${workspaceId}`,
);
return;
}
this.logger.log(
`Found ${attachments.length} attachment(s) to migrate in workspace ${workspaceId}`,
);
const attachments = await attachmentRepository.find({
where: {
fullPath: Not(IsNull()),
file: Or(IsNull(), Equal([])),
},
select: ['id', 'name', 'fullPath'],
});
const fileRepository = this.coreDataSource.getRepository(FileEntity);
if (attachments.length === 0) {
this.logger.log(`No attachments to migrate for workspace ${workspaceId}`);
return;
}
this.logger.log(
`Found ${attachments.length} attachment(s) to migrate in workspace ${workspaceId}`,
);
const fileRepository = this.coreDataSource.getRepository(FileEntity);
for (const attachment of attachments) {
if (!isNonEmptyString(attachment.fullPath)) {
this.logger.warn(
`Skipping attachment ${attachment.id} - invalid fullPath`,
);
continue;
}
try {
const { type: fileExtension, filename } =
extractFolderPathFilenameAndTypeOrThrow(attachment.fullPath);
const fileId = v4();
const newFilename = `${fileId}${isNonEmptyString(fileExtension) ? `.${fileExtension}` : ''}`;
const newResourcePath = `${FileFolder.FilesField}/${attachmentFileflatFieldMetadata.universalIdentifier}/${newFilename}`;
if (!isDryRun) {
await this.fileStorageService.copyLegacy({
from: {
folderPath: `workspace-${workspaceId}`,
filename: attachment.fullPath,
},
to: {
folderPath: `${workspaceId}/${twentyStandardFlatApplication.universalIdentifier}`,
filename: newResourcePath,
},
});
const fileEntity = fileRepository.create({
id: fileId,
path: newResourcePath,
workspaceId,
applicationId: twentyStandardFlatApplication.id,
size: -1,
settings: {
isTemporaryFile: true,
toDelete: false,
},
});
await fileRepository.save(fileEntity);
await attachmentRepository.update(
{ id: attachment.id },
{
file: [
{
fileId: fileEntity.id,
label: attachment.name || filename,
},
],
},
for (const attachment of attachments) {
if (!isNonEmptyString(attachment.fullPath)) {
this.logger.warn(
`Skipping attachment ${attachment.id} - invalid fullPath`,
);
continue;
}
this.logger.log(
`Migrated attachment ${attachment.id} (${attachment.name})`,
);
} catch (error) {
this.logger.error(
`Failed to migrate attachment ${attachment.id} in workspace ${workspaceId}: ${error.message}`,
);
throw error;
}
}
try {
const { type: fileExtension, filename } =
extractFolderPathFilenameAndTypeOrThrow(attachment.fullPath);
this.logger.log(
`${isDryRun ? '[DRY RUN] ' : ''}Completed attachment files migration for workspace ${workspaceId}`,
);
const fileId = v4();
const newFilename = `${fileId}${isNonEmptyString(fileExtension) ? `.${fileExtension}` : ''}`;
const newResourcePath = `${FileFolder.FilesField}/${attachmentFileflatFieldMetadata.universalIdentifier}/${newFilename}`;
if (!isDryRun) {
await this.fileStorageService.copyLegacy({
from: {
folderPath: `workspace-${workspaceId}`,
filename: attachment.fullPath,
},
to: {
folderPath: `${workspaceId}/${twentyStandardFlatApplication.universalIdentifier}`,
filename: newResourcePath,
},
});
const fileEntity = fileRepository.create({
id: fileId,
path: newResourcePath,
workspaceId,
applicationId: twentyStandardFlatApplication.id,
size: -1,
settings: {
isTemporaryFile: true,
toDelete: false,
},
});
await fileRepository.save(fileEntity);
await attachmentRepository.update(
{ id: attachment.id },
{
file: [
{
fileId: fileEntity.id,
label: attachment.name || filename,
},
],
},
);
}
this.logger.log(
`Migrated attachment ${attachment.id} (${attachment.name})`,
);
} catch (error) {
this.logger.error(
`Failed to migrate attachment ${attachment.id} in workspace ${workspaceId}: ${error.message}`,
);
throw error;
}
}
this.logger.log(
`${isDryRun ? '[DRY RUN] ' : ''}Completed attachment files migration for workspace ${workspaceId}`,
);
}, systemAuthContext);
}
}
@@ -26,6 +26,7 @@ import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata
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 { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
@@ -205,97 +206,100 @@ export class MigratePersonAvatarFilesCommand extends ActiveOrSuspendedWorkspaces
return;
}
const personRepository =
await this.twentyORMGlobalManager.getRepository<PersonWorkspaceEntity>(
workspaceId,
'person',
{ shouldBypassPermissionChecks: true },
);
const systemAuthContext = buildSystemAuthContext(workspaceId);
const personsWithAvatars = await personRepository.find({
where: {
avatarUrl: ILike(`%${FileFolder.PersonPicture}%`),
avatarFile: Or(IsNull(), Equal([])),
},
select: ['id', 'avatarUrl'],
});
if (personsWithAvatars.length === 0) {
this.logger.log(
`No persons with avatarUrl found in workspace ${workspaceId}`,
);
return;
}
this.logger.log(
`Found ${personsWithAvatars.length} person(s) with avatarUrl containing 'people' folder in workspace ${workspaceId}`,
);
const fileRepository = this.coreDataSource.getRepository(FileEntity);
for (const person of personsWithAvatars) {
assertIsDefinedOrThrow(person.avatarUrl);
try {
const { type: fileExtension } = extractFolderPathFilenameAndTypeOrThrow(
person.avatarUrl,
await this.twentyORMGlobalManager.executeInWorkspaceContext(async () => {
const personRepository =
await this.twentyORMGlobalManager.getRepository<PersonWorkspaceEntity>(
workspaceId,
'person',
{ shouldBypassPermissionChecks: true },
);
const fileId = v4();
const newFileName = `${fileId}${isNonEmptyString(fileExtension) ? `.${fileExtension}` : ''}`;
const newResourcePath = `${FileFolder.FilesField}/${avatarFileFieldMetadata.universalIdentifier}/${newFileName}`;
const personsWithAvatars = await personRepository.find({
where: {
avatarUrl: ILike(`%${FileFolder.PersonPicture}%`),
avatarFile: Or(IsNull(), Equal([])),
},
select: ['id', 'avatarUrl'],
});
if (!isDryRun) {
await this.fileStorageService.copyLegacy({
from: {
folderPath: `workspace-${workspaceId}`,
filename: person.avatarUrl,
},
to: {
folderPath: `${workspaceId}/${twentyStandardFlatApplication.universalIdentifier}`,
filename: newResourcePath,
},
});
const fileEntity = fileRepository.create({
id: fileId,
path: newResourcePath,
workspaceId,
applicationId: twentyStandardFlatApplication.id,
size: -1,
settings: {
isTemporaryFile: true,
toDelete: false,
},
});
await fileRepository.save(fileEntity);
await personRepository.update(
{ id: person.id },
{
avatarFile: [
{
fileId: fileEntity.id,
label: newFileName,
},
],
},
);
}
this.logger.log(`Migrated avatar for person ${person.id}`);
} catch (error) {
this.logger.error(
`Failed to migrate avatar for person ${person.id} in workspace ${workspaceId}: ${error.message}`,
if (personsWithAvatars.length === 0) {
this.logger.log(
`No persons with avatarUrl found in workspace ${workspaceId}`,
);
throw error;
return;
}
}
this.logger.log(
`${isDryRun ? '[DRY RUN] ' : ''}Completed person avatar files migration for workspace ${workspaceId}`,
);
this.logger.log(
`Found ${personsWithAvatars.length} person(s) with avatarUrl containing 'people' folder in workspace ${workspaceId}`,
);
const fileRepository = this.coreDataSource.getRepository(FileEntity);
for (const person of personsWithAvatars) {
assertIsDefinedOrThrow(person.avatarUrl);
try {
const { type: fileExtension } =
extractFolderPathFilenameAndTypeOrThrow(person.avatarUrl);
const fileId = v4();
const newFileName = `${fileId}${isNonEmptyString(fileExtension) ? `.${fileExtension}` : ''}`;
const newResourcePath = `${FileFolder.FilesField}/${avatarFileFieldMetadata.universalIdentifier}/${newFileName}`;
if (!isDryRun) {
await this.fileStorageService.copyLegacy({
from: {
folderPath: `workspace-${workspaceId}`,
filename: person.avatarUrl,
},
to: {
folderPath: `${workspaceId}/${twentyStandardFlatApplication.universalIdentifier}`,
filename: newResourcePath,
},
});
const fileEntity = fileRepository.create({
id: fileId,
path: newResourcePath,
workspaceId,
applicationId: twentyStandardFlatApplication.id,
size: -1,
settings: {
isTemporaryFile: true,
toDelete: false,
},
});
await fileRepository.save(fileEntity);
await personRepository.update(
{ id: person.id },
{
avatarFile: [
{
fileId: fileEntity.id,
label: newFileName,
},
],
},
);
}
this.logger.log(`Migrated avatar for person ${person.id}`);
} catch (error) {
this.logger.error(
`Failed to migrate avatar for person ${person.id} in workspace ${workspaceId}: ${error.message}`,
);
throw error;
}
}
this.logger.log(
`${isDryRun ? '[DRY RUN] ' : ''}Completed person avatar files migration for workspace ${workspaceId}`,
);
}, systemAuthContext);
}
}
@@ -71,8 +71,8 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
BackfillMessageChannelThrottleRetryAfterCommand,
BackfillStandardViewsAndFieldMetadataCommand,
MigrateWorkspacePicturesCommand,
BackfillFileSizeAndMimeTypeCommand,
MigrateWorkflowSendEmailAttachmentsCommand,
BackfillFileSizeAndMimeTypeCommand,
],
})
export class V1_18_UpgradeVersionCommandModule {}