diff --git a/packages/twenty-server/src/engine/api/common/common-result-getters/common-result-getters.service.ts b/packages/twenty-server/src/engine/api/common/common-result-getters/common-result-getters.service.ts index b0eb84d0d9..c0321483f0 100644 --- a/packages/twenty-server/src/engine/api/common/common-result-getters/common-result-getters.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-result-getters/common-result-getters.service.ts @@ -12,11 +12,8 @@ import { type QueryResultGetterHandlerInterface } from 'src/engine/api/graphql/w import { FilesFieldQueryResultGetterHandler } from 'src/engine/api/common/common-result-getters/handlers/field-handlers/files-field-query-result-getter.handler'; import { RichTextFieldQueryResultGetterHandler } from 'src/engine/api/common/common-result-getters/handlers/field-handlers/rich-text-field-query-result-getter.handler'; -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 { 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'; import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; @@ -39,18 +36,13 @@ export class CommonResultGettersService { QueryResultGetterHandlerInterface >; - constructor( - private readonly fileService: FileService, - private readonly fileUrlService: FileUrlService, - ) { + constructor(private readonly fileUrlService: FileUrlService) { this.initializeObjectHandlers(); this.initializeFieldHandlers(); } private initializeObjectHandlers() { this.objectHandlers = new Map([ - ['attachment', new AttachmentQueryResultGetterHandler(this.fileService)], - ['person', new PersonQueryResultGetterHandler(this.fileService)], [ 'workspaceMember', new WorkspaceMemberQueryResultGetterHandler(this.fileUrlService), diff --git a/packages/twenty-server/src/engine/api/common/common-result-getters/handlers/field-handlers/__tests__/rich-text-field-query-result-getter.handler.spec.ts b/packages/twenty-server/src/engine/api/common/common-result-getters/handlers/field-handlers/__tests__/rich-text-field-query-result-getter.handler.spec.ts index 100f8aefe4..45cfdb11a7 100644 --- a/packages/twenty-server/src/engine/api/common/common-result-getters/handlers/field-handlers/__tests__/rich-text-field-query-result-getter.handler.spec.ts +++ b/packages/twenty-server/src/engine/api/common/common-result-getters/handlers/field-handlers/__tests__/rich-text-field-query-result-getter.handler.spec.ts @@ -19,7 +19,6 @@ const richTextFieldMetadata = [ ] as FlatFieldMetadata[]; const mockFileUrlService = { - signFileUrl: jest.fn().mockReturnValue('signed-path'), signFileByIdUrl: jest.fn().mockReturnValue('signed-path'), } as unknown as FileUrlService; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/attachment-query-result-getter.handler.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/attachment-query-result-getter.handler.ts deleted file mode 100644 index 5c14f45528..0000000000 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/attachment-query-result-getter.handler.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { type QueryResultGetterHandlerInterface } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-getter-handler.interface'; - -import { type FileService } from 'src/engine/core-modules/file/services/file.service'; -import { type AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity'; - -export class AttachmentQueryResultGetterHandler - implements QueryResultGetterHandlerInterface -{ - constructor(private readonly fileService: FileService) {} - - async handle( - attachment: AttachmentWorkspaceEntity, - workspaceId: string, - ): Promise { - if (!attachment.id || !attachment?.fullPath) { - return attachment; - } - - const signedPath = this.fileService.signFileUrl({ - url: attachment.fullPath, - workspaceId, - }); - - const fullPath = `${process.env.SERVER_URL}/files/${signedPath}`; - - return { - ...attachment, - fullPath, - }; - } -} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/person-query-result-getter.handler.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/person-query-result-getter.handler.ts deleted file mode 100644 index 389cc908be..0000000000 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/person-query-result-getter.handler.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { type QueryResultGetterHandlerInterface } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-getter-handler.interface'; - -import { type FileService } from 'src/engine/core-modules/file/services/file.service'; -import { type PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity'; - -export class PersonQueryResultGetterHandler - implements QueryResultGetterHandlerInterface -{ - constructor(private readonly fileService: FileService) {} - - async handle( - person: PersonWorkspaceEntity, - workspaceId: string, - ): Promise { - if (!person.id || !person?.avatarUrl) { - return person; - } - - const signedPath = this.fileService.signFileUrl({ - url: person.avatarUrl, - workspaceId, - }); - - return { - ...person, - avatarUrl: signedPath, - }; - } -} diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/__tests__/admin-panel.service.spec.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/__tests__/admin-panel.service.spec.ts index 24db2aefed..b963d78993 100644 --- a/packages/twenty-server/src/engine/core-modules/admin-panel/__tests__/admin-panel.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/__tests__/admin-panel.service.spec.ts @@ -5,7 +5,7 @@ import { AdminPanelService } from 'src/engine/core-modules/admin-panel/admin-pan import { AuditService } from 'src/engine/core-modules/audit/services/audit.service'; import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service'; import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; -import { FileService } from 'src/engine/core-modules/file/services/file.service'; +import { FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service'; import { SecureHttpClientService } from 'src/engine/core-modules/secure-http-client/secure-http-client.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { UserEntity } from 'src/engine/core-modules/user/user.entity'; @@ -85,7 +85,7 @@ describe('AdminPanelService', () => { }, }, { - provide: FileService, + provide: FileUrlService, useValue: {}, }, { diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts index 7a92b4f132..dfd383df69 100644 --- a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts @@ -3,10 +3,10 @@ import { InjectRepository } from '@nestjs/typeorm'; import { msg } from '@lingui/core/macro'; import semver from 'semver'; +import { FeatureFlagKey, FileFolder } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { Repository } from 'typeorm'; import * as z from 'zod'; -import { FeatureFlagKey } from 'twenty-shared/types'; import { type ConfigVariableDTO } from 'src/engine/core-modules/admin-panel/dtos/config-variable.dto'; import { type ConfigVariablesGroupDataDTO } from 'src/engine/core-modules/admin-panel/dtos/config-variables-group.dto'; @@ -19,7 +19,7 @@ import { } from 'src/engine/core-modules/auth/auth.exception'; import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; import { type FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; -import { FileService } from 'src/engine/core-modules/file/services/file.service'; +import { FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service'; import { SecureHttpClientService } from 'src/engine/core-modules/secure-http-client/secure-http-client.service'; import { type ConfigVariables } from 'src/engine/core-modules/twenty-config/config-variables'; import { CONFIG_VARIABLES_GROUP_METADATA } from 'src/engine/core-modules/twenty-config/constants/config-variables-group-metadata'; @@ -33,7 +33,7 @@ export class AdminPanelService { constructor( private readonly twentyConfigService: TwentyConfigService, private readonly workspaceDomainsService: WorkspaceDomainsService, - private readonly fileService: FileService, + private readonly fileUrlService: FileUrlService, private readonly secureHttpClientService: SecureHttpClientService, @InjectRepository(UserEntity) private readonly userRepository: Repository, @@ -81,12 +81,13 @@ export class AdminPanelService { id: userWorkspace.workspace.id, name: userWorkspace.workspace.displayName ?? '', totalUsers: userWorkspace.workspace.workspaceUsers.length, - logo: userWorkspace.workspace.logo - ? this.fileService.signFileUrl({ - url: userWorkspace.workspace.logo, + logo: isDefined(userWorkspace.workspace.logoFileId) + ? this.fileUrlService.signFileByIdUrl({ + fileId: userWorkspace.workspace.logoFileId, workspaceId: userWorkspace.workspace.id, + fileFolder: FileFolder.CorePicture, }) - : userWorkspace.workspace.logo, + : undefined, allowImpersonation: userWorkspace.workspace.allowImpersonation, workspaceUrls: this.workspaceDomainsService.getWorkspaceUrls({ subdomain: userWorkspace.workspace.subdomain, diff --git a/packages/twenty-server/src/engine/core-modules/approved-access-domain/services/approved-access-domain.service.ts b/packages/twenty-server/src/engine/core-modules/approved-access-domain/services/approved-access-domain.service.ts index ce5555646c..1f3b09a0db 100644 --- a/packages/twenty-server/src/engine/core-modules/approved-access-domain/services/approved-access-domain.service.ts +++ b/packages/twenty-server/src/engine/core-modules/approved-access-domain/services/approved-access-domain.service.ts @@ -6,7 +6,7 @@ import crypto from 'crypto'; import { msg } from '@lingui/core/macro'; import { render } from '@react-email/render'; import { SendApprovedAccessDomainValidation } from 'twenty-emails'; -import { SettingsPath } from 'twenty-shared/types'; +import { FileFolder, SettingsPath } from 'twenty-shared/types'; import { getSettingsPath, isDefined } from 'twenty-shared/utils'; import { Repository } from 'typeorm'; @@ -18,7 +18,7 @@ import { import { approvedAccessDomainValidator } from 'src/engine/core-modules/approved-access-domain/approved-access-domain.validate'; import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; import { EmailService } from 'src/engine/core-modules/email/email.service'; -import { FileService } from 'src/engine/core-modules/file/services/file.service'; +import { FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; @@ -31,7 +31,7 @@ export class ApprovedAccessDomainService { private readonly approvedAccessDomainRepository: Repository, private readonly emailService: EmailService, private readonly twentyConfigService: TwentyConfigService, - private readonly fileService: FileService, + private readonly fileUrlService: FileUrlService, private readonly workspaceDomainsService: WorkspaceDomainsService, ) {} @@ -78,12 +78,13 @@ export class ApprovedAccessDomainService { link: link.toString(), workspace: { name: workspace.displayName, - logo: workspace.logo - ? this.fileService.signFileUrl({ - url: workspace.logo, + logo: isDefined(workspace.logoFileId) + ? this.fileUrlService.signFileByIdUrl({ + fileId: workspace.logoFileId, workspaceId: workspace.id, + fileFolder: FileFolder.CorePicture, }) - : workspace.logo, + : undefined, }, domain: approvedAccessDomain.domain, sender: { diff --git a/packages/twenty-server/src/engine/core-modules/approved-access-domain/services/approved-access-domain.spec.ts b/packages/twenty-server/src/engine/core-modules/approved-access-domain/services/approved-access-domain.spec.ts index 6d2489258f..32a3ad11f7 100644 --- a/packages/twenty-server/src/engine/core-modules/approved-access-domain/services/approved-access-domain.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/approved-access-domain/services/approved-access-domain.spec.ts @@ -12,11 +12,11 @@ import { } from 'src/engine/core-modules/approved-access-domain/approved-access-domain.exception'; import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; import { EmailService } from 'src/engine/core-modules/email/email.service'; -import { FileService } from 'src/engine/core-modules/file/services/file.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; +import { FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service'; import { ApprovedAccessDomainService } from './approved-access-domain.service'; // To avoid dynamic import issues in Jest @@ -69,9 +69,9 @@ describe('ApprovedAccessDomainService', () => { }, }, { - provide: FileService, + provide: FileUrlService, useValue: { - signFileUrl: jest + signFileByIdUrl: jest .fn() .mockReturnValue('https://signed-url.com/logo.png'), }, diff --git a/packages/twenty-server/src/engine/core-modules/file/services/file.service.ts b/packages/twenty-server/src/engine/core-modules/file/services/file.service.ts index 79be22c723..1cf234676a 100644 --- a/packages/twenty-server/src/engine/core-modules/file/services/file.service.ts +++ b/packages/twenty-server/src/engine/core-modules/file/services/file.service.ts @@ -3,24 +3,15 @@ import { InjectRepository } from '@nestjs/typeorm'; import { type Readable } from 'stream'; -import { isNonEmptyString } from '@sniptt/guards'; import { FileFolder } from 'twenty-shared/types'; -import { - buildSignedPath, - extractFolderPathFilenameAndTypeOrThrow, -} from 'twenty-shared/utils'; import { Like, Repository } from 'typeorm'; import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; -import { - type FileTokenJwtPayloadLegacy, - JwtTokenTypeEnum, -} from 'src/engine/core-modules/auth/types/auth-context.type'; import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity'; -import { removeFileFolderFromFileEntityPath } from 'src/engine/core-modules/file/utils/remove-file-folder-from-file-entity-path.utils'; import { type FileResponse } from 'src/engine/core-modules/file/types/file-response.type'; import { getContentDisposition } from 'src/engine/core-modules/file/utils/get-content-disposition.utils'; +import { removeFileFolderFromFileEntityPath } from 'src/engine/core-modules/file/utils/remove-file-folder-from-file-entity-path.utils'; import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { streamToBuffer } from 'src/utils/stream-to-buffer'; @@ -198,44 +189,7 @@ export class FileService { }; } - signFileUrl({ url, workspaceId }: { url: string; workspaceId: string }) { - if (!isNonEmptyString(url)) { - return url; - } - - return buildSignedPath({ - path: url, - token: this.encodeFileToken({ - filename: extractFolderPathFilenameAndTypeOrThrow(url).filename, - workspaceId, - }), - }); - } - - encodeFileToken( - payloadToEncode: Omit, - ) { - const fileTokenExpiresIn = this.twentyConfigService.get( - 'FILE_TOKEN_EXPIRES_IN', - ); - - const payload: FileTokenJwtPayloadLegacy = { - ...payloadToEncode, - sub: payloadToEncode.workspaceId, - type: JwtTokenTypeEnum.FILE, - }; - - const secret = this.jwtWrapperService.generateAppSecret( - payload.type, - payloadToEncode.workspaceId, - ); - - return this.jwtWrapperService.sign(payload, { - secret, - expiresIn: fileTokenExpiresIn, - }); - } - + /** @deprecated Use FileStorageService.deleteByFileId instead */ async deleteFile({ folderPath, filename, @@ -253,6 +207,7 @@ export class FileService { }); } + /** @deprecated */ async deleteWorkspaceFolder(workspaceId: string) { const workspaceFolderPath = `workspace-${workspaceId}`; diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/utils/get-record-image-identifier.util.ts b/packages/twenty-server/src/engine/core-modules/record-crud/utils/get-record-image-identifier.util.ts index 14f55d51a2..d4559d89eb 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/utils/get-record-image-identifier.util.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/utils/get-record-image-identifier.util.ts @@ -1,16 +1,19 @@ import { isNonEmptyString } from '@sniptt/guards'; import { getLogoUrlFromDomainName, isDefined } from 'twenty-shared/utils'; +import { FileOutput } from 'src/engine/api/common/common-args-processors/data-arg-processor/types/file-item.type'; +import { extractFileIdFromUrl } from 'src/engine/core-modules/file/files-field/utils/extract-file-id-from-url.util'; import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; +import { FileFolder } from 'twenty-shared/types'; type GetRecordImageIdentifierOptions = { record: Record; flatObjectMetadata: FlatObjectMetadata; flatFieldMetadataMaps: FlatEntityMaps; - signUrl?: (url: string) => string | null; + signUrl?: (fileId: string, fileFolder: FileFolder) => string | null; }; export const getRecordImageIdentifier = ({ @@ -30,6 +33,30 @@ export const getRecordImageIdentifier = ({ : null; } + //TODO: Temporary solution before imageIdentifier refactor + if (signUrl && flatObjectMetadata.nameSingular === 'person') { + const avatarFileId = (record.avatarFile as FileOutput[])?.[0]?.fileId; + if (!isDefined(avatarFileId)) { + return null; + } + return signUrl(avatarFileId, FileFolder.FilesField); + } + + if ( + signUrl && + flatObjectMetadata.nameSingular === 'workspaceMember' && + isDefined(record.avatarUrl) + ) { + const avatarFileId = extractFileIdFromUrl( + record.avatarUrl as string, + FileFolder.CorePicture, + ); + if (!isDefined(avatarFileId)) { + return null; + } + return signUrl(avatarFileId, FileFolder.CorePicture); + } + if (!isDefined(flatObjectMetadata.imageIdentifierFieldMetadataId)) { return null; } @@ -55,12 +82,8 @@ export const getRecordImageIdentifier = ({ return null; } - if ( - signUrl && - (flatObjectMetadata.nameSingular === 'person' || - flatObjectMetadata.nameSingular === 'workspaceMember') - ) { - return signUrl(rawImageValue); + if (signUrl && flatObjectMetadata.nameSingular === 'workspaceMember') { + return signUrl(rawImageValue, FileFolder.FilesField); } return rawImageValue; diff --git a/packages/twenty-server/src/engine/core-modules/search/__tests__/search.service.spec.ts b/packages/twenty-server/src/engine/core-modules/search/__tests__/search.service.spec.ts index 4aded65c77..ef47e8ac78 100644 --- a/packages/twenty-server/src/engine/core-modules/search/__tests__/search.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/search/__tests__/search.service.spec.ts @@ -5,7 +5,7 @@ import { mockFlatFieldMetadataMaps, mockFlatObjectMetadatas, } from 'src/engine/core-modules/__mocks__/mockFlatObjectMetadatas'; -import { FileService } from 'src/engine/core-modules/file/services/file.service'; +import { FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service'; import { SearchService } from 'src/engine/core-modules/search/services/search.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; @@ -18,7 +18,7 @@ describe('SearchService', () => { providers: [ SearchService, { provide: GlobalWorkspaceOrmManager, useValue: {} }, - { provide: FileService, useValue: {} }, + { provide: FileUrlService, useValue: {} }, { provide: TwentyConfigService, useValue: { get: () => false } }, ], }).compile(); @@ -105,13 +105,13 @@ describe('SearchService', () => { }); describe('getImageIdentifierColumn', () => { - it('should return null if the object metadata item does not have an image identifier', () => { + it('should return `avatarFile` if the object metadata item is a person', () => { const imageIdentifierColumn = service.getImageIdentifierColumn( mockFlatObjectMetadatas[0], mockFlatFieldMetadataMaps, ); - expect(imageIdentifierColumn).toBeNull(); + expect(imageIdentifierColumn).toEqual('avatarFile'); }); it('should return `domainNamePrimaryLinkUrl` column for a company object metadata item', () => { const imageIdentifierColumn = service.getImageIdentifierColumn( diff --git a/packages/twenty-server/src/engine/core-modules/search/services/search.service.ts b/packages/twenty-server/src/engine/core-modules/search/services/search.service.ts index ed455fa345..05d4669acc 100644 --- a/packages/twenty-server/src/engine/core-modules/search/services/search.service.ts +++ b/packages/twenty-server/src/engine/core-modules/search/services/search.service.ts @@ -3,18 +3,24 @@ import { Injectable } from '@nestjs/common'; import { isNonEmptyString } from '@sniptt/guards'; import chunk from 'lodash.chunk'; import { OBJECTS_WITH_CHANNEL_VISIBILITY_CONSTRAINTS } from 'twenty-shared/constants'; -import { FieldMetadataType, ObjectRecord } from 'twenty-shared/types'; +import { + FieldMetadataType, + FileFolder, + ObjectRecord, +} from 'twenty-shared/types'; import { getLogoUrlFromDomainName, isDefined } from 'twenty-shared/utils'; import { Brackets, type ObjectLiteral } from 'typeorm'; import { type ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; +import { FileOutput } from 'src/engine/api/common/common-args-processors/data-arg-processor/types/file-item.type'; import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser'; import { decodeCursor, encodeCursorData, } from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util'; -import { FileService } from 'src/engine/core-modules/file/services/file.service'; +import { 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 { STANDARD_OBJECTS_BY_PRIORITY_RANK } from 'src/engine/core-modules/search/constants/standard-objects-by-priority-rank'; import { type ObjectRecordFilterInput } from 'src/engine/core-modules/search/dtos/object-record-filter-input'; import { type SearchArgs } from 'src/engine/core-modules/search/dtos/search-args'; @@ -52,7 +58,7 @@ const OBJECT_METADATA_ITEMS_CHUNK_SIZE = 5; export class SearchService { constructor( private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, - private readonly fileService: FileService, + private readonly fileUrlService: FileUrlService, private readonly twentyConfigService: TwentyConfigService, ) {} @@ -512,6 +518,15 @@ export class SearchService { return 'domainNamePrimaryLinkUrl'; } + //TODO: Temporary solution before imageIdentifier refactor + if (flatObjectMetadata.nameSingular === 'person') { + return 'avatarFile'; + } + + if (flatObjectMetadata.nameSingular === 'workspaceMember') { + return 'avatarUrl'; + } + if (!flatObjectMetadata.imageIdentifierFieldMetadataId) { return null; } @@ -528,10 +543,15 @@ export class SearchService { return imageIdentifierField.name; } - private getImageUrlWithToken(avatarUrl: string, workspaceId: string): string { - return this.fileService.signFileUrl({ - url: avatarUrl, + private getImageUrlWithToken( + avatarFileId: string, + fileFolder: FileFolder, + workspaceId: string, + ): string { + return this.fileUrlService.signFileByIdUrl({ + fileId: avatarFileId, workspaceId, + fileFolder, }); } @@ -553,9 +573,41 @@ export class SearchService { return getLogoUrlFromDomainName(record.domainNamePrimaryLinkUrl) || ''; } + //TODO: Temporary solution before imageIdentifier refactor + if (flatObjectMetadata.nameSingular === 'person') { + const avatarFileId = (record.avatarFile as FileOutput[])?.[0]?.fileId; + if (!isDefined(avatarFileId)) { + return ''; + } + return this.getImageUrlWithToken( + avatarFileId, + FileFolder.FilesField, + workspaceId, + ); + } + + if (flatObjectMetadata.nameSingular === 'workspaceMember') { + const avatarFileId = extractFileIdFromUrl( + record.avatarUrl, + FileFolder.CorePicture, + ); + if (!isDefined(avatarFileId)) { + return ''; + } + return this.getImageUrlWithToken( + avatarFileId, + FileFolder.CorePicture, + workspaceId, + ); + } + return imageIdentifierField && isNonEmptyString(record[imageIdentifierField]) - ? this.getImageUrlWithToken(record[imageIdentifierField], workspaceId) + ? this.getImageUrlWithToken( + record[imageIdentifierField], + FileFolder.FilesField, + workspaceId, + ) : ''; } diff --git a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.spec.ts b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.spec.ts index fe2ec36c4e..4805e451c0 100644 --- a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.spec.ts @@ -6,13 +6,13 @@ import { type DataSource, type Repository } from 'typeorm'; import { type ApprovedAccessDomainEntity } from 'src/engine/core-modules/approved-access-domain/approved-access-domain.entity'; import { ApprovedAccessDomainService } from 'src/engine/core-modules/approved-access-domain/services/approved-access-domain.service'; import { AuthException } from 'src/engine/core-modules/auth/auth.exception'; -import { type AuthContextUser } from 'src/engine/core-modules/auth/types/auth-context.type'; import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service'; +import { type AuthContextUser } from 'src/engine/core-modules/auth/types/auth-context.type'; import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; import { FileCorePictureService } from 'src/engine/core-modules/file/file-core-picture/services/file-core-picture.service'; -import { FileService } from 'src/engine/core-modules/file/services/file.service'; +import { FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service'; import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service'; import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; @@ -126,7 +126,7 @@ describe('UserWorkspaceService', () => { useValue: {}, }, { - provide: FileService, + provide: FileUrlService, useValue: {}, }, { diff --git a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts index fdee768692..4168a474a4 100644 --- a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts +++ b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts @@ -19,8 +19,8 @@ import { type AvailableWorkspace } from 'src/engine/core-modules/auth/dto/availa import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service'; import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; import { FileCorePictureService } from 'src/engine/core-modules/file/file-core-picture/services/file-core-picture.service'; +import { 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 { FileService } from 'src/engine/core-modules/file/services/file.service'; import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service'; import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; import { UserEntity } from 'src/engine/core-modules/user/user.entity'; @@ -60,7 +60,7 @@ export class UserWorkspaceService extends TypeOrmQueryService diff --git a/packages/twenty-server/src/engine/core-modules/user/services/workspace-member-transpiler.service.ts b/packages/twenty-server/src/engine/core-modules/user/services/workspace-member-transpiler.service.ts index 01b306901c..c83d4d6888 100644 --- a/packages/twenty-server/src/engine/core-modules/user/services/workspace-member-transpiler.service.ts +++ b/packages/twenty-server/src/engine/core-modules/user/services/workspace-member-transpiler.service.ts @@ -3,7 +3,8 @@ import { Injectable } from '@nestjs/common'; import { isNonEmptyString } from '@sniptt/guards'; import { isDefined } from 'twenty-shared/utils'; -import { FileService } from 'src/engine/core-modules/file/services/file.service'; +import { 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 { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; import { type DeletedWorkspaceMemberDTO } from 'src/engine/core-modules/user/dtos/deleted-workspace-member.dto'; import { type WorkspaceMemberDTO } from 'src/engine/core-modules/user/dtos/workspace-member.dto'; @@ -15,6 +16,7 @@ import { type WorkspaceMemberTimeFormatEnum, type WorkspaceMemberWorkspaceEntity, } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; +import { FileFolder } from 'twenty-shared/types'; export type ToWorkspaceMemberDtoArgs = { workspaceMemberEntity: WorkspaceMemberWorkspaceEntity; @@ -24,7 +26,7 @@ export type ToWorkspaceMemberDtoArgs = { @Injectable() export class WorkspaceMemberTranspiler { - constructor(private readonly fileService: FileService) {} + constructor(private readonly fileUrlService: FileUrlService) {} generateSignedAvatarUrl({ workspaceId, @@ -40,9 +42,19 @@ export class WorkspaceMemberTranspiler { return ''; } - return this.fileService.signFileUrl({ - url: workspaceMember.avatarUrl, + const fileId = extractFileIdFromUrl( + workspaceMember.avatarUrl, + FileFolder.CorePicture, + ); + + if (!isDefined(fileId)) { + return ''; + } + + return this.fileUrlService.signFileByIdUrl({ + fileId, workspaceId, + fileFolder: FileFolder.CorePicture, }); } diff --git a/packages/twenty-server/src/engine/core-modules/workspace-invitation/services/workspace-invitation.service.spec.ts b/packages/twenty-server/src/engine/core-modules/workspace-invitation/services/workspace-invitation.service.spec.ts index 067560d86d..1e8f0607ce 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace-invitation/services/workspace-invitation.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace-invitation/services/workspace-invitation.service.spec.ts @@ -9,18 +9,18 @@ import { } from 'src/engine/core-modules/app-token/app-token.entity'; import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; import { EmailService } from 'src/engine/core-modules/email/email.service'; -import { FileService } from 'src/engine/core-modules/file/services/file.service'; import { I18nService } from 'src/engine/core-modules/i18n/i18n.service'; import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service'; import { ThrottlerService } from 'src/engine/core-modules/throttler/throttler.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; import { WorkspaceInvitationException } from 'src/engine/core-modules/workspace-invitation/workspace-invitation.exception'; -import { RoleValidationService } from 'src/engine/metadata-modules/role-validation/services/role-validation.service'; import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; +import { RoleValidationService } from 'src/engine/metadata-modules/role-validation/services/role-validation.service'; import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; +import { FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service'; import { WorkspaceInvitationService } from './workspace-invitation.service'; // To fix a circular dependency issue @@ -113,9 +113,9 @@ describe('WorkspaceInvitationService', () => { }, }, { - provide: FileService, + provide: FileUrlService, useValue: { - signFileUrl: jest + signFileByIdUrl: jest .fn() .mockReturnValue('https://signed-url.com/logo.png'), }, diff --git a/packages/twenty-server/src/engine/core-modules/workspace-invitation/services/workspace-invitation.service.ts b/packages/twenty-server/src/engine/core-modules/workspace-invitation/services/workspace-invitation.service.ts index 6a3789a1a2..ff781137cc 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace-invitation/services/workspace-invitation.service.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace-invitation/services/workspace-invitation.service.ts @@ -8,7 +8,7 @@ import { render } from '@react-email/render'; import { addMilliseconds } from 'date-fns'; import ms from 'ms'; import { SendInviteLinkEmail } from 'twenty-emails'; -import { AppPath } from 'twenty-shared/types'; +import { AppPath, FileFolder } from 'twenty-shared/types'; import { getAppPath, isDefined } from 'twenty-shared/utils'; import { IsNull, Repository } from 'typeorm'; @@ -22,7 +22,7 @@ import { } from 'src/engine/core-modules/auth/auth.exception'; import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; import { EmailService } from 'src/engine/core-modules/email/email.service'; -import { FileService } from 'src/engine/core-modules/file/services/file.service'; +import { FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service'; import { I18nService } from 'src/engine/core-modules/i18n/i18n.service'; import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service'; import { ThrottlerService } from 'src/engine/core-modules/throttler/throttler.service'; @@ -52,8 +52,8 @@ export class WorkspaceInvitationService { private readonly onboardingService: OnboardingService, private readonly workspaceDomainsService: WorkspaceDomainsService, private readonly i18nService: I18nService, - private readonly fileService: FileService, private readonly throttlerService: ThrottlerService, + private readonly fileUrlService: FileUrlService, ) {} async validatePersonalInvitation({ @@ -318,12 +318,13 @@ export class WorkspaceInvitationService { link: link.toString(), workspace: { name: workspace.displayName, - logo: workspace.logo - ? this.fileService.signFileUrl({ - url: workspace.logo, + logo: isDefined(workspace.logoFileId) + ? this.fileUrlService.signFileByIdUrl({ + fileId: workspace.logoFileId, workspaceId: workspace.id, + fileFolder: FileFolder.CorePicture, }) - : workspace.logo, + : undefined, }, sender: { email: sender.userEmail, diff --git a/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts b/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts index bb70210a5f..0c4e10a120 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts @@ -15,9 +15,10 @@ import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils'; import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator'; import { ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity'; -import { ApplicationDTO } from 'src/engine/core-modules/application/dtos/application.dto'; import { ApplicationService } from 'src/engine/core-modules/application/application.service'; +import { ApplicationDTO } from 'src/engine/core-modules/application/dtos/application.dto'; import { fromFlatApplicationToApplicationDto } from 'src/engine/core-modules/application/utils/from-flat-application-to-application-dto.util'; +import { type AuthContextUser } from 'src/engine/core-modules/auth/types/auth-context.type'; import { BillingEntitlementDTO } from 'src/engine/core-modules/billing/dtos/billing-entitlement.dto'; import { BillingSubscriptionEntity } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; @@ -34,7 +35,6 @@ import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; -import { type AuthContextUser } from 'src/engine/core-modules/auth/types/auth-context.type'; import { ActivateWorkspaceInput } from 'src/engine/core-modules/workspace/dtos/activate-workspace-input'; import { type AuthProvidersDTO, @@ -391,15 +391,12 @@ export class WorkspaceResolver { let workspaceLogoWithToken = ''; - if (workspace.logo) { - try { - workspaceLogoWithToken = this.fileService.signFileUrl({ - url: workspace.logo, - workspaceId: workspace.id, - }); - } catch { - workspaceLogoWithToken = workspace.logo; - } + if (isDefined(workspace.logoFileId)) { + workspaceLogoWithToken = this.fileUrlService.signFileByIdUrl({ + fileId: workspace.logoFileId, + workspaceId: workspace.id, + fileFolder: FileFolder.CorePicture, + }); } return { diff --git a/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/services/navigation-menu-item-record-identifier.service.ts b/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/services/navigation-menu-item-record-identifier.service.ts index b4470d26e9..3c595c54e5 100644 --- a/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/services/navigation-menu-item-record-identifier.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/services/navigation-menu-item-record-identifier.service.ts @@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common'; import { isDefined } from 'twenty-shared/utils'; import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type'; -import { FileService } from 'src/engine/core-modules/file/services/file.service'; +import { FileUrlService } from 'src/engine/core-modules/file/file-url/file-url.service'; import { getRecordDisplayName } from 'src/engine/core-modules/record-crud/utils/get-record-display-name.util'; import { getRecordImageIdentifier } from 'src/engine/core-modules/record-crud/utils/get-record-image-identifier.util'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; @@ -14,13 +14,14 @@ import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspac import { getWorkspaceContext } from 'src/engine/twenty-orm/storage/orm-workspace-context.storage'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; import { resolveRolePermissionConfig } from 'src/engine/twenty-orm/utils/resolve-role-permission-config.util'; +import { FileFolder } from 'twenty-shared/types'; @Injectable() export class NavigationMenuItemRecordIdentifierService { constructor( private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager, - private readonly fileService: FileService, + private readonly fileUrlService: FileUrlService, ) {} async resolveRecordIdentifier({ @@ -124,10 +125,11 @@ export class NavigationMenuItemRecordIdentifierService { record, flatObjectMetadata: objectMetadata, flatFieldMetadataMaps, - signUrl: (url: string) => - this.fileService.signFileUrl({ - url, + signUrl: (fileId: string, fileFolder: FileFolder) => + this.fileUrlService.signFileByIdUrl({ + fileId, workspaceId, + fileFolder, }), }); diff --git a/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/utils/get-minimal-select-for-record-identifier.util.ts b/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/utils/get-minimal-select-for-record-identifier.util.ts index fb640790ed..0792ca2fa4 100644 --- a/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/utils/get-minimal-select-for-record-identifier.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/navigation-menu-item/utils/get-minimal-select-for-record-identifier.util.ts @@ -11,6 +11,7 @@ import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object const ID_FIELD = 'id' as const; const COMPANY_AVATAR_COLUMN = 'domainNamePrimaryLinkUrl' as const; +const PERSON_AVATAR_COLUMN = 'avatarFile' as const; export const getMinimalSelectForRecordIdentifier = ({ flatObjectMetadata, @@ -48,6 +49,9 @@ export const getMinimalSelectForRecordIdentifier = ({ if (flatObjectMetadata.nameSingular === 'company') { selectColumns.push(COMPANY_AVATAR_COLUMN); + //TODO: Temporary solution before imageIdentifier refactor + } else if (flatObjectMetadata.nameSingular === 'person') { + selectColumns.push(PERSON_AVATAR_COLUMN); } else if (isDefined(flatObjectMetadata.imageIdentifierFieldMetadataId)) { const imageField = findFlatEntityByIdInFlatEntityMaps({ flatEntityMaps: flatFieldMetadataMaps,