Fix attachment body not being loaded (#12770)

Closes https://github.com/twentyhq/twenty/issues/12756
This commit is contained in:
Charles Bochet
2025-06-20 17:50:49 +02:00
committed by GitHub
parent a8ff02efc3
commit 94557e7447
4 changed files with 202 additions and 29 deletions

3
.vscode/launch.json vendored
View File

@ -5,6 +5,7 @@
{
"name": "twenty-server - start debug",
"type": "node",
"runtimeVersion": "22.12",
"request": "launch",
"runtimeExecutable": "npx",
"runtimeArgs": [
@ -37,7 +38,7 @@
"type": "node",
"request": "launch",
"runtimeExecutable": "npx",
"runtimeVersion": "^22.12.0",
"runtimeVersion": "22.12",
"runtimeArgs": [
"nx",
"run",

View File

@ -0,0 +1,179 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ActivityQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/activity-query-result-getter.handler';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
import { NoteWorkspaceEntity } from 'src/modules/note/standard-objects/note.workspace-entity';
const baseNote = {
id: '1',
bodyV2: {
markdown: null,
blocknote: null,
},
position: 1,
title: 'Test',
body: 'Test',
createdBy: {
name: 'Test',
source: FieldActorSource.MANUAL,
workspaceMemberId: '1',
context: {},
},
createdAt: '2021-01-01',
updatedAt: '2021-01-01',
noteTargets: [],
attachments: [],
timelineActivities: [],
favorites: [],
searchVector: '',
deletedAt: null,
} satisfies NoteWorkspaceEntity;
const baseTask = {
...baseNote,
type: 'task',
};
describe('ActivityQueryResultGetterHandler', () => {
let handler: ActivityQueryResultGetterHandler;
beforeEach(async () => {
process.env.SERVER_URL = 'https://my-domain.twenty.com';
const module: TestingModule = await Test.createTestingModule({
providers: [
ActivityQueryResultGetterHandler,
{
provide: FileService,
useValue: {
signFileUrl: jest.fn().mockReturnValue('signed-path'),
},
},
],
}).compile();
handler = module.get<ActivityQueryResultGetterHandler>(
ActivityQueryResultGetterHandler,
);
});
afterEach(() => {
jest.clearAllMocks();
delete process.env.SERVER_URL;
});
describe('should do nothing', () => {
it('when activity is a note and no image is found', async () => {
const note = {
...baseNote,
bodyV2: {
markdown: null,
blocknote: JSON.stringify([
{ type: 'paragraph', text: 'Hello, world!' },
]),
},
};
const result = await handler.handle(note, '1');
expect(result).toEqual(note);
});
it('when activity is a note and link is external', async () => {
const note = {
...baseNote,
bodyV2: {
markdown: null,
blocknote: JSON.stringify([
{
id: 'c6a5f700-5e56-480d-90a9-7f295216370e',
type: 'image',
props: {
backgroundColor: 'default',
textAlignment: 'left',
name: '20240529_123208.jpg',
url: 'http://external-content.com/image.jpg',
caption: '',
showPreview: true,
},
children: [],
},
{
id: 'e2454736-51c1-4e61-a02d-71f0890bdda7',
type: 'paragraph',
props: {
textColor: 'default',
backgroundColor: 'default',
textAlignment: 'left',
},
content: [],
children: [],
},
]),
},
};
const result = await handler.handle(note, '1');
expect(result).toEqual(note);
});
it('when activity is a task and no image is found', async () => {
const task = {
...baseTask,
bodyV2: {
markdown: null,
blocknote: null,
},
};
const result = await handler.handle(task, '1');
expect(result).toEqual(task);
});
});
describe('should update token in file link', () => {
it('when file link is in the body', async () => {
const imageBlock = {
id: 'c6a5f700-5e56-480d-90a9-7f295216370e',
type: 'image',
props: {
backgroundColor: 'default',
textAlignment: 'left',
name: '20240529_123208.jpg',
url: 'https://my-domain.twenty.com/files/attachment/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmaWxlbmFtZSI6ImU0NWNiNDhhLTM2MmYtNGU4Zi1iOTEzLWM5MmI1ZTNlMGFhNi5qcGciLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsInN1YiI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsInR5cGUiOiJGSUxFIiwiaWF0IjoxNzUwNDI4NDQ1LCJleHAiOjE3NTA1MTQ4NDV9.qTN1b9IcmZvfVAqt1UlfJ_nn3GwIAEp7G9IoPtRJDxk/e45cb48a-362f-4e8f-b913-c92b5e3e0aa6.jpg',
caption: '',
showPreview: true,
},
children: [],
};
const note = {
...baseNote,
bodyV2: {
markdown: null,
blocknote: JSON.stringify([imageBlock]),
},
};
const result = await handler.handle(note, '1');
expect(result).toEqual({
...note,
bodyV2: {
markdown: null,
blocknote: JSON.stringify([
{
...imageBlock,
props: {
...imageBlock.props,
url: 'https://my-domain.twenty.com/files/signed-path',
},
},
]),
},
});
});
});
});

View File

@ -1,6 +1,7 @@
import { Injectable } from '@nestjs/common';
import { QueryResultGetterHandlerInterface } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-getter-handler.interface';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { NoteWorkspaceEntity } from 'src/modules/note/standard-objects/note.workspace-entity';
import { TaskWorkspaceEntity } from 'src/modules/task/standard-objects/task.workspace-entity';
@ -10,13 +11,11 @@ type RichTextBlock = Record<string, any>;
type RichTextBody = RichTextBlock[];
@Injectable()
export class ActivityQueryResultGetterHandler
implements QueryResultGetterHandlerInterface
{
constructor(
private readonly fileService: FileService,
private readonly featureFlagService: FeatureFlagService,
) {}
constructor(private readonly fileService: FileService) {}
async handle(
activity: TaskWorkspaceEntity | NoteWorkspaceEntity,
@ -50,12 +49,22 @@ export class ActivityQueryResultGetterHandler
}
const imageProps = block.props;
const imageUrl = new URL(imageProps.url);
const url = new URL(imageProps.url);
imageUrl.searchParams.delete('token');
const pathname = url.pathname;
const isLinkExternal = !pathname.startsWith('/files/attachment/');
if (isLinkExternal) {
return block;
}
const fileName = pathname.match(
/files\/attachment\/(?:.+)\/(.+)$/,
)?.[1];
const signedPath = this.fileService.signFileUrl({
url: imageProps.url.toString(),
url: `attachment/${fileName}`,
workspaceId,
});
@ -63,7 +72,7 @@ export class ActivityQueryResultGetterHandler
...block,
props: {
...imageProps,
url: signedPath,
url: `${process.env.SERVER_URL}/files/${signedPath}`,
},
};
}),

View File

@ -19,7 +19,6 @@ import { AttachmentQueryResultGetterHandler } from 'src/engine/api/graphql/works
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 { CompositeInputTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-input-type-definition.factory';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
@ -34,10 +33,7 @@ export class QueryResultGettersFactory {
);
private handlers: Map<string, QueryResultGetterHandlerInterface>;
constructor(
private readonly fileService: FileService,
private readonly featureFlagService: FeatureFlagService,
) {
constructor(private readonly fileService: FileService) {
this.initializeHandlers();
}
@ -49,20 +45,8 @@ export class QueryResultGettersFactory {
'workspaceMember',
new WorkspaceMemberQueryResultGetterHandler(this.fileService),
],
[
'note',
new ActivityQueryResultGetterHandler(
this.fileService,
this.featureFlagService,
),
],
[
'task',
new ActivityQueryResultGetterHandler(
this.fileService,
this.featureFlagService,
),
],
['note', new ActivityQueryResultGetterHandler(this.fileService)],
['task', new ActivityQueryResultGetterHandler(this.fileService)],
]);
}