Fix attachment body not being loaded (#12770)
Closes https://github.com/twentyhq/twenty/issues/12756
This commit is contained in:
3
.vscode/launch.json
vendored
3
.vscode/launch.json
vendored
@ -5,6 +5,7 @@
|
|||||||
{
|
{
|
||||||
"name": "twenty-server - start debug",
|
"name": "twenty-server - start debug",
|
||||||
"type": "node",
|
"type": "node",
|
||||||
|
"runtimeVersion": "22.12",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"runtimeExecutable": "npx",
|
"runtimeExecutable": "npx",
|
||||||
"runtimeArgs": [
|
"runtimeArgs": [
|
||||||
@ -37,7 +38,7 @@
|
|||||||
"type": "node",
|
"type": "node",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"runtimeExecutable": "npx",
|
"runtimeExecutable": "npx",
|
||||||
"runtimeVersion": "^22.12.0",
|
"runtimeVersion": "22.12",
|
||||||
"runtimeArgs": [
|
"runtimeArgs": [
|
||||||
"nx",
|
"nx",
|
||||||
"run",
|
"run",
|
||||||
|
|||||||
@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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 { 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 { FileService } from 'src/engine/core-modules/file/services/file.service';
|
||||||
import { NoteWorkspaceEntity } from 'src/modules/note/standard-objects/note.workspace-entity';
|
import { NoteWorkspaceEntity } from 'src/modules/note/standard-objects/note.workspace-entity';
|
||||||
import { TaskWorkspaceEntity } from 'src/modules/task/standard-objects/task.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[];
|
type RichTextBody = RichTextBlock[];
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
export class ActivityQueryResultGetterHandler
|
export class ActivityQueryResultGetterHandler
|
||||||
implements QueryResultGetterHandlerInterface
|
implements QueryResultGetterHandlerInterface
|
||||||
{
|
{
|
||||||
constructor(
|
constructor(private readonly fileService: FileService) {}
|
||||||
private readonly fileService: FileService,
|
|
||||||
private readonly featureFlagService: FeatureFlagService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async handle(
|
async handle(
|
||||||
activity: TaskWorkspaceEntity | NoteWorkspaceEntity,
|
activity: TaskWorkspaceEntity | NoteWorkspaceEntity,
|
||||||
@ -50,12 +49,22 @@ export class ActivityQueryResultGetterHandler
|
|||||||
}
|
}
|
||||||
|
|
||||||
const imageProps = block.props;
|
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({
|
const signedPath = this.fileService.signFileUrl({
|
||||||
url: imageProps.url.toString(),
|
url: `attachment/${fileName}`,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -63,7 +72,7 @@ export class ActivityQueryResultGetterHandler
|
|||||||
...block,
|
...block,
|
||||||
props: {
|
props: {
|
||||||
...imageProps,
|
...imageProps,
|
||||||
url: signedPath,
|
url: `${process.env.SERVER_URL}/files/${signedPath}`,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -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 { 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 { 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 { 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 { FileService } from 'src/engine/core-modules/file/services/file.service';
|
||||||
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||||
import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
|
import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
|
||||||
@ -34,10 +33,7 @@ export class QueryResultGettersFactory {
|
|||||||
);
|
);
|
||||||
private handlers: Map<string, QueryResultGetterHandlerInterface>;
|
private handlers: Map<string, QueryResultGetterHandlerInterface>;
|
||||||
|
|
||||||
constructor(
|
constructor(private readonly fileService: FileService) {
|
||||||
private readonly fileService: FileService,
|
|
||||||
private readonly featureFlagService: FeatureFlagService,
|
|
||||||
) {
|
|
||||||
this.initializeHandlers();
|
this.initializeHandlers();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,20 +45,8 @@ export class QueryResultGettersFactory {
|
|||||||
'workspaceMember',
|
'workspaceMember',
|
||||||
new WorkspaceMemberQueryResultGetterHandler(this.fileService),
|
new WorkspaceMemberQueryResultGetterHandler(this.fileService),
|
||||||
],
|
],
|
||||||
[
|
['note', new ActivityQueryResultGetterHandler(this.fileService)],
|
||||||
'note',
|
['task', new ActivityQueryResultGetterHandler(this.fileService)],
|
||||||
new ActivityQueryResultGetterHandler(
|
|
||||||
this.fileService,
|
|
||||||
this.featureFlagService,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'task',
|
|
||||||
new ActivityQueryResultGetterHandler(
|
|
||||||
this.fileService,
|
|
||||||
this.featureFlagService,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user