Migrate object permission to syncable entity (#18609)

Closes [#2223](https://github.com/twentyhq/core-team-issues/issues/2223)
This commit is contained in:
Abdul Rahman
2026-03-18 22:02:18 +05:30
committed by GitHub
parent 8ccaf635bc
commit 6d4d1a97bc
86 changed files with 2475 additions and 424 deletions
@@ -136,6 +136,17 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma
"standardOverrides",
],
},
"objectPermission": {
"propertiesToCompare": [
"roleUniversalIdentifier",
"objectMetadataUniversalIdentifier",
"canReadObjectRecords",
"canUpdateObjectRecords",
"canSoftDeleteObjectRecords",
"canDestroyObjectRecords",
],
"propertiesToStringify": [],
},
"pageLayout": {
"propertiesToCompare": [
"name",
@@ -1156,6 +1156,48 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
universalProperty: undefined,
},
},
objectPermission: {
roleId: {
toCompare: true,
toStringify: false,
universalProperty: 'roleUniversalIdentifier',
},
objectMetadataId: {
toCompare: true,
toStringify: false,
universalProperty: 'objectMetadataUniversalIdentifier',
},
canReadObjectRecords: {
toCompare: true,
toStringify: false,
universalProperty: undefined,
},
canUpdateObjectRecords: {
toCompare: true,
toStringify: false,
universalProperty: undefined,
},
canSoftDeleteObjectRecords: {
toCompare: true,
toStringify: false,
universalProperty: undefined,
},
canDestroyObjectRecords: {
toCompare: true,
toStringify: false,
universalProperty: undefined,
},
createdAt: {
toCompare: false,
toStringify: false,
universalProperty: undefined,
},
updatedAt: {
toCompare: false,
toStringify: false,
universalProperty: undefined,
},
},
rowLevelPermissionPredicate: {
fieldMetadataId: {
toCompare: true,
@@ -170,6 +170,16 @@ export const ALL_MANY_TO_ONE_METADATA_FOREIGN_KEY = {
foreignKey: 'roleId',
},
},
objectPermission: {
workspace: null,
application: null,
role: {
foreignKey: 'roleId',
},
objectMetadata: {
foreignKey: 'objectMetadataId',
},
},
pageLayout: {
workspace: null,
objectMetadata: {
@@ -291,6 +291,24 @@ export const ALL_MANY_TO_ONE_METADATA_RELATIONS = {
universalForeignKey: 'roleUniversalIdentifier',
},
},
objectPermission: {
workspace: null,
application: null,
role: {
metadataName: 'role',
foreignKey: 'roleId',
inverseOneToManyProperty: 'objectPermissions',
isNullable: false,
universalForeignKey: 'roleUniversalIdentifier',
},
objectMetadata: {
metadataName: 'objectMetadata',
foreignKey: 'objectMetadataId',
inverseOneToManyProperty: 'objectPermissions',
isNullable: false,
universalForeignKey: 'objectMetadataUniversalIdentifier',
},
},
pageLayout: {
workspace: null,
objectMetadata: {
@@ -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 { 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';
import { PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity';
@@ -46,6 +47,7 @@ export const ALL_METADATA_ENTITY_BY_METADATA_NAME = {
skill: SkillEntity,
logicFunction: LogicFunctionEntity,
objectMetadata: ObjectMetadataEntity,
objectPermission: ObjectPermissionEntity,
role: RoleEntity,
agent: AgentEntity,
commandMenuItem: CommandMenuItemEntity,
@@ -77,6 +77,10 @@ export const ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION = {
permissionFlag: {
role: true,
},
objectPermission: {
role: true,
objectMetadata: true,
},
pageLayout: {
objectMetadata: true,
pageLayoutTab: true,
@@ -34,6 +34,7 @@ export const ALL_METADATA_SERIALIZED_RELATION = {
role: {},
roleTarget: {},
permissionFlag: {},
objectPermission: {},
pageLayout: {},
pageLayoutTab: {},
pageLayoutWidget: {
@@ -85,7 +85,12 @@ export const ALL_ONE_TO_MANY_METADATA_RELATIONS = {
universalFlatEntityForeignKeyAggregator:
'indexMetadataUniversalIdentifiers',
},
objectPermissions: null,
objectPermissions: {
metadataName: 'objectPermission',
flatEntityForeignKeyAggregator: 'objectPermissionIds',
universalFlatEntityForeignKeyAggregator:
'objectPermissionUniversalIdentifiers',
},
fieldPermissions: null,
views: {
metadataName: 'view',
@@ -144,7 +149,12 @@ export const ALL_ONE_TO_MANY_METADATA_RELATIONS = {
flatEntityForeignKeyAggregator: 'roleTargetIds',
universalFlatEntityForeignKeyAggregator: 'roleTargetUniversalIdentifiers',
},
objectPermissions: null,
objectPermissions: {
metadataName: 'objectPermission',
flatEntityForeignKeyAggregator: 'objectPermissionIds',
universalFlatEntityForeignKeyAggregator:
'objectPermissionUniversalIdentifiers',
},
permissionFlags: {
metadataName: 'permissionFlag',
flatEntityForeignKeyAggregator: 'permissionFlagIds',
@@ -167,6 +177,7 @@ export const ALL_ONE_TO_MANY_METADATA_RELATIONS = {
},
roleTarget: {},
permissionFlag: {},
objectPermission: {},
pageLayout: {
tabs: {
metadataName: 'pageLayoutTab',
@@ -3,14 +3,15 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { FrontComponentEntity } from 'src/engine/metadata-modules/front-component/entities/front-component.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 { 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';
import { WorkspaceFlatPageLayoutTabMapCacheService } from 'src/engine/metadata-modules/flat-page-layout-tab/services/workspace-flat-page-layout-tab-map-cache.service';
import { WorkspaceFlatPageLayoutWidgetMapCacheService } from 'src/engine/metadata-modules/flat-page-layout-widget/services/workspace-flat-page-layout-widget-map-cache.service';
import { WorkspaceFlatPageLayoutMapCacheService } from 'src/engine/metadata-modules/flat-page-layout/services/workspace-flat-page-layout-map-cache.service';
import { WorkspaceFlatPermissionFlagMapCacheService } from 'src/engine/metadata-modules/flat-permission-flag/services/workspace-flat-permission-flag-map-cache.service';
import { WorkspaceFlatRowLevelPermissionPredicateGroupMapCacheService } from 'src/engine/metadata-modules/flat-row-level-permission-predicate/services/workspace-flat-row-level-permission-predicate-group-map-cache.service';
import { WorkspaceFlatRowLevelPermissionPredicateMapCacheService } from 'src/engine/metadata-modules/flat-row-level-permission-predicate/services/workspace-flat-row-level-permission-predicate-map-cache.service';
import { WorkspaceFlatViewFieldGroupMapCacheService } from 'src/engine/metadata-modules/flat-view-field-group/services/workspace-flat-view-field-group-map-cache.service';
@@ -18,10 +19,13 @@ import { WorkspaceFlatViewFieldMapCacheService } from 'src/engine/metadata-modul
import { WorkspaceFlatViewFilterGroupMapCacheService } from 'src/engine/metadata-modules/flat-view-filter-group/services/workspace-flat-view-filter-group-map-cache.service';
import { WorkspaceFlatViewFilterMapCacheService } from 'src/engine/metadata-modules/flat-view-filter/services/workspace-flat-view-filter-map-cache.service';
import { WorkspaceFlatViewGroupMapCacheService } from 'src/engine/metadata-modules/flat-view-group/services/workspace-flat-view-group-map-cache.service';
import { WorkspaceFlatViewSortMapCacheService } from 'src/engine/metadata-modules/flat-view-sort/services/workspace-flat-view-sort-map-cache.service';
import { WorkspaceFlatViewMapCacheService } from 'src/engine/metadata-modules/flat-view/services/workspace-flat-view-map-cache.service';
import { FrontComponentEntity } from 'src/engine/metadata-modules/front-component/entities/front-component.entity';
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 { 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';
import { PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity';
@@ -34,11 +38,9 @@ import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities
import { ViewFilterGroupEntity } from 'src/engine/metadata-modules/view-filter-group/entities/view-filter-group.entity';
import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity';
import { ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity';
import { ViewSortEntity } from 'src/engine/metadata-modules/view-sort/entities/view-sort.entity';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
import { ViewSortEntity } from 'src/engine/metadata-modules/view-sort/entities/view-sort.entity';
import { WorkspaceFlatPermissionFlagMapCacheService } from 'src/engine/metadata-modules/flat-permission-flag/services/workspace-flat-permission-flag-map-cache.service';
import { WorkspaceFlatViewSortMapCacheService } from 'src/engine/metadata-modules/flat-view-sort/services/workspace-flat-view-sort-map-cache.service';
@Module({
imports: [
@@ -55,6 +57,7 @@ import { WorkspaceFlatViewSortMapCacheService } from 'src/engine/metadata-module
IndexFieldMetadataEntity,
FieldMetadataEntity,
ObjectMetadataEntity,
ObjectPermissionEntity,
PageLayoutEntity,
PageLayoutTabEntity,
PageLayoutWidgetEntity,
@@ -77,6 +80,7 @@ import { WorkspaceFlatViewSortMapCacheService } from 'src/engine/metadata-module
WorkspaceFlatIndexMapCacheService,
WorkspaceFlatFieldMetadataMapCacheService,
WorkspaceFlatViewGroupMapCacheService,
WorkspaceFlatObjectPermissionMapCacheService,
WorkspaceFlatPermissionFlagMapCacheService,
WorkspaceFlatViewSortMapCacheService,
WorkspaceFlatPageLayoutMapCacheService,
@@ -96,6 +100,7 @@ import { WorkspaceFlatViewSortMapCacheService } from 'src/engine/metadata-module
WorkspaceFlatIndexMapCacheService,
WorkspaceFlatFieldMetadataMapCacheService,
WorkspaceFlatViewGroupMapCacheService,
WorkspaceFlatObjectPermissionMapCacheService,
WorkspaceFlatPermissionFlagMapCacheService,
WorkspaceFlatViewSortMapCacheService,
WorkspaceFlatPageLayoutMapCacheService,
@@ -12,14 +12,16 @@ import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-m
import { type FlatNavigationMenuItemMaps } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item-maps.type';
import { type FlatNavigationMenuItem } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { type FlatObjectPermissionMaps } from 'src/engine/metadata-modules/flat-object-permission/types/flat-object-permission-maps.type';
import { type FlatObjectPermission } from 'src/engine/metadata-modules/flat-object-permission/types/flat-object-permission.type';
import { type FlatPageLayoutTabMaps } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab-maps.type';
import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type';
import { type FlatPageLayoutWidgetMaps } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-maps.type';
import { type FlatPermissionFlagMaps } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag-maps.type';
import { type FlatPermissionFlag } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag.type';
import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
import { type FlatPageLayoutMaps } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout-maps.type';
import { type FlatPageLayout } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout.type';
import { type FlatPermissionFlagMaps } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag-maps.type';
import { type FlatPermissionFlag } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag.type';
import { type FlatRoleTargetMaps } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target-maps.type';
import { type FlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target.type';
import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type';
@@ -55,22 +57,23 @@ import { type UniversalFlatIndexMetadata } from 'src/engine/workspace-manager/wo
import { type UniversalFlatLogicFunction } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-logic-function.type';
import { type UniversalFlatNavigationMenuItem } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-navigation-menu-item.type';
import { type UniversalFlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-metadata.type';
import { type UniversalFlatPageLayout } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-page-layout.type';
import { type UniversalFlatPermissionFlag } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-permission-flag.type';
import { type UniversalFlatObjectPermission } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-permission.type';
import { type UniversalFlatPageLayoutTab } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-page-layout-tab.type';
import { type UniversalFlatPageLayoutWidget } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-page-layout-widget.type';
import { type UniversalFlatRole } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-role.type';
import { type UniversalFlatPageLayout } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-page-layout.type';
import { type UniversalFlatPermissionFlag } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-permission-flag.type';
import { type UniversalFlatRoleTarget } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-role-target.type';
import { type UniversalFlatRowLevelPermissionPredicate } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-row-level-permission-predicate.type';
import { type UniversalFlatRole } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-role.type';
import { type UniversalFlatRowLevelPermissionPredicateGroup } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-row-level-permission-predicate-group.type';
import { type UniversalFlatRowLevelPermissionPredicate } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-row-level-permission-predicate.type';
import { type UniversalFlatSkill } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-skill.type';
import { type UniversalFlatViewFieldGroup } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field-group.type';
import { type UniversalFlatViewField } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field.type';
import { type UniversalFlatViewFilterGroup } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-filter-group.type';
import { type UniversalFlatViewFilter } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-filter.type';
import { type UniversalFlatViewGroup } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-group.type';
import { type UniversalFlatView } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view.type';
import { type UniversalFlatViewSort } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-sort.type';
import { type UniversalFlatView } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view.type';
import { type UniversalFlatWebhook } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-webhook.type';
import {
type FlatCreateAgentAction,
@@ -105,10 +108,10 @@ import {
type UniversalUpdateFrontComponentAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/front-component/types/workspace-migration-front-component-action.type';
import {
type UniversalCreateIndexAction,
type FlatCreateIndexAction,
type FlatDeleteIndexAction,
type FlatUpdateIndexAction,
type UniversalCreateIndexAction,
type UniversalDeleteIndexAction,
type UniversalUpdateIndexAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/index/types/workspace-migration-index-action';
@@ -128,6 +131,14 @@ import {
type UniversalDeleteNavigationMenuItemAction,
type UniversalUpdateNavigationMenuItemAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/navigation-menu-item/types/workspace-migration-navigation-menu-item-action.type';
import {
type FlatCreateObjectPermissionAction,
type FlatDeleteObjectPermissionAction,
type FlatUpdateObjectPermissionAction,
type UniversalCreateObjectPermissionAction,
type UniversalDeleteObjectPermissionAction,
type UniversalUpdateObjectPermissionAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object-permission/types/workspace-migration-object-permission-action.type';
import {
type FlatCreateObjectAction,
type FlatDeleteObjectAction,
@@ -136,14 +147,6 @@ import {
type UniversalDeleteObjectAction,
type UniversalUpdateObjectAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/object/types/workspace-migration-object-action';
import {
type FlatCreatePermissionFlagAction,
type FlatDeletePermissionFlagAction,
type FlatUpdatePermissionFlagAction,
type UniversalCreatePermissionFlagAction,
type UniversalDeletePermissionFlagAction,
type UniversalUpdatePermissionFlagAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/permission-flag/types/workspace-migration-permission-flag-action.type';
import {
type FlatCreatePageLayoutTabAction,
type FlatDeletePageLayoutTabAction,
@@ -168,6 +171,14 @@ import {
type UniversalDeletePageLayoutAction,
type UniversalUpdatePageLayoutAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/page-layout/types/workspace-migration-page-layout-action.type';
import {
type FlatCreatePermissionFlagAction,
type FlatDeletePermissionFlagAction,
type FlatUpdatePermissionFlagAction,
type UniversalCreatePermissionFlagAction,
type UniversalDeletePermissionFlagAction,
type UniversalUpdatePermissionFlagAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/permission-flag/types/workspace-migration-permission-flag-action.type';
import {
type FlatCreateRoleTargetAction,
type FlatDeleteRoleTargetAction,
@@ -208,14 +219,6 @@ import {
type UniversalDeleteSkillAction,
type UniversalUpdateSkillAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/skill/types/workspace-migration-skill-action.type';
import {
type FlatCreateViewFieldAction,
type FlatDeleteViewFieldAction,
type FlatUpdateViewFieldAction,
type UniversalCreateViewFieldAction,
type UniversalDeleteViewFieldAction,
type UniversalUpdateViewFieldAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-field/types/workspace-migration-view-field-action.type';
import {
type FlatCreateViewFieldGroupAction,
type FlatDeleteViewFieldGroupAction,
@@ -224,6 +227,14 @@ import {
type UniversalDeleteViewFieldGroupAction,
type UniversalUpdateViewFieldGroupAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-field-group/types/workspace-migration-view-field-group-action.type';
import {
type FlatCreateViewFieldAction,
type FlatDeleteViewFieldAction,
type FlatUpdateViewFieldAction,
type UniversalCreateViewFieldAction,
type UniversalDeleteViewFieldAction,
type UniversalUpdateViewFieldAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-field/types/workspace-migration-view-field-action.type';
import {
type FlatCreateViewFilterGroupAction,
type FlatDeleteViewFilterGroupAction,
@@ -248,6 +259,14 @@ import {
type UniversalDeleteViewGroupAction,
type UniversalUpdateViewGroupAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-group/types/workspace-migration-view-group-action.type';
import {
type FlatCreateViewSortAction,
type FlatDeleteViewSortAction,
type FlatUpdateViewSortAction,
type UniversalCreateViewSortAction,
type UniversalDeleteViewSortAction,
type UniversalUpdateViewSortAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-sort/types/workspace-migration-view-sort-action.type';
import {
type FlatCreateViewAction,
type FlatDeleteViewAction,
@@ -264,14 +283,6 @@ import {
type UniversalDeleteWebhookAction,
type UniversalUpdateWebhookAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/webhook/types/workspace-migration-webhook-action.type';
import {
type FlatCreateViewSortAction,
type FlatDeleteViewSortAction,
type FlatUpdateViewSortAction,
type UniversalCreateViewSortAction,
type UniversalDeleteViewSortAction,
type UniversalUpdateViewSortAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-sort/types/workspace-migration-view-sort-action.type';
export type AllFlatEntityTypesByMetadataName = {
fieldMetadata: {
@@ -597,6 +608,22 @@ export type AllFlatEntityTypesByMetadataName = {
universalFlatEntity: UniversalFlatPermissionFlag;
entity: MetadataEntity<'permissionFlag'>;
};
objectPermission: {
flatEntityMaps: FlatObjectPermissionMaps;
universalActions: {
create: UniversalCreateObjectPermissionAction;
update: UniversalUpdateObjectPermissionAction;
delete: UniversalDeleteObjectPermissionAction;
};
flatActions: {
create: FlatCreateObjectPermissionAction;
update: FlatUpdateObjectPermissionAction;
delete: FlatDeleteObjectPermissionAction;
};
flatEntity: FlatObjectPermission;
universalFlatEntity: UniversalFlatObjectPermission;
entity: MetadataEntity<'objectPermission'>;
};
pageLayout: {
flatEntityMaps: FlatPageLayoutMaps;
universalActions: {
@@ -42,10 +42,18 @@ exports[`getMetadataRelatedMetadataNames should return related metadata names fo
[
"fieldMetadata",
"index",
"objectPermission",
"view",
]
`;
exports[`getMetadataRelatedMetadataNames should return related metadata names for objectPermission 1`] = `
[
"role",
"objectMetadata",
]
`;
exports[`getMetadataRelatedMetadataNames should return related metadata names for pageLayout 1`] = `
[
"objectMetadata",
@@ -76,6 +84,7 @@ exports[`getMetadataRelatedMetadataNames should return related metadata names fo
exports[`getMetadataRelatedMetadataNames should return related metadata names for role 1`] = `
[
"roleTarget",
"objectPermission",
"permissionFlag",
"rowLevelPermissionPredicate",
"rowLevelPermissionPredicateGroup",
@@ -7,6 +7,7 @@ exports[`sortMetadataNamesChildrenFirst should return metadata names sorted with
"viewField",
"viewFilter",
"commandMenuItem",
"objectPermission",
"pageLayoutWidget",
"viewSort",
"index",
@@ -19,6 +19,7 @@ export const getFlatObjectMetadataMock = (
return {
viewIds: [],
indexMetadataIds: [],
objectPermissionIds: [],
fieldIds: [],
description: 'default flat object metadata description',
icon: 'icon',
@@ -48,6 +49,7 @@ export const getFlatObjectMetadataMock = (
duplicateCriteria: null,
applicationUniversalIdentifier: applicationId,
fieldUniversalIdentifiers: [],
objectPermissionUniversalIdentifiers: [],
viewUniversalIdentifiers: [],
indexMetadataUniversalIdentifiers: [],
labelIdentifierFieldMetadataUniversalIdentifier:
@@ -13,6 +13,7 @@ import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-meta
import { fromObjectMetadataEntityToFlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-object-metadata-entity-to-flat-object-metadata.util';
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 { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator';
import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util';
@@ -35,6 +36,8 @@ export class WorkspaceFlatObjectMetadataMapCacheService extends WorkspaceCachePr
private readonly indexMetadataRepository: Repository<IndexMetadataEntity>,
@InjectRepository(ViewEntity)
private readonly viewRepository: Repository<ViewEntity>,
@InjectRepository(ObjectPermissionEntity)
private readonly objectPermissionRepository: Repository<ObjectPermissionEntity>,
) {
super();
}
@@ -42,35 +45,51 @@ export class WorkspaceFlatObjectMetadataMapCacheService extends WorkspaceCachePr
async computeForCache(
workspaceId: string,
): Promise<FlatEntityMaps<FlatObjectMetadata>> {
const [objectMetadatas, applications, fields, indexMetadatas, views] =
await Promise.all([
this.objectMetadataRepository.find({
where: { workspaceId },
withDeleted: true,
}),
this.applicationRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
this.fieldMetadataRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier', 'objectMetadataId'],
withDeleted: true,
}),
this.indexMetadataRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier', 'objectMetadataId'],
withDeleted: true,
}),
this.viewRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier', 'objectMetadataId'],
withDeleted: true,
}),
]);
const [
objectMetadatas,
applications,
fields,
indexMetadatas,
views,
objectPermissions,
] = await Promise.all([
this.objectMetadataRepository.find({
where: { workspaceId },
withDeleted: true,
}),
this.applicationRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
this.fieldMetadataRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier', 'objectMetadataId'],
withDeleted: true,
}),
this.indexMetadataRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier', 'objectMetadataId'],
withDeleted: true,
}),
this.viewRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier', 'objectMetadataId'],
withDeleted: true,
}),
this.objectPermissionRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier', 'objectMetadataId'],
withDeleted: true,
}),
]);
const [fieldsByObjectId, indexesByObjectId, viewsByObjectId] = (
const [
fieldsByObjectId,
indexesByObjectId,
viewsByObjectId,
objectPermissionsByObjectId,
] = (
[
{
entities: fields,
@@ -84,6 +103,10 @@ export class WorkspaceFlatObjectMetadataMapCacheService extends WorkspaceCachePr
entities: views,
foreignKey: 'objectMetadataId',
},
{
entities: objectPermissions,
foreignKey: 'objectMetadataId',
},
] as const
).map(regroupEntitiesByRelatedEntityId);
@@ -101,6 +124,8 @@ export class WorkspaceFlatObjectMetadataMapCacheService extends WorkspaceCachePr
fields: fieldsByObjectId.get(objectMetadataEntity.id) || [],
indexMetadatas: indexesByObjectId.get(objectMetadataEntity.id) || [],
views: viewsByObjectId.get(objectMetadataEntity.id) || [],
objectPermissions:
objectPermissionsByObjectId.get(objectMetadataEntity.id) || [],
},
applicationIdToUniversalIdentifierMap,
fieldMetadataIdToUniversalIdentifierMap,
@@ -94,6 +94,7 @@ export const fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCre
targetTableName: 'DEPRECATED',
applicationUniversalIdentifier: flatApplication.universalIdentifier,
fieldUniversalIdentifiers: [],
objectPermissionUniversalIdentifiers: [],
viewUniversalIdentifiers: [],
indexMetadataUniversalIdentifiers: [],
labelIdentifierFieldMetadataUniversalIdentifier,
@@ -78,6 +78,9 @@ export const fromObjectMetadataEntityToFlatObjectMetadata = ({
viewIds: objectMetadataEntity.views.map(({ id }) => id),
indexMetadataIds: objectMetadataEntity.indexMetadatas.map(({ id }) => id),
fieldIds: objectMetadataEntity.fields.map(({ id }) => id),
objectPermissionIds: objectMetadataEntity.objectPermissions.map(
({ id }) => id,
),
applicationUniversalIdentifier,
labelIdentifierFieldMetadataUniversalIdentifier,
imageIdentifierFieldMetadataUniversalIdentifier,
@@ -90,5 +93,9 @@ export const fromObjectMetadataEntityToFlatObjectMetadata = ({
viewUniversalIdentifiers: objectMetadataEntity.views.map(
({ universalIdentifier }) => universalIdentifier,
),
objectPermissionUniversalIdentifiers:
objectMetadataEntity.objectPermissions.map(
({ universalIdentifier }) => universalIdentifier,
),
};
};
@@ -0,0 +1,10 @@
import { type MetadataEntityPropertyName } from 'src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant';
export const FLAT_OBJECT_PERMISSION_EDITABLE_PROPERTIES = [
'roleId',
'objectMetadataId',
'canReadObjectRecords',
'canUpdateObjectRecords',
'canSoftDeleteObjectRecords',
'canDestroyObjectRecords',
] as const satisfies MetadataEntityPropertyName<'objectPermission'>[];
@@ -0,0 +1,86 @@
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 { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
import { type FlatObjectPermissionMaps } from 'src/engine/metadata-modules/flat-object-permission/types/flat-object-permission-maps.type';
import { fromObjectPermissionEntityToFlatObjectPermission } from 'src/engine/metadata-modules/flat-object-permission/utils/from-object-permission-entity-to-flat-object-permission.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-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('flatObjectPermissionMaps')
export class WorkspaceFlatObjectPermissionMapCacheService extends WorkspaceCacheProvider<FlatObjectPermissionMaps> {
constructor(
@InjectRepository(ObjectPermissionEntity)
private readonly objectPermissionRepository: Repository<ObjectPermissionEntity>,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
@InjectRepository(RoleEntity)
private readonly roleRepository: Repository<RoleEntity>,
@InjectRepository(ObjectMetadataEntity)
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
) {
super();
}
async computeForCache(
workspaceId: string,
): Promise<FlatObjectPermissionMaps> {
const [objectPermissions, applications, roles, objectMetadatas] =
await Promise.all([
this.objectPermissionRepository.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,
}),
]);
const applicationIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(applications);
const roleIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(roles);
const objectMetadataIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(objectMetadatas);
const flatObjectPermissionMaps = createEmptyFlatEntityMaps();
for (const objectPermissionEntity of objectPermissions) {
const flatObjectPermission =
fromObjectPermissionEntityToFlatObjectPermission({
entity: objectPermissionEntity,
applicationIdToUniversalIdentifierMap,
roleIdToUniversalIdentifierMap,
objectMetadataIdToUniversalIdentifierMap,
});
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
flatEntity: flatObjectPermission,
flatEntityMapsToMutate: flatObjectPermissionMaps,
});
}
return flatObjectPermissionMaps;
}
}
@@ -0,0 +1,4 @@
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type FlatObjectPermission } from 'src/engine/metadata-modules/flat-object-permission/types/flat-object-permission.type';
export type FlatObjectPermissionMaps = FlatEntityMaps<FlatObjectPermission>;
@@ -0,0 +1,4 @@
import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
import { type ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity';
export type FlatObjectPermission = FlatEntityFrom<ObjectPermissionEntity>;
@@ -0,0 +1,54 @@
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 CreateObjectPermissionInput } from 'src/engine/metadata-modules/object-permission/dtos/create-object-permission.input';
import { type UniversalFlatObjectPermission } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-permission.type';
export const fromCreateObjectPermissionInputToUniversalFlatObjectPermission = ({
createObjectPermissionInput,
flatApplication,
flatRoleMaps,
flatObjectMetadataMaps,
}: {
createObjectPermissionInput: CreateObjectPermissionInput;
flatApplication: FlatApplication;
} & Pick<
AllFlatEntityMaps,
'flatRoleMaps' | 'flatObjectMetadataMaps'
>): UniversalFlatObjectPermission & { id: string } => {
const {
roleId,
objectMetadataId,
canReadObjectRecords,
canUpdateObjectRecords,
canSoftDeleteObjectRecords,
canDestroyObjectRecords,
universalIdentifier,
} = createObjectPermissionInput;
const now = new Date().toISOString();
const { roleUniversalIdentifier, objectMetadataUniversalIdentifier } =
resolveEntityRelationUniversalIdentifiers({
metadataName: 'objectPermission',
foreignKeyValues: { roleId, objectMetadataId },
flatEntityMaps: {
flatRoleMaps,
flatObjectMetadataMaps,
},
});
return {
id: v4(),
universalIdentifier: universalIdentifier ?? v4(),
applicationUniversalIdentifier: flatApplication.universalIdentifier,
roleUniversalIdentifier,
objectMetadataUniversalIdentifier,
canReadObjectRecords: canReadObjectRecords ?? undefined,
canUpdateObjectRecords: canUpdateObjectRecords ?? undefined,
canSoftDeleteObjectRecords: canSoftDeleteObjectRecords ?? undefined,
canDestroyObjectRecords: canDestroyObjectRecords ?? undefined,
createdAt: now,
updatedAt: now,
};
};
@@ -0,0 +1,67 @@
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 FlatObjectPermission } from 'src/engine/metadata-modules/flat-object-permission/types/flat-object-permission.type';
import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type';
export const fromObjectPermissionEntityToFlatObjectPermission = ({
entity: objectPermissionEntity,
applicationIdToUniversalIdentifierMap,
roleIdToUniversalIdentifierMap,
objectMetadataIdToUniversalIdentifierMap,
}: FromEntityToFlatEntityArgs<'objectPermission'>): FlatObjectPermission => {
const objectPermissionEntityWithoutRelations = removePropertiesFromRecord(
objectPermissionEntity,
getMetadataEntityRelationProperties('objectPermission'),
);
const applicationUniversalIdentifier =
applicationIdToUniversalIdentifierMap.get(
objectPermissionEntity.applicationId,
);
if (!isDefined(applicationUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Application with id ${objectPermissionEntity.applicationId} not found for objectPermission ${objectPermissionEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
const roleUniversalIdentifier = roleIdToUniversalIdentifierMap.get(
objectPermissionEntity.roleId,
);
if (!isDefined(roleUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Role with id ${objectPermissionEntity.roleId} not found for objectPermission ${objectPermissionEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
const objectMetadataUniversalIdentifier =
objectMetadataIdToUniversalIdentifierMap.get(
objectPermissionEntity.objectMetadataId,
);
if (!isDefined(objectMetadataUniversalIdentifier)) {
throw new FlatEntityMapsException(
`ObjectMetadata with id ${objectPermissionEntity.objectMetadataId} not found for objectPermission ${objectPermissionEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
return {
...objectPermissionEntityWithoutRelations,
createdAt: objectPermissionEntity.createdAt.toISOString(),
updatedAt: objectPermissionEntity.updatedAt.toISOString(),
universalIdentifier:
objectPermissionEntityWithoutRelations.universalIdentifier,
applicationUniversalIdentifier,
roleUniversalIdentifier,
objectMetadataUniversalIdentifier,
};
};
@@ -55,6 +55,7 @@ export const fromCreateRoleInputToFlatRoleToCreate = ({
roleTargetUniversalIdentifiers: [],
objectPermissionIds: [],
permissionFlagIds: [],
objectPermissionUniversalIdentifiers: [],
permissionFlagUniversalIdentifiers: [],
fieldPermissionIds: [],
rowLevelPermissionPredicateIds: [],
@@ -68,6 +68,9 @@ export const fromRoleEntityToFlatRole = ({
roleTargetUniversalIdentifiers: roleEntity.roleTargets.map(
({ universalIdentifier }) => universalIdentifier,
),
objectPermissionUniversalIdentifiers: roleEntity.objectPermissions.map(
({ universalIdentifier }) => universalIdentifier,
),
permissionFlagUniversalIdentifiers: roleEntity.permissionFlags.map(
({ universalIdentifier }) => universalIdentifier,
),
@@ -1,58 +1,67 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { type Repository } from 'typeorm';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { type UpsertObjectPermissionsInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-object-permissions.input';
import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity';
import { ObjectPermissionService } from 'src/engine/metadata-modules/object-permission/object-permission.service';
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 { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
const createMockFlatRoleMaps = (roleId: string, isEditable: boolean) => ({
byUniversalIdentifier: {
[roleId]: {
id: roleId,
universalIdentifier: roleId,
isEditable,
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: true,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
},
},
universalIdentifierById: { [roleId]: roleId },
byId: { [roleId]: { id: roleId, universalIdentifier: roleId, isEditable } },
});
const createMockFlatObjectMetadataMaps = (
objectMetadataId: string,
isSystem: boolean,
) => ({
byUniversalIdentifier: {
[objectMetadataId]: {
id: objectMetadataId,
universalIdentifier: objectMetadataId,
isSystem,
},
},
universalIdentifierById: { [objectMetadataId]: objectMetadataId },
byId: { [objectMetadataId]: { id: objectMetadataId, isSystem } },
});
describe('ObjectPermissionService', () => {
let service: ObjectPermissionService;
let objectPermissionRepository: jest.Mocked<
Repository<ObjectPermissionEntity>
>;
let roleRepository: jest.Mocked<Repository<RoleEntity>>;
let workspaceCacheService: jest.Mocked<WorkspaceCacheService>;
let workspaceManyOrAllFlatEntityMapsCacheService: jest.Mocked<WorkspaceManyOrAllFlatEntityMapsCacheService>;
let workspaceMigrationValidateBuildAndRunService: jest.Mocked<WorkspaceMigrationValidateBuildAndRunService>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ObjectPermissionService,
{
provide: getRepositoryToken(ObjectPermissionEntity),
provide: ApplicationService,
useValue: {
upsert: jest.fn(),
find: jest.fn(),
},
},
{
provide: getRepositoryToken(RoleEntity),
useValue: {
findOne: jest.fn(),
},
},
{
provide: getRepositoryToken(ObjectMetadataEntity),
useValue: {
find: jest.fn(),
},
},
{
provide: WorkspaceCacheService,
useValue: {
invalidate: jest.fn(),
invalidateAndRecompute: jest.fn(),
findWorkspaceTwentyStandardAndCustomApplicationOrThrow: jest
.fn()
.mockResolvedValue({
workspaceCustomFlatApplication: {
id: 'app-id',
universalIdentifier: 'app-universal-id',
},
}),
},
},
{
@@ -61,18 +70,22 @@ describe('ObjectPermissionService', () => {
getOrRecomputeManyOrAllFlatEntityMaps: jest.fn(),
},
},
{
provide: WorkspaceMigrationValidateBuildAndRunService,
useValue: {
validateBuildAndRunWorkspaceMigration: jest.fn(),
},
},
],
}).compile();
service = module.get<ObjectPermissionService>(ObjectPermissionService);
objectPermissionRepository = module.get(
getRepositoryToken(ObjectPermissionEntity),
);
roleRepository = module.get(getRepositoryToken(RoleEntity));
workspaceCacheService = module.get(WorkspaceCacheService);
workspaceManyOrAllFlatEntityMapsCacheService = module.get(
WorkspaceManyOrAllFlatEntityMapsCacheService,
);
workspaceMigrationValidateBuildAndRunService = module.get(
WorkspaceMigrationValidateBuildAndRunService,
);
});
describe('upsertObjectPermissions', () => {
@@ -81,18 +94,7 @@ describe('ObjectPermissionService', () => {
const systemObjectMetadataId = 'system-object-id';
const customObjectMetadataId = 'custom-object-id';
beforeEach(() => {
// Mock role validation
roleRepository.findOne.mockResolvedValue({
id: roleId,
workspaceId,
isEditable: true,
objectPermissions: [],
} as unknown as RoleEntity);
});
it('should throw PermissionsException when trying to add object permission on system object', async () => {
// Arrange
const input: UpsertObjectPermissionsInput = {
roleId,
objectPermissions: [
@@ -106,31 +108,21 @@ describe('ObjectPermissionService', () => {
],
};
// Mock flat object metadata maps with a system object
workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps.mockResolvedValue(
{
flatObjectMetadataMaps: {
byUniversalIdentifier: {
[systemObjectMetadataId]: {
id: systemObjectMetadataId,
isSystem: true,
workspaceId,
fieldIds: [],
indexMetadataIds: [],
viewIds: [],
universalIdentifier: systemObjectMetadataId,
applicationId: null,
} as any,
},
universalIdentifierById: {
[systemObjectMetadataId]: systemObjectMetadataId,
},
universalIdentifiersByApplicationId: {},
flatObjectPermissionMaps: {
byUniversalIdentifier: {},
universalIdentifierById: {},
byId: {},
},
flatRoleMaps: createMockFlatRoleMaps(roleId, true),
flatObjectMetadataMaps: createMockFlatObjectMetadataMaps(
systemObjectMetadataId,
true,
),
} as any,
);
// Act & Assert
await expect(
service.upsertObjectPermissions({
workspaceId,
@@ -143,15 +135,12 @@ describe('ObjectPermissionService', () => {
),
);
// Verify that upsert was never called
expect(objectPermissionRepository.upsert).not.toHaveBeenCalled();
expect(
workspaceCacheService.invalidateAndRecompute,
workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration,
).not.toHaveBeenCalled();
});
it('should successfully create object permission for custom (non-system) object', async () => {
// Arrange
const input: UpsertObjectPermissionsInput = {
roleId,
objectPermissions: [
@@ -165,82 +154,70 @@ describe('ObjectPermissionService', () => {
],
};
// Mock flat object metadata maps with a custom object
workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps.mockResolvedValue(
{
flatObjectMetadataMaps: {
byUniversalIdentifier: {
[customObjectMetadataId]: {
id: customObjectMetadataId,
isSystem: false,
workspaceId,
fieldIds: [],
indexMetadataIds: [],
viewIds: [],
universalIdentifier: customObjectMetadataId,
applicationId: null,
} as any,
},
universalIdentifierById: {
[customObjectMetadataId]: customObjectMetadataId,
},
universalIdentifiersByApplicationId: {},
},
} as any,
);
// Mock successful upsert
const mockObjectPermission = {
const permissionUniversalId = 'permission-universal-id';
const freshFlatObjectPermission = {
id: 'permission-id',
universalIdentifier: permissionUniversalId,
roleId,
roleUniversalIdentifier: roleId,
objectMetadataId: customObjectMetadataId,
workspaceId,
objectMetadataUniversalIdentifier: customObjectMetadataId,
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: false,
canDestroyObjectRecords: false,
} as ObjectPermissionEntity;
};
objectPermissionRepository.upsert.mockResolvedValue({
generatedMaps: [{ id: 'permission-id' }],
identifiers: [],
raw: [],
});
workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps
.mockResolvedValueOnce({
flatObjectPermissionMaps: {
byUniversalIdentifier: {},
universalIdentifierById: {},
byId: {},
},
flatRoleMaps: createMockFlatRoleMaps(roleId, true),
flatObjectMetadataMaps: createMockFlatObjectMetadataMaps(
customObjectMetadataId,
false,
),
} as any)
.mockResolvedValueOnce({
flatObjectPermissionMaps: {
byUniversalIdentifier: {
[permissionUniversalId]: freshFlatObjectPermission,
},
universalIdentifierById: {
[freshFlatObjectPermission.id]: permissionUniversalId,
},
byId: {
[freshFlatObjectPermission.id]: freshFlatObjectPermission,
},
},
} as any);
objectPermissionRepository.find.mockResolvedValue([mockObjectPermission]);
workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration.mockResolvedValue(
{ status: 'success' } as any,
);
// Act
const result = await service.upsertObjectPermissions({
workspaceId,
input,
});
// Assert
expect(result).toEqual([mockObjectPermission]);
expect(objectPermissionRepository.upsert).toHaveBeenCalledWith(
[
{
objectMetadataId: customObjectMetadataId,
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: false,
canDestroyObjectRecords: false,
roleId,
workspaceId,
},
],
{
conflictPaths: ['objectMetadataId', 'roleId'],
},
);
expect(workspaceCacheService.invalidateAndRecompute).toHaveBeenCalledWith(
workspaceId,
['rolesPermissions'],
);
expect(result).toHaveLength(1);
expect(result[0]).toMatchObject({
objectMetadataId: customObjectMetadataId,
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: false,
canDestroyObjectRecords: false,
});
expect(
workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration,
).toHaveBeenCalled();
});
it('should throw PermissionsException when object metadata is not found', async () => {
// Arrange
const input: UpsertObjectPermissionsInput = {
roleId,
objectPermissions: [
@@ -254,18 +231,22 @@ describe('ObjectPermissionService', () => {
],
};
// Mock empty flat object metadata maps
workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps.mockResolvedValue(
{
flatObjectPermissionMaps: {
byUniversalIdentifier: {},
universalIdentifierById: {},
byId: {},
},
flatRoleMaps: createMockFlatRoleMaps(roleId, true),
flatObjectMetadataMaps: {
byUniversalIdentifier: {},
universalIdentifierById: {},
universalIdentifiersByApplicationId: {},
byId: {},
},
} as any,
);
// Act & Assert
await expect(
service.upsertObjectPermissions({
workspaceId,
@@ -0,0 +1,9 @@
export type CreateObjectPermissionInput = {
roleId: string;
objectMetadataId: string;
canReadObjectRecords?: boolean;
canUpdateObjectRecords?: boolean;
canSoftDeleteObjectRecords?: boolean;
canDestroyObjectRecords?: boolean;
universalIdentifier?: string;
};
@@ -13,7 +13,7 @@ import {
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('objectPermission')
@Unique('IDX_OBJECT_PERMISSION_OBJECT_METADATA_ID_ROLE_ID_UNIQUE', [
@@ -21,7 +21,7 @@ import { WorkspaceRelatedEntity } from 'src/engine/workspace-manager/types/works
'roleId',
])
@Index('IDX_OBJECT_PERMISSION_WORKSPACE_ID_ROLE_ID', ['workspaceId', 'roleId'])
export class ObjectPermissionEntity extends WorkspaceRelatedEntity {
export class ObjectPermissionEntity extends SyncableEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@@ -1,6 +1,7 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
@@ -10,6 +11,7 @@ import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permi
import { ObjectPermissionService } from 'src/engine/metadata-modules/object-permission/object-permission.service';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
@Module({
imports: [
@@ -20,8 +22,10 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
FieldPermissionEntity,
FieldMetadataEntity,
]),
WorkspaceManyOrAllFlatEntityMapsCacheModule,
WorkspaceCacheModule,
ApplicationModule,
WorkspaceMigrationModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
],
providers: [ObjectPermissionService, FieldPermissionService],
exports: [ObjectPermissionService, FieldPermissionService],
@@ -1,35 +1,33 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Injectable } from '@nestjs/common';
import { msg } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { In, Repository } from 'typeorm';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
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 { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { type FlatObjectPermission } from 'src/engine/metadata-modules/flat-object-permission/types/flat-object-permission.type';
import { fromCreateObjectPermissionInputToUniversalFlatObjectPermission } from 'src/engine/metadata-modules/flat-object-permission/utils/from-create-object-permission-input-to-universal-flat-object-permission.util';
import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type';
import {
type ObjectPermissionInput,
type UpsertObjectPermissionsInput,
} from 'src/engine/metadata-modules/object-permission/dtos/upsert-object-permissions.input';
import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-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 UniversalFlatObjectPermission } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-permission.type';
@Injectable()
export class ObjectPermissionService {
constructor(
@InjectRepository(ObjectPermissionEntity)
private readonly objectPermissionRepository: Repository<ObjectPermissionEntity>,
@InjectRepository(RoleEntity)
private readonly roleRepository: Repository<RoleEntity>,
@InjectRepository(ObjectMetadataEntity)
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
private readonly workspaceCacheService: WorkspaceCacheService,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly applicationService: ApplicationService,
) {}
public async upsertObjectPermissions({
@@ -38,144 +36,269 @@ export class ObjectPermissionService {
}: {
workspaceId: string;
input: UpsertObjectPermissionsInput;
}): Promise<ObjectPermissionEntity[]> {
try {
const role = await this.getRoleOrThrow({
roleId: input.roleId,
workspaceId,
}): Promise<FlatObjectPermission[]> {
const { flatObjectPermissionMaps, flatRoleMaps, flatObjectMetadataMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: [
'flatObjectPermissionMaps',
'flatRoleMaps',
'flatObjectMetadataMaps',
],
},
);
const roleUniversalIdentifier =
flatRoleMaps.universalIdentifierById[input.roleId];
const flatRole = isDefined(roleUniversalIdentifier)
? flatRoleMaps.byUniversalIdentifier[roleUniversalIdentifier]
: undefined;
if (!isDefined(flatRole)) {
throw new PermissionsException(
PermissionsExceptionMessage.ROLE_NOT_FOUND,
PermissionsExceptionCode.ROLE_NOT_FOUND,
{
userFriendlyMessage: msg`The role you are trying to modify could not be found.`,
},
);
}
const currentObjectPermissionsForRole = Object.values(
flatObjectPermissionMaps.byUniversalIdentifier,
).filter(
(op): op is FlatObjectPermission =>
isDefined(op) && op.roleUniversalIdentifier === roleUniversalIdentifier,
);
this.validateObjectPermissionsReadAndWriteConsistencyOrThrow({
objectPermissions: input.objectPermissions,
flatRole,
currentObjectPermissionsForRole,
});
const flatApplication =
await this.getFlatApplicationForWorkspace(workspaceId);
const desiredByObjectMetadataId = new Map(
input.objectPermissions.map((op) => [op.objectMetadataId, op]),
);
for (const desired of input.objectPermissions) {
const objectMetadata = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: desired.objectMetadataId,
flatEntityMaps: flatObjectMetadataMaps,
});
await this.validateRoleIsEditableOrThrow({
role,
});
await this.validateObjectPermissionsReadAndWriteConsistencyOrThrow({
objectPermissions: input.objectPermissions,
roleWithObjectPermissions: role,
});
const { flatObjectMetadataMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
if (!isDefined(objectMetadata)) {
throw new PermissionsException(
'Object metadata id not found',
PermissionsExceptionCode.OBJECT_METADATA_NOT_FOUND,
{
workspaceId,
flatMapsKeys: ['flatObjectMetadataMaps'],
userFriendlyMessage: msg`The object you are trying to set permissions for could not be found. It may have been deleted.`,
},
);
input.objectPermissions.forEach((objectPermission) => {
const objectMetadataForObjectPermission =
findFlatEntityByIdInFlatEntityMaps({
flatEntityId: objectPermission.objectMetadataId,
flatEntityMaps: flatObjectMetadataMaps,
});
if (!isDefined(objectMetadataForObjectPermission)) {
throw new PermissionsException(
'Object metadata id not found',
PermissionsExceptionCode.OBJECT_METADATA_NOT_FOUND,
{
userFriendlyMessage: msg`The object you are trying to set permissions for could not be found. It may have been deleted.`,
},
);
}
if (objectMetadataForObjectPermission.isSystem === true) {
throw new PermissionsException(
PermissionsExceptionMessage.CANNOT_ADD_OBJECT_PERMISSION_ON_SYSTEM_OBJECT,
PermissionsExceptionCode.CANNOT_ADD_OBJECT_PERMISSION_ON_SYSTEM_OBJECT,
{
userFriendlyMessage: msg`You cannot set permissions on system objects as they are managed by the platform.`,
},
);
}
});
const objectPermissions = input.objectPermissions.map(
(objectPermission) => ({
...objectPermission,
roleId: input.roleId,
workspaceId,
}),
);
const result = await this.objectPermissionRepository.upsert(
objectPermissions,
{
conflictPaths: ['objectMetadataId', 'roleId'],
},
);
const objectPermissionId = result.generatedMaps?.[0]?.id;
if (!isDefined(objectPermissionId)) {
throw new Error('Failed to upsert object permission');
}
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
'rolesPermissions',
]);
return this.objectPermissionRepository.find({
where: {
roleId: input.roleId,
objectMetadataId: In(
input.objectPermissions.map(
(objectPermission) => objectPermission.objectMetadataId,
),
),
},
});
} catch (error) {
await this.handleForeignKeyError({
error,
roleId: input.roleId,
workspaceId,
objectMetadataIds: input.objectPermissions.map(
(objectPermission) => objectPermission.objectMetadataId,
),
});
throw error;
if (objectMetadata.isSystem === true) {
throw new PermissionsException(
PermissionsExceptionMessage.CANNOT_ADD_OBJECT_PERMISSION_ON_SYSTEM_OBJECT,
PermissionsExceptionCode.CANNOT_ADD_OBJECT_PERMISSION_ON_SYSTEM_OBJECT,
{
userFriendlyMessage: msg`You cannot set permissions on system objects as they are managed by the platform.`,
},
);
}
}
const flatEntityToCreate: (UniversalFlatObjectPermission & {
id: string;
})[] = [];
const flatEntityToUpdate: UniversalFlatObjectPermission[] = [];
const flatEntityToDelete: UniversalFlatObjectPermission[] = [];
const currentByObjectMetadataId = new Map(
currentObjectPermissionsForRole.map((op) => [op.objectMetadataId, op]),
);
for (const desired of input.objectPermissions) {
const current = currentByObjectMetadataId.get(desired.objectMetadataId);
if (!isDefined(current)) {
flatEntityToCreate.push(
fromCreateObjectPermissionInputToUniversalFlatObjectPermission({
createObjectPermissionInput: {
roleId: input.roleId,
objectMetadataId: desired.objectMetadataId,
canReadObjectRecords: desired.canReadObjectRecords,
canUpdateObjectRecords: desired.canUpdateObjectRecords,
canSoftDeleteObjectRecords: desired.canSoftDeleteObjectRecords,
canDestroyObjectRecords: desired.canDestroyObjectRecords,
},
flatApplication,
flatRoleMaps,
flatObjectMetadataMaps,
}),
);
} else {
const effectiveCanRead =
desired.canReadObjectRecords ?? current.canReadObjectRecords;
const effectiveCanUpdate =
desired.canUpdateObjectRecords ?? current.canUpdateObjectRecords;
const effectiveCanSoftDelete =
desired.canSoftDeleteObjectRecords ??
current.canSoftDeleteObjectRecords;
const effectiveCanDestroy =
desired.canDestroyObjectRecords ?? current.canDestroyObjectRecords;
const canChanged =
effectiveCanRead !== current.canReadObjectRecords ||
effectiveCanUpdate !== current.canUpdateObjectRecords ||
effectiveCanSoftDelete !== current.canSoftDeleteObjectRecords ||
effectiveCanDestroy !== current.canDestroyObjectRecords;
if (canChanged) {
const now = new Date().toISOString();
flatEntityToUpdate.push({
universalIdentifier: current.universalIdentifier,
applicationUniversalIdentifier:
current.applicationUniversalIdentifier,
roleUniversalIdentifier: current.roleUniversalIdentifier,
objectMetadataUniversalIdentifier:
current.objectMetadataUniversalIdentifier,
canReadObjectRecords: effectiveCanRead,
canUpdateObjectRecords: effectiveCanUpdate,
canSoftDeleteObjectRecords: effectiveCanSoftDelete,
canDestroyObjectRecords: effectiveCanDestroy,
createdAt: current.createdAt,
updatedAt: now,
});
}
}
}
for (const current of currentObjectPermissionsForRole) {
if (!desiredByObjectMetadataId.has(current.objectMetadataId)) {
flatEntityToDelete.push({
universalIdentifier: current.universalIdentifier,
applicationUniversalIdentifier:
current.applicationUniversalIdentifier,
roleUniversalIdentifier: current.roleUniversalIdentifier,
objectMetadataUniversalIdentifier:
current.objectMetadataUniversalIdentifier,
canReadObjectRecords: current.canReadObjectRecords,
canUpdateObjectRecords: current.canUpdateObjectRecords,
canSoftDeleteObjectRecords: current.canSoftDeleteObjectRecords,
canDestroyObjectRecords: current.canDestroyObjectRecords,
createdAt: current.createdAt,
updatedAt: current.updatedAt,
});
}
}
if (
flatEntityToCreate.length === 0 &&
flatEntityToUpdate.length === 0 &&
flatEntityToDelete.length === 0
) {
const unchanged = currentObjectPermissionsForRole.filter((op) =>
desiredByObjectMetadataId.has(op.objectMetadataId),
);
return unchanged;
}
const buildAndRunResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
objectPermission: {
flatEntityToCreate,
flatEntityToUpdate,
flatEntityToDelete,
},
},
workspaceId,
isSystemBuild: false,
applicationUniversalIdentifier: flatApplication.universalIdentifier,
},
);
if (buildAndRunResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
buildAndRunResult,
'Validation errors occurred while upserting object permissions',
);
}
const { flatObjectPermissionMaps: freshFlatObjectPermissionMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatObjectPermissionMaps'],
},
);
const resultObjectPermissions = Object.values(
freshFlatObjectPermissionMaps.byUniversalIdentifier,
).filter(
(op): op is FlatObjectPermission =>
isDefined(op) && op.roleUniversalIdentifier === roleUniversalIdentifier,
);
const desiredObjectMetadataIds = new Set(
input.objectPermissions.map((op) => op.objectMetadataId),
);
const filtered = resultObjectPermissions.filter((op) =>
desiredObjectMetadataIds.has(op.objectMetadataId),
);
return filtered;
}
private async validateObjectPermissionsReadAndWriteConsistencyOrThrow({
private validateObjectPermissionsReadAndWriteConsistencyOrThrow({
objectPermissions: newObjectPermissions,
roleWithObjectPermissions,
flatRole,
currentObjectPermissionsForRole,
}: {
objectPermissions: ObjectPermissionInput[];
roleWithObjectPermissions: RoleEntity;
}) {
const existingObjectPermissions =
roleWithObjectPermissions.objectPermissions;
flatRole: Pick<
FlatRole,
| 'canReadAllObjectRecords'
| 'canUpdateAllObjectRecords'
| 'canSoftDeleteAllObjectRecords'
| 'canDestroyAllObjectRecords'
>;
currentObjectPermissionsForRole: FlatObjectPermission[];
}): void {
for (const newObjectPermission of newObjectPermissions) {
const existingObjectRecordPermission = existingObjectPermissions.find(
(objectPermission) =>
objectPermission.objectMetadataId ===
newObjectPermission.objectMetadataId,
);
const existingObjectRecordPermission =
currentObjectPermissionsForRole.find(
(objectPermission) =>
objectPermission.objectMetadataId ===
newObjectPermission.objectMetadataId,
);
const hasReadPermissionAfterUpdate =
newObjectPermission.canReadObjectRecords ??
existingObjectRecordPermission?.canReadObjectRecords ??
roleWithObjectPermissions.canReadAllObjectRecords;
flatRole.canReadAllObjectRecords;
if (hasReadPermissionAfterUpdate === false) {
const hasUpdatePermissionAfterUpdate =
newObjectPermission.canUpdateObjectRecords ??
existingObjectRecordPermission?.canUpdateObjectRecords ??
roleWithObjectPermissions.canUpdateAllObjectRecords;
flatRole.canUpdateAllObjectRecords;
const hasSoftDeletePermissionAfterUpdate =
newObjectPermission.canSoftDeleteObjectRecords ??
existingObjectRecordPermission?.canSoftDeleteObjectRecords ??
roleWithObjectPermissions.canSoftDeleteAllObjectRecords;
flatRole.canSoftDeleteAllObjectRecords;
const hasDestroyPermissionAfterUpdate =
newObjectPermission.canDestroyObjectRecords ??
existingObjectRecordPermission?.canDestroyObjectRecords ??
roleWithObjectPermissions.canDestroyAllObjectRecords;
flatRole.canDestroyAllObjectRecords;
if (
hasUpdatePermissionAfterUpdate ||
@@ -194,91 +317,11 @@ export class ObjectPermissionService {
}
}
private async handleForeignKeyError({
error,
roleId,
workspaceId,
objectMetadataIds,
}: {
error: Error;
roleId: string;
workspaceId: string;
objectMetadataIds: string[];
}) {
if (error.message.includes('violates foreign key constraint')) {
const role = await this.roleRepository.findOne({
where: {
id: roleId,
workspaceId,
},
});
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.`,
},
);
}
const objectMetadata = await this.objectMetadataRepository.find({
where: {
workspaceId,
id: In(objectMetadataIds),
},
});
if (objectMetadata.length !== objectMetadataIds.length) {
throw new PermissionsException(
PermissionsExceptionMessage.OBJECT_METADATA_NOT_FOUND,
PermissionsExceptionCode.OBJECT_METADATA_NOT_FOUND,
{
userFriendlyMessage: msg`One or more objects you are trying to set permissions for could not be found. They may have been deleted.`,
},
);
}
}
}
private async getRoleOrThrow({
roleId,
workspaceId,
}: {
roleId: string;
workspaceId: string;
}) {
const role = await this.roleRepository.findOne({
where: {
id: roleId,
workspaceId,
},
relations: ['objectPermissions'],
});
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.`,
},
private async getFlatApplicationForWorkspace(workspaceId: string) {
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
}
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.`,
},
);
}
return workspaceCustomFlatApplication;
}
}
@@ -0,0 +1,12 @@
import { type FlatObjectPermission } from 'src/engine/metadata-modules/flat-object-permission/types/flat-object-permission.type';
import { type ObjectPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/object-permission.dto';
export const fromFlatObjectPermissionToObjectPermissionDto = (
flatObjectPermission: FlatObjectPermission,
): ObjectPermissionDTO => ({
objectMetadataId: flatObjectPermission.objectMetadataId,
canReadObjectRecords: flatObjectPermission.canReadObjectRecords,
canUpdateObjectRecords: flatObjectPermission.canUpdateObjectRecords,
canSoftDeleteObjectRecords: flatObjectPermission.canSoftDeleteObjectRecords,
canDestroyObjectRecords: flatObjectPermission.canDestroyObjectRecords,
});
@@ -39,6 +39,7 @@ 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 { 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';
@@ -211,10 +212,14 @@ export class RoleResolver {
@Args('upsertObjectPermissionsInput')
upsertObjectPermissionsInput: UpsertObjectPermissionsInput,
): Promise<ObjectPermissionDTO[]> {
return this.objectPermissionService.upsertObjectPermissions({
workspaceId: workspace.id,
input: upsertObjectPermissionsInput,
});
const flatObjectPermissions =
await this.objectPermissionService.upsertObjectPermissions({
workspaceId: workspace.id,
input: upsertObjectPermissionsInput,
});
return flatObjectPermissions.map(
fromFlatObjectPermissionToObjectPermissionDto,
);
}
@Mutation(() => [PermissionFlagDTO])
@@ -77,7 +77,7 @@ export class WorkspaceFlatRoleMapCacheService extends WorkspaceCacheProvider<
}),
this.objectPermissionRepository.find({
where: { workspaceId },
select: ['id', 'roleId'],
select: ['id', 'universalIdentifier', 'roleId'],
withDeleted: true,
}),
this.permissionFlagRepository.find({