Migrate field permission to syncable entity (#18751)
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
+272
-90
@@ -2,13 +2,14 @@ import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
import { type ObjectsPermissionsByRoleId } from 'twenty-shared/types';
|
||||
import { In, type Repository } from 'typeorm';
|
||||
import { type Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
fieldRelationMock,
|
||||
fieldTextMock,
|
||||
objectMetadataItemMock,
|
||||
} from 'src/engine/api/__mocks__/object-metadata-item.mock';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { type UpsertFieldPermissionsInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-field-permissions.input';
|
||||
@@ -21,8 +22,18 @@ import {
|
||||
} from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
||||
import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock';
|
||||
|
||||
const emptyFlatFieldPermissionMaps = {
|
||||
byUniversalIdentifier: {},
|
||||
universalIdentifierById: {},
|
||||
byId: {},
|
||||
idByUniversalIdentifier: {},
|
||||
universalIdentifiersByApplicationId: {},
|
||||
};
|
||||
|
||||
describe('FieldPermissionService', () => {
|
||||
let service: FieldPermissionService;
|
||||
let fieldPermissionsRepository: jest.Mocked<
|
||||
@@ -32,6 +43,17 @@ describe('FieldPermissionService', () => {
|
||||
let fieldMetadataRepository: jest.Mocked<Repository<FieldMetadataEntity>>;
|
||||
let workspaceCacheService: jest.Mocked<WorkspaceCacheService>;
|
||||
let workspaceManyOrAllFlatEntityMapsCacheService: jest.Mocked<WorkspaceManyOrAllFlatEntityMapsCacheService>;
|
||||
let workspaceMigrationValidateBuildAndRunService: jest.Mocked<WorkspaceMigrationValidateBuildAndRunService>;
|
||||
let flatRoleMaps: {
|
||||
byUniversalIdentifier: Record<
|
||||
string,
|
||||
{ id: string; isEditable: boolean; universalIdentifier: string }
|
||||
>;
|
||||
universalIdentifierById: Record<string, string>;
|
||||
byId: Record<string, unknown>;
|
||||
idByUniversalIdentifier: Record<string, string>;
|
||||
universalIdentifiersByApplicationId: Record<string, string[]>;
|
||||
};
|
||||
|
||||
const testWorkspaceId = '20202020-0000-0000-0000-000000000000';
|
||||
const testRoleId = '20202020-0000-0000-0000-000000000001';
|
||||
@@ -114,10 +136,32 @@ describe('FieldPermissionService', () => {
|
||||
find: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: ApplicationService,
|
||||
useValue: {
|
||||
findWorkspaceTwentyStandardAndCustomApplicationOrThrow: jest
|
||||
.fn()
|
||||
.mockResolvedValue({
|
||||
workspaceCustomFlatApplication: {
|
||||
id: 'app-id',
|
||||
universalIdentifier: 'app-universal-id',
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: WorkspaceMigrationValidateBuildAndRunService,
|
||||
useValue: {
|
||||
validateBuildAndRunWorkspaceMigration: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<FieldPermissionService>(FieldPermissionService);
|
||||
workspaceMigrationValidateBuildAndRunService = module.get(
|
||||
WorkspaceMigrationValidateBuildAndRunService,
|
||||
);
|
||||
fieldPermissionsRepository = module.get(
|
||||
getRepositoryToken(FieldPermissionEntity),
|
||||
);
|
||||
@@ -136,9 +180,19 @@ describe('FieldPermissionService', () => {
|
||||
fieldTextMock,
|
||||
fieldRelationMock,
|
||||
]);
|
||||
(workspaceCacheService.getOrRecompute as jest.Mock).mockResolvedValue({
|
||||
rolesPermissions: mockRolesPermissions,
|
||||
} as any);
|
||||
(workspaceCacheService.getOrRecompute as jest.Mock).mockImplementation(
|
||||
(_workspaceId: string, keys: string[]) => {
|
||||
if (keys?.includes('flatFieldPermissionMaps')) {
|
||||
return Promise.resolve({
|
||||
flatFieldPermissionMaps: emptyFlatFieldPermissionMaps,
|
||||
});
|
||||
}
|
||||
if (keys?.includes('rolesPermissions')) {
|
||||
return Promise.resolve({ rolesPermissions: mockRolesPermissions });
|
||||
}
|
||||
return Promise.resolve({});
|
||||
},
|
||||
);
|
||||
const testFieldMetadata = getMockFieldMetadataEntity({
|
||||
...fieldTextMock,
|
||||
label: 'Test Field',
|
||||
@@ -147,8 +201,22 @@ describe('FieldPermissionService', () => {
|
||||
id: testFieldMetadataId,
|
||||
});
|
||||
|
||||
flatRoleMaps = {
|
||||
byUniversalIdentifier: {
|
||||
[testRoleId]: {
|
||||
id: testRoleId,
|
||||
isEditable: true,
|
||||
universalIdentifier: testRoleId,
|
||||
},
|
||||
},
|
||||
universalIdentifierById: { [testRoleId]: testRoleId },
|
||||
byId: {},
|
||||
idByUniversalIdentifier: {},
|
||||
universalIdentifiersByApplicationId: {},
|
||||
};
|
||||
workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps.mockResolvedValue(
|
||||
{
|
||||
flatRoleMaps,
|
||||
flatObjectMetadataMaps: {
|
||||
byUniversalIdentifier: {
|
||||
[testObjectMetadataId]: {
|
||||
@@ -169,11 +237,23 @@ describe('FieldPermissionService', () => {
|
||||
universalIdentifier: fieldRelationMock.objectMetadataId,
|
||||
applicationId: null,
|
||||
} as any,
|
||||
[fieldRelationMock.relationTargetObjectMetadataId!]: {
|
||||
...objectMetadataItemMock,
|
||||
id: fieldRelationMock.relationTargetObjectMetadataId,
|
||||
fieldIds: [fieldRelationMock.relationTargetFieldMetadataId!],
|
||||
indexMetadataIds: [],
|
||||
viewIds: [],
|
||||
universalIdentifier:
|
||||
fieldRelationMock.relationTargetObjectMetadataId,
|
||||
applicationId: null,
|
||||
} as any,
|
||||
},
|
||||
universalIdentifierById: {
|
||||
[testObjectMetadataId]: testObjectMetadataId,
|
||||
[fieldRelationMock.objectMetadataId]:
|
||||
fieldRelationMock.objectMetadataId,
|
||||
[fieldRelationMock.relationTargetObjectMetadataId!]:
|
||||
fieldRelationMock.relationTargetObjectMetadataId!,
|
||||
},
|
||||
universalIdentifiersByApplicationId: {},
|
||||
},
|
||||
@@ -181,10 +261,18 @@ describe('FieldPermissionService', () => {
|
||||
byUniversalIdentifier: {
|
||||
[testFieldMetadata.universalIdentifier]: testFieldMetadata as any,
|
||||
[fieldRelationMock.universalIdentifier]: fieldRelationMock as any,
|
||||
[fieldRelationMock.relationTargetFieldMetadataId!]: {
|
||||
...fieldRelationMock,
|
||||
id: fieldRelationMock.relationTargetFieldMetadataId,
|
||||
universalIdentifier:
|
||||
fieldRelationMock.relationTargetFieldMetadataId,
|
||||
} as any,
|
||||
},
|
||||
universalIdentifierById: {
|
||||
[testFieldMetadataId]: testFieldMetadata.universalIdentifier,
|
||||
[fieldRelationMock.id]: fieldRelationMock.universalIdentifier,
|
||||
[fieldRelationMock.relationTargetFieldMetadataId!]:
|
||||
fieldRelationMock.relationTargetFieldMetadataId!,
|
||||
},
|
||||
universalIdentifiersByApplicationId: {},
|
||||
},
|
||||
@@ -218,6 +306,10 @@ describe('FieldPermissionService', () => {
|
||||
|
||||
describe('successful cases', () => {
|
||||
it('should successfully upsert field permissions', async () => {
|
||||
workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration.mockResolvedValue(
|
||||
{ status: 'success' } as any,
|
||||
);
|
||||
|
||||
const input = createUpsertInput([
|
||||
{
|
||||
canReadFieldValue: false,
|
||||
@@ -225,51 +317,81 @@ describe('FieldPermissionService', () => {
|
||||
},
|
||||
]);
|
||||
|
||||
await service.upsertFieldPermissions({
|
||||
const result = await service.upsertFieldPermissions({
|
||||
workspaceId: testWorkspaceId,
|
||||
input,
|
||||
});
|
||||
|
||||
expect(fieldPermissionsRepository.upsert).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
roleId: testRoleId,
|
||||
workspaceId: testWorkspaceId,
|
||||
objectMetadataId: testObjectMetadataId,
|
||||
fieldMetadataId: testFieldMetadataId,
|
||||
canReadFieldValue: false,
|
||||
canUpdateFieldValue: false,
|
||||
expect(
|
||||
workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration,
|
||||
).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
workspaceId: testWorkspaceId,
|
||||
isSystemBuild: false,
|
||||
allFlatEntityOperationByMetadataName: expect.objectContaining({
|
||||
fieldPermission: expect.objectContaining({
|
||||
flatEntityToCreate: expect.any(Array),
|
||||
flatEntityToUpdate: expect.any(Array),
|
||||
flatEntityToDelete: expect.any(Array),
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
{
|
||||
conflictPaths: ['fieldMetadataId', 'roleId'],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result).toEqual(expect.any(Array));
|
||||
expect(
|
||||
workspaceCacheService.invalidateAndRecompute,
|
||||
).toHaveBeenCalledWith(testWorkspaceId, ['rolesPermissions']);
|
||||
});
|
||||
|
||||
it('should delete field permissions when both canReadFieldValue and canUpdateFieldValue are null', async () => {
|
||||
const existingFieldPermission: FieldPermissionEntity = {
|
||||
id: 'existing-field-permission-id',
|
||||
roleId: testRoleId,
|
||||
objectMetadataId: testObjectMetadataId,
|
||||
fieldMetadataId: testFieldMetadataId,
|
||||
canReadFieldValue: null,
|
||||
canUpdateFieldValue: false,
|
||||
workspaceId: testWorkspaceId,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as unknown as FieldPermissionEntity;
|
||||
|
||||
fieldPermissionsRepository.find.mockResolvedValue([
|
||||
existingFieldPermission,
|
||||
]);
|
||||
const existingUniversalId = 'existing-fp-universal-id';
|
||||
const mapsWithOneCurrentPermission = {
|
||||
...emptyFlatFieldPermissionMaps,
|
||||
byUniversalIdentifier: {
|
||||
[existingUniversalId]: {
|
||||
id: 'existing-fp-id',
|
||||
universalIdentifier: existingUniversalId,
|
||||
roleUniversalIdentifier: testRoleId,
|
||||
objectMetadataId: testObjectMetadataId,
|
||||
fieldMetadataId: testFieldMetadataId,
|
||||
canReadFieldValue: undefined,
|
||||
canUpdateFieldValue: false,
|
||||
applicationUniversalIdentifier: 'app-ui',
|
||||
objectMetadataUniversalIdentifier: testObjectMetadataId,
|
||||
fieldMetadataUniversalIdentifier: testFieldMetadataId,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
universalIdentifierById: { 'existing-fp-id': existingUniversalId },
|
||||
};
|
||||
let flatFieldPermissionMapsCallCount = 0;
|
||||
(workspaceCacheService.getOrRecompute as jest.Mock).mockImplementation(
|
||||
(_w: string, keys: string[]) => {
|
||||
if (keys?.includes('flatFieldPermissionMaps')) {
|
||||
flatFieldPermissionMapsCallCount += 1;
|
||||
return Promise.resolve({
|
||||
flatFieldPermissionMaps:
|
||||
flatFieldPermissionMapsCallCount === 1
|
||||
? mapsWithOneCurrentPermission
|
||||
: emptyFlatFieldPermissionMaps,
|
||||
});
|
||||
}
|
||||
if (keys?.includes('rolesPermissions')) {
|
||||
return Promise.resolve({
|
||||
rolesPermissions: mockRolesPermissions,
|
||||
});
|
||||
}
|
||||
return Promise.resolve({});
|
||||
},
|
||||
);
|
||||
workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration.mockResolvedValue(
|
||||
{ status: 'success' } as any,
|
||||
);
|
||||
|
||||
const input = createUpsertInput([
|
||||
{
|
||||
canReadFieldValue: null,
|
||||
canUpdateFieldValue: null,
|
||||
},
|
||||
]);
|
||||
@@ -279,27 +401,68 @@ describe('FieldPermissionService', () => {
|
||||
input,
|
||||
});
|
||||
|
||||
expect(fieldPermissionsRepository.delete).toHaveBeenCalledWith({
|
||||
id: In(['existing-field-permission-id']),
|
||||
});
|
||||
expect(
|
||||
workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration,
|
||||
).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
allFlatEntityOperationByMetadataName: expect.objectContaining({
|
||||
fieldPermission: expect.objectContaining({
|
||||
flatEntityToDelete: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
universalIdentifier: existingUniversalId,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not delete field permissions when one value is null and the other is false', async () => {
|
||||
const existingFieldPermission: FieldPermissionEntity = {
|
||||
id: 'existing-field-permission-id',
|
||||
roleId: testRoleId,
|
||||
objectMetadataId: testObjectMetadataId,
|
||||
fieldMetadataId: testFieldMetadataId,
|
||||
canReadFieldValue: false,
|
||||
canUpdateFieldValue: null,
|
||||
workspaceId: testWorkspaceId,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as FieldPermissionEntity;
|
||||
|
||||
fieldPermissionsRepository.find.mockResolvedValue([
|
||||
existingFieldPermission,
|
||||
]);
|
||||
const existingUniversalId = 'existing-fp-ui-2';
|
||||
const mapsWithOneCurrent = {
|
||||
...emptyFlatFieldPermissionMaps,
|
||||
byUniversalIdentifier: {
|
||||
[existingUniversalId]: {
|
||||
id: 'existing-fp-id-2',
|
||||
universalIdentifier: existingUniversalId,
|
||||
roleUniversalIdentifier: testRoleId,
|
||||
objectMetadataId: testObjectMetadataId,
|
||||
fieldMetadataId: testFieldMetadataId,
|
||||
canReadFieldValue: false,
|
||||
canUpdateFieldValue: undefined,
|
||||
applicationUniversalIdentifier: 'app-ui',
|
||||
objectMetadataUniversalIdentifier: testObjectMetadataId,
|
||||
fieldMetadataUniversalIdentifier: testFieldMetadataId,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
universalIdentifierById: { 'existing-fp-id-2': existingUniversalId },
|
||||
};
|
||||
let flatMapsCallCount = 0;
|
||||
(workspaceCacheService.getOrRecompute as jest.Mock).mockImplementation(
|
||||
(_w: string, keys: string[]) => {
|
||||
if (keys?.includes('flatFieldPermissionMaps')) {
|
||||
flatMapsCallCount += 1;
|
||||
return Promise.resolve({
|
||||
flatFieldPermissionMaps:
|
||||
flatMapsCallCount === 1
|
||||
? mapsWithOneCurrent
|
||||
: emptyFlatFieldPermissionMaps,
|
||||
});
|
||||
}
|
||||
if (keys?.includes('rolesPermissions')) {
|
||||
return Promise.resolve({
|
||||
rolesPermissions: mockRolesPermissions,
|
||||
});
|
||||
}
|
||||
return Promise.resolve({});
|
||||
},
|
||||
);
|
||||
workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration.mockResolvedValue(
|
||||
{ status: 'success' } as any,
|
||||
);
|
||||
|
||||
const input = createUpsertInput([
|
||||
{
|
||||
@@ -313,12 +476,26 @@ describe('FieldPermissionService', () => {
|
||||
input,
|
||||
});
|
||||
|
||||
expect(fieldPermissionsRepository.delete).not.toHaveBeenCalled();
|
||||
const callArg = workspaceMigrationValidateBuildAndRunService
|
||||
.validateBuildAndRunWorkspaceMigration.mock
|
||||
.calls[0][0] as unknown as {
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
fieldPermission: { flatEntityToDelete: unknown[] };
|
||||
};
|
||||
};
|
||||
expect(
|
||||
callArg.allFlatEntityOperationByMetadataName.fieldPermission
|
||||
.flatEntityToDelete,
|
||||
).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('relation cases', () => {
|
||||
it('should create two field permissions when a relation field permission is created', async () => {
|
||||
workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration.mockResolvedValue(
|
||||
{ status: 'success' } as any,
|
||||
);
|
||||
|
||||
const input = createUpsertInput([
|
||||
{
|
||||
canReadFieldValue: false,
|
||||
@@ -333,28 +510,17 @@ describe('FieldPermissionService', () => {
|
||||
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'] },
|
||||
);
|
||||
const callArg = workspaceMigrationValidateBuildAndRunService
|
||||
.validateBuildAndRunWorkspaceMigration.mock
|
||||
.calls[0][0] as unknown as {
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
fieldPermission: { flatEntityToCreate: unknown[] };
|
||||
};
|
||||
};
|
||||
expect(
|
||||
callArg.allFlatEntityOperationByMetadataName.fieldPermission
|
||||
.flatEntityToCreate,
|
||||
).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -404,6 +570,7 @@ describe('FieldPermissionService', () => {
|
||||
it('should throw error when object metadata is not found', async () => {
|
||||
workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps.mockResolvedValue(
|
||||
{
|
||||
flatRoleMaps,
|
||||
flatObjectMetadataMaps: {
|
||||
byUniversalIdentifier: {},
|
||||
universalIdentifierById: {},
|
||||
@@ -446,6 +613,7 @@ describe('FieldPermissionService', () => {
|
||||
|
||||
workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps.mockResolvedValue(
|
||||
{
|
||||
flatRoleMaps,
|
||||
flatObjectMetadataMaps: {
|
||||
byUniversalIdentifier: {
|
||||
[testObjectMetadataId]: {
|
||||
@@ -494,6 +662,7 @@ describe('FieldPermissionService', () => {
|
||||
it('should throw error when field metadata is not found', async () => {
|
||||
workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps.mockResolvedValue(
|
||||
{
|
||||
flatRoleMaps,
|
||||
flatObjectMetadataMaps: {
|
||||
byUniversalIdentifier: {
|
||||
[testObjectMetadataId]: {
|
||||
@@ -541,9 +710,19 @@ describe('FieldPermissionService', () => {
|
||||
});
|
||||
|
||||
it('should throw error when object permission is not found', async () => {
|
||||
(workspaceCacheService.getOrRecompute as jest.Mock).mockResolvedValue({
|
||||
rolesPermissions: {},
|
||||
} as any);
|
||||
(workspaceCacheService.getOrRecompute as jest.Mock).mockImplementation(
|
||||
(_w: string, keys: string[]) => {
|
||||
if (keys?.includes('flatFieldPermissionMaps')) {
|
||||
return Promise.resolve({
|
||||
flatFieldPermissionMaps: emptyFlatFieldPermissionMaps,
|
||||
});
|
||||
}
|
||||
if (keys?.includes('rolesPermissions')) {
|
||||
return Promise.resolve({ rolesPermissions: {} });
|
||||
}
|
||||
return Promise.resolve({});
|
||||
},
|
||||
);
|
||||
|
||||
const input = createUpsertInput([
|
||||
{
|
||||
@@ -568,12 +747,20 @@ describe('FieldPermissionService', () => {
|
||||
|
||||
describe('role validation errors', () => {
|
||||
it('should throw error when role is not editable', async () => {
|
||||
const nonEditableRole: RoleEntity = {
|
||||
...mockRole,
|
||||
isEditable: false,
|
||||
};
|
||||
|
||||
roleRepository.findOne.mockResolvedValue(nonEditableRole);
|
||||
workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration.mockResolvedValue(
|
||||
{
|
||||
status: 'fail',
|
||||
report: {
|
||||
fieldPermission: [
|
||||
{
|
||||
code: PermissionsExceptionCode.ROLE_NOT_EDITABLE,
|
||||
message: 'Role is not editable',
|
||||
userFriendlyMessage: 'This role cannot be modified.',
|
||||
},
|
||||
],
|
||||
},
|
||||
} as any,
|
||||
);
|
||||
|
||||
const input = createUpsertInput([
|
||||
{
|
||||
@@ -587,12 +774,7 @@ describe('FieldPermissionService', () => {
|
||||
workspaceId: testWorkspaceId,
|
||||
input,
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
new PermissionsException(
|
||||
PermissionsExceptionMessage.ROLE_NOT_EDITABLE,
|
||||
PermissionsExceptionCode.ROLE_NOT_EDITABLE,
|
||||
),
|
||||
);
|
||||
).rejects.toThrow(WorkspaceMigrationBuilderException);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+2
-2
@@ -15,7 +15,7 @@ import {
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { WorkspaceRelatedEntity } from 'src/engine/workspace-manager/types/workspace-related-entity';
|
||||
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
|
||||
|
||||
@Entity('fieldPermission')
|
||||
@Unique('IDX_FIELD_PERMISSION_FIELD_METADATA_ID_ROLE_ID_UNIQUE', [
|
||||
@@ -23,7 +23,7 @@ import { WorkspaceRelatedEntity } from 'src/engine/workspace-manager/types/works
|
||||
'roleId',
|
||||
])
|
||||
@Index('IDX_FIELD_PERMISSION_WORKSPACE_ID_ROLE_ID', ['workspaceId', 'roleId'])
|
||||
export class FieldPermissionEntity extends WorkspaceRelatedEntity {
|
||||
export class FieldPermissionEntity extends SyncableEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
|
||||
+302
-294
@@ -1,45 +1,49 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { type ObjectsPermissionsByRoleId } from 'twenty-shared/types';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { In, Repository } from 'typeorm';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
|
||||
import {
|
||||
InternalServerError,
|
||||
UserInputError,
|
||||
} 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 { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.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 FlatFieldPermissionMaps } from 'src/engine/metadata-modules/flat-field-permission/types/flat-field-permission-maps.type';
|
||||
import { type FlatFieldPermission } from 'src/engine/metadata-modules/flat-field-permission/types/flat-field-permission.type';
|
||||
import { fromCreateFieldPermissionInputToUniversalFlatFieldPermission } from 'src/engine/metadata-modules/flat-field-permission/utils/from-create-field-permission-input-to-universal-flat-field-permission.util';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type';
|
||||
import { type 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 {
|
||||
PermissionsException,
|
||||
PermissionsExceptionCode,
|
||||
PermissionsExceptionMessage,
|
||||
} from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
||||
import { type UniversalFlatFieldPermission } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-permission.type';
|
||||
|
||||
type DesiredFieldPermission = {
|
||||
objectMetadataId: string;
|
||||
fieldMetadataId: string;
|
||||
canReadFieldValue?: boolean | null;
|
||||
canUpdateFieldValue?: boolean | null;
|
||||
};
|
||||
|
||||
const keyFrom = (objectMetadataId: string, fieldMetadataId: string) =>
|
||||
`${objectMetadataId}:${fieldMetadataId}`;
|
||||
|
||||
@Injectable()
|
||||
export class FieldPermissionService {
|
||||
constructor(
|
||||
@InjectRepository(RoleEntity)
|
||||
private readonly roleRepository: Repository<RoleEntity>,
|
||||
@InjectRepository(FieldMetadataEntity)
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
@InjectRepository(FieldPermissionEntity)
|
||||
private readonly fieldPermissionsRepository: Repository<FieldPermissionEntity>,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
) {}
|
||||
|
||||
public async upsertFieldPermissions({
|
||||
@@ -48,135 +52,249 @@ export class FieldPermissionService {
|
||||
}: {
|
||||
workspaceId: string;
|
||||
input: UpsertFieldPermissionsInput;
|
||||
}): Promise<FieldPermissionEntity[]> {
|
||||
const role = await this.getRoleOrThrow({
|
||||
roleId: input.roleId,
|
||||
workspaceId,
|
||||
});
|
||||
}): Promise<FlatFieldPermission[]> {
|
||||
const [flatMapsForRoleObjectField, flatFieldPermissionMapsResult] =
|
||||
await Promise.all([
|
||||
this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: [
|
||||
'flatRoleMaps',
|
||||
'flatObjectMetadataMaps',
|
||||
'flatFieldMetadataMaps',
|
||||
],
|
||||
},
|
||||
),
|
||||
this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatFieldPermissionMaps',
|
||||
] as unknown as Parameters<WorkspaceCacheService['getOrRecompute']>[1]),
|
||||
]);
|
||||
const flatRoleMaps = flatMapsForRoleObjectField.flatRoleMaps;
|
||||
const flatObjectMetadataMaps =
|
||||
flatMapsForRoleObjectField.flatObjectMetadataMaps;
|
||||
const flatFieldMetadataMaps =
|
||||
flatMapsForRoleObjectField.flatFieldMetadataMaps;
|
||||
const flatFieldPermissionMapsResolved: FlatFieldPermissionMaps = (
|
||||
flatFieldPermissionMapsResult as unknown as {
|
||||
flatFieldPermissionMaps: FlatFieldPermissionMaps;
|
||||
}
|
||||
).flatFieldPermissionMaps;
|
||||
|
||||
const { rolesPermissions } =
|
||||
await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'rolesPermissions',
|
||||
]);
|
||||
|
||||
await this.validateRoleIsEditableOrThrow({
|
||||
role,
|
||||
});
|
||||
const roleUniversalIdentifier =
|
||||
flatRoleMaps.universalIdentifierById[input.roleId];
|
||||
const flatRole: FlatRole | undefined = isDefined(roleUniversalIdentifier)
|
||||
? (flatRoleMaps.byUniversalIdentifier[roleUniversalIdentifier] as
|
||||
| FlatRole
|
||||
| undefined)
|
||||
: undefined;
|
||||
|
||||
const { flatObjectMetadataMaps, flatFieldMetadataMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatObjectMetadataMaps', 'flatFieldMetadataMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const existingFieldPermissions = await this.fieldPermissionsRepository.find(
|
||||
{
|
||||
where: {
|
||||
roleId: input.roleId,
|
||||
workspaceId,
|
||||
},
|
||||
},
|
||||
const currentFieldPermissionsForRole = Object.values(
|
||||
flatFieldPermissionMapsResolved.byUniversalIdentifier,
|
||||
).filter(
|
||||
(fp): fp is FlatFieldPermission =>
|
||||
isDefined(fp) && fp.roleUniversalIdentifier === roleUniversalIdentifier,
|
||||
);
|
||||
|
||||
const fieldPermissionsToDeleteIds: string[] = [];
|
||||
const desiredMap = new Map<string, DesiredFieldPermission>();
|
||||
|
||||
input.fieldPermissions.forEach((fieldPermission) => {
|
||||
for (const fieldPermission of input.fieldPermissions) {
|
||||
this.validateFieldPermission({
|
||||
allFieldPermissions: input.fieldPermissions,
|
||||
fieldPermission,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
rolesPermissions,
|
||||
role,
|
||||
flatRole,
|
||||
});
|
||||
|
||||
if (
|
||||
fieldPermission.canReadFieldValue === null ||
|
||||
fieldPermission.canUpdateFieldValue === null
|
||||
) {
|
||||
this.checkIfFieldPermissionShouldBeDeleted({
|
||||
fieldPermission,
|
||||
existingFieldPermissions,
|
||||
fieldPermissionsToDeleteIds,
|
||||
});
|
||||
const bothNull =
|
||||
(fieldPermission.canReadFieldValue === null ||
|
||||
fieldPermission.canReadFieldValue === undefined) &&
|
||||
(fieldPermission.canUpdateFieldValue === null ||
|
||||
fieldPermission.canUpdateFieldValue === undefined);
|
||||
|
||||
if (bothNull) {
|
||||
continue;
|
||||
}
|
||||
|
||||
desiredMap.set(
|
||||
keyFrom(
|
||||
fieldPermission.objectMetadataId,
|
||||
fieldPermission.fieldMetadataId,
|
||||
),
|
||||
{
|
||||
objectMetadataId: fieldPermission.objectMetadataId,
|
||||
fieldMetadataId: fieldPermission.fieldMetadataId,
|
||||
canReadFieldValue: fieldPermission.canReadFieldValue ?? undefined,
|
||||
canUpdateFieldValue: fieldPermission.canUpdateFieldValue ?? undefined,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
this.addRelatedFieldPermissionsToDesired({
|
||||
desiredMap,
|
||||
inputFieldPermissions: input.fieldPermissions,
|
||||
flatFieldMetadataMaps,
|
||||
});
|
||||
|
||||
const fieldPermissions = input.fieldPermissions.map((fieldPermission) => ({
|
||||
...fieldPermission,
|
||||
roleId: input.roleId,
|
||||
workspaceId,
|
||||
}));
|
||||
const { workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const existingFieldPermissionsToDelete = existingFieldPermissions.filter(
|
||||
(existingFieldPermissionToFilter) =>
|
||||
fieldPermissionsToDeleteIds.includes(
|
||||
existingFieldPermissionToFilter.id,
|
||||
),
|
||||
const flatEntityToCreate: (UniversalFlatFieldPermission & {
|
||||
id: string;
|
||||
})[] = [];
|
||||
const flatEntityToUpdate: UniversalFlatFieldPermission[] = [];
|
||||
const flatEntityToDelete: UniversalFlatFieldPermission[] = [];
|
||||
|
||||
const currentByKey = new Map(
|
||||
currentFieldPermissionsForRole.map((fp) => [
|
||||
keyFrom(fp.objectMetadataId, fp.fieldMetadataId),
|
||||
fp,
|
||||
]),
|
||||
);
|
||||
|
||||
const fieldPermissionsToUpsert = fieldPermissions.filter(
|
||||
(fieldPermissionToUpsert) =>
|
||||
!existingFieldPermissionsToDelete.some(
|
||||
(existingFieldPermissionToDelete) =>
|
||||
existingFieldPermissionToDelete.fieldMetadataId ===
|
||||
fieldPermissionToUpsert.fieldMetadataId,
|
||||
),
|
||||
);
|
||||
for (const [, desired] of desiredMap) {
|
||||
const current = currentByKey.get(
|
||||
keyFrom(desired.objectMetadataId, desired.fieldMetadataId),
|
||||
);
|
||||
|
||||
const fieldMetadatasForFieldPermissions =
|
||||
await this.fieldMetadataRepository.find({
|
||||
where: {
|
||||
id: In(fieldPermissions.map((fp) => fp.fieldMetadataId)),
|
||||
},
|
||||
});
|
||||
if (!isDefined(current)) {
|
||||
flatEntityToCreate.push(
|
||||
fromCreateFieldPermissionInputToUniversalFlatFieldPermission({
|
||||
fieldPermissionInput: {
|
||||
objectMetadataId: desired.objectMetadataId,
|
||||
fieldMetadataId: desired.fieldMetadataId,
|
||||
canReadFieldValue: desired.canReadFieldValue ?? null,
|
||||
canUpdateFieldValue: desired.canUpdateFieldValue ?? null,
|
||||
},
|
||||
roleId: input.roleId,
|
||||
flatApplication: workspaceCustomFlatApplication,
|
||||
flatRoleMaps,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
const effectiveCanRead =
|
||||
desired.canReadFieldValue ?? current.canReadFieldValue;
|
||||
const effectiveCanUpdate =
|
||||
desired.canUpdateFieldValue ?? current.canUpdateFieldValue;
|
||||
const changed =
|
||||
effectiveCanRead !== current.canReadFieldValue ||
|
||||
effectiveCanUpdate !== current.canUpdateFieldValue;
|
||||
|
||||
const relatedFieldPermissionsToUpsert =
|
||||
this.computeFieldPermissionForRelationTargetFieldMetadata({
|
||||
fieldPermissions: fieldPermissionsToUpsert,
|
||||
fieldMetadatasForFieldPermissions,
|
||||
});
|
||||
if (changed) {
|
||||
const now = new Date().toISOString();
|
||||
flatEntityToUpdate.push({
|
||||
universalIdentifier: current.universalIdentifier,
|
||||
applicationUniversalIdentifier:
|
||||
current.applicationUniversalIdentifier,
|
||||
roleUniversalIdentifier: current.roleUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier:
|
||||
current.objectMetadataUniversalIdentifier,
|
||||
fieldMetadataUniversalIdentifier:
|
||||
current.fieldMetadataUniversalIdentifier,
|
||||
canReadFieldValue: effectiveCanRead ?? undefined,
|
||||
canUpdateFieldValue: effectiveCanUpdate ?? undefined,
|
||||
createdAt: current.createdAt,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.fieldPermissionsRepository.upsert(
|
||||
[...fieldPermissionsToUpsert, ...relatedFieldPermissionsToUpsert],
|
||||
{
|
||||
conflictPaths: ['fieldMetadataId', 'roleId'],
|
||||
},
|
||||
);
|
||||
|
||||
if (fieldPermissionsToDeleteIds.length > 0) {
|
||||
const relatedFieldPermissionToDeleteIds =
|
||||
this.getRelatedFieldPermissionsToDeleteIds({
|
||||
allFieldPermissions: existingFieldPermissions,
|
||||
fieldPermissionsToDelete: existingFieldPermissionsToDelete,
|
||||
fieldMetadatas: fieldMetadatasForFieldPermissions,
|
||||
for (const current of currentFieldPermissionsForRole) {
|
||||
const key = keyFrom(current.objectMetadataId, current.fieldMetadataId);
|
||||
if (!desiredMap.has(key)) {
|
||||
flatEntityToDelete.push({
|
||||
universalIdentifier: current.universalIdentifier,
|
||||
applicationUniversalIdentifier:
|
||||
current.applicationUniversalIdentifier,
|
||||
roleUniversalIdentifier: current.roleUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier:
|
||||
current.objectMetadataUniversalIdentifier,
|
||||
fieldMetadataUniversalIdentifier:
|
||||
current.fieldMetadataUniversalIdentifier,
|
||||
canReadFieldValue: current.canReadFieldValue ?? undefined,
|
||||
canUpdateFieldValue: current.canUpdateFieldValue ?? undefined,
|
||||
createdAt: current.createdAt,
|
||||
updatedAt: current.updatedAt,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await this.fieldPermissionsRepository.delete({
|
||||
id: In([
|
||||
...fieldPermissionsToDeleteIds,
|
||||
...relatedFieldPermissionToDeleteIds,
|
||||
]),
|
||||
});
|
||||
if (
|
||||
flatEntityToCreate.length === 0 &&
|
||||
flatEntityToUpdate.length === 0 &&
|
||||
flatEntityToDelete.length === 0
|
||||
) {
|
||||
const desiredObjectMetadataIds = new Set(
|
||||
input.fieldPermissions.map((fp) => fp.objectMetadataId),
|
||||
);
|
||||
return currentFieldPermissionsForRole.filter((fp) =>
|
||||
desiredObjectMetadataIds.has(fp.objectMetadataId),
|
||||
);
|
||||
}
|
||||
|
||||
const buildAndRunResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
fieldPermission: {
|
||||
flatEntityToCreate,
|
||||
flatEntityToUpdate,
|
||||
flatEntityToDelete,
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
isSystemBuild: false,
|
||||
applicationUniversalIdentifier:
|
||||
workspaceCustomFlatApplication.universalIdentifier,
|
||||
} as Parameters<
|
||||
WorkspaceMigrationValidateBuildAndRunService['validateBuildAndRunWorkspaceMigration']
|
||||
>[0],
|
||||
);
|
||||
|
||||
if (buildAndRunResult.status === 'fail') {
|
||||
throw new WorkspaceMigrationBuilderException(
|
||||
buildAndRunResult,
|
||||
'Validation errors occurred while upserting field permissions',
|
||||
);
|
||||
}
|
||||
|
||||
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
|
||||
'rolesPermissions',
|
||||
]);
|
||||
|
||||
return this.fieldPermissionsRepository.find({
|
||||
where: {
|
||||
roleId: input.roleId,
|
||||
objectMetadataId: In(
|
||||
input.fieldPermissions.map(
|
||||
(fieldPermission) => fieldPermission.objectMetadataId,
|
||||
),
|
||||
),
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
const freshFlatFieldPermissionMaps: FlatFieldPermissionMaps = (
|
||||
(await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatFieldPermissionMaps',
|
||||
] as unknown as Parameters<
|
||||
WorkspaceCacheService['getOrRecompute']
|
||||
>[1])) as unknown as { flatFieldPermissionMaps: FlatFieldPermissionMaps }
|
||||
).flatFieldPermissionMaps;
|
||||
|
||||
const resultFieldPermissions = Object.values(
|
||||
freshFlatFieldPermissionMaps.byUniversalIdentifier,
|
||||
).filter(
|
||||
(fp): fp is FlatFieldPermission =>
|
||||
isDefined(fp) && fp.roleUniversalIdentifier === roleUniversalIdentifier,
|
||||
);
|
||||
|
||||
const desiredObjectMetadataIds = new Set(
|
||||
input.fieldPermissions.map((fp) => fp.objectMetadataId),
|
||||
);
|
||||
const filtered = resultFieldPermissions.filter((fp) =>
|
||||
desiredObjectMetadataIds.has(fp.objectMetadataId),
|
||||
);
|
||||
return filtered;
|
||||
}
|
||||
|
||||
private validateFieldPermission({
|
||||
@@ -185,14 +303,14 @@ export class FieldPermissionService {
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
rolesPermissions,
|
||||
role,
|
||||
flatRole,
|
||||
}: {
|
||||
allFieldPermissions: UpsertFieldPermissionsInput['fieldPermissions'];
|
||||
fieldPermission: UpsertFieldPermissionsInput['fieldPermissions'][0];
|
||||
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
rolesPermissions: ObjectsPermissionsByRoleId;
|
||||
role: RoleEntity;
|
||||
rolesPermissions: Record<string, Record<string, unknown>> | undefined;
|
||||
flatRole: { id: string } | undefined;
|
||||
}) {
|
||||
const duplicateFieldPermissions = allFieldPermissions.filter(
|
||||
(permission) =>
|
||||
@@ -204,12 +322,11 @@ export class FieldPermissionService {
|
||||
`Cannot accept more than one fieldPermission for field ${fieldPermission.fieldMetadataId} in input.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
('canUpdateFieldValue' in fieldPermission &&
|
||||
fieldPermission.canUpdateFieldValue !== null &&
|
||||
(fieldPermission.canUpdateFieldValue !== null &&
|
||||
fieldPermission.canUpdateFieldValue !== false) ||
|
||||
('canReadFieldValue' in fieldPermission &&
|
||||
fieldPermission.canReadFieldValue !== null &&
|
||||
(fieldPermission.canReadFieldValue !== null &&
|
||||
fieldPermission.canReadFieldValue !== false)
|
||||
) {
|
||||
throw new PermissionsException(
|
||||
@@ -261,10 +378,11 @@ export class FieldPermissionService {
|
||||
);
|
||||
}
|
||||
|
||||
const rolePermissionOnObject =
|
||||
rolesPermissions?.[role.id]?.[fieldPermission.objectMetadataId];
|
||||
const rolePermissionOnObject = isDefined(flatRole)
|
||||
? rolesPermissions?.[flatRole.id]?.[fieldPermission.objectMetadataId]
|
||||
: undefined;
|
||||
|
||||
if (!isDefined(rolePermissionOnObject)) {
|
||||
if (isDefined(flatRole) && !isDefined(rolePermissionOnObject)) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.OBJECT_PERMISSION_NOT_FOUND,
|
||||
PermissionsExceptionCode.OBJECT_PERMISSION_NOT_FOUND,
|
||||
@@ -275,194 +393,84 @@ export class FieldPermissionService {
|
||||
}
|
||||
}
|
||||
|
||||
private async getRoleOrThrow({
|
||||
roleId,
|
||||
workspaceId,
|
||||
private addRelatedFieldPermissionsToDesired({
|
||||
desiredMap,
|
||||
inputFieldPermissions,
|
||||
flatFieldMetadataMaps,
|
||||
}: {
|
||||
roleId: string;
|
||||
workspaceId: string;
|
||||
desiredMap: Map<string, DesiredFieldPermission>;
|
||||
inputFieldPermissions: UpsertFieldPermissionsInput['fieldPermissions'];
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
}) {
|
||||
const role = await this.roleRepository.findOne({
|
||||
where: {
|
||||
id: roleId,
|
||||
workspaceId,
|
||||
},
|
||||
relations: ['objectPermissions', 'fieldPermissions'],
|
||||
});
|
||||
|
||||
if (!isDefined(role)) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.ROLE_NOT_FOUND,
|
||||
PermissionsExceptionCode.ROLE_NOT_FOUND,
|
||||
{
|
||||
userFriendlyMessage: msg`The role you are trying to modify could not be found. It may have been deleted or you may not have access to it.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return role;
|
||||
}
|
||||
|
||||
private async validateRoleIsEditableOrThrow({ role }: { role: RoleEntity }) {
|
||||
if (!role.isEditable) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.ROLE_NOT_EDITABLE,
|
||||
PermissionsExceptionCode.ROLE_NOT_EDITABLE,
|
||||
{
|
||||
userFriendlyMessage: msg`This role cannot be modified because it is a system role. Only custom roles can be edited.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private checkIfFieldPermissionShouldBeDeleted({
|
||||
fieldPermission,
|
||||
existingFieldPermissions,
|
||||
fieldPermissionsToDeleteIds,
|
||||
}: {
|
||||
fieldPermission: UpsertFieldPermissionsInput['fieldPermissions'][0];
|
||||
existingFieldPermissions: FieldPermissionEntity[];
|
||||
fieldPermissionsToDeleteIds: string[];
|
||||
}) {
|
||||
const existingFieldPermission = existingFieldPermissions.find(
|
||||
(existingFieldPermission) =>
|
||||
existingFieldPermission.fieldMetadataId ===
|
||||
fieldPermission.fieldMetadataId,
|
||||
const inputKeys = new Set(
|
||||
inputFieldPermissions.map((fp) =>
|
||||
keyFrom(fp.objectMetadataId, fp.fieldMetadataId),
|
||||
),
|
||||
);
|
||||
|
||||
if (existingFieldPermission) {
|
||||
const finalCanReadFieldValue =
|
||||
'canReadFieldValue' in fieldPermission
|
||||
? fieldPermission.canReadFieldValue
|
||||
: existingFieldPermission.canReadFieldValue;
|
||||
const finalCanUpdateFieldValue =
|
||||
'canUpdateFieldValue' in fieldPermission
|
||||
? fieldPermission.canUpdateFieldValue
|
||||
: existingFieldPermission.canUpdateFieldValue;
|
||||
for (const fieldPermission of inputFieldPermissions) {
|
||||
const flatFieldMetadata = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: fieldPermission.fieldMetadataId,
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
});
|
||||
|
||||
if (
|
||||
finalCanReadFieldValue === null &&
|
||||
finalCanUpdateFieldValue === null
|
||||
!isDefined(flatFieldMetadata) ||
|
||||
flatFieldMetadata.type !== FieldMetadataType.RELATION
|
||||
) {
|
||||
fieldPermissionsToDeleteIds.push(existingFieldPermission.id);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
const relationType = (
|
||||
flatFieldMetadata.settings as { relationType?: string } | undefined
|
||||
)?.relationType;
|
||||
if (
|
||||
isFieldMetadataTypeRelation(fieldMetadataForFieldPermissionToDelete)
|
||||
relationType !== RelationType.ONE_TO_MANY &&
|
||||
relationType !== RelationType.MANY_TO_ONE
|
||||
) {
|
||||
if (
|
||||
fieldMetadataForFieldPermissionToDelete.settings?.relationType ===
|
||||
RelationType.ONE_TO_MANY ||
|
||||
fieldMetadataForFieldPermissionToDelete.settings?.relationType ===
|
||||
RelationType.MANY_TO_ONE
|
||||
) {
|
||||
relationTargetFieldMetadataIds.push(
|
||||
fieldMetadataForFieldPermissionToDelete.relationTargetFieldMetadataId,
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const fieldPermissionsForRelationTargetFieldMetadataIds =
|
||||
allFieldPermissions
|
||||
.filter((fieldPermission) =>
|
||||
relationTargetFieldMetadataIds.includes(
|
||||
fieldPermission.fieldMetadataId,
|
||||
),
|
||||
)
|
||||
.map((fieldPermission) => fieldPermission.id);
|
||||
const targetObjectId =
|
||||
flatFieldMetadata.relationTargetObjectMetadataId ?? undefined;
|
||||
const targetFieldId =
|
||||
flatFieldMetadata.relationTargetFieldMetadataId ?? undefined;
|
||||
|
||||
return fieldPermissionsForRelationTargetFieldMetadataIds;
|
||||
}
|
||||
if (!targetObjectId || !targetFieldId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
private computeFieldPermissionForRelationTargetFieldMetadata({
|
||||
fieldPermissions,
|
||||
fieldMetadatasForFieldPermissions,
|
||||
}: {
|
||||
fieldPermissions: UpsertFieldPermissionsInput['fieldPermissions'];
|
||||
fieldMetadatasForFieldPermissions: FieldMetadataEntity[];
|
||||
}) {
|
||||
return fieldPermissions
|
||||
.map((fieldPermission) => {
|
||||
const fieldMetadata = fieldMetadatasForFieldPermissions.find(
|
||||
(fm) => fm.id === fieldPermission.fieldMetadataId,
|
||||
const targetKey = keyFrom(targetObjectId, targetFieldId);
|
||||
if (inputKeys.has(targetKey)) {
|
||||
const targetInInput = inputFieldPermissions.find(
|
||||
(fp) =>
|
||||
fp.objectMetadataId === targetObjectId &&
|
||||
fp.fieldMetadataId === targetFieldId,
|
||||
);
|
||||
|
||||
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 fieldPermissionsOnRelationTargetField =
|
||||
fieldPermissions.filter(
|
||||
(fieldPermissionInput) =>
|
||||
fieldPermissionInput.fieldMetadataId ===
|
||||
fieldMetadata.relationTargetFieldMetadataId,
|
||||
);
|
||||
|
||||
if (fieldPermissionsOnRelationTargetField.length > 0) {
|
||||
const firstFieldPermission =
|
||||
fieldPermissionsOnRelationTargetField[0]; // validation rules guarantee there can only be one
|
||||
|
||||
const hasConflictingPermissions =
|
||||
fieldPermission.canReadFieldValue !==
|
||||
firstFieldPermission.canReadFieldValue ||
|
||||
fieldPermission.canUpdateFieldValue !==
|
||||
firstFieldPermission.canUpdateFieldValue;
|
||||
|
||||
if (hasConflictingPermissions) {
|
||||
const fieldName = fieldMetadata.name;
|
||||
|
||||
throw new UserInputError(
|
||||
'Conflicting field permissions found for relation target field',
|
||||
{
|
||||
userFriendlyMessage: msg`Contradicting field permissions have been detected on a relation field (${fieldName}).`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
...fieldPermission,
|
||||
objectMetadataId: fieldMetadata.relationTargetObjectMetadataId,
|
||||
fieldMetadataId: fieldMetadata.relationTargetFieldMetadataId,
|
||||
};
|
||||
if (isDefined(targetInInput)) {
|
||||
const hasConflict =
|
||||
fieldPermission.canReadFieldValue !==
|
||||
targetInInput.canReadFieldValue ||
|
||||
fieldPermission.canUpdateFieldValue !==
|
||||
targetInInput.canUpdateFieldValue;
|
||||
if (hasConflict) {
|
||||
throw new UserInputError(
|
||||
'Conflicting field permissions found for relation target field',
|
||||
{
|
||||
userFriendlyMessage: msg`Contradicting field permissions have been detected on a relation field.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter(isDefined);
|
||||
desiredMap.set(targetKey, {
|
||||
objectMetadataId: targetObjectId,
|
||||
fieldMetadataId: targetFieldId,
|
||||
canReadFieldValue: fieldPermission.canReadFieldValue ?? undefined,
|
||||
canUpdateFieldValue: fieldPermission.canUpdateFieldValue ?? undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { type FlatFieldPermission } from 'src/engine/metadata-modules/flat-field-permission/types/flat-field-permission.type';
|
||||
import { type FieldPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/field-permission.dto';
|
||||
|
||||
export const fromFlatFieldPermissionToFieldPermissionDto = (
|
||||
flatFieldPermission: FlatFieldPermission,
|
||||
): FieldPermissionDTO => ({
|
||||
id: flatFieldPermission.id,
|
||||
objectMetadataId: flatFieldPermission.objectMetadataId,
|
||||
fieldMetadataId: flatFieldPermission.fieldMetadataId,
|
||||
roleId: flatFieldPermission.roleId,
|
||||
canReadFieldValue: flatFieldPermission.canReadFieldValue ?? null,
|
||||
canUpdateFieldValue: flatFieldPermission.canUpdateFieldValue ?? null,
|
||||
});
|
||||
Reference in New Issue
Block a user