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:
+2
-12
@@ -15,7 +15,6 @@ import { RichTextV2FieldQueryResultGetterHandler } from 'src/engine/api/common/c
|
||||
import { AttachmentQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/attachment-query-result-getter.handler';
|
||||
import { PersonQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/person-query-result-getter.handler';
|
||||
import { WorkspaceMemberQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/workspace-member-query-result-getter.handler';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service';
|
||||
import { FileService } from 'src/engine/core-modules/file/services/file.service';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
@@ -43,7 +42,6 @@ export class CommonResultGettersService {
|
||||
constructor(
|
||||
private readonly fileService: FileService,
|
||||
private readonly fileUrlService: FileUrlService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
) {
|
||||
this.initializeObjectHandlers();
|
||||
this.initializeFieldHandlers();
|
||||
@@ -55,11 +53,7 @@ export class CommonResultGettersService {
|
||||
['person', new PersonQueryResultGetterHandler(this.fileService)],
|
||||
[
|
||||
'workspaceMember',
|
||||
new WorkspaceMemberQueryResultGetterHandler(
|
||||
this.fileService,
|
||||
this.featureFlagService,
|
||||
this.fileUrlService,
|
||||
),
|
||||
new WorkspaceMemberQueryResultGetterHandler(this.fileUrlService),
|
||||
],
|
||||
]);
|
||||
}
|
||||
@@ -75,11 +69,7 @@ export class CommonResultGettersService {
|
||||
],
|
||||
[
|
||||
FieldMetadataType.RICH_TEXT_V2,
|
||||
new RichTextV2FieldQueryResultGetterHandler(
|
||||
this.fileService,
|
||||
this.fileUrlService,
|
||||
this.featureFlagService,
|
||||
),
|
||||
new RichTextV2FieldQueryResultGetterHandler(this.fileUrlService),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
+2
-63
@@ -1,9 +1,7 @@
|
||||
import { FieldMetadataType, type ObjectRecord } from 'twenty-shared/types';
|
||||
|
||||
import { RichTextV2FieldQueryResultGetterHandler } from 'src/engine/api/common/common-result-getters/handlers/field-handlers/rich-text-v2-field-query-result-getter.handler';
|
||||
import { type FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { type FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service';
|
||||
import { type FileService } from 'src/engine/core-modules/file/services/file.service';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
|
||||
const baseRecord: ObjectRecord = {
|
||||
@@ -20,28 +18,17 @@ const richTextFieldMetadata = [
|
||||
},
|
||||
] as FlatFieldMetadata[];
|
||||
|
||||
const mockFileService = {
|
||||
signFileUrl: jest.fn().mockReturnValue('signed-path'),
|
||||
} as unknown as FileService;
|
||||
|
||||
const mockFileUrlService = {
|
||||
signFileUrl: jest.fn().mockReturnValue('signed-path'),
|
||||
signFileByIdUrl: jest.fn().mockReturnValue('signed-path'),
|
||||
} as unknown as FileUrlService;
|
||||
|
||||
const mockFeatureFlagService = {
|
||||
isFeatureEnabled: jest.fn().mockReturnValue(true),
|
||||
} as unknown as FeatureFlagService;
|
||||
|
||||
describe('RichTextV2FieldQueryResultGetterHandler', () => {
|
||||
let handler: RichTextV2FieldQueryResultGetterHandler;
|
||||
|
||||
beforeEach(() => {
|
||||
process.env.SERVER_URL = 'https://my-domain.twenty.com';
|
||||
handler = new RichTextV2FieldQueryResultGetterHandler(
|
||||
mockFileService,
|
||||
mockFileUrlService,
|
||||
mockFeatureFlagService,
|
||||
);
|
||||
handler = new RichTextV2FieldQueryResultGetterHandler(mockFileUrlService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -169,54 +156,6 @@ describe('RichTextV2FieldQueryResultGetterHandler', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('should sign internal image URLs', () => {
|
||||
it('when image block has an internal attachment URL (legacy path)', async () => {
|
||||
jest
|
||||
.spyOn(mockFeatureFlagService, 'isFeatureEnabled')
|
||||
.mockResolvedValue(false);
|
||||
|
||||
const imageBlock = {
|
||||
type: 'image',
|
||||
props: {
|
||||
name: 'photo.jpg',
|
||||
url: 'https://my-domain.twenty.com/files/attachment/some-token/photo.jpg',
|
||||
caption: '',
|
||||
},
|
||||
children: [],
|
||||
};
|
||||
|
||||
const record = {
|
||||
...baseRecord,
|
||||
bodyV2: {
|
||||
markdown: null,
|
||||
blocknote: JSON.stringify([imageBlock]),
|
||||
},
|
||||
};
|
||||
|
||||
const result = await handler.handle(
|
||||
record,
|
||||
'ws-1',
|
||||
richTextFieldMetadata,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
...baseRecord,
|
||||
bodyV2: {
|
||||
markdown: null,
|
||||
blocknote: JSON.stringify([
|
||||
{
|
||||
...imageBlock,
|
||||
props: {
|
||||
...imageBlock.props,
|
||||
url: 'https://my-domain.twenty.com/files/signed-path',
|
||||
},
|
||||
},
|
||||
]),
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('should handle multiple RICH_TEXT_V2 fields', () => {
|
||||
it('when record has multiple rich text fields', async () => {
|
||||
const multiFieldMetadata = [
|
||||
|
||||
+11
-61
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
FeatureFlagKey,
|
||||
FieldMetadataType,
|
||||
FileFolder,
|
||||
type ObjectRecord,
|
||||
@@ -8,10 +7,8 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type QueryResultGetterHandlerInterface } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-getter-handler.interface';
|
||||
|
||||
import { type FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { type 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 { type FileService } from 'src/engine/core-modules/file/services/file.service';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@@ -36,11 +33,7 @@ const parseBlocknoteJsonSafely = (
|
||||
export class RichTextV2FieldQueryResultGetterHandler
|
||||
implements QueryResultGetterHandlerInterface
|
||||
{
|
||||
constructor(
|
||||
private readonly fileService: FileService,
|
||||
private readonly fileUrlService: FileUrlService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
) {}
|
||||
constructor(private readonly fileUrlService: FileUrlService) {}
|
||||
|
||||
async handle(
|
||||
record: ObjectRecord,
|
||||
@@ -55,11 +48,6 @@ export class RichTextV2FieldQueryResultGetterHandler
|
||||
return record;
|
||||
}
|
||||
|
||||
const isFilesFieldMigrated = await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_FILES_FIELD_MIGRATED,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
for (const field of richTextV2Fields) {
|
||||
const fieldValue = record[field.name];
|
||||
const blocknoteJson = fieldValue?.blocknote;
|
||||
@@ -77,7 +65,6 @@ export class RichTextV2FieldQueryResultGetterHandler
|
||||
const signedBlocks = this.signBlocknoteImageUrls(
|
||||
blocknoteBlocks,
|
||||
workspaceId,
|
||||
isFilesFieldMigrated,
|
||||
);
|
||||
|
||||
record[field.name] = {
|
||||
@@ -92,69 +79,32 @@ export class RichTextV2FieldQueryResultGetterHandler
|
||||
signBlocknoteImageUrls = (
|
||||
blocknoteBlocks: RichTextBlock[],
|
||||
workspaceId: string,
|
||||
isFilesFieldMigrated: boolean,
|
||||
): RichTextBlock[] => {
|
||||
return blocknoteBlocks.map((block: RichTextBlock) => {
|
||||
if (isFilesFieldMigrated && isDefined(block.props?.url)) {
|
||||
const fileIdFromUrl = extractFileIdFromUrl(
|
||||
block.props.url,
|
||||
FileFolder.FilesField,
|
||||
);
|
||||
|
||||
if (!isDefined(fileIdFromUrl)) {
|
||||
return block;
|
||||
}
|
||||
|
||||
const url = this.fileUrlService.signFileByIdUrl({
|
||||
fileId: fileIdFromUrl,
|
||||
workspaceId,
|
||||
fileFolder: FileFolder.FilesField,
|
||||
});
|
||||
|
||||
return {
|
||||
...block,
|
||||
props: {
|
||||
...block.props,
|
||||
url,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (block.type !== 'image' || !block.props?.url) {
|
||||
if (!isDefined(block.props?.url)) {
|
||||
return block;
|
||||
}
|
||||
|
||||
let url: URL;
|
||||
const fileIdFromUrl = extractFileIdFromUrl(
|
||||
block.props.url,
|
||||
FileFolder.FilesField,
|
||||
);
|
||||
|
||||
try {
|
||||
url = new URL(block.props.url);
|
||||
} catch {
|
||||
if (!isDefined(fileIdFromUrl)) {
|
||||
return block;
|
||||
}
|
||||
|
||||
const pathname = url.pathname;
|
||||
const isLinkExternal = !pathname.startsWith('/files/attachment/');
|
||||
|
||||
if (isLinkExternal) {
|
||||
return block;
|
||||
}
|
||||
|
||||
const fileName = pathname.match(/files\/attachment\/(?:.+)\/(.+)$/)?.[1];
|
||||
|
||||
if (!isDefined(fileName)) {
|
||||
return block;
|
||||
}
|
||||
|
||||
const signedPath = this.fileService.signFileUrl({
|
||||
url: `attachment/${fileName}`,
|
||||
const url = this.fileUrlService.signFileByIdUrl({
|
||||
fileId: fileIdFromUrl,
|
||||
workspaceId,
|
||||
fileFolder: FileFolder.FilesField,
|
||||
});
|
||||
|
||||
return {
|
||||
...block,
|
||||
props: {
|
||||
...block.props,
|
||||
url: `${process.env.SERVER_URL}/files/${signedPath}`,
|
||||
url,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user