diff --git a/packages/twenty-server/src/engine/api/__mocks__/object-metadata-item.mock.ts b/packages/twenty-server/src/engine/api/__mocks__/object-metadata-item.mock.ts index bbdb192546..2e286410ac 100644 --- a/packages/twenty-server/src/engine/api/__mocks__/object-metadata-item.mock.ts +++ b/packages/twenty-server/src/engine/api/__mocks__/object-metadata-item.mock.ts @@ -141,10 +141,12 @@ export const fieldRelationMock = getMockFieldMetadataEntity({ nameSingular: 'relationTargetObject', namePlural: 'relationTargetObjects', } as ObjectMetadataEntity, + relationTargetObjectMetadataId: 'relationTargetObjectId', relationTargetFieldMetadata: { id: 'relationTargetFieldId', name: 'relationTargetField', } as FieldMetadataEntity, + relationTargetFieldMetadataId: 'relationTargetFieldId', isLabelSyncedWithName: true, createdAt: new Date(), updatedAt: new Date(), diff --git a/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/__tests__/field-permissions.service.spec.ts b/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/__tests__/field-permissions.service.spec.ts index cf80978116..7f7907e0bc 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/__tests__/field-permissions.service.spec.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/__tests__/field-permissions.service.spec.ts @@ -5,9 +5,11 @@ import { ObjectRecordsPermissionsByRoleId } from 'twenty-shared/types'; import { In, Repository } from 'typeorm'; import { + fieldRelationMock, fieldTextMock, objectMetadataItemMock, } from 'src/engine/api/__mocks__/object-metadata-item.mock'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { UpsertFieldPermissionsInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-field-permissions.input'; import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity'; import { FieldPermissionService } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.service'; @@ -27,6 +29,7 @@ describe('FieldPermissionService', () => { Repository >; let roleRepository: jest.Mocked>; + let fieldMetadataRepository: jest.Mocked>; let workspacePermissionsCacheService: jest.Mocked; let workspaceCacheStorageService: jest.Mocked; @@ -59,6 +62,13 @@ describe('FieldPermissionService', () => { canDestroy: false, restrictedFields: {}, }, + [fieldRelationMock.objectMetadataId]: { + canRead: true, + canUpdate: true, + canSoftDelete: false, + canDestroy: false, + restrictedFields: {}, + }, }, }; @@ -93,6 +103,12 @@ describe('FieldPermissionService', () => { getObjectMetadataMapsOrThrow: jest.fn(), }, }, + { + provide: getRepositoryToken(FieldMetadataEntity, 'core'), + useValue: { + find: jest.fn(), + }, + }, ], }).compile(); @@ -101,6 +117,9 @@ describe('FieldPermissionService', () => { getRepositoryToken(FieldPermissionEntity, 'core'), ); roleRepository = module.get(getRepositoryToken(RoleEntity, 'core')); + fieldMetadataRepository = module.get( + getRepositoryToken(FieldMetadataEntity, 'core'), + ); workspacePermissionsCacheService = module.get( WorkspacePermissionsCacheService, ); @@ -108,6 +127,10 @@ describe('FieldPermissionService', () => { // Setup default mocks roleRepository.findOne.mockResolvedValue(mockRole); + fieldMetadataRepository.find.mockResolvedValue([ + fieldTextMock, + fieldRelationMock, + ]); workspacePermissionsCacheService.getRolesPermissionsFromCache.mockResolvedValue( { version: '1', @@ -132,6 +155,16 @@ describe('FieldPermissionService', () => { fieldIdByName: {}, indexMetadatas: [], }, + [fieldRelationMock.objectMetadataId]: { + ...objectMetadataItemMock, + id: fieldRelationMock.objectMetadataId, + fieldsById: { + [fieldRelationMock.id]: fieldRelationMock, + }, + fieldIdByJoinColumnName: {}, + fieldIdByName: {}, + indexMetadatas: [], + }, }, idByNameSingular: { testObject: testObjectMetadataId, @@ -268,6 +301,47 @@ describe('FieldPermissionService', () => { }); }); + describe('relation cases', () => { + it('should create two field permissions when a relation field permission is created', async () => { + const input = createUpsertInput([ + { + canReadFieldValue: false, + canUpdateFieldValue: false, + fieldMetadataId: fieldRelationMock.id, + objectMetadataId: fieldRelationMock.objectMetadataId, + }, + ]); + + await service.upsertFieldPermissions({ + workspaceId: testWorkspaceId, + input, + }); + + expect(fieldPermissionsRepository.upsert).toHaveBeenCalledWith( + [ + { + fieldMetadataId: fieldRelationMock.id, + objectMetadataId: fieldRelationMock.objectMetadataId, + canReadFieldValue: false, + canUpdateFieldValue: false, + roleId: testRoleId, + workspaceId: testWorkspaceId, + }, + { + fieldMetadataId: fieldRelationMock.relationTargetFieldMetadataId, + objectMetadataId: + fieldRelationMock.relationTargetObjectMetadataId, + canReadFieldValue: false, + canUpdateFieldValue: false, + roleId: testRoleId, + workspaceId: testWorkspaceId, + }, + ], + { conflictPaths: ['fieldMetadataId', 'roleId'] }, + ); + }); + }); + describe('validation errors', () => { it('should throw error when canReadFieldValue is true', async () => { const input = createUpsertInput([ diff --git a/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/field-permission.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/field-permission.service.ts index aa8951ba11..4e9cc3f9ad 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/field-permission.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/field-permission.service.ts @@ -5,6 +5,11 @@ import { ObjectRecordsPermissionsByRoleId } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { In, Repository } from 'typeorm'; +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; + +import { InternalServerError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { isFieldMetadataTypeRelation } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-relation.util'; import { UpsertFieldPermissionsInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-field-permissions.input'; import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity'; import { @@ -22,6 +27,8 @@ export class FieldPermissionService { constructor( @InjectRepository(RoleEntity, 'core') private readonly roleRepository: Repository, + @InjectRepository(FieldMetadataEntity, 'core') + private readonly fieldMetadataRepository: Repository, @InjectRepository(FieldPermissionEntity, 'core') private readonly fieldPermissionsRepository: Repository, private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService, @@ -107,13 +114,39 @@ export class FieldPermissionService { ), ); - await this.fieldPermissionsRepository.upsert(fieldPermissionsToUpsert, { - conflictPaths: ['fieldMetadataId', 'roleId'], - }); + const fieldMetadatasForFieldPermissions = + await this.fieldMetadataRepository.find({ + where: { + id: In(fieldPermissions.map((fp) => fp.fieldMetadataId)), + }, + }); + + const relatedFieldPermissionsToUpsert = + this.computeFieldPermissionForRelationTargetFieldMetadata({ + fieldPermissions: fieldPermissionsToUpsert, + fieldMetadatasForFieldPermissions, + }); + + await this.fieldPermissionsRepository.upsert( + [...fieldPermissionsToUpsert, ...relatedFieldPermissionsToUpsert], + { + conflictPaths: ['fieldMetadataId', 'roleId'], + }, + ); if (fieldPermissionsToDeleteIds.length > 0) { + const relatedFieldPermissionToDeleteIds = + this.getRelatedFieldPermissionsToDeleteIds({ + allFieldPermissions: existingFieldPermissions, + fieldPermissionsToDelete: existingFieldPermissionsToDelete, + fieldMetadatas: fieldMetadatasForFieldPermissions, + }); + await this.fieldPermissionsRepository.delete({ - id: In(fieldPermissionsToDeleteIds), + id: In([ + ...fieldPermissionsToDeleteIds, + ...relatedFieldPermissionToDeleteIds, + ]), }); } @@ -267,4 +300,103 @@ export class FieldPermissionService { } } } + + private getRelatedFieldPermissionsToDeleteIds({ + allFieldPermissions, + fieldPermissionsToDelete, + fieldMetadatas, + }: { + allFieldPermissions: FieldPermissionEntity[]; + fieldPermissionsToDelete: FieldPermissionEntity[]; + fieldMetadatas: FieldMetadataEntity[]; + }) { + const fieldMetadatasForFieldPermissionsToDelete = fieldMetadatas.filter( + (fieldMetadata) => + fieldPermissionsToDelete.some( + (existingFieldPermissionToDelete) => + existingFieldPermissionToDelete.fieldMetadataId === + fieldMetadata.id, + ), + ); + + const relationTargetFieldMetadataIds: string[] = []; + + for (const fieldMetadataForFieldPermissionToDelete of fieldMetadatasForFieldPermissionsToDelete) { + if ( + isFieldMetadataTypeRelation(fieldMetadataForFieldPermissionToDelete) + ) { + if ( + fieldMetadataForFieldPermissionToDelete.settings?.relationType === + RelationType.ONE_TO_MANY || + fieldMetadataForFieldPermissionToDelete.settings?.relationType === + RelationType.MANY_TO_ONE + ) { + relationTargetFieldMetadataIds.push( + fieldMetadataForFieldPermissionToDelete.relationTargetFieldMetadataId, + ); + } + } + } + + const fieldPermissionsForRelationTargetFieldMetadataIds = + allFieldPermissions + .filter((fieldPermission) => + relationTargetFieldMetadataIds.includes( + fieldPermission.fieldMetadataId, + ), + ) + .map((fieldPermission) => fieldPermission.id); + + return fieldPermissionsForRelationTargetFieldMetadataIds; + } + + private computeFieldPermissionForRelationTargetFieldMetadata({ + fieldPermissions, + fieldMetadatasForFieldPermissions, + }: { + fieldPermissions: UpsertFieldPermissionsInput['fieldPermissions']; + fieldMetadatasForFieldPermissions: FieldMetadataEntity[]; + }) { + return fieldPermissions + .map((fieldPermission) => { + const fieldMetadata = fieldMetadatasForFieldPermissions.find( + (fm) => fm.id === fieldPermission.fieldMetadataId, + ); + + if (!isDefined(fieldMetadata)) { + throw new InternalServerError( + 'Field metadata not found for field permission', + ); + } + + if (isFieldMetadataTypeRelation(fieldMetadata)) { + if ( + fieldMetadata.settings?.relationType === RelationType.ONE_TO_MANY || + fieldMetadata.settings?.relationType === RelationType.MANY_TO_ONE + ) { + const fieldPermissionInputHasFieldPermissionOnRelationTargetFieldMetadata = + fieldPermissions.filter( + (fieldPermissionInput) => + fieldPermissionInput.fieldMetadataId === + fieldMetadata.relationTargetFieldMetadataId, + ).length > 0; + + if ( + fieldPermissionInputHasFieldPermissionOnRelationTargetFieldMetadata + ) { + return; + } + + return { + ...fieldPermission, + objectMetadataId: fieldMetadata.relationTargetObjectMetadataId, + fieldMetadataId: fieldMetadata.relationTargetFieldMetadataId, + }; + } + } + + return null; + }) + .filter(isDefined); + } }