diff --git a/.vscode/launch.json b/.vscode/launch.json index 6eea92be6..f9f802e42 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -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", diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/__tests__/activity-query-result-getter.handler.spec.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/__tests__/activity-query-result-getter.handler.spec.ts new file mode 100644 index 000000000..5eb57285c --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/__tests__/activity-query-result-getter.handler.spec.ts @@ -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, + ); + }); + + 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', + }, + }, + ]), + }, + }); + }); + }); +}); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/activity-query-result-getter.handler.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/activity-query-result-getter.handler.ts index 6c9c16455..23c73edc0 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/activity-query-result-getter.handler.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/activity-query-result-getter.handler.ts @@ -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; 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}`, }, }; }), diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory.ts index a957d0fce..0444f2140 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory.ts @@ -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; - 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)], ]); }