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:
+1
@@ -146,6 +146,7 @@ export class FieldMetadataDTO<T extends FieldMetadataType = FieldMetadataType> {
|
||||
@HideField()
|
||||
workspaceId: string;
|
||||
|
||||
@FilterableField(() => UUIDScalarType)
|
||||
objectMetadataId: string;
|
||||
|
||||
@IsBoolean()
|
||||
|
||||
+10
@@ -58,6 +58,16 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma
|
||||
"universalSettings",
|
||||
],
|
||||
},
|
||||
"fieldPermission": {
|
||||
"propertiesToCompare": [
|
||||
"roleUniversalIdentifier",
|
||||
"objectMetadataUniversalIdentifier",
|
||||
"fieldMetadataUniversalIdentifier",
|
||||
"canReadFieldValue",
|
||||
"canUpdateFieldValue",
|
||||
],
|
||||
"propertiesToStringify": [],
|
||||
},
|
||||
"frontComponent": {
|
||||
"propertiesToCompare": [
|
||||
"name",
|
||||
|
||||
+37
@@ -1156,6 +1156,43 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
|
||||
universalProperty: undefined,
|
||||
},
|
||||
},
|
||||
fieldPermission: {
|
||||
roleId: {
|
||||
toCompare: true,
|
||||
toStringify: false,
|
||||
universalProperty: 'roleUniversalIdentifier',
|
||||
},
|
||||
objectMetadataId: {
|
||||
toCompare: true,
|
||||
toStringify: false,
|
||||
universalProperty: 'objectMetadataUniversalIdentifier',
|
||||
},
|
||||
fieldMetadataId: {
|
||||
toCompare: true,
|
||||
toStringify: false,
|
||||
universalProperty: 'fieldMetadataUniversalIdentifier',
|
||||
},
|
||||
canReadFieldValue: {
|
||||
toCompare: true,
|
||||
toStringify: false,
|
||||
universalProperty: undefined,
|
||||
},
|
||||
canUpdateFieldValue: {
|
||||
toCompare: true,
|
||||
toStringify: false,
|
||||
universalProperty: undefined,
|
||||
},
|
||||
createdAt: {
|
||||
toCompare: false,
|
||||
toStringify: false,
|
||||
universalProperty: undefined,
|
||||
},
|
||||
updatedAt: {
|
||||
toCompare: false,
|
||||
toStringify: false,
|
||||
universalProperty: undefined,
|
||||
},
|
||||
},
|
||||
objectPermission: {
|
||||
roleId: {
|
||||
toCompare: true,
|
||||
|
||||
+13
@@ -180,6 +180,19 @@ export const ALL_MANY_TO_ONE_METADATA_FOREIGN_KEY = {
|
||||
foreignKey: 'objectMetadataId',
|
||||
},
|
||||
},
|
||||
fieldPermission: {
|
||||
workspace: null,
|
||||
application: null,
|
||||
role: {
|
||||
foreignKey: 'roleId',
|
||||
},
|
||||
objectMetadata: {
|
||||
foreignKey: 'objectMetadataId',
|
||||
},
|
||||
fieldMetadata: {
|
||||
foreignKey: 'fieldMetadataId',
|
||||
},
|
||||
},
|
||||
pageLayout: {
|
||||
workspace: null,
|
||||
objectMetadata: {
|
||||
|
||||
+25
@@ -309,6 +309,31 @@ export const ALL_MANY_TO_ONE_METADATA_RELATIONS = {
|
||||
universalForeignKey: 'objectMetadataUniversalIdentifier',
|
||||
},
|
||||
},
|
||||
fieldPermission: {
|
||||
workspace: null,
|
||||
application: null,
|
||||
role: {
|
||||
metadataName: 'role',
|
||||
foreignKey: 'roleId',
|
||||
inverseOneToManyProperty: 'fieldPermissions',
|
||||
isNullable: false,
|
||||
universalForeignKey: 'roleUniversalIdentifier',
|
||||
},
|
||||
objectMetadata: {
|
||||
metadataName: 'objectMetadata',
|
||||
foreignKey: 'objectMetadataId',
|
||||
inverseOneToManyProperty: 'fieldPermissions',
|
||||
isNullable: false,
|
||||
universalForeignKey: 'objectMetadataUniversalIdentifier',
|
||||
},
|
||||
fieldMetadata: {
|
||||
metadataName: 'fieldMetadata',
|
||||
foreignKey: 'fieldMetadataId',
|
||||
inverseOneToManyProperty: 'fieldPermissions',
|
||||
isNullable: false,
|
||||
universalForeignKey: 'fieldMetadataUniversalIdentifier',
|
||||
},
|
||||
},
|
||||
pageLayout: {
|
||||
workspace: null,
|
||||
objectMetadata: {
|
||||
|
||||
+2
@@ -9,6 +9,7 @@ import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/
|
||||
import { WebhookEntity } from 'src/engine/metadata-modules/webhook/entities/webhook.entity';
|
||||
import { NavigationMenuItemEntity } from 'src/engine/metadata-modules/navigation-menu-item/entities/navigation-menu-item.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity';
|
||||
import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity';
|
||||
import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout-tab/entities/page-layout-tab.entity';
|
||||
import { PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity';
|
||||
@@ -48,6 +49,7 @@ export const ALL_METADATA_ENTITY_BY_METADATA_NAME = {
|
||||
logicFunction: LogicFunctionEntity,
|
||||
objectMetadata: ObjectMetadataEntity,
|
||||
objectPermission: ObjectPermissionEntity,
|
||||
fieldPermission: FieldPermissionEntity,
|
||||
role: RoleEntity,
|
||||
agent: AgentEntity,
|
||||
commandMenuItem: CommandMenuItemEntity,
|
||||
|
||||
+5
@@ -81,6 +81,11 @@ export const ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION = {
|
||||
role: true,
|
||||
objectMetadata: true,
|
||||
},
|
||||
fieldPermission: {
|
||||
role: true,
|
||||
objectMetadata: true,
|
||||
fieldMetadata: true,
|
||||
},
|
||||
pageLayout: {
|
||||
objectMetadata: true,
|
||||
pageLayoutTab: true,
|
||||
|
||||
+1
@@ -35,6 +35,7 @@ export const ALL_METADATA_SERIALIZED_RELATION = {
|
||||
roleTarget: {},
|
||||
permissionFlag: {},
|
||||
objectPermission: {},
|
||||
fieldPermission: {},
|
||||
pageLayout: {},
|
||||
pageLayoutTab: {},
|
||||
pageLayoutWidget: {
|
||||
|
||||
+19
-3
@@ -42,7 +42,12 @@ export const ALL_ONE_TO_MANY_METADATA_RELATIONS = {
|
||||
metadataName: 'viewSort',
|
||||
universalFlatEntityForeignKeyAggregator: 'viewSortUniversalIdentifiers',
|
||||
},
|
||||
fieldPermissions: null,
|
||||
fieldPermissions: {
|
||||
metadataName: 'fieldPermission',
|
||||
flatEntityForeignKeyAggregator: 'fieldPermissionIds',
|
||||
universalFlatEntityForeignKeyAggregator:
|
||||
'fieldPermissionUniversalIdentifiers',
|
||||
},
|
||||
indexFieldMetadatas: null,
|
||||
viewFields: {
|
||||
metadataName: 'viewField',
|
||||
@@ -91,7 +96,12 @@ export const ALL_ONE_TO_MANY_METADATA_RELATIONS = {
|
||||
universalFlatEntityForeignKeyAggregator:
|
||||
'objectPermissionUniversalIdentifiers',
|
||||
},
|
||||
fieldPermissions: null,
|
||||
fieldPermissions: {
|
||||
metadataName: 'fieldPermission',
|
||||
flatEntityForeignKeyAggregator: 'fieldPermissionIds',
|
||||
universalFlatEntityForeignKeyAggregator:
|
||||
'fieldPermissionUniversalIdentifiers',
|
||||
},
|
||||
views: {
|
||||
metadataName: 'view',
|
||||
flatEntityForeignKeyAggregator: 'viewIds',
|
||||
@@ -161,7 +171,12 @@ export const ALL_ONE_TO_MANY_METADATA_RELATIONS = {
|
||||
universalFlatEntityForeignKeyAggregator:
|
||||
'permissionFlagUniversalIdentifiers',
|
||||
},
|
||||
fieldPermissions: null,
|
||||
fieldPermissions: {
|
||||
metadataName: 'fieldPermission',
|
||||
flatEntityForeignKeyAggregator: 'fieldPermissionIds',
|
||||
universalFlatEntityForeignKeyAggregator:
|
||||
'fieldPermissionUniversalIdentifiers',
|
||||
},
|
||||
rowLevelPermissionPredicates: {
|
||||
metadataName: 'rowLevelPermissionPredicate',
|
||||
flatEntityForeignKeyAggregator: 'rowLevelPermissionPredicateIds',
|
||||
@@ -178,6 +193,7 @@ export const ALL_ONE_TO_MANY_METADATA_RELATIONS = {
|
||||
roleTarget: {},
|
||||
permissionFlag: {},
|
||||
objectPermission: {},
|
||||
fieldPermission: {},
|
||||
pageLayout: {
|
||||
tabs: {
|
||||
metadataName: 'pageLayoutTab',
|
||||
|
||||
+5
@@ -5,6 +5,7 @@ import { ApplicationEntity } from 'src/engine/core-modules/application/applicati
|
||||
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 { WorkspaceFlatFieldMetadataMapCacheService } from 'src/engine/metadata-modules/flat-field-metadata/services/workspace-flat-field-metadata-map-cache.service';
|
||||
import { WorkspaceFlatFieldPermissionMapCacheService } from 'src/engine/metadata-modules/flat-field-permission/services/workspace-flat-field-permission-map-cache.service';
|
||||
import { WorkspaceFlatIndexMapCacheService } from 'src/engine/metadata-modules/flat-index-metadata/services/workspace-flat-index-map-cache.service';
|
||||
import { WorkspaceFlatObjectMetadataMapCacheService } from 'src/engine/metadata-modules/flat-object-metadata/services/workspace-flat-object-metadata-map-cache.service';
|
||||
import { WorkspaceFlatObjectPermissionMapCacheService } from 'src/engine/metadata-modules/flat-object-permission/services/workspace-flat-object-permission-map-cache.service';
|
||||
@@ -25,6 +26,7 @@ import { FrontComponentEntity } from 'src/engine/metadata-modules/front-componen
|
||||
import { IndexFieldMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-field-metadata.entity';
|
||||
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity';
|
||||
import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity';
|
||||
import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout-tab/entities/page-layout-tab.entity';
|
||||
import { PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity';
|
||||
@@ -58,6 +60,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
|
||||
FieldMetadataEntity,
|
||||
ObjectMetadataEntity,
|
||||
ObjectPermissionEntity,
|
||||
FieldPermissionEntity,
|
||||
PageLayoutEntity,
|
||||
PageLayoutTabEntity,
|
||||
PageLayoutWidgetEntity,
|
||||
@@ -81,6 +84,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
|
||||
WorkspaceFlatFieldMetadataMapCacheService,
|
||||
WorkspaceFlatViewGroupMapCacheService,
|
||||
WorkspaceFlatObjectPermissionMapCacheService,
|
||||
WorkspaceFlatFieldPermissionMapCacheService,
|
||||
WorkspaceFlatPermissionFlagMapCacheService,
|
||||
WorkspaceFlatViewSortMapCacheService,
|
||||
WorkspaceFlatPageLayoutMapCacheService,
|
||||
@@ -101,6 +105,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
|
||||
WorkspaceFlatFieldMetadataMapCacheService,
|
||||
WorkspaceFlatViewGroupMapCacheService,
|
||||
WorkspaceFlatObjectPermissionMapCacheService,
|
||||
WorkspaceFlatFieldPermissionMapCacheService,
|
||||
WorkspaceFlatPermissionFlagMapCacheService,
|
||||
WorkspaceFlatViewSortMapCacheService,
|
||||
WorkspaceFlatPageLayoutMapCacheService,
|
||||
|
||||
+1
@@ -36,6 +36,7 @@ type Assertions = [
|
||||
Expect<
|
||||
Equal<
|
||||
FieldMetadataOneToManySyncableRelations,
|
||||
| 'fieldPermissions'
|
||||
| 'viewFields'
|
||||
| 'viewFilters'
|
||||
| 'kanbanAggregateOperationViews'
|
||||
|
||||
+27
@@ -6,6 +6,8 @@ import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-comma
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { type MetadataEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-entity.type';
|
||||
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 { type FlatFrontComponentMaps } from 'src/engine/metadata-modules/flat-front-component/types/flat-front-component-maps.type';
|
||||
import { type FlatFrontComponent } from 'src/engine/metadata-modules/flat-front-component/types/flat-front-component.type';
|
||||
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
|
||||
@@ -52,6 +54,7 @@ import { type FlatRowLevelPermissionPredicate } from 'src/engine/metadata-module
|
||||
import { type UniversalFlatAgent } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-agent.type';
|
||||
import { type UniversalFlatCommandMenuItem } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-command-menu-item.type';
|
||||
import { type UniversalFlatEntityFrom } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-from.type';
|
||||
import { type UniversalFlatFieldPermission } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-permission.type';
|
||||
import { type UniversalFlatFrontComponent } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-front-component.type';
|
||||
import { type UniversalFlatIndexMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-index-metadata.type';
|
||||
import { type UniversalFlatLogicFunction } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-logic-function.type';
|
||||
@@ -91,6 +94,14 @@ import {
|
||||
type UniversalDeleteCommandMenuItemAction,
|
||||
type UniversalUpdateCommandMenuItemAction,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/command-menu-item/types/workspace-migration-command-menu-item-action.type';
|
||||
import {
|
||||
type FlatCreateFieldPermissionAction,
|
||||
type FlatDeleteFieldPermissionAction,
|
||||
type FlatUpdateFieldPermissionAction,
|
||||
type UniversalCreateFieldPermissionAction,
|
||||
type UniversalDeleteFieldPermissionAction,
|
||||
type UniversalUpdateFieldPermissionAction,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field-permission/types/workspace-migration-field-permission-action.type';
|
||||
import {
|
||||
type FlatCreateFieldAction,
|
||||
type FlatDeleteFieldAction,
|
||||
@@ -624,6 +635,22 @@ export type AllFlatEntityTypesByMetadataName = {
|
||||
universalFlatEntity: UniversalFlatObjectPermission;
|
||||
entity: MetadataEntity<'objectPermission'>;
|
||||
};
|
||||
fieldPermission: {
|
||||
flatEntityMaps: FlatFieldPermissionMaps;
|
||||
universalActions: {
|
||||
create: UniversalCreateFieldPermissionAction;
|
||||
update: UniversalUpdateFieldPermissionAction;
|
||||
delete: UniversalDeleteFieldPermissionAction;
|
||||
};
|
||||
flatActions: {
|
||||
create: FlatCreateFieldPermissionAction;
|
||||
update: FlatUpdateFieldPermissionAction;
|
||||
delete: FlatDeleteFieldPermissionAction;
|
||||
};
|
||||
flatEntity: FlatFieldPermission;
|
||||
universalFlatEntity: UniversalFlatFieldPermission;
|
||||
entity: MetadataEntity<'fieldPermission'>;
|
||||
};
|
||||
pageLayout: {
|
||||
flatEntityMaps: FlatPageLayoutMaps;
|
||||
universalActions: {
|
||||
|
||||
+11
@@ -14,12 +14,21 @@ exports[`getMetadataRelatedMetadataNames should return related metadata names fo
|
||||
"objectMetadata",
|
||||
"fieldMetadata",
|
||||
"viewSort",
|
||||
"fieldPermission",
|
||||
"viewField",
|
||||
"viewFilter",
|
||||
"view",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`getMetadataRelatedMetadataNames should return related metadata names for fieldPermission 1`] = `
|
||||
[
|
||||
"role",
|
||||
"objectMetadata",
|
||||
"fieldMetadata",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`getMetadataRelatedMetadataNames should return related metadata names for frontComponent 1`] = `[]`;
|
||||
|
||||
exports[`getMetadataRelatedMetadataNames should return related metadata names for index 1`] = `
|
||||
@@ -43,6 +52,7 @@ exports[`getMetadataRelatedMetadataNames should return related metadata names fo
|
||||
"fieldMetadata",
|
||||
"index",
|
||||
"objectPermission",
|
||||
"fieldPermission",
|
||||
"view",
|
||||
]
|
||||
`;
|
||||
@@ -86,6 +96,7 @@ exports[`getMetadataRelatedMetadataNames should return related metadata names fo
|
||||
"roleTarget",
|
||||
"objectPermission",
|
||||
"permissionFlag",
|
||||
"fieldPermission",
|
||||
"rowLevelPermissionPredicate",
|
||||
"rowLevelPermissionPredicateGroup",
|
||||
]
|
||||
|
||||
+1
@@ -3,6 +3,7 @@
|
||||
exports[`sortMetadataNamesChildrenFirst should return metadata names sorted with children first (most manyToOne relations first) 1`] = `
|
||||
[
|
||||
"rowLevelPermissionPredicate",
|
||||
"fieldPermission",
|
||||
"navigationMenuItem",
|
||||
"viewField",
|
||||
"viewFilter",
|
||||
|
||||
+2
@@ -20,6 +20,7 @@ export const getFlatFieldMetadataMock = <T extends FieldMetadataType>(
|
||||
viewFilterIds: [],
|
||||
kanbanAggregateOperationViewIds: [],
|
||||
viewFieldIds: [],
|
||||
fieldPermissionIds: [],
|
||||
createdAt,
|
||||
mainGroupByFieldMetadataViewIds: [],
|
||||
updatedAt: createdAt,
|
||||
@@ -50,6 +51,7 @@ export const getFlatFieldMetadataMock = <T extends FieldMetadataType>(
|
||||
relationTargetFieldMetadataUniversalIdentifier: null,
|
||||
viewFilterUniversalIdentifiers: [],
|
||||
viewFieldUniversalIdentifiers: [],
|
||||
fieldPermissionUniversalIdentifiers: [],
|
||||
kanbanAggregateOperationViewUniversalIdentifiers: [],
|
||||
calendarViewUniversalIdentifiers: [],
|
||||
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
|
||||
|
||||
+2
@@ -37,6 +37,8 @@ export const getRelationTargetFlatFieldMetadataMock = ({
|
||||
viewFilterIds: [],
|
||||
viewFieldIds: [],
|
||||
kanbanAggregateOperationViewIds: [],
|
||||
fieldPermissionIds: [],
|
||||
fieldPermissionUniversalIdentifiers: [],
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
description: 'default flat field metadata description',
|
||||
|
||||
+4
@@ -121,6 +121,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
|
||||
"createdAt": Any<String>,
|
||||
"defaultValue": null,
|
||||
"description": "new field description",
|
||||
"fieldPermissionUniversalIdentifiers": [],
|
||||
"icon": "IconRelationOneToMany",
|
||||
"isActive": true,
|
||||
"isCustom": true,
|
||||
@@ -155,6 +156,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
|
||||
"createdAt": Any<String>,
|
||||
"defaultValue": null,
|
||||
"description": "new field description",
|
||||
"fieldPermissionUniversalIdentifiers": [],
|
||||
"icon": "IconPet",
|
||||
"isActive": true,
|
||||
"isCustom": true,
|
||||
@@ -191,6 +193,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
|
||||
"createdAt": Any<String>,
|
||||
"defaultValue": null,
|
||||
"description": "new field description",
|
||||
"fieldPermissionUniversalIdentifiers": [],
|
||||
"icon": "IconRelationOneToMany",
|
||||
"isActive": true,
|
||||
"isCustom": true,
|
||||
@@ -225,6 +228,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
|
||||
"createdAt": Any<String>,
|
||||
"defaultValue": null,
|
||||
"description": "new field description",
|
||||
"fieldPermissionUniversalIdentifiers": [],
|
||||
"icon": "IconBuilding",
|
||||
"isActive": true,
|
||||
"isCustom": true,
|
||||
|
||||
+6
@@ -106,6 +106,8 @@ export const fromFieldMetadataEntityToFlatFieldMetadata = ({
|
||||
[],
|
||||
viewFieldIds: fieldMetadataEntity.viewFields.map(({ id }) => id),
|
||||
viewFilterIds: fieldMetadataEntity.viewFilters.map(({ id }) => id),
|
||||
fieldPermissionIds:
|
||||
fieldMetadataEntity.fieldPermissions?.map(({ id }) => id) ?? [],
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
relationTargetObjectMetadataUniversalIdentifier,
|
||||
@@ -132,6 +134,10 @@ export const fromFieldMetadataEntityToFlatFieldMetadata = ({
|
||||
fieldMetadataEntity.viewSorts?.map(
|
||||
({ universalIdentifier }) => universalIdentifier,
|
||||
) ?? [],
|
||||
fieldPermissionUniversalIdentifiers:
|
||||
fieldMetadataEntity.fieldPermissions?.map(
|
||||
({ universalIdentifier }) => universalIdentifier,
|
||||
) ?? [],
|
||||
universalSettings: settingsWithUniversalIdentifiers,
|
||||
};
|
||||
};
|
||||
|
||||
+1
@@ -53,6 +53,7 @@ export const getDefaultFlatFieldMetadata = ({
|
||||
relationTargetFieldMetadataUniversalIdentifier: null,
|
||||
viewFilterUniversalIdentifiers: [],
|
||||
viewFieldUniversalIdentifiers: [],
|
||||
fieldPermissionUniversalIdentifiers: [],
|
||||
kanbanAggregateOperationViewUniversalIdentifiers: [],
|
||||
calendarViewUniversalIdentifiers: [],
|
||||
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import { type MetadataEntityPropertyName } from 'src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant';
|
||||
|
||||
export const FLAT_FIELD_PERMISSION_EDITABLE_PROPERTIES = [
|
||||
'roleId',
|
||||
'objectMetadataId',
|
||||
'fieldMetadataId',
|
||||
'canReadFieldValue',
|
||||
'canUpdateFieldValue',
|
||||
] as const satisfies MetadataEntityPropertyName<'fieldPermission'>[];
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
|
||||
import { type FlatFieldPermissionMaps } from 'src/engine/metadata-modules/flat-field-permission/types/flat-field-permission-maps.type';
|
||||
import { fromFieldPermissionEntityToFlatFieldPermission } from 'src/engine/metadata-modules/flat-field-permission/utils/from-field-permission-entity-to-flat-field-permission.util';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator';
|
||||
import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service';
|
||||
import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util';
|
||||
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';
|
||||
|
||||
@Injectable()
|
||||
@WorkspaceCache('flatFieldPermissionMaps')
|
||||
export class WorkspaceFlatFieldPermissionMapCacheService extends WorkspaceCacheProvider<FlatFieldPermissionMaps> {
|
||||
constructor(
|
||||
@InjectRepository(FieldPermissionEntity)
|
||||
private readonly fieldPermissionRepository: Repository<FieldPermissionEntity>,
|
||||
@InjectRepository(ApplicationEntity)
|
||||
private readonly applicationRepository: Repository<ApplicationEntity>,
|
||||
@InjectRepository(RoleEntity)
|
||||
private readonly roleRepository: Repository<RoleEntity>,
|
||||
@InjectRepository(ObjectMetadataEntity)
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||
@InjectRepository(FieldMetadataEntity)
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async computeForCache(workspaceId: string): Promise<FlatFieldPermissionMaps> {
|
||||
const [
|
||||
fieldPermissions,
|
||||
applications,
|
||||
roles,
|
||||
objectMetadatas,
|
||||
fieldMetadatas,
|
||||
] = await Promise.all([
|
||||
this.fieldPermissionRepository.find({
|
||||
where: { workspaceId },
|
||||
withDeleted: true,
|
||||
}),
|
||||
this.applicationRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id', 'universalIdentifier'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
this.roleRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id', 'universalIdentifier'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
this.objectMetadataRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id', 'universalIdentifier'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
this.fieldMetadataRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id', 'universalIdentifier'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
const applicationIdToUniversalIdentifierMap =
|
||||
createIdToUniversalIdentifierMap(applications);
|
||||
const roleIdToUniversalIdentifierMap =
|
||||
createIdToUniversalIdentifierMap(roles);
|
||||
const objectMetadataIdToUniversalIdentifierMap =
|
||||
createIdToUniversalIdentifierMap(objectMetadatas);
|
||||
const fieldMetadataIdToUniversalIdentifierMap =
|
||||
createIdToUniversalIdentifierMap(fieldMetadatas);
|
||||
|
||||
const flatFieldPermissionMaps = createEmptyFlatEntityMaps();
|
||||
|
||||
for (const fieldPermissionEntity of fieldPermissions) {
|
||||
const flatFieldPermission =
|
||||
fromFieldPermissionEntityToFlatFieldPermission({
|
||||
entity: fieldPermissionEntity,
|
||||
applicationIdToUniversalIdentifierMap,
|
||||
roleIdToUniversalIdentifierMap,
|
||||
objectMetadataIdToUniversalIdentifierMap,
|
||||
fieldMetadataIdToUniversalIdentifierMap,
|
||||
});
|
||||
|
||||
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
|
||||
flatEntity: flatFieldPermission,
|
||||
flatEntityMapsToMutate: flatFieldPermissionMaps,
|
||||
});
|
||||
}
|
||||
|
||||
return flatFieldPermissionMaps;
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { type FlatFieldPermission } from 'src/engine/metadata-modules/flat-field-permission/types/flat-field-permission.type';
|
||||
|
||||
export type FlatFieldPermissionMaps = FlatEntityMaps<FlatFieldPermission>;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
|
||||
import { type FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity';
|
||||
|
||||
export type FlatFieldPermission = FlatEntityFrom<FieldPermissionEntity>;
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
|
||||
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
|
||||
import { resolveEntityRelationUniversalIdentifiers } from 'src/engine/metadata-modules/flat-entity/utils/resolve-entity-relation-universal-identifiers.util';
|
||||
import { type FieldPermissionInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-field-permissions.input';
|
||||
import { type UniversalFlatFieldPermission } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-permission.type';
|
||||
|
||||
export const fromCreateFieldPermissionInputToUniversalFlatFieldPermission = ({
|
||||
fieldPermissionInput,
|
||||
roleId,
|
||||
flatApplication,
|
||||
flatRoleMaps,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
}: {
|
||||
fieldPermissionInput: FieldPermissionInput;
|
||||
roleId: string;
|
||||
flatApplication: FlatApplication;
|
||||
} & Pick<
|
||||
AllFlatEntityMaps,
|
||||
'flatRoleMaps' | 'flatObjectMetadataMaps' | 'flatFieldMetadataMaps'
|
||||
>): UniversalFlatFieldPermission & { id: string } => {
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const {
|
||||
roleUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
fieldMetadataUniversalIdentifier,
|
||||
} = resolveEntityRelationUniversalIdentifiers({
|
||||
metadataName: 'fieldPermission',
|
||||
foreignKeyValues: {
|
||||
roleId,
|
||||
objectMetadataId: fieldPermissionInput.objectMetadataId,
|
||||
fieldMetadataId: fieldPermissionInput.fieldMetadataId,
|
||||
},
|
||||
flatEntityMaps: {
|
||||
flatRoleMaps,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id: v4(),
|
||||
universalIdentifier: v4(),
|
||||
applicationUniversalIdentifier: flatApplication.universalIdentifier,
|
||||
roleUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
fieldMetadataUniversalIdentifier,
|
||||
canReadFieldValue: fieldPermissionInput.canReadFieldValue ?? undefined,
|
||||
canUpdateFieldValue: fieldPermissionInput.canUpdateFieldValue ?? undefined,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
};
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
FlatEntityMapsException,
|
||||
FlatEntityMapsExceptionCode,
|
||||
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
|
||||
import { getMetadataEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-entity-relation-properties.util';
|
||||
import { type FlatFieldPermission } from 'src/engine/metadata-modules/flat-field-permission/types/flat-field-permission.type';
|
||||
import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type';
|
||||
|
||||
export const fromFieldPermissionEntityToFlatFieldPermission = ({
|
||||
entity: fieldPermissionEntity,
|
||||
applicationIdToUniversalIdentifierMap,
|
||||
roleIdToUniversalIdentifierMap,
|
||||
objectMetadataIdToUniversalIdentifierMap,
|
||||
fieldMetadataIdToUniversalIdentifierMap,
|
||||
}: FromEntityToFlatEntityArgs<'fieldPermission'>): FlatFieldPermission => {
|
||||
const fieldPermissionEntityWithoutRelations = removePropertiesFromRecord(
|
||||
fieldPermissionEntity,
|
||||
getMetadataEntityRelationProperties('fieldPermission'),
|
||||
);
|
||||
|
||||
const applicationUniversalIdentifier =
|
||||
applicationIdToUniversalIdentifierMap.get(
|
||||
fieldPermissionEntity.applicationId,
|
||||
);
|
||||
|
||||
if (!isDefined(applicationUniversalIdentifier)) {
|
||||
throw new FlatEntityMapsException(
|
||||
`Application with id ${fieldPermissionEntity.applicationId} not found for fieldPermission ${fieldPermissionEntity.id}`,
|
||||
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const roleUniversalIdentifier = roleIdToUniversalIdentifierMap.get(
|
||||
fieldPermissionEntity.roleId,
|
||||
);
|
||||
|
||||
if (!isDefined(roleUniversalIdentifier)) {
|
||||
throw new FlatEntityMapsException(
|
||||
`Role with id ${fieldPermissionEntity.roleId} not found for fieldPermission ${fieldPermissionEntity.id}`,
|
||||
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const objectMetadataUniversalIdentifier =
|
||||
objectMetadataIdToUniversalIdentifierMap.get(
|
||||
fieldPermissionEntity.objectMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(objectMetadataUniversalIdentifier)) {
|
||||
throw new FlatEntityMapsException(
|
||||
`ObjectMetadata with id ${fieldPermissionEntity.objectMetadataId} not found for fieldPermission ${fieldPermissionEntity.id}`,
|
||||
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const fieldMetadataUniversalIdentifier =
|
||||
fieldMetadataIdToUniversalIdentifierMap.get(
|
||||
fieldPermissionEntity.fieldMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(fieldMetadataUniversalIdentifier)) {
|
||||
throw new FlatEntityMapsException(
|
||||
`FieldMetadata with id ${fieldPermissionEntity.fieldMetadataId} not found for fieldPermission ${fieldPermissionEntity.id}`,
|
||||
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...fieldPermissionEntityWithoutRelations,
|
||||
createdAt: fieldPermissionEntity.createdAt.toISOString(),
|
||||
updatedAt: fieldPermissionEntity.updatedAt.toISOString(),
|
||||
universalIdentifier:
|
||||
fieldPermissionEntityWithoutRelations.universalIdentifier,
|
||||
applicationUniversalIdentifier,
|
||||
roleUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
fieldMetadataUniversalIdentifier,
|
||||
};
|
||||
};
|
||||
+2
@@ -20,6 +20,7 @@ export const getFlatObjectMetadataMock = (
|
||||
viewIds: [],
|
||||
indexMetadataIds: [],
|
||||
objectPermissionIds: [],
|
||||
fieldPermissionIds: [],
|
||||
fieldIds: [],
|
||||
description: 'default flat object metadata description',
|
||||
icon: 'icon',
|
||||
@@ -50,6 +51,7 @@ export const getFlatObjectMetadataMock = (
|
||||
applicationUniversalIdentifier: applicationId,
|
||||
fieldUniversalIdentifiers: [],
|
||||
objectPermissionUniversalIdentifiers: [],
|
||||
fieldPermissionUniversalIdentifiers: [],
|
||||
viewUniversalIdentifiers: [],
|
||||
indexMetadataUniversalIdentifiers: [],
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
|
||||
+1
@@ -95,6 +95,7 @@ export const fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCre
|
||||
applicationUniversalIdentifier: flatApplication.universalIdentifier,
|
||||
fieldUniversalIdentifiers: [],
|
||||
objectPermissionUniversalIdentifiers: [],
|
||||
fieldPermissionUniversalIdentifiers: [],
|
||||
viewUniversalIdentifiers: [],
|
||||
indexMetadataUniversalIdentifiers: [],
|
||||
labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
|
||||
+6
@@ -81,6 +81,8 @@ export const fromObjectMetadataEntityToFlatObjectMetadata = ({
|
||||
objectPermissionIds: objectMetadataEntity.objectPermissions.map(
|
||||
({ id }) => id,
|
||||
),
|
||||
fieldPermissionIds:
|
||||
objectMetadataEntity.fieldPermissions?.map(({ id }) => id) ?? [],
|
||||
applicationUniversalIdentifier,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
imageIdentifierFieldMetadataUniversalIdentifier,
|
||||
@@ -97,5 +99,9 @@ export const fromObjectMetadataEntityToFlatObjectMetadata = ({
|
||||
objectMetadataEntity.objectPermissions.map(
|
||||
({ universalIdentifier }) => universalIdentifier,
|
||||
),
|
||||
fieldPermissionUniversalIdentifiers:
|
||||
objectMetadataEntity.fieldPermissions?.map(
|
||||
({ universalIdentifier }) => universalIdentifier,
|
||||
) ?? [],
|
||||
};
|
||||
};
|
||||
|
||||
+1
@@ -58,6 +58,7 @@ export const fromCreateRoleInputToFlatRoleToCreate = ({
|
||||
objectPermissionUniversalIdentifiers: [],
|
||||
permissionFlagUniversalIdentifiers: [],
|
||||
fieldPermissionIds: [],
|
||||
fieldPermissionUniversalIdentifiers: [],
|
||||
rowLevelPermissionPredicateIds: [],
|
||||
rowLevelPermissionPredicateUniversalIdentifiers: [],
|
||||
rowLevelPermissionPredicateGroupIds: [],
|
||||
|
||||
+3
@@ -74,6 +74,9 @@ export const fromRoleEntityToFlatRole = ({
|
||||
permissionFlagUniversalIdentifiers: roleEntity.permissionFlags.map(
|
||||
({ universalIdentifier }) => universalIdentifier,
|
||||
),
|
||||
fieldPermissionUniversalIdentifiers: roleEntity.fieldPermissions.map(
|
||||
({ universalIdentifier }) => universalIdentifier,
|
||||
),
|
||||
rowLevelPermissionPredicateUniversalIdentifiers:
|
||||
roleEntity.rowLevelPermissionPredicates.map(
|
||||
({ universalIdentifier }) => universalIdentifier,
|
||||
|
||||
+8
@@ -38,6 +38,7 @@ const PARTIAL_ID_FIELD = {
|
||||
kanbanAggregateOperationViewUniversalIdentifiers: [],
|
||||
calendarViewUniversalIdentifiers: [],
|
||||
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
|
||||
fieldPermissionUniversalIdentifiers: [],
|
||||
viewSortUniversalIdentifiers: [],
|
||||
} as const satisfies PartialSystemFlatFieldMetadata<FieldMetadataType.UUID>;
|
||||
|
||||
@@ -66,6 +67,7 @@ const PARTIAL_CREATED_AT_FIELD = {
|
||||
kanbanAggregateOperationViewUniversalIdentifiers: [],
|
||||
calendarViewUniversalIdentifiers: [],
|
||||
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
|
||||
fieldPermissionUniversalIdentifiers: [],
|
||||
viewSortUniversalIdentifiers: [],
|
||||
} as const satisfies PartialSystemFlatFieldMetadata<FieldMetadataType.DATE_TIME>;
|
||||
|
||||
@@ -94,6 +96,7 @@ const PARTIAL_UPDATED_AT_FIELD = {
|
||||
kanbanAggregateOperationViewUniversalIdentifiers: [],
|
||||
calendarViewUniversalIdentifiers: [],
|
||||
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
|
||||
fieldPermissionUniversalIdentifiers: [],
|
||||
viewSortUniversalIdentifiers: [],
|
||||
} as const satisfies PartialSystemFlatFieldMetadata<FieldMetadataType.DATE_TIME>;
|
||||
|
||||
@@ -122,6 +125,7 @@ const PARTIAL_DELETED_AT_FIELD = {
|
||||
kanbanAggregateOperationViewUniversalIdentifiers: [],
|
||||
calendarViewUniversalIdentifiers: [],
|
||||
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
|
||||
fieldPermissionUniversalIdentifiers: [],
|
||||
viewSortUniversalIdentifiers: [],
|
||||
} as const satisfies PartialSystemFlatFieldMetadata<FieldMetadataType.DATE_TIME>;
|
||||
|
||||
@@ -150,6 +154,7 @@ const PARTIAL_CREATED_BY_FIELD = {
|
||||
kanbanAggregateOperationViewUniversalIdentifiers: [],
|
||||
calendarViewUniversalIdentifiers: [],
|
||||
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
|
||||
fieldPermissionUniversalIdentifiers: [],
|
||||
viewSortUniversalIdentifiers: [],
|
||||
} as const satisfies PartialSystemFlatFieldMetadata<FieldMetadataType.ACTOR>;
|
||||
|
||||
@@ -178,6 +183,7 @@ const PARTIAL_UPDATED_BY_FIELD = {
|
||||
kanbanAggregateOperationViewUniversalIdentifiers: [],
|
||||
calendarViewUniversalIdentifiers: [],
|
||||
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
|
||||
fieldPermissionUniversalIdentifiers: [],
|
||||
viewSortUniversalIdentifiers: [],
|
||||
} as const satisfies PartialSystemFlatFieldMetadata<FieldMetadataType.ACTOR>;
|
||||
|
||||
@@ -206,6 +212,7 @@ const PARTIAL_POSITION_FIELD = {
|
||||
kanbanAggregateOperationViewUniversalIdentifiers: [],
|
||||
calendarViewUniversalIdentifiers: [],
|
||||
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
|
||||
fieldPermissionUniversalIdentifiers: [],
|
||||
viewSortUniversalIdentifiers: [],
|
||||
} as const satisfies PartialSystemFlatFieldMetadata<FieldMetadataType.POSITION>;
|
||||
|
||||
@@ -236,6 +243,7 @@ const PARTIAL_SEARCH_VECTOR_FIELD = {
|
||||
kanbanAggregateOperationViewUniversalIdentifiers: [],
|
||||
calendarViewUniversalIdentifiers: [],
|
||||
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
|
||||
fieldPermissionUniversalIdentifiers: [],
|
||||
viewSortUniversalIdentifiers: [],
|
||||
} as const satisfies PartialSystemFlatFieldMetadata<FieldMetadataType.TS_VECTOR>;
|
||||
|
||||
|
||||
+1
@@ -151,6 +151,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
|
||||
kanbanAggregateOperationViewUniversalIdentifiers: [],
|
||||
calendarViewUniversalIdentifiers: [],
|
||||
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
|
||||
fieldPermissionUniversalIdentifiers: [],
|
||||
universalSettings: null,
|
||||
viewSortUniversalIdentifiers: [],
|
||||
};
|
||||
|
||||
+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,
|
||||
});
|
||||
+4
@@ -42,6 +42,7 @@ export enum PermissionsExceptionCode {
|
||||
UPSERT_FIELD_PERMISSION_FAILED = 'UPSERT_FIELD_PERMISSION_FAILED',
|
||||
PERMISSION_NOT_FOUND = 'PERMISSION_NOT_FOUND',
|
||||
OBJECT_PERMISSION_NOT_FOUND = 'OBJECT_PERMISSION_NOT_FOUND',
|
||||
FIELD_PERMISSION_NOT_FOUND = 'FIELD_PERMISSION_NOT_FOUND',
|
||||
EMPTY_FIELD_PERMISSION_NOT_ALLOWED = 'EMPTY_FIELD_PERMISSION_NOT_ALLOWED',
|
||||
JOIN_COLUMN_NAME_REQUIRED = 'JOIN_COLUMN_NAME_REQUIRED',
|
||||
COMPOSITE_TYPE_NOT_FOUND = 'COMPOSITE_TYPE_NOT_FOUND',
|
||||
@@ -128,6 +129,8 @@ const getPermissionsExceptionUserFriendlyMessage = (
|
||||
return msg`Permission not found.`;
|
||||
case PermissionsExceptionCode.OBJECT_PERMISSION_NOT_FOUND:
|
||||
return msg`Object permission not found.`;
|
||||
case PermissionsExceptionCode.FIELD_PERMISSION_NOT_FOUND:
|
||||
return msg`Field permission not found.`;
|
||||
case PermissionsExceptionCode.EMPTY_FIELD_PERMISSION_NOT_ALLOWED:
|
||||
return msg`Empty field permissions are not allowed.`;
|
||||
case PermissionsExceptionCode.JOIN_COLUMN_NAME_REQUIRED:
|
||||
@@ -184,6 +187,7 @@ export enum PermissionsExceptionMessage {
|
||||
FIELD_RESTRICTION_ONLY_ALLOWED_ON_READABLE_OBJECT = 'Field restriction only makes sense on readable object',
|
||||
FIELD_RESTRICTION_ON_UPDATE_ONLY_ALLOWED_ON_UPDATABLE_OBJECT = 'Field restriction on update only makes sense on updatable object',
|
||||
OBJECT_PERMISSION_NOT_FOUND = 'Object permission not found',
|
||||
FIELD_PERMISSION_NOT_FOUND = 'Field permission not found',
|
||||
EMPTY_FIELD_PERMISSION_NOT_ALLOWED = 'Empty field permission not allowed',
|
||||
ROLE_MUST_HAVE_AT_LEAST_ONE_TARGET = 'Role must be assignable to at least one target type',
|
||||
ROLE_CANNOT_BE_ASSIGNED_TO_USERS = 'Role cannot be assigned to users',
|
||||
|
||||
+1
@@ -48,6 +48,7 @@ export const permissionGraphqlApiExceptionHandler = (
|
||||
case PermissionsExceptionCode.ROLE_NOT_FOUND:
|
||||
case PermissionsExceptionCode.OBJECT_METADATA_NOT_FOUND:
|
||||
case PermissionsExceptionCode.FIELD_METADATA_NOT_FOUND:
|
||||
case PermissionsExceptionCode.FIELD_PERMISSION_NOT_FOUND:
|
||||
case PermissionsExceptionCode.PERMISSION_NOT_FOUND:
|
||||
throw new NotFoundError(error);
|
||||
case PermissionsExceptionCode.UPSERT_FIELD_PERMISSION_FAILED:
|
||||
|
||||
@@ -8,9 +8,10 @@ import { Args, Mutation, Parent, Query, ResolveField } from '@nestjs/graphql';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { FeatureFlagKey } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/services/api-key-role.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
@@ -20,7 +21,6 @@ import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/use
|
||||
import { WorkspaceMemberDTO } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthWorkspaceMemberId } from 'src/engine/decorators/auth/auth-workspace-member-id.decorator';
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { RequireFeatureFlag } from 'src/engine/guards/feature-flag.guard';
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
@@ -39,11 +39,12 @@ import { UpsertFieldPermissionsInput } from 'src/engine/metadata-modules/object-
|
||||
import { UpsertObjectPermissionsInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-object-permissions.input';
|
||||
import { FieldPermissionService } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.service';
|
||||
import { ObjectPermissionService } from 'src/engine/metadata-modules/object-permission/object-permission.service';
|
||||
import { fromFlatFieldPermissionToFieldPermissionDto } from 'src/engine/metadata-modules/object-permission/utils/from-flat-field-permission-to-field-permission-dto.util';
|
||||
import { fromFlatObjectPermissionToObjectPermissionDto } from 'src/engine/metadata-modules/object-permission/utils/from-flat-object-permission-to-object-permission-dto.util';
|
||||
import { PermissionFlagDTO } from 'src/engine/metadata-modules/permission-flag/dtos/permission-flag.dto';
|
||||
import { fromFlatPermissionFlagToPermissionFlagDto } from 'src/engine/metadata-modules/permission-flag/utils/from-flat-permission-flag-to-permission-flag-dto.util';
|
||||
import { UpsertPermissionFlagsInput } from 'src/engine/metadata-modules/permission-flag/dtos/upsert-permission-flag-input';
|
||||
import { PermissionFlagService } from 'src/engine/metadata-modules/permission-flag/permission-flag.service';
|
||||
import { fromFlatPermissionFlagToPermissionFlagDto } from 'src/engine/metadata-modules/permission-flag/utils/from-flat-permission-flag-to-permission-flag-dto.util';
|
||||
import {
|
||||
PermissionsException,
|
||||
PermissionsExceptionCode,
|
||||
@@ -242,10 +243,14 @@ export class RoleResolver {
|
||||
@Args('upsertFieldPermissionsInput')
|
||||
upsertFieldPermissionsInput: UpsertFieldPermissionsInput,
|
||||
): Promise<FieldPermissionDTO[]> {
|
||||
return this.fieldPermissionService.upsertFieldPermissions({
|
||||
workspaceId: workspace.id,
|
||||
input: upsertFieldPermissionsInput,
|
||||
});
|
||||
const flatFieldPermissions =
|
||||
await this.fieldPermissionService.upsertFieldPermissions({
|
||||
workspaceId: workspace.id,
|
||||
input: upsertFieldPermissionsInput,
|
||||
});
|
||||
return flatFieldPermissions.map(
|
||||
fromFlatFieldPermissionToFieldPermissionDto,
|
||||
);
|
||||
}
|
||||
|
||||
@Mutation(() => UpsertRowLevelPermissionPredicatesResultDTO)
|
||||
|
||||
+1
-1
@@ -87,7 +87,7 @@ export class WorkspaceFlatRoleMapCacheService extends WorkspaceCacheProvider<
|
||||
}),
|
||||
this.fieldPermissionRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id', 'roleId'],
|
||||
select: ['id', 'universalIdentifier', 'roleId'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
this.rowLevelPermissionPredicateRepository.find({
|
||||
|
||||
Reference in New Issue
Block a user