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",
|
||||
"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",
|
||||
|
||||
@ -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 { 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}`,
|
||||
},
|
||||
};
|
||||
}),
|
||||
|
||||
@ -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)],
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user