Fix - Remove signFileUrl method (#19121)
`signFileUrl` is the old way to resolve file url. Replaced by `signFileByIdUrl` which needs `FileFolder` and `fileId`. Fix also avatar for person and workspaceMember. To be continued with imageIdentifier refactor. Fix : - https://discord.com/channels/1130383047699738754/1486723854910099576 - https://discord.com/channels/1130383047699738754/1484566445584285870 - https://discord.com/channels/1130383047699738754/1487124612889313420 --------- Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
This commit is contained in:
+1
-9
@@ -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<string, QueryResultGetterHandlerInterface>([
|
||||
['attachment', new AttachmentQueryResultGetterHandler(this.fileService)],
|
||||
['person', new PersonQueryResultGetterHandler(this.fileService)],
|
||||
[
|
||||
'workspaceMember',
|
||||
new WorkspaceMemberQueryResultGetterHandler(this.fileUrlService),
|
||||
|
||||
-1
@@ -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;
|
||||
|
||||
|
||||
-31
@@ -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<AttachmentWorkspaceEntity> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
-29
@@ -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<PersonWorkspaceEntity> {
|
||||
if (!person.id || !person?.avatarUrl) {
|
||||
return person;
|
||||
}
|
||||
|
||||
const signedPath = this.fileService.signFileUrl({
|
||||
url: person.avatarUrl,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
return {
|
||||
...person,
|
||||
avatarUrl: signedPath,
|
||||
};
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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: {},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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<UserEntity>,
|
||||
@@ -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,
|
||||
|
||||
+8
-7
@@ -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<ApprovedAccessDomainEntity>,
|
||||
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: {
|
||||
|
||||
+3
-3
@@ -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'),
|
||||
},
|
||||
|
||||
@@ -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<FileTokenJwtPayloadLegacy, 'type' | 'sub'>,
|
||||
) {
|
||||
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}`;
|
||||
|
||||
|
||||
+30
-7
@@ -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<string, unknown>;
|
||||
flatObjectMetadata: FlatObjectMetadata;
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
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;
|
||||
|
||||
+4
-4
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
: '';
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -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: {},
|
||||
},
|
||||
{
|
||||
|
||||
+7
-6
@@ -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<UserWorkspaceEntit
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly userRoleService: UserRoleService,
|
||||
private readonly fileCorePictureService: FileCorePictureService,
|
||||
private readonly fileService: FileService,
|
||||
private readonly fileUrlService: FileUrlService,
|
||||
private readonly onboardingService: OnboardingService,
|
||||
) {
|
||||
super(userWorkspaceRepository);
|
||||
@@ -503,12 +503,13 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspaceEntit
|
||||
id: workspace.id,
|
||||
displayName: workspace.displayName,
|
||||
workspaceUrls: this.workspaceDomainsService.getWorkspaceUrls(workspace),
|
||||
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,
|
||||
: '',
|
||||
sso:
|
||||
workspace.workspaceSSOIdentityProviders?.reduce(
|
||||
(acc, identityProvider) =>
|
||||
|
||||
+16
-4
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -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'),
|
||||
},
|
||||
|
||||
+8
-7
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+7
-5
@@ -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,
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
+4
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user