Files - Migrate attachments in activities (#17808)

As attachment files have migrated from fullPath to file files field,
need to migrate richText logic to fit to new attachment file handling +
data migration
This commit is contained in:
Etienne
2026-02-13 10:11:23 +01:00
committed by GitHub
parent 90a30263ac
commit 5c2c588885
31 changed files with 798 additions and 141 deletions
@@ -0,0 +1,21 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@ObjectType('FilesFieldFile')
export class FilesFieldFileDTO {
@Field(() => UUIDScalarType)
id: string;
@Field()
path: string;
@Field()
size: number;
@Field(() => Date, { nullable: false })
createdAt: Date;
@Field()
url: string;
}
@@ -6,7 +6,7 @@ import { Readable } from 'stream';
import { msg } from '@lingui/core/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { FileFolder } from 'twenty-shared/types';
import { Repository } from 'typeorm';
import { Like, Repository } from 'typeorm';
import { v4 } from 'uuid';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
@@ -16,6 +16,7 @@ import {
} from 'src/engine/core-modules/auth/types/auth-context.type';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
import { FilesFieldFileDTO } from 'src/engine/core-modules/file/files-field/dtos/files-field-file.dto';
import { extractFileInfo } from 'src/engine/core-modules/file/utils/extract-file-info.utils';
import { removeFileFolderFromFileEntityPath } from 'src/engine/core-modules/file/utils/remove-file-folder-from-file-entity-path.utils';
import { sanitizeFile } from 'src/engine/core-modules/file/utils/sanitize-file.utils';
@@ -52,7 +53,7 @@ export class FilesFieldService {
filename: string;
workspaceId: string;
fieldMetadataId: string;
}): Promise<FileEntity> {
}): Promise<FilesFieldFileDTO> {
const { mimeType, ext } = await extractFileInfo({
file,
filename,
@@ -78,7 +79,7 @@ export class FilesFieldService {
},
});
return await this.fileStorageService.writeFile({
const savedFile = await this.fileStorageService.writeFile({
sourceFile: sanitizedFile,
resourcePath: `${fieldMetadata.universalIdentifier}/${name}`,
mimeType,
@@ -91,6 +92,11 @@ export class FilesFieldService {
toDelete: false,
},
});
return {
...savedFile,
url: this.signFileUrl({ fileId, workspaceId }),
};
}
async deleteFilesFieldFile({
@@ -127,10 +133,8 @@ export class FilesFieldService {
const file = await this.fileRepository.findOneOrFail({
where: {
id: fileId,
path: Like(`${FileFolder.FilesField}/%`),
workspaceId,
application: {
workspaceId,
},
},
});
@@ -6,7 +6,7 @@ import { PermissionFlagType } from 'twenty-shared/constants';
import type { FileUpload } from 'graphql-upload/processRequest.mjs';
import { FileDTO } from 'src/engine/core-modules/file/dtos/file.dto';
import { FilesFieldFileDTO } from 'src/engine/core-modules/file/files-field/dtos/files-field-file.dto';
import { FilesFieldService } from 'src/engine/core-modules/file/files-field/files-field.service';
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
@@ -24,7 +24,7 @@ import { streamToBuffer } from 'src/utils/stream-to-buffer';
export class FilesFieldResolver {
constructor(private readonly filesFieldService: FilesFieldService) {}
@Mutation(() => FileDTO)
@Mutation(() => FilesFieldFileDTO)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.UPLOAD_FILE))
async uploadFilesFieldFile(
@AuthWorkspace()
@@ -37,7 +37,7 @@ export class FilesFieldResolver {
nullable: false,
})
fieldMetadataId: string,
): Promise<FileDTO> {
): Promise<FilesFieldFileDTO> {
const stream = createReadStream();
const buffer = await streamToBuffer(stream);
@@ -0,0 +1,22 @@
import { isDefined, isValidUuid } from 'twenty-shared/utils';
export const extractFileIdFromUrl = (url: string): string | null => {
let parsedUrl: URL;
try {
parsedUrl = new URL(url);
} catch {
return null;
}
const pathname = parsedUrl.pathname;
const isLinkExternal = !pathname.startsWith('/files-field/');
if (isLinkExternal) {
return null;
}
const fileId = pathname.match(/files-field\/([^/]+)/)?.[1];
return isDefined(fileId) && isValidUuid(fileId) ? fileId : null;
};