[1/3] Rename permissionFlag to rolePermissionFlag + add permissionFlag catalog/backfill (#20481)

Split of #20377.

## Summary

This PR separates available permission flags from per-role permission
flag grants.

Previously, `core.permissionFlag` stored the role assignment directly:
`roleId + flag`. This PR renames that legacy grant table to
`core.rolePermissionFlag`, then recreates `core.permissionFlag` as the
catalog of available permission flags.

## What changed

- Rename the existing `core.permissionFlag` grant table to
`core.rolePermissionFlag`.
- Add the new syncable `core.permissionFlag` catalog entity with key,
label, description, icon, permission type, relevance flags, and
custom/standard metadata.
- Add stable `SystemPermissionFlag` universal identifiers for the
built-in `PermissionFlagType` values.
- Seed the standard permission flags for every workspace under the
Twenty standard application.
- Backfill existing role grants:
  - create missing catalog rows for existing grant keys,
  - add `rolePermissionFlag.permissionFlagId`,
- migrate grants from the old string `flag` column to the new catalog
FK,
- replace the old `(flag, roleId)` uniqueness with `(permissionFlagId,
roleId)`.
- Rewire role permission flag caches, permission checks, role DTO
mapping, and `upsertPermissionFlags` to resolve through the catalog.
- Keep the existing public role permission API shape: product/app
surfaces still talk about `permissionFlags` and return `{ id, roleId,
flag }`.
- Update metadata flat-entity machinery, migration builders, validators,
action handlers, snapshots, generated schemas, docs, and app fixtures
for the new `permissionFlag` / `rolePermissionFlag` split.

## Behavior after this PR

- Existing permission flag grants keep working.
- Existing GraphQL role permission flows keep the same public naming.
- Standard permission flags are represented as catalog rows.
- Permission checks now compare grants through catalog universal
identifiers instead of the legacy `flag` column.
- Workspace deletion cleanup now verifies both `permissionFlag` and
`rolePermissionFlag`.

## What is not in this PR

- Public GraphQL CRUD for custom permission flags.
- App manifest support for declaring new custom permission flags.
- Frontend UI for creating or assigning custom permission flags beyond
the existing role permission flow.

---------

Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
nitin
2026-05-18 17:27:47 +05:30
committed by GitHub
parent 01535a3b3e
commit db0547f503
132 changed files with 3663 additions and 719 deletions
@@ -1,23 +0,0 @@
import { type UniversalFlatPermissionFlag } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-permission-flag.type';
import { PermissionFlagManifest } from 'twenty-shared/application';
export const fromPermissionFlagToUniversalFlatPermissionFlag = ({
permissionFlag,
roleUniversalIdentifier,
applicationUniversalIdentifier,
now,
}: {
permissionFlag: PermissionFlagManifest;
roleUniversalIdentifier: string;
applicationUniversalIdentifier: string;
now: string;
}): UniversalFlatPermissionFlag => {
return {
universalIdentifier: permissionFlag.universalIdentifier,
applicationUniversalIdentifier,
roleUniversalIdentifier,
flag: permissionFlag.flag,
createdAt: now,
updatedAt: now,
};
};
@@ -0,0 +1,27 @@
import { type PermissionFlagManifest } from 'twenty-shared/application';
import { SystemPermissionFlag } from 'twenty-shared/constants';
import { type UniversalFlatRolePermissionFlag } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-role-permission-flag.type';
export const fromPermissionFlagToUniversalFlatRolePermissionFlag = ({
permissionFlag,
roleUniversalIdentifier,
applicationUniversalIdentifier,
now,
}: {
permissionFlag: PermissionFlagManifest;
roleUniversalIdentifier: string;
applicationUniversalIdentifier: string;
now: string;
}): UniversalFlatRolePermissionFlag => {
return {
universalIdentifier: permissionFlag.universalIdentifier,
applicationUniversalIdentifier,
roleUniversalIdentifier,
permissionFlagUniversalIdentifier:
SystemPermissionFlag[permissionFlag.flag],
flag: permissionFlag.flag,
createdAt: now,
updatedAt: now,
};
};
@@ -32,7 +32,7 @@ export const fromRoleManifestToUniversalFlatRole = ({
roleTargetUniversalIdentifiers: [],
objectPermissionUniversalIdentifiers: [],
fieldPermissionUniversalIdentifiers: [],
permissionFlagUniversalIdentifiers: [],
rolePermissionFlagUniversalIdentifiers: [],
rowLevelPermissionPredicateUniversalIdentifiers: [],
rowLevelPermissionPredicateGroupUniversalIdentifiers: [],
createdAt: now,
@@ -17,7 +17,7 @@ import { fromObjectPermissionManifestToUniversalFlatObjectPermission } from 'src
import { fromPageLayoutManifestToUniversalFlatPageLayout } from 'src/engine/core-modules/application/application-manifest/converters/from-page-layout-manifest-to-universal-flat-page-layout.util';
import { fromPageLayoutTabManifestToUniversalFlatPageLayoutTab } from 'src/engine/core-modules/application/application-manifest/converters/from-page-layout-tab-manifest-to-universal-flat-page-layout-tab.util';
import { fromPageLayoutWidgetManifestToUniversalFlatPageLayoutWidget } from 'src/engine/core-modules/application/application-manifest/converters/from-page-layout-widget-manifest-to-universal-flat-page-layout-widget.util';
import { fromPermissionFlagToUniversalFlatPermissionFlag } from 'src/engine/core-modules/application/application-manifest/converters/from-permission-flag-to-universal-flat-permission-flag.util';
import { fromPermissionFlagToUniversalFlatRolePermissionFlag } from 'src/engine/core-modules/application/application-manifest/converters/from-permission-flag-to-universal-flat-role-permission-flag.util';
import { fromRoleManifestToUniversalFlatRole } from 'src/engine/core-modules/application/application-manifest/converters/from-role-manifest-to-universal-flat-role.util';
import { fromSkillManifestToUniversalFlatSkill } from 'src/engine/core-modules/application/application-manifest/converters/from-skill-manifest-to-universal-flat-skill.util';
import { computeSearchVectorUniversalSettingsFromObjectManifest } from 'src/engine/core-modules/application/application-manifest/utils/compute-search-vector-universal-settings-from-object-manifest.util';
@@ -214,14 +214,15 @@ export const computeApplicationManifestAllUniversalFlatEntityMaps = ({
for (const permissionFlag of roleManifest.permissionFlags ?? []) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity: fromPermissionFlagToUniversalFlatPermissionFlag({
permissionFlag,
roleUniversalIdentifier: roleManifest.universalIdentifier,
applicationUniversalIdentifier,
now,
}),
universalFlatEntity:
fromPermissionFlagToUniversalFlatRolePermissionFlag({
permissionFlag,
roleUniversalIdentifier: roleManifest.universalIdentifier,
applicationUniversalIdentifier,
now,
}),
universalFlatEntityMapsToMutate:
allUniversalFlatEntityMaps.flatPermissionFlagMaps,
allUniversalFlatEntityMaps.flatRolePermissionFlagMaps,
});
}
}
@@ -229,8 +229,11 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma
},
"permissionFlag": {
"propertiesToCompare": [
"flag",
"roleUniversalIdentifier",
"key",
"label",
"description",
"icon",
"permissionType",
],
"propertiesToStringify": [],
},
@@ -251,6 +254,14 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma
],
"propertiesToStringify": [],
},
"rolePermissionFlag": {
"propertiesToCompare": [
"permissionFlagUniversalIdentifier",
"roleUniversalIdentifier",
"flag",
],
"propertiesToStringify": [],
},
"roleTarget": {
"propertiesToCompare": [
"roleUniversalIdentifier",
@@ -1186,17 +1186,59 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
universalProperty: 'pageLayoutUniversalIdentifier',
},
},
permissionFlag: {
flag: {
rolePermissionFlag: {
permissionFlagId: {
toCompare: true,
toStringify: false,
universalProperty: undefined,
universalProperty: 'permissionFlagUniversalIdentifier',
},
roleId: {
toCompare: true,
toStringify: false,
universalProperty: 'roleUniversalIdentifier',
},
flag: {
toCompare: true,
toStringify: false,
universalProperty: undefined,
},
createdAt: {
toCompare: false,
toStringify: false,
universalProperty: undefined,
},
updatedAt: {
toCompare: false,
toStringify: false,
universalProperty: undefined,
},
},
permissionFlag: {
key: {
toCompare: true,
toStringify: false,
universalProperty: undefined,
},
label: {
toCompare: true,
toStringify: false,
universalProperty: undefined,
},
description: {
toCompare: true,
toStringify: false,
universalProperty: undefined,
},
icon: {
toCompare: true,
toStringify: false,
universalProperty: undefined,
},
permissionType: {
toCompare: true,
toStringify: false,
universalProperty: undefined,
},
createdAt: {
toCompare: false,
toStringify: false,
@@ -171,12 +171,19 @@ export const ALL_MANY_TO_ONE_METADATA_FOREIGN_KEY = {
workspace: null,
application: null,
},
permissionFlag: {
rolePermissionFlag: {
workspace: null,
application: null,
role: {
foreignKey: 'roleId',
},
permissionFlag: {
foreignKey: 'permissionFlagId',
},
},
permissionFlag: {
workspace: null,
application: null,
},
objectPermission: {
workspace: null,
@@ -300,16 +300,27 @@ export const ALL_MANY_TO_ONE_METADATA_RELATIONS = {
workspace: null,
application: null,
},
permissionFlag: {
rolePermissionFlag: {
workspace: null,
application: null,
role: {
metadataName: 'role',
foreignKey: 'roleId',
inverseOneToManyProperty: 'permissionFlags',
inverseOneToManyProperty: 'rolePermissionFlags',
isNullable: false,
universalForeignKey: 'roleUniversalIdentifier',
},
permissionFlag: {
metadataName: 'permissionFlag',
foreignKey: 'permissionFlagId',
inverseOneToManyProperty: 'rolePermissionFlags',
isNullable: true,
universalForeignKey: 'permissionFlagUniversalIdentifier',
},
},
permissionFlag: {
workspace: null,
application: null,
},
objectPermission: {
workspace: null,
@@ -17,6 +17,7 @@ import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout-tab
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';
import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity';
import { RolePermissionFlagEntity } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.entity';
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity';
@@ -56,6 +57,7 @@ export const ALL_METADATA_ENTITY_BY_METADATA_NAME = {
agent: AgentEntity,
commandMenuItem: CommandMenuItemEntity,
navigationMenuItem: NavigationMenuItemEntity,
rolePermissionFlag: RolePermissionFlagEntity,
permissionFlag: PermissionFlagEntity,
webhook: WebhookEntity,
applicationVariable: ApplicationVariableEntity,
@@ -76,9 +76,13 @@ export const ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION = {
view: true,
pageLayout: true,
},
permissionFlag: {
rolePermissionFlag: {
permissionFlag: true,
role: true,
},
permissionFlag: {
rolePermissionFlag: true,
},
objectPermission: {
role: true,
objectMetadata: true,
@@ -33,6 +33,7 @@ export const ALL_METADATA_SERIALIZED_RELATION = {
logicFunction: {},
role: {},
roleTarget: {},
rolePermissionFlag: {},
permissionFlag: {},
objectPermission: {},
fieldPermission: {},
@@ -168,11 +168,11 @@ export const ALL_ONE_TO_MANY_METADATA_RELATIONS = {
universalFlatEntityForeignKeyAggregator:
'objectPermissionUniversalIdentifiers',
},
permissionFlags: {
metadataName: 'permissionFlag',
flatEntityForeignKeyAggregator: 'permissionFlagIds',
rolePermissionFlags: {
metadataName: 'rolePermissionFlag',
flatEntityForeignKeyAggregator: 'rolePermissionFlagIds',
universalFlatEntityForeignKeyAggregator:
'permissionFlagUniversalIdentifiers',
'rolePermissionFlagUniversalIdentifiers',
},
fieldPermissions: {
metadataName: 'fieldPermission',
@@ -194,7 +194,15 @@ export const ALL_ONE_TO_MANY_METADATA_RELATIONS = {
},
},
roleTarget: {},
permissionFlag: {},
rolePermissionFlag: {},
permissionFlag: {
rolePermissionFlags: {
metadataName: 'rolePermissionFlag',
flatEntityForeignKeyAggregator: 'rolePermissionFlagIds',
universalFlatEntityForeignKeyAggregator:
'rolePermissionFlagUniversalIdentifiers',
},
},
objectPermission: {},
fieldPermission: {},
pageLayout: {
@@ -13,6 +13,7 @@ import { WorkspaceFlatPageLayoutTabMapCacheService } from 'src/engine/metadata-m
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 { WorkspaceFlatRolePermissionFlagMapCacheService } from 'src/engine/metadata-modules/flat-role-permission-flag/services/workspace-flat-role-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';
@@ -32,6 +33,7 @@ import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout-tab
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';
import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity';
import { RolePermissionFlagEntity } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity';
import { RowLevelPermissionPredicateEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate.entity';
@@ -64,6 +66,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
PageLayoutEntity,
PageLayoutTabEntity,
PageLayoutWidgetEntity,
RolePermissionFlagEntity,
PermissionFlagEntity,
RowLevelPermissionPredicateEntity,
RowLevelPermissionPredicateGroupEntity,
@@ -85,6 +88,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
WorkspaceFlatViewGroupMapCacheService,
WorkspaceFlatObjectPermissionMapCacheService,
WorkspaceFlatFieldPermissionMapCacheService,
WorkspaceFlatRolePermissionFlagMapCacheService,
WorkspaceFlatPermissionFlagMapCacheService,
WorkspaceFlatViewSortMapCacheService,
WorkspaceFlatPageLayoutMapCacheService,
@@ -106,6 +110,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
WorkspaceFlatViewGroupMapCacheService,
WorkspaceFlatObjectPermissionMapCacheService,
WorkspaceFlatFieldPermissionMapCacheService,
WorkspaceFlatRolePermissionFlagMapCacheService,
WorkspaceFlatPermissionFlagMapCacheService,
WorkspaceFlatViewSortMapCacheService,
WorkspaceFlatPageLayoutMapCacheService,
@@ -26,6 +26,8 @@ import { type FlatPageLayoutMaps } from 'src/engine/metadata-modules/flat-page-l
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 FlatRolePermissionFlagMaps } from 'src/engine/metadata-modules/flat-role-permission-flag/types/flat-role-permission-flag-maps.type';
import { type FlatRolePermissionFlag } from 'src/engine/metadata-modules/flat-role-permission-flag/types/flat-role-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';
@@ -70,6 +72,7 @@ import { type UniversalFlatPageLayoutTab } from 'src/engine/workspace-manager/wo
import { type UniversalFlatPageLayoutWidget } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-page-layout-widget.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 UniversalFlatRolePermissionFlag } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-role-permission-flag.type';
import { type UniversalFlatRoleTarget } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-role-target.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';
@@ -204,6 +207,14 @@ import {
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 FlatCreateRolePermissionFlagAction,
type FlatDeleteRolePermissionFlagAction,
type FlatUpdateRolePermissionFlagAction,
type UniversalCreateRolePermissionFlagAction,
type UniversalDeleteRolePermissionFlagAction,
type UniversalUpdateRolePermissionFlagAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/role-permission-flag/types/workspace-migration-role-permission-flag-action.type';
import {
type FlatCreateRoleTargetAction,
type FlatDeleteRoleTargetAction,
@@ -625,6 +636,22 @@ export type AllFlatEntityTypesByMetadataName = {
universalFlatEntity: UniversalFlatNavigationMenuItem;
entity: NavigationMenuItemEntity;
};
rolePermissionFlag: {
flatEntityMaps: FlatRolePermissionFlagMaps;
universalActions: {
create: UniversalCreateRolePermissionFlagAction;
update: UniversalUpdateRolePermissionFlagAction;
delete: UniversalDeleteRolePermissionFlagAction;
};
flatActions: {
create: FlatCreateRolePermissionFlagAction;
update: FlatUpdateRolePermissionFlagAction;
delete: FlatDeleteRolePermissionFlagAction;
};
flatEntity: FlatRolePermissionFlag;
universalFlatEntity: UniversalFlatRolePermissionFlag;
entity: MetadataEntity<'rolePermissionFlag'>;
};
permissionFlag: {
flatEntityMaps: FlatPermissionFlagMaps;
universalActions: {
@@ -93,7 +93,7 @@ exports[`getMetadataRelatedMetadataNames should return related metadata names fo
exports[`getMetadataRelatedMetadataNames should return related metadata names for permissionFlag 1`] = `
[
"role",
"rolePermissionFlag",
]
`;
@@ -101,13 +101,20 @@ exports[`getMetadataRelatedMetadataNames should return related metadata names fo
[
"roleTarget",
"objectPermission",
"permissionFlag",
"rolePermissionFlag",
"fieldPermission",
"rowLevelPermissionPredicate",
"rowLevelPermissionPredicateGroup",
]
`;
exports[`getMetadataRelatedMetadataNames should return related metadata names for rolePermissionFlag 1`] = `
[
"role",
"permissionFlag",
]
`;
exports[`getMetadataRelatedMetadataNames should return related metadata names for roleTarget 1`] = `
[
"role",
@@ -10,10 +10,10 @@ exports[`sortMetadataNamesChildrenFirst should return metadata names sorted with
"viewField",
"objectPermission",
"pageLayoutWidget",
"rolePermissionFlag",
"viewSort",
"index",
"pageLayout",
"permissionFlag",
"roleTarget",
"rowLevelPermissionPredicateGroup",
"viewGroup",
@@ -27,6 +27,7 @@ exports[`sortMetadataNamesChildrenFirst should return metadata names sorted with
"viewFieldGroup",
"viewFilterGroup",
"webhook",
"permissionFlag",
"view",
"fieldMetadata",
"objectMetadata",
@@ -1,6 +1,8 @@
import { type MetadataEntityPropertyName } from 'src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant';
export const FLAT_PERMISSION_FLAG_EDITABLE_PROPERTIES = [
'flag',
'roleId',
'label',
'description',
'icon',
'permissionType',
] as const satisfies MetadataEntityPropertyName<'permissionFlag'>[];
@@ -0,0 +1,22 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { WorkspaceFlatPermissionFlagMapCacheService } from 'src/engine/metadata-modules/flat-permission-flag/services/workspace-flat-permission-flag-map-cache.service';
import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity';
import { RolePermissionFlagEntity } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.entity';
@Module({
imports: [
TypeOrmModule.forFeature([
ApplicationEntity,
PermissionFlagEntity,
RolePermissionFlagEntity,
]),
WorkspaceManyOrAllFlatEntityMapsCacheModule,
],
providers: [WorkspaceFlatPermissionFlagMapCacheService],
exports: [WorkspaceFlatPermissionFlagMapCacheService],
})
export class FlatPermissionFlagModule {}
@@ -8,10 +8,11 @@ import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-enti
import { type FlatPermissionFlagMaps } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag-maps.type';
import { fromPermissionFlagEntityToFlatPermissionFlag } from 'src/engine/metadata-modules/flat-permission-flag/utils/from-permission-flag-entity-to-flat-permission-flag.util';
import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service';
import { RolePermissionFlagEntity } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.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 { regroupEntitiesByRelatedEntityId } from 'src/engine/workspace-cache/utils/regroup-entities-by-related-entity-id';
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';
@Injectable()
@@ -22,46 +23,52 @@ export class WorkspaceFlatPermissionFlagMapCacheService extends WorkspaceCachePr
private readonly permissionFlagRepository: Repository<PermissionFlagEntity>,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
@InjectRepository(RoleEntity)
private readonly roleRepository: Repository<RoleEntity>,
@InjectRepository(RolePermissionFlagEntity)
private readonly rolePermissionFlagRepository: Repository<RolePermissionFlagEntity>,
) {
super();
}
async computeForCache(workspaceId: string): Promise<FlatPermissionFlagMaps> {
const [permissionFlags, applications, roles] = await Promise.all([
this.permissionFlagRepository.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,
}),
]);
const [permissionFlags, applications, rolePermissionFlags] =
await Promise.all([
this.permissionFlagRepository.find({
where: { workspaceId },
withDeleted: true,
}),
this.applicationRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
this.rolePermissionFlagRepository.find({
where: { workspaceId },
withDeleted: true,
}),
]);
const applicationIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(applications);
const roleIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(roles);
const rolePermissionFlagsByPermissionFlagId =
regroupEntitiesByRelatedEntityId<'rolePermissionFlag'>({
entities: rolePermissionFlags,
foreignKey: 'permissionFlagId',
});
const flatPermissionFlagMaps = createEmptyFlatEntityMaps();
for (const permissionFlagEntity of permissionFlags) {
const flatPermissionFlag = fromPermissionFlagEntityToFlatPermissionFlag({
entity: permissionFlagEntity,
for (const definition of permissionFlags) {
const flatDefinition = fromPermissionFlagEntityToFlatPermissionFlag({
entity: {
...definition,
rolePermissionFlags:
rolePermissionFlagsByPermissionFlagId.get(definition.id) ?? [],
},
applicationIdToUniversalIdentifierMap,
roleIdToUniversalIdentifierMap,
});
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
flatEntity: flatPermissionFlag,
flatEntity: flatDefinition,
flatEntityMapsToMutate: flatPermissionFlagMaps,
});
}
@@ -1,38 +1,44 @@
import { trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties } from 'twenty-shared/utils';
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 FlatPermissionFlag } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag.type';
import { type CreatePermissionFlagInput } from 'src/engine/metadata-modules/permission-flag/dtos/create-permission-flag.input';
import { type UniversalFlatPermissionFlag } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-permission-flag.type';
export const fromCreatePermissionFlagInputToFlatPermissionFlagToCreate = ({
createPermissionFlagInput,
workspaceId,
flatApplication,
flatRoleMaps,
}: {
createPermissionFlagInput: CreatePermissionFlagInput;
workspaceId: string;
flatApplication: FlatApplication;
} & Pick<AllFlatEntityMaps, 'flatRoleMaps'>): UniversalFlatPermissionFlag & {
id: string;
} => {
const { roleId, flag, universalIdentifier } = createPermissionFlagInput;
}): FlatPermissionFlag => {
const now = new Date().toISOString();
const { roleUniversalIdentifier } = resolveEntityRelationUniversalIdentifiers(
{
metadataName: 'permissionFlag',
foreignKeyValues: { roleId },
flatEntityMaps: { flatRoleMaps },
},
);
const { key, label, description, icon } =
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
createPermissionFlagInput,
['key', 'label', 'description', 'icon'],
);
const id = createPermissionFlagInput.id ?? v4();
const universalIdentifier =
createPermissionFlagInput.universalIdentifier ?? id;
return {
id: v4(),
flag,
universalIdentifier: universalIdentifier ?? v4(),
id,
universalIdentifier,
key,
label,
description: description ?? null,
icon: icon ?? null,
permissionType: createPermissionFlagInput.permissionType,
workspaceId,
applicationId: flatApplication.id,
applicationUniversalIdentifier: flatApplication.universalIdentifier,
roleUniversalIdentifier,
rolePermissionFlagIds: [],
rolePermissionFlagUniversalIdentifiers: [],
createdAt: now,
updatedAt: now,
};
@@ -0,0 +1,31 @@
import { isDefined } from 'twenty-shared/utils';
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 FlatPermissionFlag } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag.type';
import {
PermissionFlagException,
PermissionFlagExceptionCode,
} from 'src/engine/metadata-modules/permission-flag/permission-flag.exception';
export const fromDeletePermissionFlagInputToFlatPermissionFlagOrThrow = ({
flatPermissionFlagMaps,
permissionFlagId,
}: {
flatPermissionFlagMaps: FlatEntityMaps<FlatPermissionFlag>;
permissionFlagId: string;
}): FlatPermissionFlag => {
const existing = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: permissionFlagId,
flatEntityMaps: flatPermissionFlagMaps,
});
if (!isDefined(existing)) {
throw new PermissionFlagException(
'Permission flag not found',
PermissionFlagExceptionCode.PERMISSION_FLAG_NOT_FOUND,
);
}
return existing;
};
@@ -0,0 +1,18 @@
import { type FlatPermissionFlag } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag.type';
import { type PermissionFlagDTO } from 'src/engine/metadata-modules/permission-flag/dtos/permission-flag.dto';
export const fromFlatPermissionFlagToPermissionFlagDto = (
flatDefinition: FlatPermissionFlag,
): PermissionFlagDTO => ({
id: flatDefinition.id,
universalIdentifier: flatDefinition.universalIdentifier,
key: flatDefinition.key,
label: flatDefinition.label,
description: flatDefinition.description,
icon: flatDefinition.icon,
permissionType: flatDefinition.permissionType,
workspaceId: flatDefinition.workspaceId,
applicationId: flatDefinition.applicationId,
createdAt: new Date(flatDefinition.createdAt),
updatedAt: new Date(flatDefinition.updatedAt),
});
@@ -11,9 +11,8 @@ import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/type
export const fromPermissionFlagEntityToFlatPermissionFlag = ({
entity: permissionFlagEntity,
applicationIdToUniversalIdentifierMap,
roleIdToUniversalIdentifierMap,
}: FromEntityToFlatEntityArgs<'permissionFlag'>): FlatPermissionFlag => {
const permissionFlagEntityWithoutRelations = removePropertiesFromRecord(
const entityWithoutRelations = removePropertiesFromRecord(
permissionFlagEntity,
getMetadataEntityRelationProperties('permissionFlag'),
);
@@ -30,24 +29,18 @@ export const fromPermissionFlagEntityToFlatPermissionFlag = ({
);
}
const roleUniversalIdentifier = roleIdToUniversalIdentifierMap.get(
permissionFlagEntity.roleId,
);
if (!isDefined(roleUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Role with id ${permissionFlagEntity.roleId} not found for permissionFlag ${permissionFlagEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
return {
...permissionFlagEntityWithoutRelations,
...entityWithoutRelations,
createdAt: permissionFlagEntity.createdAt.toISOString(),
updatedAt: permissionFlagEntity.updatedAt.toISOString(),
universalIdentifier:
permissionFlagEntityWithoutRelations.universalIdentifier,
universalIdentifier: entityWithoutRelations.universalIdentifier,
applicationUniversalIdentifier,
roleUniversalIdentifier,
rolePermissionFlagIds: permissionFlagEntity.rolePermissionFlags.map(
({ id }) => id,
),
rolePermissionFlagUniversalIdentifiers:
permissionFlagEntity.rolePermissionFlags.map(
({ universalIdentifier }) => universalIdentifier,
),
};
};
@@ -0,0 +1,42 @@
import { isDefined } from 'twenty-shared/utils';
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 { FLAT_PERMISSION_FLAG_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-permission-flag/constants/flat-permission-flag-editable-properties.constant';
import { type FlatPermissionFlag } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag.type';
import {
PermissionFlagException,
PermissionFlagExceptionCode,
} from 'src/engine/metadata-modules/permission-flag/permission-flag.exception';
import { type UpdatePermissionFlagInput } from 'src/engine/metadata-modules/permission-flag/dtos/update-permission-flag.input';
import { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing-record.util';
export const fromUpdatePermissionFlagInputToFlatPermissionFlagToUpdateOrThrow =
({
flatPermissionFlagMaps,
updatePermissionFlagInput,
}: {
flatPermissionFlagMaps: FlatEntityMaps<FlatPermissionFlag>;
updatePermissionFlagInput: UpdatePermissionFlagInput;
}): FlatPermissionFlag => {
const existing = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: updatePermissionFlagInput.id,
flatEntityMaps: flatPermissionFlagMaps,
});
if (!isDefined(existing)) {
throw new PermissionFlagException(
'Permission flag not found',
PermissionFlagExceptionCode.PERMISSION_FLAG_NOT_FOUND,
);
}
return {
...mergeUpdateInExistingRecord({
existing,
properties: [...FLAT_PERMISSION_FLAG_EDITABLE_PROPERTIES],
update: updatePermissionFlagInput.update,
}),
updatedAt: new Date().toISOString(),
};
};
@@ -0,0 +1,57 @@
import {
PermissionFlagType,
SystemPermissionFlag,
} from 'twenty-shared/constants';
import { type FlatPermissionFlag } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag.type';
import {
STANDARD_PERMISSION_FLAG_DEFINITIONS,
type StandardPermissionFlagDefinition,
} from 'src/engine/metadata-modules/permission-flag/constants/standard-permission-flag-definitions.constant';
const STANDARD_PERMISSION_FLAG_DEFINITION_BY_KEY: Record<
PermissionFlagType,
StandardPermissionFlagDefinition
> = STANDARD_PERMISSION_FLAG_DEFINITIONS.reduce(
(accumulator, definition) => {
accumulator[definition.key] = definition;
return accumulator;
},
{} as Record<PermissionFlagType, StandardPermissionFlagDefinition>,
);
const SYNTHESIZED_TIMESTAMP = new Date(0).toISOString();
// TODO: Remove this once we have a proper permission flag catalog backfilled
export const synthesizeFlatPermissionFlagFromFlag = ({
flag,
workspaceId,
applicationId,
applicationUniversalIdentifier,
}: {
flag: PermissionFlagType;
workspaceId: string;
applicationId: string;
applicationUniversalIdentifier: string;
}): FlatPermissionFlag => {
const definition = STANDARD_PERMISSION_FLAG_DEFINITION_BY_KEY[flag];
const universalIdentifier = SystemPermissionFlag[flag];
return {
id: universalIdentifier,
universalIdentifier,
workspaceId,
applicationId,
applicationUniversalIdentifier,
key: flag,
label: definition.label,
description: definition.description,
icon: definition.icon,
permissionType: definition.permissionType,
rolePermissionFlagIds: [],
rolePermissionFlagUniversalIdentifiers: [],
createdAt: SYNTHESIZED_TIMESTAMP,
updatedAt: SYNTHESIZED_TIMESTAMP,
};
};
@@ -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 FlatRolePermissionFlagMaps } from 'src/engine/metadata-modules/flat-role-permission-flag/types/flat-role-permission-flag-maps.type';
import { fromRolePermissionFlagEntityToFlatRolePermissionFlag } from 'src/engine/metadata-modules/flat-role-permission-flag/utils/from-role-permission-flag-entity-to-flat-role-permission-flag.util';
import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity';
import { RolePermissionFlagEntity } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service';
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';
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';
@Injectable()
@WorkspaceCache('flatRolePermissionFlagMaps')
export class WorkspaceFlatRolePermissionFlagMapCacheService extends WorkspaceCacheProvider<FlatRolePermissionFlagMaps> {
constructor(
@InjectRepository(RolePermissionFlagEntity)
private readonly rolePermissionFlagRepository: Repository<RolePermissionFlagEntity>,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
@InjectRepository(RoleEntity)
private readonly roleRepository: Repository<RoleEntity>,
@InjectRepository(PermissionFlagEntity)
private readonly permissionFlagRepository: Repository<PermissionFlagEntity>,
) {
super();
}
async computeForCache(
workspaceId: string,
): Promise<FlatRolePermissionFlagMaps> {
const [rolePermissionFlags, applications, roles, permissionFlags] =
await Promise.all([
this.rolePermissionFlagRepository.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.permissionFlagRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
]);
const applicationIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(applications);
const roleIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(roles);
const permissionFlagIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(permissionFlags);
const flatRolePermissionFlagMaps = createEmptyFlatEntityMaps();
for (const rolePermissionFlagEntity of rolePermissionFlags) {
const flatRolePermissionFlag =
fromRolePermissionFlagEntityToFlatRolePermissionFlag({
entity: rolePermissionFlagEntity,
applicationIdToUniversalIdentifierMap,
permissionFlagIdToUniversalIdentifierMap,
roleIdToUniversalIdentifierMap,
});
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
flatEntity: flatRolePermissionFlag,
flatEntityMapsToMutate: flatRolePermissionFlagMaps,
});
}
return flatRolePermissionFlagMaps;
}
}
@@ -0,0 +1,4 @@
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type FlatRolePermissionFlag } from 'src/engine/metadata-modules/flat-role-permission-flag/types/flat-role-permission-flag.type';
export type FlatRolePermissionFlagMaps = FlatEntityMaps<FlatRolePermissionFlag>;
@@ -0,0 +1,4 @@
import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
import { type RolePermissionFlagEntity } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.entity';
export type FlatRolePermissionFlag = FlatEntityFrom<RolePermissionFlagEntity>;
@@ -0,0 +1,45 @@
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 CreateRolePermissionFlagInput } from 'src/engine/metadata-modules/role-permission-flag/dtos/create-role-permission-flag.input';
import { type UniversalFlatRolePermissionFlag } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-role-permission-flag.type';
export const fromCreateRolePermissionFlagInputToFlatRolePermissionFlagToCreate =
({
createRolePermissionFlagInput,
flatApplication,
flatPermissionFlagMaps,
flatRoleMaps,
}: {
createRolePermissionFlagInput: CreateRolePermissionFlagInput;
flatApplication: FlatApplication;
} & Pick<
AllFlatEntityMaps,
'flatPermissionFlagMaps' | 'flatRoleMaps'
>): UniversalFlatRolePermissionFlag & {
id: string;
} => {
const { roleId, permissionFlagId, flag, universalIdentifier } =
createRolePermissionFlagInput;
const now = new Date().toISOString();
const { permissionFlagUniversalIdentifier, roleUniversalIdentifier } =
resolveEntityRelationUniversalIdentifiers({
metadataName: 'rolePermissionFlag',
foreignKeyValues: { permissionFlagId, roleId },
flatEntityMaps: { flatPermissionFlagMaps, flatRoleMaps },
});
return {
id: v4(),
universalIdentifier: universalIdentifier ?? v4(),
applicationUniversalIdentifier: flatApplication.universalIdentifier,
permissionFlagUniversalIdentifier,
roleUniversalIdentifier,
flag,
createdAt: now,
updatedAt: now,
};
};
@@ -0,0 +1,71 @@
import { SystemPermissionFlag } from 'twenty-shared/constants';
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 FlatRolePermissionFlag } from 'src/engine/metadata-modules/flat-role-permission-flag/types/flat-role-permission-flag.type';
import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type';
export const fromRolePermissionFlagEntityToFlatRolePermissionFlag = ({
entity: rolePermissionFlagEntity,
applicationIdToUniversalIdentifierMap,
permissionFlagIdToUniversalIdentifierMap,
roleIdToUniversalIdentifierMap,
}: FromEntityToFlatEntityArgs<'rolePermissionFlag'>): FlatRolePermissionFlag => {
const rolePermissionFlagEntityWithoutRelations = removePropertiesFromRecord(
rolePermissionFlagEntity,
getMetadataEntityRelationProperties('rolePermissionFlag'),
);
const applicationUniversalIdentifier =
applicationIdToUniversalIdentifierMap.get(
rolePermissionFlagEntity.applicationId,
);
if (!isDefined(applicationUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Application with id ${rolePermissionFlagEntity.applicationId} not found for rolePermissionFlag ${rolePermissionFlagEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
const roleUniversalIdentifier = roleIdToUniversalIdentifierMap.get(
rolePermissionFlagEntity.roleId,
);
if (!isDefined(roleUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Role with id ${rolePermissionFlagEntity.roleId} not found for rolePermissionFlag ${rolePermissionFlagEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
const permissionFlagUniversalIdentifier = isDefined(
rolePermissionFlagEntity.permissionFlagId,
)
? permissionFlagIdToUniversalIdentifierMap.get(
rolePermissionFlagEntity.permissionFlagId,
)
: SystemPermissionFlag[rolePermissionFlagEntity.flag];
if (!isDefined(permissionFlagUniversalIdentifier)) {
throw new FlatEntityMapsException(
`PermissionFlag with id ${rolePermissionFlagEntity.permissionFlagId} not found for rolePermissionFlag ${rolePermissionFlagEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
return {
...rolePermissionFlagEntityWithoutRelations,
createdAt: rolePermissionFlagEntity.createdAt.toISOString(),
updatedAt: rolePermissionFlagEntity.updatedAt.toISOString(),
universalIdentifier:
rolePermissionFlagEntityWithoutRelations.universalIdentifier,
applicationUniversalIdentifier,
permissionFlagUniversalIdentifier,
roleUniversalIdentifier,
};
};
@@ -2,8 +2,8 @@ import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/typ
import { type RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
export type FlatRole = FlatEntityFrom<RoleEntity> & {
// TODO remove once objectPermission permissionFlag fieldPermission have been migrated to v2
// TODO remove once objectPermission rolePermissionFlag fieldPermission have been migrated to v2
objectPermissionIds: string[];
permissionFlagIds: string[];
rolePermissionFlagIds: string[];
fieldPermissionIds: string[];
};
@@ -54,9 +54,9 @@ export const fromCreateRoleInputToFlatRoleToCreate = ({
roleTargetIds: [],
roleTargetUniversalIdentifiers: [],
objectPermissionIds: [],
permissionFlagIds: [],
rolePermissionFlagIds: [],
objectPermissionUniversalIdentifiers: [],
permissionFlagUniversalIdentifiers: [],
rolePermissionFlagUniversalIdentifiers: [],
fieldPermissionIds: [],
fieldPermissionUniversalIdentifiers: [],
rowLevelPermissionPredicateIds: [],
@@ -13,10 +13,10 @@ import { type RegroupedEntity } from 'src/engine/workspace-cache/utils/regroup-e
type FromRoleEntityToFlatRoleArgs = {
entity: Omit<
EntityWithRegroupedOneToManyRelations<MetadataEntity<'role'>>,
'objectPermissions' | 'permissionFlags' | 'fieldPermissions'
'objectPermissions' | 'rolePermissionFlags' | 'fieldPermissions'
> & {
objectPermissions: RegroupedEntity[];
permissionFlags: RegroupedEntity[];
rolePermissionFlags: RegroupedEntity[];
fieldPermissions: RegroupedEntity[];
};
} & EntityManyToOneIdByUniversalIdentifierMaps<'role'>;
@@ -57,7 +57,7 @@ export const fromRoleEntityToFlatRole = ({
applicationId: roleEntity.applicationId,
roleTargetIds: roleEntity.roleTargets.map(({ id }) => id),
objectPermissionIds: roleEntity.objectPermissions.map(({ id }) => id),
permissionFlagIds: roleEntity.permissionFlags.map(({ id }) => id),
rolePermissionFlagIds: roleEntity.rolePermissionFlags.map(({ id }) => id),
fieldPermissionIds: roleEntity.fieldPermissions.map(({ id }) => id),
rowLevelPermissionPredicateIds: roleEntity.rowLevelPermissionPredicates.map(
({ id }) => id,
@@ -71,7 +71,7 @@ export const fromRoleEntityToFlatRole = ({
objectPermissionUniversalIdentifiers: roleEntity.objectPermissions.map(
({ universalIdentifier }) => universalIdentifier,
),
permissionFlagUniversalIdentifiers: roleEntity.permissionFlags.map(
rolePermissionFlagUniversalIdentifiers: roleEntity.rolePermissionFlags.map(
({ universalIdentifier }) => universalIdentifier,
),
fieldPermissionUniversalIdentifiers: roleEntity.fieldPermissions.map(
@@ -17,6 +17,7 @@ import { MessageChannelMetadataModule } from 'src/engine/metadata-modules/messag
import { MessageFolderMetadataModule } from 'src/engine/metadata-modules/message-folder/message-folder-metadata.module';
import { NavigationMenuItemModule } from 'src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.module';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { PermissionFlagModule } from 'src/engine/metadata-modules/permission-flag/permission-flag.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { MinimalMetadataModule } from 'src/engine/metadata-modules/minimal-metadata/minimal-metadata.module';
import { RoleModule } from 'src/engine/metadata-modules/role/role.module';
@@ -47,6 +48,7 @@ import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/work
WorkspaceMetadataVersionModule,
RoleModule,
PermissionsModule,
PermissionFlagModule,
RouteTriggerModule,
WebhookModule,
ConnectedAccountMetadataModule,
@@ -75,6 +77,7 @@ import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/work
ViewModule,
RoleModule,
PermissionsModule,
PermissionFlagModule,
WebhookModule,
ConnectedAccountMetadataModule,
MessageChannelMetadataModule,
@@ -0,0 +1,4 @@
export const PERMISSION_FLAG_PERMISSION_TYPES = ['settings', 'tool'] as const;
export type PermissionFlagPermissionType =
(typeof PERMISSION_FLAG_PERMISSION_TYPES)[number];
@@ -0,0 +1,160 @@
import {
PermissionFlagType,
SystemPermissionFlag,
} from 'twenty-shared/constants';
import { type PermissionFlagPermissionType } from 'src/engine/metadata-modules/permission-flag/constants/permission-flag-permission-type.constant';
import { TOOL_PERMISSION_FLAGS } from 'src/engine/metadata-modules/permissions/constants/tool-permission-flags';
export type StandardPermissionFlagDefinition = {
key: PermissionFlagType;
universalIdentifier: string;
label: string;
description: string | null;
icon: string | null;
permissionType: PermissionFlagPermissionType;
};
type StandardPermissionFlagMetadata = Pick<
StandardPermissionFlagDefinition,
'label' | 'description' | 'icon'
>;
const STANDARD_PERMISSION_FLAG_METADATA: Record<
PermissionFlagType,
StandardPermissionFlagMetadata
> = {
[PermissionFlagType.API_KEYS_AND_WEBHOOKS]: {
label: 'API Keys & Webhooks',
description: 'Manage API keys and webhooks',
icon: 'IconCode',
},
[PermissionFlagType.WORKSPACE]: {
label: 'Workspace',
description: 'Set global workspace preferences',
icon: 'IconSettings',
},
[PermissionFlagType.WORKSPACE_MEMBERS]: {
label: 'Users',
description: 'Add or remove users',
icon: 'IconUsers',
},
[PermissionFlagType.ROLES]: {
label: 'Roles',
description: 'Define user roles and access levels',
icon: 'IconLockOpen',
},
[PermissionFlagType.DATA_MODEL]: {
label: 'Data Model',
description: 'Edit data structure and fields',
icon: 'IconHierarchy',
},
[PermissionFlagType.SECURITY]: {
label: 'Security',
description: 'Manage security policies',
icon: 'IconKey',
},
[PermissionFlagType.WORKFLOWS]: {
label: 'Workflows',
description: 'Manage workflows',
icon: 'IconSettingsAutomation',
},
[PermissionFlagType.IMPERSONATE]: {
label: 'Impersonate',
description: 'Impersonate workspace users',
icon: 'IconSpy',
},
[PermissionFlagType.SSO_BYPASS]: {
label: 'SSO Bypass',
description: 'Enable bypass options',
icon: 'IconShield',
},
[PermissionFlagType.APPLICATIONS]: {
label: 'Applications',
description: 'Install and manage applications',
icon: 'IconApps',
},
[PermissionFlagType.MARKETPLACE_APPS]: {
label: 'Marketplace Apps',
description: 'Browse and install marketplace apps',
icon: 'IconShoppingBag',
},
[PermissionFlagType.LAYOUTS]: {
label: 'Layouts',
description: 'Customize page layouts and UI structure',
icon: 'IconLayoutSidebarRightCollapse',
},
[PermissionFlagType.BILLING]: {
label: 'Billing',
description: 'Manage billing and subscriptions',
icon: 'IconCreditCard',
},
[PermissionFlagType.AI_SETTINGS]: {
label: 'AI',
description: 'Create and configure AI agents',
icon: 'IconSparkles',
},
[PermissionFlagType.AI]: {
label: 'Ask AI',
description: 'Chat with AI agents and use AI features',
icon: 'IconSparkles',
},
[PermissionFlagType.VIEWS]: {
label: 'Manage Views',
description: 'Create, edit, and delete workspace views',
icon: 'IconTable',
},
[PermissionFlagType.UPLOAD_FILE]: {
label: 'Upload Files',
description: 'Allow uploading files and attachments',
icon: 'IconFileUpload',
},
[PermissionFlagType.DOWNLOAD_FILE]: {
label: 'Download Files',
description: 'Allow downloading files and attachments',
icon: 'IconDownload',
},
[PermissionFlagType.SEND_EMAIL_TOOL]: {
label: 'Send Email',
description: 'Send emails via connected accounts',
icon: 'IconMail',
},
[PermissionFlagType.HTTP_REQUEST_TOOL]: {
label: 'HTTP Request',
description: 'Make HTTP requests to external APIs',
icon: 'IconApi',
},
[PermissionFlagType.CODE_INTERPRETER_TOOL]: {
label: 'Code Interpreter',
description: 'Run code to analyze files and data',
icon: 'IconCode',
},
[PermissionFlagType.IMPORT_CSV]: {
label: 'Import CSV',
description: 'Allow importing data from CSV files',
icon: 'IconFileImport',
},
[PermissionFlagType.EXPORT_CSV]: {
label: 'Export CSV',
description: 'Allow exporting data to CSV files',
icon: 'IconFileExport',
},
[PermissionFlagType.CONNECTED_ACCOUNTS]: {
label: 'Sync Account',
description: 'Sync email and calendar accounts',
icon: 'IconAt',
},
[PermissionFlagType.PROFILE_INFORMATION]: {
label: 'Edit Profile',
description: 'Edit own profile information',
icon: 'IconUser',
},
};
export const STANDARD_PERMISSION_FLAG_DEFINITIONS: StandardPermissionFlagDefinition[] =
Object.values(PermissionFlagType).map((key) => ({
key,
universalIdentifier: SystemPermissionFlag[key],
...STANDARD_PERMISSION_FLAG_METADATA[key],
permissionType: TOOL_PERMISSION_FLAGS.includes(key) ? 'tool' : 'settings',
}));
@@ -1,7 +1,52 @@
import { PermissionFlagType } from 'twenty-shared/constants';
import { Field, InputType } from '@nestjs/graphql';
export type CreatePermissionFlagInput = {
roleId: string;
flag: PermissionFlagType;
import {
IsIn,
IsNotEmpty,
IsOptional,
IsString,
IsUUID,
} from 'class-validator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import {
PERMISSION_FLAG_PERMISSION_TYPES,
type PermissionFlagPermissionType,
} from 'src/engine/metadata-modules/permission-flag/constants/permission-flag-permission-type.constant';
@InputType()
export class CreatePermissionFlagInput {
@IsUUID()
@IsOptional()
@Field(() => UUIDScalarType, { nullable: true })
id?: string;
@IsUUID()
@IsOptional()
@Field(() => UUIDScalarType, { nullable: true })
universalIdentifier?: string;
};
@IsString()
@IsNotEmpty()
@Field()
key: string;
@IsString()
@IsNotEmpty()
@Field()
label: string;
@IsString()
@IsOptional()
@Field({ nullable: true })
description?: string;
@IsString()
@IsOptional()
@Field({ nullable: true })
icon?: string;
@IsIn(PERMISSION_FLAG_PERMISSION_TYPES)
@Field(() => String)
permissionType: PermissionFlagPermissionType;
}
@@ -1,17 +1,67 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { Field, HideField, ObjectType } from '@nestjs/graphql';
import { PermissionFlagType } from 'twenty-shared/constants';
import {
IsDateString,
IsIn,
IsNotEmpty,
IsOptional,
IsString,
IsUUID,
} from 'class-validator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import {
PERMISSION_FLAG_PERMISSION_TYPES,
type PermissionFlagPermissionType,
} from 'src/engine/metadata-modules/permission-flag/constants/permission-flag-permission-type.constant';
@ObjectType('PermissionFlag')
export class PermissionFlagDTO {
@Field(() => UUIDScalarType, { nullable: false })
@IsUUID()
@IsNotEmpty()
@Field(() => UUIDScalarType)
id: string;
@Field(() => UUIDScalarType, { nullable: false })
roleId: string;
@IsUUID()
@IsNotEmpty()
@Field(() => UUIDScalarType)
universalIdentifier: string;
@Field({ nullable: false })
flag: PermissionFlagType;
@IsString()
@IsNotEmpty()
@Field()
key: string;
@IsString()
@IsNotEmpty()
@Field()
label: string;
@IsString()
@IsOptional()
@Field(() => String, { nullable: true })
description: string | null;
@IsString()
@IsOptional()
@Field(() => String, { nullable: true })
icon: string | null;
@IsIn(PERMISSION_FLAG_PERMISSION_TYPES)
@Field(() => String)
permissionType: PermissionFlagPermissionType;
@HideField()
workspaceId: string;
@Field(() => UUIDScalarType)
applicationId: string;
@IsDateString()
@Field()
createdAt: Date;
@IsDateString()
@Field()
updatedAt: Date;
}
@@ -0,0 +1,57 @@
import { Field, InputType } from '@nestjs/graphql';
import { Type } from 'class-transformer';
import {
IsIn,
IsNotEmpty,
IsOptional,
IsString,
IsUUID,
ValidateNested,
} from 'class-validator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import {
PERMISSION_FLAG_PERMISSION_TYPES,
type PermissionFlagPermissionType,
} from 'src/engine/metadata-modules/permission-flag/constants/permission-flag-permission-type.constant';
@InputType()
export class UpdatePermissionFlagInputUpdates {
@IsOptional()
@IsString()
@Field({ nullable: true })
label?: string;
@IsOptional()
@IsString()
@Field({ nullable: true })
description?: string;
@IsOptional()
@IsString()
@Field({ nullable: true })
icon?: string;
@IsOptional()
@IsIn(PERMISSION_FLAG_PERMISSION_TYPES)
@Field(() => String, { nullable: true })
permissionType?: PermissionFlagPermissionType;
}
@InputType()
export class UpdatePermissionFlagInput {
@IsUUID()
@IsNotEmpty()
@Field(() => UUIDScalarType, {
description: 'The id of the permission flag definition to update',
})
id: string;
@Type(() => UpdatePermissionFlagInputUpdates)
@ValidateNested()
@Field(() => UpdatePermissionFlagInputUpdates, {
description: 'The fields to update',
})
update: UpdatePermissionFlagInputUpdates;
}
@@ -1,38 +1,46 @@
import { PermissionFlagType } from 'twenty-shared/constants';
import {
Column,
CreateDateColumn,
Entity,
Index,
JoinColumn,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
Relation,
Unique,
UpdateDateColumn,
} from 'typeorm';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { type PermissionFlagPermissionType } from 'src/engine/metadata-modules/permission-flag/constants/permission-flag-permission-type.constant';
import { RolePermissionFlagEntity } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.entity';
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
@Entity('permissionFlag')
@Unique('IDX_PERMISSION_FLAG_FLAG_ROLE_ID_UNIQUE', ['flag', 'roleId'])
@Index('IDX_PERMISSION_FLAG_ROLE_ID', ['roleId'])
@Unique('IDX_PERMISSION_FLAG_KEY_WORKSPACE_ID_UNIQUE', ['key', 'workspaceId'])
@Index('IDX_PERMISSION_FLAG_APPLICATION_ID', ['applicationId'])
export class PermissionFlagEntity extends SyncableEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ nullable: false, type: 'uuid' })
roleId: string;
@ManyToOne(() => RoleEntity, (role) => role.permissionFlags, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'roleId' })
role: Relation<RoleEntity>;
@Column({ nullable: false, type: 'varchar' })
key: string;
@Column({ nullable: false, type: 'varchar' })
flag: PermissionFlagType;
label: string;
@Column({ nullable: true, type: 'text' })
description: string | null;
@Column({ nullable: true, type: 'varchar' })
icon: string | null;
@Column({ nullable: false, type: 'varchar' })
permissionType: PermissionFlagPermissionType;
@OneToMany(
() => RolePermissionFlagEntity,
(rolePermissionFlag) => rolePermissionFlag.permissionFlag,
)
rolePermissionFlags: Relation<RolePermissionFlagEntity[]>;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@@ -0,0 +1,52 @@
import { type MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { assertUnreachable } from 'twenty-shared/utils';
import { CustomException } from 'src/utils/custom-exception';
export enum PermissionFlagExceptionCode {
PERMISSION_FLAG_NOT_FOUND = 'PERMISSION_FLAG_NOT_FOUND',
PERMISSION_FLAG_ALREADY_EXISTS = 'PERMISSION_FLAG_ALREADY_EXISTS',
INVALID_PERMISSION_FLAG_KEY = 'INVALID_PERMISSION_FLAG_KEY',
INVALID_PERMISSION_FLAG_PERMISSION_TYPE = 'INVALID_PERMISSION_FLAG_PERMISSION_TYPE',
PERMISSION_FLAG_KEY_IMMUTABLE = 'PERMISSION_FLAG_KEY_IMMUTABLE',
PERMISSION_FLAG_IS_STANDARD = 'PERMISSION_FLAG_IS_STANDARD',
PERMISSION_FLAG_IN_USE = 'PERMISSION_FLAG_IN_USE',
}
const getPermissionFlagExceptionUserFriendlyMessage = (
code: PermissionFlagExceptionCode,
) => {
switch (code) {
case PermissionFlagExceptionCode.PERMISSION_FLAG_NOT_FOUND:
return msg`Permission flag not found.`;
case PermissionFlagExceptionCode.PERMISSION_FLAG_ALREADY_EXISTS:
return msg`Permission flag already exists.`;
case PermissionFlagExceptionCode.INVALID_PERMISSION_FLAG_KEY:
return msg`Invalid permission flag key.`;
case PermissionFlagExceptionCode.INVALID_PERMISSION_FLAG_PERMISSION_TYPE:
return msg`Invalid permission flag permission type.`;
case PermissionFlagExceptionCode.PERMISSION_FLAG_KEY_IMMUTABLE:
return msg`Permission flag key cannot be changed.`;
case PermissionFlagExceptionCode.PERMISSION_FLAG_IS_STANDARD:
return msg`Standard permission flags cannot be modified.`;
case PermissionFlagExceptionCode.PERMISSION_FLAG_IN_USE:
return msg`Permission flag is still assigned to a role.`;
default:
assertUnreachable(code);
}
};
export class PermissionFlagException extends CustomException<PermissionFlagExceptionCode> {
constructor(
message: string,
code: PermissionFlagExceptionCode,
{ userFriendlyMessage }: { userFriendlyMessage?: MessageDescriptor } = {},
) {
super(message, code, {
userFriendlyMessage:
userFriendlyMessage ??
getPermissionFlagExceptionUserFriendlyMessage(code),
});
}
}
@@ -2,16 +2,20 @@ import { Module } from '@nestjs/common';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { FlatPermissionFlagModule } from 'src/engine/metadata-modules/flat-permission-flag/flat-permission-flag.module';
import { PermissionFlagService } from 'src/engine/metadata-modules/permission-flag/permission-flag.service';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
@Module({
imports: [
WorkspaceCacheModule,
ApplicationModule,
WorkspaceMigrationModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
WorkspaceMigrationModule,
WorkspaceCacheStorageModule,
ApplicationModule,
FlatPermissionFlagModule,
PermissionsModule,
],
providers: [PermissionFlagService],
exports: [PermissionFlagService],
@@ -1,19 +1,18 @@
import { Injectable } from '@nestjs/common';
import { msg } from '@lingui/core/macro';
import { PermissionFlagType } from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils';
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 { type FlatPermissionFlag } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag.type';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { fromCreatePermissionFlagInputToFlatPermissionFlagToCreate } from 'src/engine/metadata-modules/flat-permission-flag/utils/from-create-permission-flag-input-to-flat-permission-flag-to-create.util';
import { type UpsertPermissionFlagsInput } from 'src/engine/metadata-modules/permission-flag/dtos/upsert-permission-flag-input';
import {
PermissionsException,
PermissionsExceptionCode,
PermissionsExceptionMessage,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { fromDeletePermissionFlagInputToFlatPermissionFlagOrThrow } from 'src/engine/metadata-modules/flat-permission-flag/utils/from-delete-permission-flag-input-to-flat-permission-flag-or-throw.util';
import { fromFlatPermissionFlagToPermissionFlagDto } from 'src/engine/metadata-modules/flat-permission-flag/utils/from-flat-permission-flag-to-permission-flag-dto.util';
import { fromUpdatePermissionFlagInputToFlatPermissionFlagToUpdateOrThrow } from 'src/engine/metadata-modules/flat-permission-flag/utils/from-update-permission-flag-input-to-flat-permission-flag-to-update-or-throw.util';
import { type CreatePermissionFlagInput } from 'src/engine/metadata-modules/permission-flag/dtos/create-permission-flag.input';
import { type PermissionFlagDTO } from 'src/engine/metadata-modules/permission-flag/dtos/permission-flag.dto';
import { type UpdatePermissionFlagInput } from 'src/engine/metadata-modules/permission-flag/dtos/update-permission-flag.input';
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';
@@ -25,115 +24,8 @@ export class PermissionFlagService {
private readonly applicationService: ApplicationService,
) {}
public async upsertPermissionFlags({
workspaceId,
input,
}: {
workspaceId: string;
input: UpsertPermissionFlagsInput;
}): Promise<FlatPermissionFlag[]> {
const { flatPermissionFlagMaps, flatRoleMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatPermissionFlagMaps', 'flatRoleMaps'],
},
);
const roleUniversalId = flatRoleMaps.universalIdentifierById[input.roleId];
const role = isDefined(roleUniversalId)
? flatRoleMaps.byUniversalIdentifier[roleUniversalId]
: undefined;
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.`,
},
);
}
const invalidFlags = input.permissionFlagKeys.filter(
(flag) => !Object.values(PermissionFlagType).includes(flag),
);
if (invalidFlags.length > 0) {
throw new PermissionsException(
`${PermissionsExceptionMessage.INVALID_SETTING}: ${invalidFlags.join(', ')}`,
PermissionsExceptionCode.INVALID_SETTING,
{
userFriendlyMessage: msg`Some of the permissions you selected are not valid. Please try again with valid permission settings.`,
},
);
}
const roleUniversalIdentifier = role.universalIdentifier;
const currentPermissionFlagsForRole = Object.values(
flatPermissionFlagMaps.byUniversalIdentifier,
).filter(
(pf): pf is FlatPermissionFlag =>
isDefined(pf) && pf.roleUniversalIdentifier === roleUniversalIdentifier,
);
const inputSet = new Set(input.permissionFlagKeys);
const existingSet = new Set(
currentPermissionFlagsForRole.map((pf) => pf.flag),
);
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
const flatEntityToCreate = input.permissionFlagKeys
.filter((flag) => !existingSet.has(flag))
.map((flag) =>
fromCreatePermissionFlagInputToFlatPermissionFlagToCreate({
createPermissionFlagInput: { roleId: input.roleId, flag },
flatApplication: workspaceCustomFlatApplication,
flatRoleMaps,
}),
);
const flatEntityToDelete = currentPermissionFlagsForRole.filter(
(pf) => !inputSet.has(pf.flag),
);
if (flatEntityToCreate.length === 0 && flatEntityToDelete.length === 0) {
const unchanged = currentPermissionFlagsForRole.filter((pf) =>
inputSet.has(pf.flag),
);
return unchanged;
}
const buildAndRunResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
permissionFlag: {
flatEntityToCreate,
flatEntityToDelete,
flatEntityToUpdate: [],
},
},
workspaceId,
isSystemBuild: false,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
},
);
if (buildAndRunResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
buildAndRunResult,
'Validation errors occurred while upserting permission flags',
);
}
const { flatPermissionFlagMaps: freshFlatPermissionFlagMaps } =
async findAll(workspaceId: string): Promise<PermissionFlagDTO[]> {
const { flatPermissionFlagMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
@@ -141,13 +33,196 @@ export class PermissionFlagService {
},
);
const resultFlags = Object.values(
freshFlatPermissionFlagMaps.byUniversalIdentifier,
).filter(
(pf): pf is FlatPermissionFlag =>
isDefined(pf) && pf.roleUniversalIdentifier === roleUniversalIdentifier,
);
return Object.values(flatPermissionFlagMaps.byUniversalIdentifier)
.filter(isDefined)
.sort((a, b) => a.createdAt.localeCompare(b.createdAt))
.map(fromFlatPermissionFlagToPermissionFlagDto);
}
return resultFlags;
async findById(
id: string,
workspaceId: string,
): Promise<PermissionFlagDTO | null> {
const { flatPermissionFlagMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatPermissionFlagMaps'],
},
);
const flatPermissionFlag = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: id,
flatEntityMaps: flatPermissionFlagMaps,
});
if (!isDefined(flatPermissionFlag)) {
return null;
}
return fromFlatPermissionFlagToPermissionFlagDto(flatPermissionFlag);
}
async create(
input: CreatePermissionFlagInput,
workspaceId: string,
): Promise<PermissionFlagDTO> {
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
const flatToCreate =
fromCreatePermissionFlagInputToFlatPermissionFlagToCreate({
createPermissionFlagInput: input,
workspaceId,
flatApplication: workspaceCustomFlatApplication,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
permissionFlag: {
flatEntityToCreate: [flatToCreate],
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
},
workspaceId,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
},
);
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Validation errors occurred while creating permission flag',
);
}
const { flatPermissionFlagMaps: recomputedMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatPermissionFlagMaps'],
},
);
return fromFlatPermissionFlagToPermissionFlagDto(
findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: flatToCreate.id,
flatEntityMaps: recomputedMaps,
}),
);
}
async update(
input: UpdatePermissionFlagInput,
workspaceId: string,
): Promise<PermissionFlagDTO> {
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
const { flatPermissionFlagMaps: existingMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatPermissionFlagMaps'],
},
);
const flatToUpdate =
fromUpdatePermissionFlagInputToFlatPermissionFlagToUpdateOrThrow({
flatPermissionFlagMaps: existingMaps,
updatePermissionFlagInput: input,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
permissionFlag: {
flatEntityToCreate: [],
flatEntityToDelete: [],
flatEntityToUpdate: [flatToUpdate],
},
},
workspaceId,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
},
);
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Validation errors occurred while updating permission flag',
);
}
const { flatPermissionFlagMaps: recomputedMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatPermissionFlagMaps'],
},
);
return fromFlatPermissionFlagToPermissionFlagDto(
findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: input.id,
flatEntityMaps: recomputedMaps,
}),
);
}
async delete(id: string, workspaceId: string): Promise<PermissionFlagDTO> {
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
const { flatPermissionFlagMaps: existingMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatPermissionFlagMaps'],
},
);
const flatToDelete =
fromDeletePermissionFlagInputToFlatPermissionFlagOrThrow({
flatPermissionFlagMaps: existingMaps,
permissionFlagId: id,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
permissionFlag: {
flatEntityToCreate: [],
flatEntityToDelete: [flatToDelete],
flatEntityToUpdate: [],
},
},
workspaceId,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
},
);
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Validation errors occurred while deleting permission flag',
);
}
return fromFlatPermissionFlagToPermissionFlagDto(flatToDelete);
}
}
@@ -1,10 +0,0 @@
import { type FlatPermissionFlag } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag.type';
import { type PermissionFlagDTO } from 'src/engine/metadata-modules/permission-flag/dtos/permission-flag.dto';
export const fromFlatPermissionFlagToPermissionFlagDto = (
flatPermissionFlag: FlatPermissionFlag,
): PermissionFlagDTO => ({
id: flatPermissionFlag.id,
roleId: flatPermissionFlag.roleId,
flag: flatPermissionFlag.flag,
});
@@ -1,7 +1,10 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { PermissionFlagType } from 'twenty-shared/constants';
import {
PermissionFlagType,
SystemPermissionFlag,
} from 'twenty-shared/constants';
import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/services/api-key-role.service';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
@@ -60,7 +63,7 @@ describe('PermissionsService', () => {
canBeAssignedToUsers: true,
canBeAssignedToAgents: true,
canBeAssignedToApiKeys: true,
permissionFlags: [],
rolePermissionFlags: [],
workspaceId: 'test-workspace-id',
createdAt: new Date(),
updatedAt: new Date(),
@@ -133,7 +136,7 @@ describe('PermissionsService', () => {
canBeAssignedToUsers: true,
canBeAssignedToAgents: true,
canBeAssignedToApiKeys: true,
permissionFlags: [],
rolePermissionFlags: [],
workspaceId: 'test-workspace-id',
createdAt: new Date(),
updatedAt: new Date(),
@@ -184,7 +187,7 @@ describe('PermissionsService', () => {
canBeAssignedToUsers: true,
canBeAssignedToAgents: true,
canBeAssignedToApiKeys: true,
permissionFlags: [],
rolePermissionFlags: [],
roleTargets: [],
objectPermissions: [],
fieldPermissions: [],
@@ -254,7 +257,7 @@ describe('PermissionsService', () => {
canBeAssignedToUsers: true,
canBeAssignedToAgents: true,
canBeAssignedToApiKeys: true,
permissionFlags: [],
rolePermissionFlags: [],
roleTargets: [],
objectPermissions: [],
fieldPermissions: [],
@@ -292,8 +295,8 @@ describe('PermissionsService', () => {
});
});
describe('Granular permissions with permissionFlags', () => {
it('should grant specific tool permission when included in permissionFlags even if canAccessAllTools is false', () => {
describe('Granular permissions with rolePermissionFlags', () => {
it('should grant specific tool permission when included in rolePermissionFlags even if canAccessAllTools is false', () => {
const roleWithSpecificPermission: Partial<RoleEntity> = {
id: 'test-role-id',
label: 'Test Role',
@@ -308,10 +311,13 @@ describe('PermissionsService', () => {
canBeAssignedToUsers: true,
canBeAssignedToAgents: true,
canBeAssignedToApiKeys: true,
permissionFlags: [
rolePermissionFlags: [
{
id: 'permission-1',
flag: PermissionFlagType.UPLOAD_FILE,
permissionFlag: {
key: PermissionFlagType.UPLOAD_FILE,
universalIdentifier: SystemPermissionFlag.UPLOAD_FILE,
},
roleId: 'test-role-id',
workspaceId: 'test-workspace-id',
createdAt: new Date(),
@@ -338,7 +344,7 @@ describe('PermissionsService', () => {
).toBe(false);
});
it('should grant specific settings permission when included in permissionFlags even if canUpdateAllSettings is false', () => {
it('should grant specific settings permission when included in rolePermissionFlags even if canUpdateAllSettings is false', () => {
const roleWithSpecificPermission: Partial<RoleEntity> = {
id: 'test-role-id',
label: 'Test Role',
@@ -353,10 +359,13 @@ describe('PermissionsService', () => {
canBeAssignedToUsers: true,
canBeAssignedToAgents: true,
canBeAssignedToApiKeys: true,
permissionFlags: [
rolePermissionFlags: [
{
id: 'permission-1',
flag: PermissionFlagType.ROLES,
permissionFlag: {
key: PermissionFlagType.ROLES,
universalIdentifier: SystemPermissionFlag.ROLES,
},
roleId: 'test-role-id',
workspaceId: 'test-workspace-id',
createdAt: new Date(),
@@ -400,7 +409,7 @@ describe('PermissionsService', () => {
canBeAssignedToUsers: true,
canBeAssignedToAgents: true,
canBeAssignedToApiKeys: true,
permissionFlags: [],
rolePermissionFlags: [],
roleTargets: [],
objectPermissions: [],
fieldPermissions: [],
@@ -2,7 +2,10 @@ import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { msg } from '@lingui/core/macro';
import { PermissionFlagType } from 'twenty-shared/constants';
import {
PermissionFlagType,
SystemPermissionFlag,
} from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils';
import { In, Repository } from 'typeorm';
@@ -66,7 +69,7 @@ export class PermissionsService {
const defaultSettingsPermissions =
this.getDefaultUserWorkspacePermissions().permissionFlags;
const permissionFlags = Object.keys(PermissionFlagType).reduce(
const permissionFlags = Object.values(PermissionFlagType).reduce(
(acc, feature) => {
const hasBasePermission = this.isToolPermission(feature)
? roleOfUserWorkspace.canAccessAllTools
@@ -76,9 +79,7 @@ export class PermissionsService {
...acc,
[feature]:
hasBasePermission ||
roleOfUserWorkspace.permissionFlags.some(
(permissionFlag) => permissionFlag.flag === feature,
),
this.roleHasPermissionFlag(roleOfUserWorkspace, feature),
};
},
defaultSettingsPermissions,
@@ -150,7 +151,10 @@ export class PermissionsService {
const role = await this.roleRepository.findOne({
where: { id: roleId, workspaceId },
relations: ['permissionFlags'],
relations: [
'rolePermissionFlags',
'rolePermissionFlags.permissionFlag',
],
});
if (!isDefined(role)) {
@@ -203,7 +207,10 @@ export class PermissionsService {
const role = await this.roleRepository.findOne({
where: { id: applicationRoleId, workspaceId },
relations: ['permissionFlags'],
relations: [
'rolePermissionFlags',
'rolePermissionFlags.permissionFlag',
],
});
if (!isDefined(role)) {
@@ -240,10 +247,22 @@ export class PermissionsService {
return true;
}
const permissionFlags = role.permissionFlags ?? [];
return this.roleHasPermissionFlag(role, setting);
}
return permissionFlags.some(
(permissionFlag) => permissionFlag.flag === setting,
private roleHasPermissionFlag(
role: RoleEntity,
flag: PermissionFlagType,
): boolean {
const rolePermissionFlags = role.rolePermissionFlags ?? [];
const permissionFlagUniversalIdentifier = SystemPermissionFlag[flag];
return rolePermissionFlags.some(
(rolePermissionFlag) =>
(rolePermissionFlag.permissionFlag?.universalIdentifier ??
SystemPermissionFlag[rolePermissionFlag.flag]) ===
permissionFlagUniversalIdentifier,
);
}
@@ -292,7 +311,7 @@ export class PermissionsService {
const result = await this.getRolesFromPermissionConfig(
rolePermissionConfig,
workspaceId,
['permissionFlags'],
['rolePermissionFlags', 'rolePermissionFlags.permissionFlag'],
);
if (result === null) {
@@ -318,7 +337,7 @@ export class PermissionsService {
const result = await this.getRolesFromPermissionConfig(
rolePermissionConfig,
workspaceId,
['permissionFlags'],
['rolePermissionFlags', 'rolePermissionFlags.permissionFlag'],
);
if (result === null) {
@@ -332,11 +351,7 @@ export class PermissionsService {
return true;
}
const permissionFlags = role.permissionFlags ?? [];
return permissionFlags.some(
(permissionFlag) => permissionFlag.flag === flag,
);
return this.roleHasPermissionFlag(role, flag);
};
return useIntersection
@@ -0,0 +1,8 @@
import { type PermissionFlagType } from 'twenty-shared/constants';
export type CreateRolePermissionFlagInput = {
roleId: string;
permissionFlagId: string;
flag: PermissionFlagType;
universalIdentifier?: string;
};
@@ -0,0 +1,17 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { PermissionFlagType } from 'twenty-shared/constants';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@ObjectType('RolePermissionFlag')
export class RolePermissionFlagDTO {
@Field(() => UUIDScalarType, { nullable: false })
id: string;
@Field(() => UUIDScalarType, { nullable: false })
roleId: string;
@Field({ nullable: false })
flag: PermissionFlagType;
}
@@ -0,0 +1,61 @@
import { PermissionFlagType } from 'twenty-shared/constants';
import {
Column,
CreateDateColumn,
Entity,
Index,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
Relation,
Unique,
UpdateDateColumn,
} from 'typeorm';
import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
@Entity('rolePermissionFlag')
@Unique('IDX_ROLE_PERMISSION_FLAG_FLAG_ROLE_ID_UNIQUE', ['flag', 'roleId'])
@Unique('IDX_ROLE_PERMISSION_FLAG_PERMISSION_FLAG_ID_ROLE_ID_UNIQUE', [
'permissionFlagId',
'roleId',
])
@Index('IDX_ROLE_PERMISSION_FLAG_ROLE_ID', ['roleId'])
@Index('IDX_ROLE_PERMISSION_FLAG_PERMISSION_FLAG_ID', ['permissionFlagId'])
export class RolePermissionFlagEntity extends SyncableEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ nullable: false, type: 'uuid' })
roleId: string;
@ManyToOne(() => RoleEntity, (role) => role.rolePermissionFlags, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'roleId' })
role: Relation<RoleEntity>;
@Column({ nullable: false, type: 'varchar' })
flag: PermissionFlagType;
@Column({ nullable: true, type: 'uuid' })
permissionFlagId: string | null;
@ManyToOne(
() => PermissionFlagEntity,
(permissionFlag) => permissionFlag.rolePermissionFlags,
{
onDelete: 'CASCADE',
},
)
@JoinColumn({ name: 'permissionFlagId' })
permissionFlag: Relation<PermissionFlagEntity> | null;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt: Date;
}
@@ -0,0 +1,19 @@
import { Module } from '@nestjs/common';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { RolePermissionFlagService } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.service';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
@Module({
imports: [
WorkspaceCacheModule,
ApplicationModule,
WorkspaceMigrationModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
],
providers: [RolePermissionFlagService],
exports: [RolePermissionFlagService],
})
export class RolePermissionFlagModule {}
@@ -0,0 +1,185 @@
import { Injectable } from '@nestjs/common';
import { msg } from '@lingui/core/macro';
import {
type PermissionFlagType,
SystemPermissionFlag,
} from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils';
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 { type FlatRolePermissionFlag } from 'src/engine/metadata-modules/flat-role-permission-flag/types/flat-role-permission-flag.type';
import { fromCreateRolePermissionFlagInputToFlatRolePermissionFlagToCreate } from 'src/engine/metadata-modules/flat-role-permission-flag/utils/from-create-role-permission-flag-input-to-flat-role-permission-flag-to-create.util';
import { type UpsertPermissionFlagsInput } from 'src/engine/metadata-modules/role-permission-flag/dtos/upsert-permission-flags.input';
import {
PermissionsException,
PermissionsExceptionCode,
PermissionsExceptionMessage,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
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';
@Injectable()
export class RolePermissionFlagService {
constructor(
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly applicationService: ApplicationService,
) {}
public async upsertPermissionFlags({
workspaceId,
input,
}: {
workspaceId: string;
input: UpsertPermissionFlagsInput;
}): Promise<FlatRolePermissionFlag[]> {
const { flatPermissionFlagMaps, flatRolePermissionFlagMaps, flatRoleMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: [
'flatPermissionFlagMaps',
'flatRolePermissionFlagMaps',
'flatRoleMaps',
],
},
);
const roleUniversalId = flatRoleMaps.universalIdentifierById[input.roleId];
const role = isDefined(roleUniversalId)
? flatRoleMaps.byUniversalIdentifier[roleUniversalId]
: undefined;
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.`,
},
);
}
const roleUniversalIdentifier = role.universalIdentifier;
const permissionFlagByKey = new Map(
Object.values(flatPermissionFlagMaps.byUniversalIdentifier)
.filter(isDefined)
.map((permissionFlag) => [permissionFlag.key, permissionFlag]),
);
const permissionFlagsByInputKey = input.permissionFlagKeys.map((flag) => ({
flag,
permissionFlag: permissionFlagByKey.get(flag),
}));
const missingPermissionFlags = permissionFlagsByInputKey
.filter(({ permissionFlag }) => !isDefined(permissionFlag))
.map(({ flag }) => flag);
if (missingPermissionFlags.length > 0) {
throw new PermissionsException(
`${PermissionsExceptionMessage.INVALID_SETTING}: ${missingPermissionFlags.join(', ')}`,
PermissionsExceptionCode.INVALID_SETTING,
{
userFriendlyMessage: msg`Some of the permissions you selected are not valid. Please try again with valid permission settings.`,
},
);
}
const currentRolePermissionFlagsForRole = Object.values(
flatRolePermissionFlagMaps.byUniversalIdentifier,
).filter(
(pf): pf is FlatRolePermissionFlag =>
isDefined(pf) && pf.roleUniversalIdentifier === roleUniversalIdentifier,
);
const inputSet = new Set(
permissionFlagsByInputKey
.map(({ permissionFlag }) => permissionFlag?.universalIdentifier)
.filter(isDefined),
);
const getEffectiveUniversalIdentifier = (
pf: FlatRolePermissionFlag,
): string =>
pf.permissionFlagUniversalIdentifier ?? SystemPermissionFlag[pf.flag];
const existingSet = new Set(
currentRolePermissionFlagsForRole.map(getEffectiveUniversalIdentifier),
);
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
const flatEntityToCreate = permissionFlagsByInputKey
.map(({ permissionFlag }) => permissionFlag)
.filter(isDefined)
.filter(
(permissionFlag) =>
!existingSet.has(permissionFlag.universalIdentifier),
)
.map((permissionFlag) =>
fromCreateRolePermissionFlagInputToFlatRolePermissionFlagToCreate({
createRolePermissionFlagInput: {
roleId: input.roleId,
permissionFlagId: permissionFlag.id,
flag: permissionFlag.key as PermissionFlagType,
},
flatApplication: workspaceCustomFlatApplication,
flatPermissionFlagMaps,
flatRoleMaps,
}),
);
const flatEntityToDelete = currentRolePermissionFlagsForRole.filter(
(pf) => !inputSet.has(getEffectiveUniversalIdentifier(pf)),
);
if (flatEntityToCreate.length === 0 && flatEntityToDelete.length === 0) {
return currentRolePermissionFlagsForRole.filter((pf) =>
inputSet.has(getEffectiveUniversalIdentifier(pf)),
);
}
const buildAndRunResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
rolePermissionFlag: {
flatEntityToCreate,
flatEntityToDelete,
flatEntityToUpdate: [],
},
},
workspaceId,
isSystemBuild: false,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
},
);
if (buildAndRunResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
buildAndRunResult,
'Validation errors occurred while upserting permission flags',
);
}
const { flatRolePermissionFlagMaps: freshFlatRolePermissionFlagMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatRolePermissionFlagMaps'],
},
);
const resultFlags = Object.values(
freshFlatRolePermissionFlagMaps.byUniversalIdentifier,
).filter(
(pf): pf is FlatRolePermissionFlag =>
isDefined(pf) && pf.roleUniversalIdentifier === roleUniversalIdentifier,
);
return resultFlags;
}
}
@@ -0,0 +1,79 @@
import { PermissionFlagType } from 'twenty-shared/constants';
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 FlatRolePermissionFlag } from 'src/engine/metadata-modules/flat-role-permission-flag/types/flat-role-permission-flag.type';
import { fromFlatRolePermissionFlagToRolePermissionFlagDto } from 'src/engine/metadata-modules/role-permission-flag/utils/from-flat-role-permission-flag-to-role-permission-flag-dto.util';
const buildFlatRolePermissionFlag = (
overrides: Partial<FlatRolePermissionFlag> = {},
): FlatRolePermissionFlag =>
({
id: 'role-permission-flag-id',
roleId: 'role-id',
workspaceId: 'workspace-id',
applicationId: 'application-id',
applicationUniversalIdentifier: 'application-universal-id',
permissionFlagUniversalIdentifier: 'permission-flag-universal-id',
flag: PermissionFlagType.WORKSPACE,
...overrides,
}) as FlatRolePermissionFlag;
const buildFlatPermissionFlag = (): FlatPermissionFlag => ({
id: 'permission-flag-id',
workspaceId: 'workspace-id',
applicationId: 'application-id',
universalIdentifier: 'permission-flag-universal-id',
applicationUniversalIdentifier: 'application-universal-id',
key: PermissionFlagType.WORKSPACE,
label: 'Workspace',
description: null,
icon: null,
permissionType: 'settings',
rolePermissionFlagIds: [],
rolePermissionFlagUniversalIdentifiers: [],
createdAt: '2026-05-13T00:00:00.000Z',
updatedAt: '2026-05-13T00:00:00.000Z',
});
const buildFlatPermissionFlagMaps = (): FlatPermissionFlagMaps => ({
byUniversalIdentifier: {
'permission-flag-universal-id': buildFlatPermissionFlag(),
},
universalIdentifierById: {
'permission-flag-id': 'permission-flag-universal-id',
},
universalIdentifiersByApplicationId: {},
});
describe('fromFlatRolePermissionFlagToRolePermissionFlagDto', () => {
it('maps role permission flags to the public permission flag DTO shape', () => {
expect(
fromFlatRolePermissionFlagToRolePermissionFlagDto(
buildFlatRolePermissionFlag(),
buildFlatPermissionFlagMaps(),
),
).toEqual({
id: 'role-permission-flag-id',
roleId: 'role-id',
flag: PermissionFlagType.WORKSPACE,
});
});
it('falls back to the legacy flag column when the catalog is missing the entry', () => {
expect(
fromFlatRolePermissionFlagToRolePermissionFlagDto(
buildFlatRolePermissionFlag(),
{
byUniversalIdentifier: {},
universalIdentifierById: {},
universalIdentifiersByApplicationId: {},
},
),
).toEqual({
id: 'role-permission-flag-id',
roleId: 'role-id',
flag: PermissionFlagType.WORKSPACE,
});
});
});
@@ -0,0 +1,36 @@
import { PermissionFlagType } from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils';
import { type FlatPermissionFlagMaps } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag-maps.type';
import { synthesizeFlatPermissionFlagFromFlag } from 'src/engine/metadata-modules/flat-permission-flag/utils/synthesize-flat-permission-flag-from-flag.util';
import { type FlatRolePermissionFlag } from 'src/engine/metadata-modules/flat-role-permission-flag/types/flat-role-permission-flag.type';
import { type RolePermissionFlagDTO } from 'src/engine/metadata-modules/role-permission-flag/dtos/role-permission-flag.dto';
export const fromFlatRolePermissionFlagToRolePermissionFlagDto = (
flatRolePermissionFlag: FlatRolePermissionFlag,
flatPermissionFlagMaps: FlatPermissionFlagMaps,
): RolePermissionFlagDTO => {
const catalogPermissionFlag = isDefined(
flatRolePermissionFlag.permissionFlagUniversalIdentifier,
)
? flatPermissionFlagMaps.byUniversalIdentifier[
flatRolePermissionFlag.permissionFlagUniversalIdentifier
]
: undefined;
const permissionFlag = isDefined(catalogPermissionFlag)
? catalogPermissionFlag
: synthesizeFlatPermissionFlagFromFlag({
flag: flatRolePermissionFlag.flag,
workspaceId: flatRolePermissionFlag.workspaceId,
applicationId: flatRolePermissionFlag.applicationId,
applicationUniversalIdentifier:
flatRolePermissionFlag.applicationUniversalIdentifier,
});
return {
id: flatRolePermissionFlag.id,
roleId: flatRolePermissionFlag.roleId,
flag: permissionFlag.key as PermissionFlagType,
};
};
@@ -7,7 +7,7 @@ import { WorkspaceMemberDTO } from 'src/engine/core-modules/user/dtos/workspace-
import { AgentDTO } from 'src/engine/metadata-modules/ai/ai-agent/dtos/agent.dto';
import { FieldPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/field-permission.dto';
import { ObjectPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/object-permission.dto';
import { PermissionFlagDTO } from 'src/engine/metadata-modules/permission-flag/dtos/permission-flag.dto';
import { RolePermissionFlagDTO } from 'src/engine/metadata-modules/role-permission-flag/dtos/role-permission-flag.dto';
import { type RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
import { RowLevelPermissionPredicateGroupDTO } from 'src/engine/metadata-modules/row-level-permission-predicate/dtos/row-level-permission-predicate-group.dto';
import { RowLevelPermissionPredicateDTO } from 'src/engine/metadata-modules/row-level-permission-predicate/dtos/row-level-permission-predicate.dto';
@@ -86,8 +86,8 @@ export class RoleDTO {
@Field({ nullable: false })
canDestroyAllObjectRecords: boolean;
@Field(() => [PermissionFlagDTO], { nullable: true })
permissionFlags?: PermissionFlagDTO[];
@Field(() => [RolePermissionFlagDTO], { nullable: true })
permissionFlags?: RolePermissionFlagDTO[];
@Field(() => [ObjectPermissionDTO], { nullable: true })
objectPermissions?: ObjectPermissionDTO[];
@@ -11,7 +11,7 @@ import {
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 { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity';
import { RolePermissionFlagEntity } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.entity';
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
import { RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity';
import { RowLevelPermissionPredicateEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate.entity';
@@ -81,10 +81,10 @@ export class RoleEntity extends SyncableEntity implements Required<RoleEntity> {
objectPermissions: Relation<ObjectPermissionEntity[]>;
@OneToMany(
() => PermissionFlagEntity,
(permissionFlag: PermissionFlagEntity) => permissionFlag.role,
() => RolePermissionFlagEntity,
(rolePermissionFlag: RolePermissionFlagEntity) => rolePermissionFlag.role,
)
permissionFlags: Relation<PermissionFlagEntity[]>;
rolePermissionFlags: Relation<RolePermissionFlagEntity[]>;
@OneToMany(
() => FieldPermissionEntity,
@@ -15,8 +15,8 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat
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 { ObjectPermissionModule } from 'src/engine/metadata-modules/object-permission/object-permission.module';
import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity';
import { PermissionFlagModule } from 'src/engine/metadata-modules/permission-flag/permission-flag.module';
import { RolePermissionFlagEntity } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.entity';
import { RolePermissionFlagModule } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
@@ -39,7 +39,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
RoleEntity,
RoleTargetEntity,
ObjectPermissionEntity,
PermissionFlagEntity,
RolePermissionFlagEntity,
FieldPermissionEntity,
UserWorkspaceEntity,
ObjectMetadataEntity,
@@ -52,7 +52,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
ApiKeyModule,
PermissionsModule,
ObjectPermissionModule,
PermissionFlagModule,
RolePermissionFlagModule,
RowLevelPermissionModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
WorkspaceMigrationModule,
@@ -33,16 +33,17 @@ import { AgentDTO } from 'src/engine/metadata-modules/ai/ai-agent/dtos/agent.dto
import { fromFlatAgentWithRoleIdToAgentDto } from 'src/engine/metadata-modules/flat-agent/utils/from-agent-entity-to-agent-dto.util';
import { FieldPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/field-permission.dto';
import { ObjectPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/object-permission.dto';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { UpsertFieldPermissionsInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-field-permissions.input';
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 { 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 { RolePermissionFlagDTO } from 'src/engine/metadata-modules/role-permission-flag/dtos/role-permission-flag.dto';
import { UpsertPermissionFlagsInput } from 'src/engine/metadata-modules/role-permission-flag/dtos/upsert-permission-flags.input';
import { RolePermissionFlagService } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.service';
import { fromFlatRolePermissionFlagToRolePermissionFlagDto } from 'src/engine/metadata-modules/role-permission-flag/utils/from-flat-role-permission-flag-to-role-permission-flag-dto.util';
import {
PermissionsException,
PermissionsExceptionCode,
@@ -87,7 +88,7 @@ export class RoleResolver {
private readonly roleService: RoleService,
private readonly userWorkspaceService: UserWorkspaceService,
private readonly objectPermissionService: ObjectPermissionService,
private readonly settingPermissionService: PermissionFlagService,
private readonly rolePermissionFlagService: RolePermissionFlagService,
private readonly agentRoleService: AiAgentRoleService,
private readonly apiKeyRoleService: ApiKeyRoleService,
private readonly fieldPermissionService: FieldPermissionService,
@@ -95,6 +96,7 @@ export class RoleResolver {
private readonly rowLevelPermissionPredicateService: RowLevelPermissionPredicateService,
private readonly rowLevelPermissionPredicateGroupService: RowLevelPermissionPredicateGroupService,
private readonly workspaceCacheService: WorkspaceCacheService,
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
) {}
@Query(() => [RoleDTO])
@@ -223,18 +225,31 @@ export class RoleResolver {
);
}
@Mutation(() => [PermissionFlagDTO])
@Mutation(() => [RolePermissionFlagDTO])
async upsertPermissionFlags(
@AuthWorkspace() workspace: WorkspaceEntity,
@Args('upsertPermissionFlagsInput')
upsertPermissionFlagsInput: UpsertPermissionFlagsInput,
): Promise<PermissionFlagDTO[]> {
const flatPermissionFlags =
await this.settingPermissionService.upsertPermissionFlags({
): Promise<RolePermissionFlagDTO[]> {
const flatRolePermissionFlags =
await this.rolePermissionFlagService.upsertPermissionFlags({
workspaceId: workspace.id,
input: upsertPermissionFlagsInput,
});
return flatPermissionFlags.map(fromFlatPermissionFlagToPermissionFlagDto);
const { flatPermissionFlagMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId: workspace.id,
flatMapsKeys: ['flatPermissionFlagMaps'],
},
);
return flatRolePermissionFlags.map((flatRolePermissionFlag) =>
fromFlatRolePermissionFlagToRolePermissionFlagDto(
flatRolePermissionFlag,
flatPermissionFlagMaps,
),
);
}
@Mutation(() => [FieldPermissionDTO])
@@ -49,7 +49,9 @@ export class RoleService {
},
relations: {
roleTargets: true,
permissionFlags: true,
rolePermissionFlags: {
permissionFlag: true,
},
objectPermissions: true,
fieldPermissions: true,
},
@@ -67,7 +69,9 @@ export class RoleService {
},
relations: {
roleTargets: true,
permissionFlags: true,
rolePermissionFlags: {
permissionFlag: true,
},
objectPermissions: true,
fieldPermissions: true,
},
@@ -1,14 +1,17 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { PermissionFlagType } from 'twenty-shared/constants';
import {
PermissionFlagType,
SystemPermissionFlag,
} from 'twenty-shared/constants';
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import { type Repository } from 'typeorm';
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 { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity';
import { RolePermissionFlagEntity } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { WorkspaceRolesPermissionsCacheService } from 'src/engine/metadata-modules/role/services/workspace-roles-permissions-cache.service';
import { RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity';
@@ -23,7 +26,7 @@ const PERSON_OBJECT_METADATA_ID = '44444444-4444-4444-8444-444444444444';
const createBaseRole = (
overrides: Partial<RoleEntity> &
Pick<RoleEntity, 'permissionFlags' | 'objectPermissions'>,
Pick<RoleEntity, 'rolePermissionFlags' | 'objectPermissions'>,
): RoleEntity =>
({
id: ROLE_ID,
@@ -56,8 +59,8 @@ describe('WorkspaceRolesPermissionsCacheService', () => {
let objectPermissionRepository: jest.Mocked<
Pick<Repository<ObjectPermissionEntity>, 'find'>
>;
let permissionFlagRepository: jest.Mocked<
Pick<Repository<PermissionFlagEntity>, 'find'>
let rolePermissionFlagRepository: jest.Mocked<
Pick<Repository<RolePermissionFlagEntity>, 'find'>
>;
const workspaceObjectMetadataFixture: ObjectMetadataEntity[] = [
@@ -94,7 +97,7 @@ describe('WorkspaceRolesPermissionsCacheService', () => {
find: jest.fn().mockResolvedValue([]),
};
permissionFlagRepository = {
rolePermissionFlagRepository = {
find: jest.fn().mockResolvedValue([]),
};
const fieldPermissionRepository = {
@@ -123,8 +126,8 @@ describe('WorkspaceRolesPermissionsCacheService', () => {
useValue: objectPermissionRepository,
},
{
provide: getRepositoryToken(PermissionFlagEntity),
useValue: permissionFlagRepository,
provide: getRepositoryToken(RolePermissionFlagEntity),
useValue: rolePermissionFlagRepository,
},
{
provide: getRepositoryToken(FieldPermissionEntity),
@@ -148,7 +151,7 @@ describe('WorkspaceRolesPermissionsCacheService', () => {
it('should deny all record permissions when role has neither workspace members access nor update-all-settings', async () => {
roleRepository.find.mockResolvedValue([
createBaseRole({
permissionFlags: [],
rolePermissionFlags: [],
objectPermissions: [],
}),
]);
@@ -164,16 +167,19 @@ describe('WorkspaceRolesPermissionsCacheService', () => {
});
it('should grant all record permissions when role has WORKSPACE_MEMBERS permission flag', async () => {
permissionFlagRepository.find.mockResolvedValue([
rolePermissionFlagRepository.find.mockResolvedValue([
{
roleId: ROLE_ID,
flag: PermissionFlagType.WORKSPACE_MEMBERS,
} as PermissionFlagEntity,
permissionFlag: {
key: PermissionFlagType.WORKSPACE_MEMBERS,
universalIdentifier: SystemPermissionFlag.WORKSPACE_MEMBERS,
},
} as RolePermissionFlagEntity,
]);
roleRepository.find.mockResolvedValue([
createBaseRole({
permissionFlags: [],
rolePermissionFlags: [],
objectPermissions: [],
}),
]);
@@ -192,7 +198,7 @@ describe('WorkspaceRolesPermissionsCacheService', () => {
roleRepository.find.mockResolvedValue([
createBaseRole({
canUpdateAllSettings: true,
permissionFlags: [],
rolePermissionFlags: [],
objectPermissions: [],
}),
]);
@@ -210,7 +216,7 @@ describe('WorkspaceRolesPermissionsCacheService', () => {
it('should deny all record permissions when role has neither workflows access nor update-all-settings', async () => {
roleRepository.find.mockResolvedValue([
createBaseRole({
permissionFlags: [],
rolePermissionFlags: [],
objectPermissions: [],
}),
]);
@@ -225,16 +231,19 @@ describe('WorkspaceRolesPermissionsCacheService', () => {
});
it('should grant all record permissions when role has WORKFLOWS permission flag', async () => {
permissionFlagRepository.find.mockResolvedValue([
rolePermissionFlagRepository.find.mockResolvedValue([
{
roleId: ROLE_ID,
flag: PermissionFlagType.WORKFLOWS,
} as PermissionFlagEntity,
permissionFlag: {
key: PermissionFlagType.WORKFLOWS,
universalIdentifier: SystemPermissionFlag.WORKFLOWS,
},
} as RolePermissionFlagEntity,
]);
roleRepository.find.mockResolvedValue([
createBaseRole({
permissionFlags: [],
rolePermissionFlags: [],
objectPermissions: [],
}),
]);
@@ -264,7 +273,7 @@ describe('WorkspaceRolesPermissionsCacheService', () => {
roleRepository.find.mockResolvedValue([
createBaseRole({
permissionFlags: [],
rolePermissionFlags: [],
objectPermissions: [],
}),
]);
@@ -285,7 +294,7 @@ describe('WorkspaceRolesPermissionsCacheService', () => {
canUpdateAllObjectRecords: true,
canSoftDeleteAllObjectRecords: true,
canDestroyAllObjectRecords: true,
permissionFlags: [],
rolePermissionFlags: [],
objectPermissions: [],
}),
]);
@@ -12,7 +12,7 @@ import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-
import { fromRoleEntityToFlatRole } from 'src/engine/metadata-modules/flat-role/utils/from-role-entity-to-flat-role.util';
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 { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity';
import { RolePermissionFlagEntity } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.entity';
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity';
@@ -36,8 +36,8 @@ export class WorkspaceFlatRoleMapCacheService extends WorkspaceCacheProvider<
private readonly roleTargetRepository: Repository<RoleTargetEntity>,
@InjectRepository(ObjectPermissionEntity)
private readonly objectPermissionRepository: Repository<ObjectPermissionEntity>,
@InjectRepository(PermissionFlagEntity)
private readonly permissionFlagRepository: Repository<PermissionFlagEntity>,
@InjectRepository(RolePermissionFlagEntity)
private readonly rolePermissionFlagRepository: Repository<RolePermissionFlagEntity>,
@InjectRepository(FieldPermissionEntity)
private readonly fieldPermissionRepository: Repository<FieldPermissionEntity>,
@InjectRepository(RowLevelPermissionPredicateEntity)
@@ -56,7 +56,7 @@ export class WorkspaceFlatRoleMapCacheService extends WorkspaceCacheProvider<
applications,
roleTargets,
objectPermissions,
permissionFlags,
rolePermissionFlags,
fieldPermissions,
rowLevelPermissionPredicates,
rowLevelPermissionPredicateGroups,
@@ -80,7 +80,7 @@ export class WorkspaceFlatRoleMapCacheService extends WorkspaceCacheProvider<
select: ['id', 'universalIdentifier', 'roleId'],
withDeleted: true,
}),
this.permissionFlagRepository.find({
this.rolePermissionFlagRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier', 'roleId'],
withDeleted: true,
@@ -105,7 +105,7 @@ export class WorkspaceFlatRoleMapCacheService extends WorkspaceCacheProvider<
const [
roleTargetsByRoleId,
objectPermissionsByRoleId,
permissionFlagsByRoleId,
rolePermissionFlagsByRoleId,
fieldPermissionsByRoleId,
rowLevelPermissionPredicatesByRoleId,
rowLevelPermissionPredicateGroupsByRoleId,
@@ -120,7 +120,7 @@ export class WorkspaceFlatRoleMapCacheService extends WorkspaceCacheProvider<
foreignKey: 'roleId',
},
{
entities: permissionFlags,
entities: rolePermissionFlags,
foreignKey: 'roleId',
},
{
@@ -149,7 +149,8 @@ export class WorkspaceFlatRoleMapCacheService extends WorkspaceCacheProvider<
...roleEntity,
roleTargets: roleTargetsByRoleId.get(roleEntity.id) || [],
objectPermissions: objectPermissionsByRoleId.get(roleEntity.id) || [],
permissionFlags: permissionFlagsByRoleId.get(roleEntity.id) || [],
rolePermissionFlags:
rolePermissionFlagsByRoleId.get(roleEntity.id) || [],
fieldPermissions: fieldPermissionsByRoleId.get(roleEntity.id) || [],
rowLevelPermissionPredicates:
rowLevelPermissionPredicatesByRoleId.get(roleEntity.id) || [],
@@ -1,7 +1,10 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { PermissionFlagType } from 'twenty-shared/constants';
import {
PermissionFlagType,
SystemPermissionFlag,
} from 'twenty-shared/constants';
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
import {
type ObjectsPermissions,
@@ -16,7 +19,7 @@ import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/wo
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 { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity';
import { RolePermissionFlagEntity } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity';
import { RowLevelPermissionPredicateEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate.entity';
@@ -41,8 +44,8 @@ export class WorkspaceRolesPermissionsCacheService extends WorkspaceCacheProvide
private readonly roleRepository: Repository<RoleEntity>,
@InjectRepository(ObjectPermissionEntity)
private readonly objectPermissionRepository: Repository<ObjectPermissionEntity>,
@InjectRepository(PermissionFlagEntity)
private readonly permissionFlagRepository: Repository<PermissionFlagEntity>,
@InjectRepository(RolePermissionFlagEntity)
private readonly rolePermissionFlagRepository: Repository<RolePermissionFlagEntity>,
@InjectRepository(FieldPermissionEntity)
private readonly fieldPermissionRepository: Repository<FieldPermissionEntity>,
@InjectRepository(RowLevelPermissionPredicateEntity)
@@ -59,7 +62,7 @@ export class WorkspaceRolesPermissionsCacheService extends WorkspaceCacheProvide
const [
roles,
objectPermissions,
permissionFlags,
rolePermissionFlags,
fieldPermissions,
rowLevelPermissionPredicates,
rowLevelPermissionPredicateGroups,
@@ -71,8 +74,9 @@ export class WorkspaceRolesPermissionsCacheService extends WorkspaceCacheProvide
this.objectPermissionRepository.find({
where: { workspaceId },
}),
this.permissionFlagRepository.find({
this.rolePermissionFlagRepository.find({
where: { workspaceId },
relations: ['permissionFlag'],
}),
this.fieldPermissionRepository.find({
where: { workspaceId },
@@ -91,9 +95,9 @@ export class WorkspaceRolesPermissionsCacheService extends WorkspaceCacheProvide
entities: objectPermissions,
foreignKey: 'roleId',
});
const permissionFlagsByRoleId =
regroupEntitiesByRelatedEntityId<'permissionFlag'>({
entities: permissionFlags,
const rolePermissionFlagsByRoleId =
regroupEntitiesByRelatedEntityId<'rolePermissionFlag'>({
entities: rolePermissionFlags,
foreignKey: 'roleId',
});
const fieldPermissionsByRoleId =
@@ -117,7 +121,8 @@ export class WorkspaceRolesPermissionsCacheService extends WorkspaceCacheProvide
for (const role of roles) {
const roleObjectPermissions =
objectPermissionsByRoleId.get(role.id) ?? [];
const rolePermissionFlags = permissionFlagsByRoleId.get(role.id) ?? [];
const roleRolePermissionFlags =
rolePermissionFlagsByRoleId.get(role.id) ?? [];
const roleFieldPermissions = fieldPermissionsByRoleId.get(role.id) ?? [];
const roleRowLevelPermissionPredicates =
rowLevelPermissionPredicatesByRoleId.get(role.id) ?? [];
@@ -150,7 +155,7 @@ export class WorkspaceRolesPermissionsCacheService extends WorkspaceCacheProvide
const hasWorkflowsPermissions =
this.hasSettingsGatedObjectPermissions(
role,
rolePermissionFlags,
roleRolePermissionFlags,
PermissionFlagType.WORKFLOWS,
);
@@ -163,7 +168,7 @@ export class WorkspaceRolesPermissionsCacheService extends WorkspaceCacheProvide
const hasWorkspaceMembersPermissions =
this.hasSettingsGatedObjectPermissions(
role,
rolePermissionFlags,
roleRolePermissionFlags,
PermissionFlagType.WORKSPACE_MEMBERS,
);
@@ -269,13 +274,18 @@ export class WorkspaceRolesPermissionsCacheService extends WorkspaceCacheProvide
private hasSettingsGatedObjectPermissions(
role: RoleEntity,
permissionFlags: PermissionFlagEntity[],
rolePermissionFlags: RolePermissionFlagEntity[],
permissionFlagType: PermissionFlagType,
): boolean {
const hasPermissionFromRole = role.canUpdateAllSettings;
const permissionFlagUniversalIdentifier =
SystemPermissionFlag[permissionFlagType];
const hasPermissionFromSettingPermissions = isDefined(
permissionFlags.find(
(permissionFlag) => permissionFlag.flag === permissionFlagType,
rolePermissionFlags.find(
(rolePermissionFlag) =>
(rolePermissionFlag.permissionFlag?.universalIdentifier ??
SystemPermissionFlag[rolePermissionFlag.flag]) ===
permissionFlagUniversalIdentifier,
),
);
@@ -0,0 +1,65 @@
import { PermissionFlagType } from 'twenty-shared/constants';
import { type RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { fromRoleEntityToRoleDto } from 'src/engine/metadata-modules/role/utils/fromRoleEntityToRoleDto.util';
const buildRoleEntity = (overrides: Partial<RoleEntity> = {}): RoleEntity =>
({
id: 'role-id',
label: 'Role',
universalIdentifier: 'role-universal-identifier',
canUpdateAllSettings: false,
canAccessAllTools: false,
description: null,
icon: null,
isEditable: true,
canReadAllObjectRecords: false,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
canBeAssignedToUsers: true,
canBeAssignedToAgents: true,
canBeAssignedToApiKeys: true,
roleTargets: [],
objectPermissions: [],
fieldPermissions: [],
rolePermissionFlags: [],
...overrides,
}) as RoleEntity;
describe('fromRoleEntityToRoleDto', () => {
it('returns undefined permission flags when role permission flags are not loaded', () => {
const roleDto = fromRoleEntityToRoleDto(
buildRoleEntity({
rolePermissionFlags:
undefined as unknown as RoleEntity['rolePermissionFlags'],
}),
);
expect(roleDto.permissionFlags).toBeUndefined();
});
it('maps loaded role permission flags to the public permission flag DTO shape', () => {
const roleDto = fromRoleEntityToRoleDto(
buildRoleEntity({
rolePermissionFlags: [
{
id: 'role-permission-flag-id',
roleId: 'role-id',
permissionFlag: {
key: PermissionFlagType.WORKSPACE,
},
},
] as RoleEntity['rolePermissionFlags'],
}),
);
expect(roleDto.permissionFlags).toEqual([
{
id: 'role-permission-flag-id',
roleId: 'role-id',
flag: PermissionFlagType.WORKSPACE,
},
]);
});
});
@@ -1,3 +1,5 @@
import { PermissionFlagType } from 'twenty-shared/constants';
import { type RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto';
import { type RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
@@ -19,7 +21,12 @@ export const fromRoleEntityToRoleDto = (role: RoleEntity): RoleDTO => {
canBeAssignedToAgents: role.canBeAssignedToAgents,
canBeAssignedToApiKeys: role.canBeAssignedToApiKeys,
roleTargets: role.roleTargets,
permissionFlags: role.permissionFlags,
permissionFlags: role.rolePermissionFlags?.map((rolePermissionFlag) => ({
id: rolePermissionFlag.id,
roleId: rolePermissionFlag.roleId,
flag: (rolePermissionFlag.permissionFlag?.key ??
rolePermissionFlag.flag) as PermissionFlagType,
})),
objectPermissions: role.objectPermissions,
fieldPermissions: role.fieldPermissions,
};
@@ -108,7 +108,9 @@ export class UserRoleService {
},
relations: {
role: {
permissionFlags: true,
rolePermissionFlags: {
permissionFlag: true,
},
},
},
});
@@ -35,7 +35,8 @@ export const WORKSPACE_CACHE_KEYS_V2 = {
flatApplicationMaps: 'flat-maps:flatApplicationMaps',
flatRoleMaps: 'flat-maps:role',
flatRoleTargetMaps: 'flat-maps:role-target',
flatPermissionFlagMaps: 'flat-maps:permission-flag',
flatRolePermissionFlagMaps: 'flat-maps:role-permission-flag',
flatPermissionFlagMaps: 'flat-maps:permission-flag-catalog',
flatObjectPermissionMaps: 'flat-maps:object-permission',
flatFieldPermissionMaps: 'flat-maps:field-permission',
ORMEntityMetadatas: 'orm:entity-metadatas',
@@ -10,6 +10,7 @@ export const TWENTY_STANDARD_ALL_METADATA_NAME = [
'viewGroup',
'view',
'navigationMenuItem',
'permissionFlag',
'role',
'agent',
'skill',
@@ -0,0 +1,41 @@
import { v4 } from 'uuid';
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util';
import { type FlatPermissionFlag } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag.type';
import { STANDARD_PERMISSION_FLAG_DEFINITIONS } from 'src/engine/metadata-modules/permission-flag/constants/standard-permission-flag-definitions.constant';
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
export const buildStandardFlatPermissionFlagMetadataMaps = ({
now,
workspaceId,
twentyStandardApplicationId,
}: {
now: string;
workspaceId: string;
twentyStandardApplicationId: string;
}): FlatEntityMaps<FlatPermissionFlag> => {
let flatPermissionFlagMaps = createEmptyFlatEntityMaps();
for (const permissionFlagDefinition of STANDARD_PERMISSION_FLAG_DEFINITIONS) {
flatPermissionFlagMaps = addFlatEntityToFlatEntityMapsOrThrow({
flatEntity: {
id: v4(),
...permissionFlagDefinition,
workspaceId,
applicationId: twentyStandardApplicationId,
universalIdentifier: permissionFlagDefinition.universalIdentifier,
applicationUniversalIdentifier:
TWENTY_STANDARD_APPLICATION.universalIdentifier,
rolePermissionFlagIds: [],
rolePermissionFlagUniversalIdentifiers: [],
createdAt: now,
updatedAt: now,
},
flatEntityMaps: flatPermissionFlagMaps,
});
}
return flatPermissionFlagMaps;
};
@@ -72,8 +72,8 @@ export const createStandardRoleFlatMetadata = ({
TWENTY_STANDARD_APPLICATION.universalIdentifier,
createdAt: now,
updatedAt: now,
permissionFlagIds: [],
permissionFlagUniversalIdentifiers: [],
rolePermissionFlagIds: [],
rolePermissionFlagUniversalIdentifiers: [],
objectPermissionUniversalIdentifiers: [],
fieldPermissionIds: [],
fieldPermissionUniversalIdentifiers: [],
@@ -15,6 +15,7 @@ import { buildStandardFlatObjectMetadataMaps } from 'src/engine/workspace-manage
import { buildStandardFlatPageLayoutTabMetadataMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/page-layout-tab/build-standard-flat-page-layout-tab-metadata-maps.util';
import { buildStandardFlatPageLayoutWidgetMetadataMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/page-layout-widget/build-standard-flat-page-layout-widget-metadata-maps.util';
import { buildStandardFlatPageLayoutMetadataMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/page-layout/build-standard-flat-page-layout-metadata-maps.util';
import { buildStandardFlatPermissionFlagMetadataMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/permission-flag/build-standard-flat-permission-flag-metadata-maps.util';
import { buildStandardFlatRoleMetadataMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/role-metadata/build-standard-flat-role-metadata-maps.util';
import { buildStandardFlatSkillMetadataMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/skill-metadata/build-standard-flat-skill-metadata-maps.util';
import { buildStandardFlatViewFieldMetadataMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/view-field/build-standard-flat-view-field-metadata-maps.util';
@@ -140,6 +141,12 @@ export const computeTwentyStandardApplicationAllFlatEntityMaps = ({
dependencyFlatEntityMaps: undefined,
});
const flatPermissionFlagMaps = buildStandardFlatPermissionFlagMetadataMaps({
now,
workspaceId,
twentyStandardApplicationId,
});
const flatAgentMaps = buildStandardFlatAgentMetadataMaps({
now,
workspaceId,
@@ -213,6 +220,7 @@ export const computeTwentyStandardApplicationAllFlatEntityMaps = ({
flatFieldMetadataMaps,
flatObjectMetadataMaps,
flatNavigationMenuItemMaps,
flatPermissionFlagMaps,
flatRoleMaps,
flatAgentMaps,
flatSkillMaps,
@@ -167,6 +167,7 @@ export const enrichCreateWorkspaceMigrationActionsWithIds = ({
case 'navigationMenuItem':
case 'frontComponent':
case 'viewSort':
case 'rolePermissionFlag':
case 'permissionFlag':
case 'objectPermission':
case 'fieldPermission':
@@ -29,6 +29,7 @@ import { WorkspaceMigrationPageLayoutTabActionsBuilderService } from 'src/engine
import { WorkspaceMigrationPageLayoutWidgetActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/page-layout-widget/workspace-migration-page-layout-widget-actions-builder.service';
import { WorkspaceMigrationPageLayoutActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/page-layout/workspace-migration-page-layout-actions-builder.service';
import { WorkspaceMigrationPermissionFlagActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/permission-flag/workspace-migration-permission-flag-actions-builder.service';
import { WorkspaceMigrationRolePermissionFlagActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/role-permission-flag/workspace-migration-role-permission-flag-actions-builder.service';
import { WorkspaceMigrationRoleTargetActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/role-target/workspace-migration-role-target-actions-builder.service';
import { WorkspaceMigrationRoleActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/role/workspace-migration-role-actions-builder.service';
import { WorkspaceMigrationRowLevelPermissionPredicateGroupActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/row-level-permission-predicate-group/workspace-migration-row-level-permission-predicate-group-actions-builder.service';
@@ -58,6 +59,7 @@ export class WorkspaceMigrationBuildOrchestratorService {
private readonly workspaceMigrationViewSortActionsBuilderService: WorkspaceMigrationViewSortActionsBuilderService,
private readonly workspaceMigrationFieldPermissionActionsBuilderService: WorkspaceMigrationFieldPermissionActionsBuilderService,
private readonly workspaceMigrationObjectPermissionActionsBuilderService: WorkspaceMigrationObjectPermissionActionsBuilderService,
private readonly workspaceMigrationRolePermissionFlagActionsBuilderService: WorkspaceMigrationRolePermissionFlagActionsBuilderService,
private readonly workspaceMigrationPermissionFlagActionsBuilderService: WorkspaceMigrationPermissionFlagActionsBuilderService,
private readonly workspaceMigrationLogicFunctionActionsBuilderService: WorkspaceMigrationLogicFunctionActionsBuilderService,
private readonly workspaceMigrationRoleTargetActionsBuilderService: WorkspaceMigrationRoleTargetActionsBuilderService,
@@ -158,6 +160,7 @@ export class WorkspaceMigrationBuildOrchestratorService {
flatRoleMaps,
flatObjectPermissionMaps,
flatFieldPermissionMaps,
flatRolePermissionFlagMaps,
flatPermissionFlagMaps,
flatRoleTargetMaps,
flatAgentMaps,
@@ -558,6 +561,34 @@ export class WorkspaceMigrationBuildOrchestratorService {
}
}
if (isDefined(flatRolePermissionFlagMaps)) {
const {
from: fromFlatRolePermissionFlagMaps,
to: toFlatRolePermissionFlagMaps,
} = flatRolePermissionFlagMaps;
const rolePermissionFlagResult =
await this.workspaceMigrationRolePermissionFlagActionsBuilderService.validateAndBuild(
{
additionalCacheDataMaps,
from: fromFlatRolePermissionFlagMaps,
to: toFlatRolePermissionFlagMaps,
buildOptions,
dependencyOptimisticFlatEntityMaps: optimisticAllFlatEntityMaps,
workspaceId,
},
);
if (rolePermissionFlagResult.status === 'fail') {
orchestratorFailureReport.rolePermissionFlag.push(
...rolePermissionFlagResult.errors,
);
} else {
orchestratorActionsReport.rolePermissionFlag =
rolePermissionFlagResult.actions;
}
}
if (isDefined(flatPermissionFlagMaps)) {
const { from: fromFlatPermissionFlagMaps, to: toFlatPermissionFlagMaps } =
flatPermissionFlagMaps;
@@ -986,6 +1017,12 @@ export class WorkspaceMigrationBuildOrchestratorService {
///
// Permission flags
...aggregatedOrchestratorActionsReport.rolePermissionFlag.delete,
...aggregatedOrchestratorActionsReport.rolePermissionFlag.create,
...aggregatedOrchestratorActionsReport.rolePermissionFlag.update,
///
// Permission flag definitions
...aggregatedOrchestratorActionsReport.permissionFlag.delete,
...aggregatedOrchestratorActionsReport.permissionFlag.create,
...aggregatedOrchestratorActionsReport.permissionFlag.update,
@@ -35,15 +35,18 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_FOREIGN_KEY_AGGREGATOR_PROPERTIES should matc
"widgetUniversalIdentifiers",
],
"pageLayoutWidget": [],
"permissionFlag": [],
"permissionFlag": [
"rolePermissionFlagUniversalIdentifiers",
],
"role": [
"roleTargetUniversalIdentifiers",
"objectPermissionUniversalIdentifiers",
"permissionFlagUniversalIdentifiers",
"rolePermissionFlagUniversalIdentifiers",
"fieldPermissionUniversalIdentifiers",
"rowLevelPermissionPredicateUniversalIdentifiers",
"rowLevelPermissionPredicateGroupUniversalIdentifiers",
],
"rolePermissionFlag": [],
"roleTarget": [],
"rowLevelPermissionPredicate": [],
"rowLevelPermissionPredicateGroup": [
@@ -38,6 +38,7 @@ export const ALL_JSONB_PROPERTIES_WITH_SERIALIZED_RELATION_BY_METADATA_NAME = {
},
commandMenuItem: {},
navigationMenuItem: {},
rolePermissionFlag: {},
permissionFlag: {},
objectPermission: {},
fieldPermission: {},
@@ -0,0 +1,7 @@
import { type RolePermissionFlagEntity } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.entity';
import { type UniversalFlatEntityFrom } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-from.type';
export type UniversalFlatRolePermissionFlag = UniversalFlatEntityFrom<
RolePermissionFlagEntity,
'rolePermissionFlag'
>;
@@ -2,12 +2,12 @@ import { Injectable } from '@nestjs/common';
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
import { type UniversalUpdatePermissionFlagAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/permission-flag/types/workspace-migration-permission-flag-action.type';
import { type FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-update-validation-args.type';
import { type UniversalFlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-validation-args.type';
import { type UniversalFlatEntityValidationReturnType } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-validation-result.type';
import { WorkspaceEntityMigrationBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/services/workspace-entity-migration-builder.service';
import { UniversalUpdatePermissionFlagAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/permission-flag/types/workspace-migration-permission-flag-action.type';
import { FlatPermissionFlagValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-permission-flag-validator.service';
import { UniversalFlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-validation-args.type';
import { UniversalFlatEntityValidationReturnType } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-validation-result.type';
import { FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-update-validation-args.type';
@Injectable()
export class WorkspaceMigrationPermissionFlagActionsBuilderService extends WorkspaceEntityMigrationBuilderService<
@@ -105,7 +105,7 @@ export class WorkspaceMigrationPermissionFlagActionsBuilderService extends Works
const { universalIdentifier, flatEntityUpdate } = args;
const updatePermissionFlagAction: UniversalUpdatePermissionFlagAction = {
const updateAction: UniversalUpdatePermissionFlagAction = {
type: 'update',
metadataName: 'permissionFlag',
universalIdentifier,
@@ -114,7 +114,7 @@ export class WorkspaceMigrationPermissionFlagActionsBuilderService extends Works
return {
status: 'success',
action: updatePermissionFlagAction,
action: updateAction,
};
}
}
@@ -0,0 +1,24 @@
import { type BaseFlatCreateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-flat-create-workspace-migration-action.type';
import { type BaseFlatDeleteWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-flat-delete-workspace-migration-action.type';
import { type BaseFlatUpdateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-flat-update-workspace-migration-action.type';
import { type BaseUniversalCreateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-universal-create-workspace-migration-action.type';
import { type BaseUniversalDeleteWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-universal-delete-workspace-migration-action.type';
import { type BaseUniversalUpdateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-universal-update-workspace-migration-action.type';
export type FlatCreateRolePermissionFlagAction =
BaseFlatCreateWorkspaceMigrationAction<'rolePermissionFlag'>;
export type FlatUpdateRolePermissionFlagAction =
BaseFlatUpdateWorkspaceMigrationAction<'rolePermissionFlag'>;
export type FlatDeleteRolePermissionFlagAction =
BaseFlatDeleteWorkspaceMigrationAction<'rolePermissionFlag'>;
export type UniversalCreateRolePermissionFlagAction =
BaseUniversalCreateWorkspaceMigrationAction<'rolePermissionFlag'>;
export type UniversalUpdateRolePermissionFlagAction =
BaseUniversalUpdateWorkspaceMigrationAction<'rolePermissionFlag'>;
export type UniversalDeleteRolePermissionFlagAction =
BaseUniversalDeleteWorkspaceMigrationAction<'rolePermissionFlag'>;
@@ -0,0 +1,122 @@
import { Injectable } from '@nestjs/common';
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
import { WorkspaceEntityMigrationBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/services/workspace-entity-migration-builder.service';
import { UniversalUpdateRolePermissionFlagAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/role-permission-flag/types/workspace-migration-role-permission-flag-action.type';
import { FlatRolePermissionFlagValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-role-permission-flag-validator.service';
import { UniversalFlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-validation-args.type';
import { UniversalFlatEntityValidationReturnType } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-validation-result.type';
import { FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-update-validation-args.type';
@Injectable()
export class WorkspaceMigrationRolePermissionFlagActionsBuilderService extends WorkspaceEntityMigrationBuilderService<
typeof ALL_METADATA_NAME.rolePermissionFlag
> {
constructor(
private readonly flatRolePermissionFlagValidatorService: FlatRolePermissionFlagValidatorService,
) {
super(ALL_METADATA_NAME.rolePermissionFlag);
}
protected validateFlatEntityCreation(
args: UniversalFlatEntityValidationArgs<
typeof ALL_METADATA_NAME.rolePermissionFlag
>,
): UniversalFlatEntityValidationReturnType<
typeof ALL_METADATA_NAME.rolePermissionFlag,
'create'
> {
const validationResult =
this.flatRolePermissionFlagValidatorService.validateFlatRolePermissionFlagCreation(
args,
);
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
const { flatEntityToValidate: flatRolePermissionFlagToValidate } = args;
return {
status: 'success',
action: {
type: 'create',
metadataName: 'rolePermissionFlag',
flatEntity: flatRolePermissionFlagToValidate,
},
};
}
protected validateFlatEntityDeletion(
args: UniversalFlatEntityValidationArgs<
typeof ALL_METADATA_NAME.rolePermissionFlag
>,
): UniversalFlatEntityValidationReturnType<
typeof ALL_METADATA_NAME.rolePermissionFlag,
'delete'
> {
const validationResult =
this.flatRolePermissionFlagValidatorService.validateFlatRolePermissionFlagDeletion(
args,
);
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
const { flatEntityToValidate: flatRolePermissionFlagToValidate } = args;
return {
status: 'success',
action: {
type: 'delete',
metadataName: 'rolePermissionFlag',
universalIdentifier:
flatRolePermissionFlagToValidate.universalIdentifier,
},
};
}
protected validateFlatEntityUpdate(
args: FlatEntityUpdateValidationArgs<
typeof ALL_METADATA_NAME.rolePermissionFlag
>,
): UniversalFlatEntityValidationReturnType<
typeof ALL_METADATA_NAME.rolePermissionFlag,
'update'
> {
const validationResult =
this.flatRolePermissionFlagValidatorService.validateFlatRolePermissionFlagUpdate(
args,
);
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
const { universalIdentifier, flatEntityUpdate } = args;
const updateRolePermissionFlagAction: UniversalUpdateRolePermissionFlagAction =
{
type: 'update',
metadataName: 'rolePermissionFlag',
universalIdentifier,
update: flatEntityUpdate,
};
return {
status: 'success',
action: updateRolePermissionFlagAction,
};
}
}
@@ -0,0 +1,381 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { PermissionFlagType } from 'twenty-shared/constants';
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type FlatRolePermissionFlag } from 'src/engine/metadata-modules/flat-role-permission-flag/types/flat-role-permission-flag.type';
import { type FlatPermissionFlag } from 'src/engine/metadata-modules/flat-permission-flag/types/flat-permission-flag.type';
import { type PermissionFlagPermissionType } from 'src/engine/metadata-modules/permission-flag/constants/permission-flag-permission-type.constant';
import { PermissionFlagExceptionCode } from 'src/engine/metadata-modules/permission-flag/permission-flag.exception';
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
import { FlatPermissionFlagValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-permission-flag-validator.service';
const buildFlatDefinition = (
overrides: Partial<FlatPermissionFlag> = {},
): FlatPermissionFlag =>
({
id: '00000000-0000-0000-0000-000000000001',
universalIdentifier: '00000000-0000-0000-0000-000000000001',
key: 'TEST_FLAG',
label: 'Test Flag',
description: 'A flag for tests',
icon: 'IconTest',
permissionType: 'tool',
workspaceId: 'workspace-id',
applicationId: '00000000-0000-0000-0000-000000000aaa',
applicationUniversalIdentifier: '00000000-0000-0000-0000-000000000aaa',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
...overrides,
}) as unknown as FlatPermissionFlag;
const buildEmptyMaps = (): FlatEntityMaps<FlatPermissionFlag> =>
createEmptyFlatEntityMaps() as unknown as FlatEntityMaps<FlatPermissionFlag>;
const buildEmptyRolePermissionFlagMaps =
(): FlatEntityMaps<FlatRolePermissionFlag> =>
createEmptyFlatEntityMaps() as unknown as FlatEntityMaps<FlatRolePermissionFlag>;
const buildFlatRolePermissionFlag = (
overrides: Partial<FlatRolePermissionFlag> = {},
): FlatRolePermissionFlag =>
({
id: '00000000-0000-0000-0000-000000000101',
universalIdentifier: '00000000-0000-0000-0000-000000000101',
permissionFlagId: '00000000-0000-0000-0000-000000000001',
permissionFlagUniversalIdentifier: '00000000-0000-0000-0000-000000000001',
roleUniversalIdentifier: '00000000-0000-0000-0000-000000000201',
workspaceId: 'workspace-id',
applicationId: '00000000-0000-0000-0000-000000000aaa',
applicationUniversalIdentifier: '00000000-0000-0000-0000-000000000aaa',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
...overrides,
}) as unknown as FlatRolePermissionFlag;
const buildArgs = (
flatEntityToValidate: FlatPermissionFlag,
optimisticMaps: FlatEntityMaps<FlatPermissionFlag> = buildEmptyMaps(),
) =>
({
flatEntityToValidate,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatPermissionFlagMaps: optimisticMaps,
},
buildOptions: {} as never,
}) as unknown as Parameters<
FlatPermissionFlagValidatorService['validateFlatPermissionFlagCreation']
>[0];
describe('FlatPermissionFlagValidatorService', () => {
let service: FlatPermissionFlagValidatorService;
beforeEach(async () => {
const moduleRef: TestingModule = await Test.createTestingModule({
providers: [FlatPermissionFlagValidatorService],
}).compile();
service = moduleRef.get(FlatPermissionFlagValidatorService);
});
describe('validateFlatPermissionFlagCreation', () => {
it('passes for a valid definition', () => {
const result = service.validateFlatPermissionFlagCreation(
buildArgs(buildFlatDefinition()),
);
expect(result.errors).toHaveLength(0);
});
it('rejects an empty key', () => {
const result = service.validateFlatPermissionFlagCreation(
buildArgs(buildFlatDefinition({ key: '' })),
);
expect(result.errors.map((error) => error.code)).toEqual([
PermissionFlagExceptionCode.INVALID_PERMISSION_FLAG_KEY,
]);
});
it('rejects an unknown permission type', () => {
const result = service.validateFlatPermissionFlagCreation(
buildArgs(
buildFlatDefinition({
permissionType: 'invalid' as PermissionFlagPermissionType,
}),
),
);
expect(result.errors.map((error) => error.code)).toEqual([
PermissionFlagExceptionCode.INVALID_PERMISSION_FLAG_PERMISSION_TYPE,
]);
});
it('rejects a duplicate key in the same workspace', () => {
const existing = buildFlatDefinition({
universalIdentifier: '00000000-0000-0000-0000-000000000999',
key: 'EXISTING_KEY',
});
const optimisticMaps = buildEmptyMaps();
optimisticMaps.byUniversalIdentifier[existing.universalIdentifier] =
existing;
const result = service.validateFlatPermissionFlagCreation(
buildArgs(buildFlatDefinition({ key: 'EXISTING_KEY' }), optimisticMaps),
);
expect(result.errors.map((error) => error.code)).toEqual([
PermissionFlagExceptionCode.PERMISSION_FLAG_ALREADY_EXISTS,
]);
});
it('rejects a duplicate universal identifier', () => {
const existing = buildFlatDefinition();
const optimisticMaps = buildEmptyMaps();
optimisticMaps.byUniversalIdentifier[existing.universalIdentifier] =
existing;
const result = service.validateFlatPermissionFlagCreation(
buildArgs(
buildFlatDefinition({
id: '00000000-0000-0000-0000-000000000002',
key: 'ANOTHER_TEST_FLAG',
}),
optimisticMaps,
),
);
expect(result.errors.map((error) => error.code)).toEqual([
PermissionFlagExceptionCode.PERMISSION_FLAG_ALREADY_EXISTS,
]);
});
});
describe('validateFlatPermissionFlagUpdate', () => {
it('passes for a valid update', () => {
const existing = buildFlatDefinition();
const optimisticMaps = buildEmptyMaps();
optimisticMaps.byUniversalIdentifier[existing.universalIdentifier] =
existing;
const result = service.validateFlatPermissionFlagUpdate({
universalIdentifier: existing.universalIdentifier,
flatEntityUpdate: { label: 'Updated Label' },
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatPermissionFlagMaps: optimisticMaps,
},
buildOptions: {} as never,
} as unknown as Parameters<
FlatPermissionFlagValidatorService['validateFlatPermissionFlagUpdate']
>[0]);
expect(result.errors).toHaveLength(0);
});
it('rejects updating the immutable key field', () => {
const existing = buildFlatDefinition({ key: 'ORIGINAL_KEY' });
const optimisticMaps = buildEmptyMaps();
optimisticMaps.byUniversalIdentifier[existing.universalIdentifier] =
existing;
const result = service.validateFlatPermissionFlagUpdate({
universalIdentifier: existing.universalIdentifier,
flatEntityUpdate: { key: 'NEW_KEY' },
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatPermissionFlagMaps: optimisticMaps,
},
buildOptions: {} as never,
} as unknown as Parameters<
FlatPermissionFlagValidatorService['validateFlatPermissionFlagUpdate']
>[0]);
expect(result.errors.map((error) => error.code)).toEqual([
PermissionFlagExceptionCode.PERMISSION_FLAG_KEY_IMMUTABLE,
]);
});
it('returns not-found if no existing definition matches the universalIdentifier', () => {
const result = service.validateFlatPermissionFlagUpdate({
universalIdentifier: '00000000-0000-0000-0000-deadbeefdead',
flatEntityUpdate: { label: 'New Label' },
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatPermissionFlagMaps: buildEmptyMaps(),
},
buildOptions: {} as never,
} as unknown as Parameters<
FlatPermissionFlagValidatorService['validateFlatPermissionFlagUpdate']
>[0]);
expect(result.errors.map((error) => error.code)).toEqual([
PermissionFlagExceptionCode.PERMISSION_FLAG_NOT_FOUND,
]);
});
it('rejects updating a standard definition from a custom application', () => {
const existing = buildFlatDefinition({
applicationUniversalIdentifier:
TWENTY_STANDARD_APPLICATION.universalIdentifier,
});
const optimisticMaps = buildEmptyMaps();
optimisticMaps.byUniversalIdentifier[existing.universalIdentifier] =
existing;
const result = service.validateFlatPermissionFlagUpdate({
universalIdentifier: existing.universalIdentifier,
flatEntityUpdate: { label: 'Updated Label' },
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatPermissionFlagMaps: optimisticMaps,
},
buildOptions: {
isSystemBuild: false,
applicationUniversalIdentifier:
'00000000-0000-0000-0000-000000000aaa',
},
} as unknown as Parameters<
FlatPermissionFlagValidatorService['validateFlatPermissionFlagUpdate']
>[0]);
expect(result.errors.map((error) => error.code)).toEqual([
PermissionFlagExceptionCode.PERMISSION_FLAG_IS_STANDARD,
]);
});
it('rejects updating to an unknown permission type', () => {
const existing = buildFlatDefinition();
const optimisticMaps = buildEmptyMaps();
optimisticMaps.byUniversalIdentifier[existing.universalIdentifier] =
existing;
const result = service.validateFlatPermissionFlagUpdate({
universalIdentifier: existing.universalIdentifier,
flatEntityUpdate: {
permissionType: 'invalid' as PermissionFlagPermissionType,
},
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatPermissionFlagMaps: optimisticMaps,
},
buildOptions: {} as never,
} as unknown as Parameters<
FlatPermissionFlagValidatorService['validateFlatPermissionFlagUpdate']
>[0]);
expect(result.errors.map((error) => error.code)).toEqual([
PermissionFlagExceptionCode.INVALID_PERMISSION_FLAG_PERMISSION_TYPE,
]);
});
});
describe('validateFlatPermissionFlagDeletion', () => {
it('passes for a valid deletion', () => {
const existing = buildFlatDefinition();
const optimisticMaps = buildEmptyMaps();
optimisticMaps.byUniversalIdentifier[existing.universalIdentifier] =
existing;
const result = service.validateFlatPermissionFlagDeletion({
flatEntityToValidate: existing,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatPermissionFlagMaps: optimisticMaps,
flatRolePermissionFlagMaps: buildEmptyRolePermissionFlagMaps(),
},
buildOptions: {} as never,
} as unknown as Parameters<
FlatPermissionFlagValidatorService['validateFlatPermissionFlagDeletion']
>[0]);
expect(result.errors).toHaveLength(0);
});
it('returns not-found if no existing definition matches the universalIdentifier', () => {
const result = service.validateFlatPermissionFlagDeletion({
flatEntityToValidate: buildFlatDefinition({
universalIdentifier: '00000000-0000-0000-0000-deadbeefdead',
}),
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatPermissionFlagMaps: buildEmptyMaps(),
flatRolePermissionFlagMaps: buildEmptyRolePermissionFlagMaps(),
},
buildOptions: {} as never,
} as unknown as Parameters<
FlatPermissionFlagValidatorService['validateFlatPermissionFlagDeletion']
>[0]);
expect(result.errors.map((error) => error.code)).toEqual([
PermissionFlagExceptionCode.PERMISSION_FLAG_NOT_FOUND,
]);
});
it('rejects deleting a standard definition from a custom application', () => {
const existing = buildFlatDefinition({
applicationUniversalIdentifier:
TWENTY_STANDARD_APPLICATION.universalIdentifier,
});
const optimisticMaps = buildEmptyMaps();
optimisticMaps.byUniversalIdentifier[existing.universalIdentifier] =
existing;
const result = service.validateFlatPermissionFlagDeletion({
flatEntityToValidate: existing,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatPermissionFlagMaps: optimisticMaps,
flatRolePermissionFlagMaps: buildEmptyRolePermissionFlagMaps(),
},
buildOptions: {
isSystemBuild: false,
applicationUniversalIdentifier:
'00000000-0000-0000-0000-000000000aaa',
},
} as unknown as Parameters<
FlatPermissionFlagValidatorService['validateFlatPermissionFlagDeletion']
>[0]);
expect(result.errors.map((error) => error.code)).toEqual([
PermissionFlagExceptionCode.PERMISSION_FLAG_IS_STANDARD,
]);
});
it('rejects deleting a definition while roles still grant its key', () => {
const existing = buildFlatDefinition({
key: PermissionFlagType.WORKSPACE,
});
const optimisticMaps = buildEmptyMaps();
optimisticMaps.byUniversalIdentifier[existing.universalIdentifier] =
existing;
const rolePermissionFlagMaps = buildEmptyRolePermissionFlagMaps();
const rolePermissionFlag = buildFlatRolePermissionFlag({
permissionFlagId: existing.id,
permissionFlagUniversalIdentifier: existing.universalIdentifier,
});
rolePermissionFlagMaps.byUniversalIdentifier[
rolePermissionFlag.universalIdentifier
] = rolePermissionFlag;
const result = service.validateFlatPermissionFlagDeletion({
flatEntityToValidate: existing,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatPermissionFlagMaps: optimisticMaps,
flatRolePermissionFlagMaps: rolePermissionFlagMaps,
},
buildOptions: {
isSystemBuild: false,
applicationUniversalIdentifier:
'00000000-0000-0000-0000-000000000aaa',
},
} as unknown as Parameters<
FlatPermissionFlagValidatorService['validateFlatPermissionFlagDeletion']
>[0]);
expect(result.errors.map((error) => error.code)).toEqual([
PermissionFlagExceptionCode.PERMISSION_FLAG_IN_USE,
]);
});
});
// ALL_METADATA_NAME is referenced as a sanity check the metadata name exists
it('uses the registered ALL_METADATA_NAME entry', () => {
expect(ALL_METADATA_NAME.permissionFlag).toBe('permissionFlag');
});
});
@@ -1,17 +1,19 @@
import { Injectable } from '@nestjs/common';
import { msg, t } from '@lingui/core/macro';
import { PermissionFlagType } from 'twenty-shared/constants';
import { isNonEmptyString } from '@sniptt/guards';
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
import { PermissionsExceptionCode } from 'src/engine/metadata-modules/permissions/permissions.exception';
import { FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/types/failed-flat-entity-validation.type';
import { PERMISSION_FLAG_PERMISSION_TYPES } from 'src/engine/metadata-modules/permission-flag/constants/permission-flag-permission-type.constant';
import { PermissionFlagExceptionCode } from 'src/engine/metadata-modules/permission-flag/permission-flag.exception';
import { belongsToTwentyStandardApp } from 'src/engine/metadata-modules/utils/belongs-to-twenty-standard-app.util';
import { isCallerTwentyStandardApp } from 'src/engine/metadata-modules/utils/is-caller-twenty-standard-app.util';
import { type FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/types/failed-flat-entity-validation.type';
import { getEmptyFlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/utils/get-flat-entity-validation-error.util';
import { FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-update-validation-args.type';
import { UniversalFlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-validation-args.type';
import { validateRoleBelongsToCallerApplication } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/utils/validate-role-belongs-to-caller-application.util';
import { type FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-update-validation-args.type';
import { type UniversalFlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-validation-args.type';
@Injectable()
export class FlatPermissionFlagValidatorService {
@@ -19,17 +21,14 @@ export class FlatPermissionFlagValidatorService {
flatEntityToValidate: flatPermissionFlagToValidate,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatPermissionFlagMaps: optimisticFlatPermissionFlagMaps,
flatRoleMaps,
},
buildOptions,
}: UniversalFlatEntityValidationArgs<
typeof ALL_METADATA_NAME.permissionFlag
>): FailedFlatEntityValidation<'permissionFlag', 'create'> {
const validationResult = getEmptyFlatEntityValidationError({
flatEntityMinimalInformation: {
universalIdentifier: flatPermissionFlagToValidate.universalIdentifier,
roleUniversalIdentifier:
flatPermissionFlagToValidate.roleUniversalIdentifier,
key: flatPermissionFlagToValidate.key,
},
metadataName: 'permissionFlag',
type: 'create',
@@ -42,71 +41,47 @@ export class FlatPermissionFlagValidatorService {
if (isDefined(existingByUniversalId)) {
validationResult.errors.push({
code: PermissionsExceptionCode.INVALID_SETTING,
message: t`Permission flag with universal identifier ${flatPermissionFlagToValidate.universalIdentifier} already exists`,
userFriendlyMessage: msg`Permission flag already exists`,
code: PermissionFlagExceptionCode.PERMISSION_FLAG_ALREADY_EXISTS,
message: t`Permission flag definition with universal identifier ${flatPermissionFlagToValidate.universalIdentifier} already exists`,
userFriendlyMessage: msg`Permission flag definition already exists`,
});
}
const referencedRole = findFlatEntityByUniversalIdentifier({
universalIdentifier: flatPermissionFlagToValidate.roleUniversalIdentifier,
flatEntityMaps: flatRoleMaps,
});
if (!isDefined(referencedRole)) {
if (!isNonEmptyString(flatPermissionFlagToValidate.key)) {
validationResult.errors.push({
code: PermissionsExceptionCode.ROLE_NOT_FOUND,
message: t`Role not found`,
userFriendlyMessage: msg`Role not found`,
});
} else {
validationResult.errors.push(
...validateRoleBelongsToCallerApplication({
referencedRole,
buildOptions,
}),
);
if (!referencedRole.isEditable) {
validationResult.errors.push({
code: PermissionsExceptionCode.ROLE_NOT_EDITABLE,
message: t`Role is not editable`,
userFriendlyMessage: msg`This role cannot be modified because it is a system role. Only custom roles can be edited.`,
});
}
}
const isValidFlag =
isDefined(flatPermissionFlagToValidate.flag) &&
Object.values(PermissionFlagType).includes(
flatPermissionFlagToValidate.flag,
);
if (!isValidFlag) {
validationResult.errors.push({
code: PermissionsExceptionCode.INVALID_SETTING,
message: t`Invalid permission flag value`,
userFriendlyMessage: msg`Invalid permission setting`,
code: PermissionFlagExceptionCode.INVALID_PERMISSION_FLAG_KEY,
message: t`Permission flag definition key is required`,
userFriendlyMessage: msg`Key is required`,
});
}
const duplicateForSameRole = Object.values(
const duplicateKey = Object.values(
optimisticFlatPermissionFlagMaps.byUniversalIdentifier,
).filter(
(pf) =>
isDefined(pf) &&
pf.roleUniversalIdentifier ===
flatPermissionFlagToValidate.roleUniversalIdentifier &&
pf.flag === flatPermissionFlagToValidate.flag &&
pf.universalIdentifier !==
(definition) =>
isDefined(definition) &&
definition.key === flatPermissionFlagToValidate.key &&
definition.universalIdentifier !==
flatPermissionFlagToValidate.universalIdentifier,
);
if (duplicateForSameRole.length > 0) {
if (duplicateKey.length > 0) {
validationResult.errors.push({
code: PermissionsExceptionCode.INVALID_SETTING,
message: t`Permission flag for this role and setting already exists`,
userFriendlyMessage: msg`This permission is already set for the role`,
code: PermissionFlagExceptionCode.PERMISSION_FLAG_ALREADY_EXISTS,
message: t`Permission flag definition with key ${flatPermissionFlagToValidate.key} already exists in this workspace`,
userFriendlyMessage: msg`A permission flag with this key already exists`,
});
}
if (
!PERMISSION_FLAG_PERMISSION_TYPES.includes(
flatPermissionFlagToValidate.permissionType,
)
) {
validationResult.errors.push({
code: PermissionFlagExceptionCode.INVALID_PERMISSION_FLAG_PERMISSION_TYPE,
message: t`Permission flag definition permission type must be 'settings' or 'tool'`,
userFriendlyMessage: msg`Invalid permission type`,
});
}
@@ -118,17 +93,11 @@ export class FlatPermissionFlagValidatorService {
flatEntityUpdate,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatPermissionFlagMaps: optimisticFlatPermissionFlagMaps,
flatRoleMaps,
},
buildOptions,
}: FlatEntityUpdateValidationArgs<
typeof ALL_METADATA_NAME.permissionFlag
>): FailedFlatEntityValidation<'permissionFlag', 'update'> {
const existingFlatPermissionFlag = findFlatEntityByUniversalIdentifier({
universalIdentifier,
flatEntityMaps: optimisticFlatPermissionFlagMaps,
});
const validationResult = getEmptyFlatEntityValidationError({
flatEntityMinimalInformation: {
universalIdentifier,
@@ -137,79 +106,56 @@ export class FlatPermissionFlagValidatorService {
type: 'update',
});
if (!isDefined(existingFlatPermissionFlag)) {
const existing = findFlatEntityByUniversalIdentifier({
universalIdentifier,
flatEntityMaps: optimisticFlatPermissionFlagMaps,
});
if (!isDefined(existing)) {
validationResult.errors.push({
code: PermissionsExceptionCode.PERMISSION_NOT_FOUND,
message: t`Permission flag to update not found`,
userFriendlyMessage: msg`Permission flag not found`,
code: PermissionFlagExceptionCode.PERMISSION_FLAG_NOT_FOUND,
message: t`Permission flag definition to update not found`,
userFriendlyMessage: msg`Permission flag definition not found`,
});
return validationResult;
}
const updatedFlatPermissionFlag = {
...existingFlatPermissionFlag,
...flatEntityUpdate,
};
const referencedRole = findFlatEntityByUniversalIdentifier({
universalIdentifier: updatedFlatPermissionFlag.roleUniversalIdentifier,
flatEntityMaps: flatRoleMaps,
});
if (!isDefined(referencedRole)) {
if (
!isCallerTwentyStandardApp(buildOptions) &&
belongsToTwentyStandardApp({
universalIdentifier: existing.universalIdentifier,
applicationUniversalIdentifier: existing.applicationUniversalIdentifier,
})
) {
validationResult.errors.push({
code: PermissionsExceptionCode.ROLE_NOT_FOUND,
message: t`Role not found`,
userFriendlyMessage: msg`Role not found`,
code: PermissionFlagExceptionCode.PERMISSION_FLAG_IS_STANDARD,
message: t`Cannot update standard permission flag definition`,
userFriendlyMessage: msg`Cannot update standard permission flag definition`,
});
} else {
validationResult.errors.push(
...validateRoleBelongsToCallerApplication({
referencedRole,
buildOptions,
}),
);
if (!referencedRole.isEditable) {
validationResult.errors.push({
code: PermissionsExceptionCode.ROLE_NOT_EDITABLE,
message: t`Role is not editable`,
userFriendlyMessage: msg`This role cannot be modified because it is a system role. Only custom roles can be edited.`,
});
}
}
if (isDefined(flatEntityUpdate.flag)) {
const isValidFlag = Object.values(PermissionFlagType).includes(
flatEntityUpdate.flag as PermissionFlagType,
);
if (!isValidFlag) {
validationResult.errors.push({
code: PermissionsExceptionCode.INVALID_SETTING,
message: t`Invalid permission flag value`,
userFriendlyMessage: msg`Invalid permission setting`,
});
}
}
const duplicateForSameRole = Object.values(
optimisticFlatPermissionFlagMaps.byUniversalIdentifier,
).filter(
(pf) =>
isDefined(pf) &&
pf.roleUniversalIdentifier ===
updatedFlatPermissionFlag.roleUniversalIdentifier &&
pf.flag === updatedFlatPermissionFlag.flag &&
pf.universalIdentifier !== universalIdentifier,
);
if (duplicateForSameRole.length > 0) {
if (
isDefined(flatEntityUpdate.key) &&
flatEntityUpdate.key !== existing.key
) {
validationResult.errors.push({
code: PermissionsExceptionCode.INVALID_SETTING,
message: t`Permission flag for this role and setting already exists`,
userFriendlyMessage: msg`This permission is already set for the role`,
code: PermissionFlagExceptionCode.PERMISSION_FLAG_KEY_IMMUTABLE,
message: t`Permission flag definition key cannot be changed after creation`,
userFriendlyMessage: msg`Key cannot be changed`,
});
}
if (
isDefined(flatEntityUpdate.permissionType) &&
!PERMISSION_FLAG_PERMISSION_TYPES.includes(
flatEntityUpdate.permissionType,
)
) {
validationResult.errors.push({
code: PermissionFlagExceptionCode.INVALID_PERMISSION_FLAG_PERMISSION_TYPE,
message: t`Permission flag definition permission type must be 'settings' or 'tool'`,
userFriendlyMessage: msg`Invalid permission type`,
});
}
@@ -220,8 +166,9 @@ export class FlatPermissionFlagValidatorService {
flatEntityToValidate: { universalIdentifier },
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatPermissionFlagMaps: optimisticFlatPermissionFlagMaps,
flatRoleMaps,
flatRolePermissionFlagMaps: optimisticFlatRolePermissionFlagMaps,
},
buildOptions,
}: UniversalFlatEntityValidationArgs<
typeof ALL_METADATA_NAME.permissionFlag
>): FailedFlatEntityValidation<'permissionFlag', 'delete'> {
@@ -233,30 +180,50 @@ export class FlatPermissionFlagValidatorService {
type: 'delete',
});
const existingFlatPermissionFlag = findFlatEntityByUniversalIdentifier({
const existing = findFlatEntityByUniversalIdentifier({
universalIdentifier,
flatEntityMaps: optimisticFlatPermissionFlagMaps,
});
if (!isDefined(existingFlatPermissionFlag)) {
if (!isDefined(existing)) {
validationResult.errors.push({
code: PermissionsExceptionCode.PERMISSION_NOT_FOUND,
message: t`Permission flag to delete not found`,
userFriendlyMessage: msg`Permission flag not found`,
});
} else {
const referencedRole = findFlatEntityByUniversalIdentifier({
universalIdentifier: existingFlatPermissionFlag.roleUniversalIdentifier,
flatEntityMaps: flatRoleMaps,
code: PermissionFlagExceptionCode.PERMISSION_FLAG_NOT_FOUND,
message: t`Permission flag definition to delete not found`,
userFriendlyMessage: msg`Permission flag definition not found`,
});
if (isDefined(referencedRole) && !referencedRole.isEditable) {
validationResult.errors.push({
code: PermissionsExceptionCode.ROLE_NOT_EDITABLE,
message: t`Role is not editable`,
userFriendlyMessage: msg`This role cannot be modified because it is a system role. Only custom roles can be edited.`,
});
}
return validationResult;
}
if (
!isCallerTwentyStandardApp(buildOptions) &&
belongsToTwentyStandardApp({
universalIdentifier: existing.universalIdentifier,
applicationUniversalIdentifier: existing.applicationUniversalIdentifier,
})
) {
validationResult.errors.push({
code: PermissionFlagExceptionCode.PERMISSION_FLAG_IS_STANDARD,
message: t`Cannot delete standard permission flag definition`,
userFriendlyMessage: msg`Cannot delete standard permission flag definition`,
});
}
const isPermissionFlagInUse = Object.values(
optimisticFlatRolePermissionFlagMaps.byUniversalIdentifier,
).some(
(rolePermissionFlag) =>
isDefined(rolePermissionFlag) &&
rolePermissionFlag.permissionFlagUniversalIdentifier ===
existing.universalIdentifier,
);
if (isPermissionFlagInUse) {
validationResult.errors.push({
code: PermissionFlagExceptionCode.PERMISSION_FLAG_IN_USE,
message: t`Permission flag definition with key ${existing.key} is still assigned to a role`,
userFriendlyMessage: msg`Remove this permission from all roles before deleting it`,
});
}
return validationResult;
@@ -0,0 +1,281 @@
import { Injectable } from '@nestjs/common';
import { msg, t } from '@lingui/core/macro';
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
import { PermissionsExceptionCode } from 'src/engine/metadata-modules/permissions/permissions.exception';
import { FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/types/failed-flat-entity-validation.type';
import { getEmptyFlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/utils/get-flat-entity-validation-error.util';
import { FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-update-validation-args.type';
import { UniversalFlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-validation-args.type';
import { validateRoleBelongsToCallerApplication } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/utils/validate-role-belongs-to-caller-application.util';
@Injectable()
export class FlatRolePermissionFlagValidatorService {
validateFlatRolePermissionFlagCreation({
flatEntityToValidate: flatRolePermissionFlagToValidate,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatPermissionFlagMaps,
flatRolePermissionFlagMaps: optimisticFlatRolePermissionFlagMaps,
flatRoleMaps,
},
buildOptions,
}: UniversalFlatEntityValidationArgs<
typeof ALL_METADATA_NAME.rolePermissionFlag
>): FailedFlatEntityValidation<'rolePermissionFlag', 'create'> {
const validationResult = getEmptyFlatEntityValidationError({
flatEntityMinimalInformation: {
universalIdentifier:
flatRolePermissionFlagToValidate.universalIdentifier,
permissionFlagUniversalIdentifier:
flatRolePermissionFlagToValidate.permissionFlagUniversalIdentifier,
roleUniversalIdentifier:
flatRolePermissionFlagToValidate.roleUniversalIdentifier,
},
metadataName: 'rolePermissionFlag',
type: 'create',
});
const existingByUniversalId = findFlatEntityByUniversalIdentifier({
universalIdentifier: flatRolePermissionFlagToValidate.universalIdentifier,
flatEntityMaps: optimisticFlatRolePermissionFlagMaps,
});
if (isDefined(existingByUniversalId)) {
validationResult.errors.push({
code: PermissionsExceptionCode.INVALID_SETTING,
message: t`Role permission flag with universal identifier ${flatRolePermissionFlagToValidate.universalIdentifier} already exists`,
userFriendlyMessage: msg`Role permission flag already exists`,
});
}
const referencedRole = findFlatEntityByUniversalIdentifier({
universalIdentifier:
flatRolePermissionFlagToValidate.roleUniversalIdentifier,
flatEntityMaps: flatRoleMaps,
});
if (!isDefined(referencedRole)) {
validationResult.errors.push({
code: PermissionsExceptionCode.ROLE_NOT_FOUND,
message: t`Role not found`,
userFriendlyMessage: msg`Role not found`,
});
} else {
validationResult.errors.push(
...validateRoleBelongsToCallerApplication({
referencedRole,
buildOptions,
}),
);
if (!referencedRole.isEditable) {
validationResult.errors.push({
code: PermissionsExceptionCode.ROLE_NOT_EDITABLE,
message: t`Role is not editable`,
userFriendlyMessage: msg`This role cannot be modified because it is a system role. Only custom roles can be edited.`,
});
}
}
if (
isDefined(
flatRolePermissionFlagToValidate.permissionFlagUniversalIdentifier,
)
) {
const referencedPermissionFlag = findFlatEntityByUniversalIdentifier({
universalIdentifier:
flatRolePermissionFlagToValidate.permissionFlagUniversalIdentifier,
flatEntityMaps: flatPermissionFlagMaps,
});
if (!isDefined(referencedPermissionFlag)) {
validationResult.errors.push({
code: PermissionsExceptionCode.INVALID_SETTING,
message: t`Permission flag not found`,
userFriendlyMessage: msg`Invalid permission setting`,
});
}
}
const duplicateForSameRole = Object.values(
optimisticFlatRolePermissionFlagMaps.byUniversalIdentifier,
).filter(
(pf) =>
isDefined(pf) &&
pf.roleUniversalIdentifier ===
flatRolePermissionFlagToValidate.roleUniversalIdentifier &&
pf.permissionFlagUniversalIdentifier ===
flatRolePermissionFlagToValidate.permissionFlagUniversalIdentifier &&
pf.universalIdentifier !==
flatRolePermissionFlagToValidate.universalIdentifier,
);
if (duplicateForSameRole.length > 0) {
validationResult.errors.push({
code: PermissionsExceptionCode.INVALID_SETTING,
message: t`Permission flag for this role and setting already exists`,
userFriendlyMessage: msg`This permission is already set for the role`,
});
}
return validationResult;
}
validateFlatRolePermissionFlagUpdate({
universalIdentifier,
flatEntityUpdate,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatPermissionFlagMaps,
flatRolePermissionFlagMaps: optimisticFlatRolePermissionFlagMaps,
flatRoleMaps,
},
buildOptions,
}: FlatEntityUpdateValidationArgs<
typeof ALL_METADATA_NAME.rolePermissionFlag
>): FailedFlatEntityValidation<'rolePermissionFlag', 'update'> {
const existingFlatRolePermissionFlag = findFlatEntityByUniversalIdentifier({
universalIdentifier,
flatEntityMaps: optimisticFlatRolePermissionFlagMaps,
});
const validationResult = getEmptyFlatEntityValidationError({
flatEntityMinimalInformation: {
universalIdentifier,
},
metadataName: 'rolePermissionFlag',
type: 'update',
});
if (!isDefined(existingFlatRolePermissionFlag)) {
validationResult.errors.push({
code: PermissionsExceptionCode.PERMISSION_NOT_FOUND,
message: t`Permission flag to update not found`,
userFriendlyMessage: msg`Permission flag not found`,
});
return validationResult;
}
const updatedFlatRolePermissionFlag = {
...existingFlatRolePermissionFlag,
...flatEntityUpdate,
};
const referencedRole = findFlatEntityByUniversalIdentifier({
universalIdentifier:
updatedFlatRolePermissionFlag.roleUniversalIdentifier,
flatEntityMaps: flatRoleMaps,
});
if (!isDefined(referencedRole)) {
validationResult.errors.push({
code: PermissionsExceptionCode.ROLE_NOT_FOUND,
message: t`Role not found`,
userFriendlyMessage: msg`Role not found`,
});
} else {
validationResult.errors.push(
...validateRoleBelongsToCallerApplication({
referencedRole,
buildOptions,
}),
);
if (!referencedRole.isEditable) {
validationResult.errors.push({
code: PermissionsExceptionCode.ROLE_NOT_EDITABLE,
message: t`Role is not editable`,
userFriendlyMessage: msg`This role cannot be modified because it is a system role. Only custom roles can be edited.`,
});
}
}
if (isDefined(flatEntityUpdate.permissionFlagUniversalIdentifier)) {
const referencedPermissionFlag = findFlatEntityByUniversalIdentifier({
universalIdentifier:
flatEntityUpdate.permissionFlagUniversalIdentifier as string,
flatEntityMaps: flatPermissionFlagMaps,
});
if (!isDefined(referencedPermissionFlag)) {
validationResult.errors.push({
code: PermissionsExceptionCode.INVALID_SETTING,
message: t`Permission flag not found`,
userFriendlyMessage: msg`Invalid permission setting`,
});
}
}
const duplicateForSameRole = Object.values(
optimisticFlatRolePermissionFlagMaps.byUniversalIdentifier,
).filter(
(pf) =>
isDefined(pf) &&
pf.roleUniversalIdentifier ===
updatedFlatRolePermissionFlag.roleUniversalIdentifier &&
pf.permissionFlagUniversalIdentifier ===
updatedFlatRolePermissionFlag.permissionFlagUniversalIdentifier &&
pf.universalIdentifier !== universalIdentifier,
);
if (duplicateForSameRole.length > 0) {
validationResult.errors.push({
code: PermissionsExceptionCode.INVALID_SETTING,
message: t`Permission flag for this role and setting already exists`,
userFriendlyMessage: msg`This permission is already set for the role`,
});
}
return validationResult;
}
validateFlatRolePermissionFlagDeletion({
flatEntityToValidate: { universalIdentifier },
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatRolePermissionFlagMaps: optimisticFlatRolePermissionFlagMaps,
flatRoleMaps,
},
}: UniversalFlatEntityValidationArgs<
typeof ALL_METADATA_NAME.rolePermissionFlag
>): FailedFlatEntityValidation<'rolePermissionFlag', 'delete'> {
const validationResult = getEmptyFlatEntityValidationError({
flatEntityMinimalInformation: {
universalIdentifier,
},
metadataName: 'rolePermissionFlag',
type: 'delete',
});
const existingFlatRolePermissionFlag = findFlatEntityByUniversalIdentifier({
universalIdentifier,
flatEntityMaps: optimisticFlatRolePermissionFlagMaps,
});
if (!isDefined(existingFlatRolePermissionFlag)) {
validationResult.errors.push({
code: PermissionsExceptionCode.PERMISSION_NOT_FOUND,
message: t`Permission flag to delete not found`,
userFriendlyMessage: msg`Permission flag not found`,
});
} else {
const referencedRole = findFlatEntityByUniversalIdentifier({
universalIdentifier:
existingFlatRolePermissionFlag.roleUniversalIdentifier,
flatEntityMaps: flatRoleMaps,
});
if (isDefined(referencedRole) && !referencedRole.isEditable) {
validationResult.errors.push({
code: PermissionsExceptionCode.ROLE_NOT_EDITABLE,
message: t`Role is not editable`,
userFriendlyMessage: msg`This role cannot be modified because it is a system role. Only custom roles can be edited.`,
});
}
}
return validationResult;
}
}
@@ -17,6 +17,7 @@ import { FlatPageLayoutTabValidatorService } from 'src/engine/workspace-manager/
import { FlatPageLayoutValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-validator.service';
import { FlatPageLayoutWidgetValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-page-layout-widget-validator.service';
import { FlatPermissionFlagValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-permission-flag-validator.service';
import { FlatRolePermissionFlagValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-role-permission-flag-validator.service';
import { FlatRoleTargetValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-role-target-validator.service';
import { FlatRoleValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-role-validator.service';
import { FlatRowLevelPermissionPredicateGroupValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-row-level-permission-predicate-group-validator.service';
@@ -45,6 +46,7 @@ import { FlatConnectionProviderValidatorService } from 'src/engine/workspace-man
FlatViewSortValidatorService,
FlatFieldPermissionValidatorService,
FlatObjectPermissionValidatorService,
FlatRolePermissionFlagValidatorService,
FlatPermissionFlagValidatorService,
FlatIndexValidatorService,
FlatFieldMetadataValidatorService,
@@ -78,6 +80,7 @@ import { FlatConnectionProviderValidatorService } from 'src/engine/workspace-man
FlatViewSortValidatorService,
FlatFieldPermissionValidatorService,
FlatObjectPermissionValidatorService,
FlatRolePermissionFlagValidatorService,
FlatPermissionFlagValidatorService,
FlatIndexValidatorService,
FlatFieldMetadataValidatorService,
@@ -16,6 +16,7 @@ import { WorkspaceMigrationPageLayoutTabActionsBuilderService } from 'src/engine
import { WorkspaceMigrationPageLayoutWidgetActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/page-layout-widget/workspace-migration-page-layout-widget-actions-builder.service';
import { WorkspaceMigrationPageLayoutActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/page-layout/workspace-migration-page-layout-actions-builder.service';
import { WorkspaceMigrationPermissionFlagActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/permission-flag/workspace-migration-permission-flag-actions-builder.service';
import { WorkspaceMigrationRolePermissionFlagActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/role-permission-flag/workspace-migration-role-permission-flag-actions-builder.service';
import { WorkspaceMigrationRoleTargetActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/role-target/workspace-migration-role-target-actions-builder.service';
import { WorkspaceMigrationRoleActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/role/workspace-migration-role-actions-builder.service';
import { WorkspaceMigrationRowLevelPermissionPredicateGroupActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/row-level-permission-predicate-group/workspace-migration-row-level-permission-predicate-group-actions-builder.service';
@@ -47,6 +48,7 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-
WorkspaceMigrationViewSortActionsBuilderService,
WorkspaceMigrationFieldPermissionActionsBuilderService,
WorkspaceMigrationObjectPermissionActionsBuilderService,
WorkspaceMigrationRolePermissionFlagActionsBuilderService,
WorkspaceMigrationPermissionFlagActionsBuilderService,
WorkspaceMigrationIndexActionsBuilderService,
WorkspaceMigrationLogicFunctionActionsBuilderService,
@@ -79,6 +81,7 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-
WorkspaceMigrationViewSortActionsBuilderService,
WorkspaceMigrationFieldPermissionActionsBuilderService,
WorkspaceMigrationObjectPermissionActionsBuilderService,
WorkspaceMigrationRolePermissionFlagActionsBuilderService,
WorkspaceMigrationPermissionFlagActionsBuilderService,
WorkspaceMigrationFieldActionsBuilderService,
WorkspaceMigrationLogicFunctionActionsBuilderService,
@@ -2,15 +2,15 @@ import { Injectable } from '@nestjs/common';
import { v4 } from 'uuid';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import { resolveUniversalRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-relation-identifiers-to-ids.util';
import { getUniversalFlatEntityEmptyForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util';
import {
FlatCreatePermissionFlagAction,
UniversalCreatePermissionFlagAction,
type FlatCreatePermissionFlagAction,
type UniversalCreatePermissionFlagAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/permission-flag/types/workspace-migration-permission-flag-action.type';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import {
WorkspaceMigrationActionRunnerArgs,
WorkspaceMigrationActionRunnerContext,
type WorkspaceMigrationActionRunnerArgs,
type WorkspaceMigrationActionRunnerContext,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type';
@Injectable()
@@ -18,26 +18,29 @@ export class CreatePermissionFlagActionHandlerService extends WorkspaceMigration
'create',
'permissionFlag',
) {
constructor() {
super();
}
override async transpileUniversalActionToFlatAction({
action,
allFlatEntityMaps,
flatApplication,
workspaceId,
}: WorkspaceMigrationActionRunnerArgs<UniversalCreatePermissionFlagAction>): Promise<FlatCreatePermissionFlagAction> {
const { roleId } = resolveUniversalRelationIdentifiersToIds({
flatEntityMaps: allFlatEntityMaps,
metadataName: action.metadataName,
universalForeignKeyValues: action.flatEntity,
});
const emptyUniversalForeignKeyAggregators =
getUniversalFlatEntityEmptyForeignKeyAggregators({
metadataName: 'permissionFlag',
});
return {
...action,
flatEntity: {
...action.flatEntity,
roleId,
applicationId: flatApplication.id,
id: action.id ?? v4(),
workspaceId,
rolePermissionFlagIds: [],
...emptyUniversalForeignKeyAggregators,
},
};
}
@@ -1,14 +1,14 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity';
import {
FlatDeletePermissionFlagAction,
UniversalDeletePermissionFlagAction,
type FlatDeletePermissionFlagAction,
type UniversalDeletePermissionFlagAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/permission-flag/types/workspace-migration-permission-flag-action.type';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import {
WorkspaceMigrationActionRunnerArgs,
WorkspaceMigrationActionRunnerContext,
type WorkspaceMigrationActionRunnerArgs,
type WorkspaceMigrationActionRunnerContext,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type';
@Injectable()
@@ -31,12 +31,12 @@ export class DeletePermissionFlagActionHandlerService extends WorkspaceMigration
): Promise<void> {
const { flatAction, queryRunner, workspaceId } = context;
const permissionFlagRepository =
const repository =
queryRunner.manager.getRepository<PermissionFlagEntity>(
PermissionFlagEntity,
);
await permissionFlagRepository.delete({
await repository.delete({
id: flatAction.entityId,
workspaceId,
});
@@ -1,16 +1,16 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity';
import { resolveUniversalUpdateRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-update-relation-identifiers-to-ids.util';
import {
FlatUpdatePermissionFlagAction,
UniversalUpdatePermissionFlagAction,
type FlatUpdatePermissionFlagAction,
type UniversalUpdatePermissionFlagAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/permission-flag/types/workspace-migration-permission-flag-action.type';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import {
WorkspaceMigrationActionRunnerArgs,
WorkspaceMigrationActionRunnerContext,
type WorkspaceMigrationActionRunnerArgs,
type WorkspaceMigrationActionRunnerContext,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type';
@Injectable()
@@ -18,16 +18,12 @@ export class UpdatePermissionFlagActionHandlerService extends WorkspaceMigration
'update',
'permissionFlag',
) {
constructor() {
super();
}
override async transpileUniversalActionToFlatAction(
context: WorkspaceMigrationActionRunnerArgs<UniversalUpdatePermissionFlagAction>,
): Promise<FlatUpdatePermissionFlagAction> {
const { action, allFlatEntityMaps } = context;
const flatPermissionFlag = findFlatEntityByUniversalIdentifierOrThrow({
const flatDefinition = findFlatEntityByUniversalIdentifierOrThrow({
flatEntityMaps: allFlatEntityMaps.flatPermissionFlagMaps,
universalIdentifier: action.universalIdentifier,
});
@@ -41,7 +37,7 @@ export class UpdatePermissionFlagActionHandlerService extends WorkspaceMigration
return {
type: 'update',
metadataName: 'permissionFlag',
entityId: flatPermissionFlag.id,
entityId: flatDefinition.id,
update,
};
}
@@ -52,15 +48,12 @@ export class UpdatePermissionFlagActionHandlerService extends WorkspaceMigration
const { flatAction, queryRunner, workspaceId } = context;
const { entityId, update } = flatAction;
const permissionFlagRepository =
const repository =
queryRunner.manager.getRepository<PermissionFlagEntity>(
PermissionFlagEntity,
);
await permissionFlagRepository.update(
{ id: entityId, workspaceId },
update,
);
await repository.update({ id: entityId, workspaceId }, update);
}
async executeForWorkspaceSchema(
@@ -0,0 +1,62 @@
import { Injectable } from '@nestjs/common';
import { v4 } from 'uuid';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import { resolveUniversalRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-relation-identifiers-to-ids.util';
import {
FlatCreateRolePermissionFlagAction,
UniversalCreateRolePermissionFlagAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/role-permission-flag/types/workspace-migration-role-permission-flag-action.type';
import {
WorkspaceMigrationActionRunnerArgs,
WorkspaceMigrationActionRunnerContext,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type';
@Injectable()
export class CreateRolePermissionFlagActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'create',
'rolePermissionFlag',
) {
override async transpileUniversalActionToFlatAction({
action,
allFlatEntityMaps,
flatApplication,
workspaceId,
}: WorkspaceMigrationActionRunnerArgs<UniversalCreateRolePermissionFlagAction>): Promise<FlatCreateRolePermissionFlagAction> {
const relationIds = resolveUniversalRelationIdentifiersToIds({
flatEntityMaps: allFlatEntityMaps,
metadataName: action.metadataName,
universalForeignKeyValues: action.flatEntity,
});
return {
...action,
flatEntity: {
...action.flatEntity,
...relationIds,
applicationId: flatApplication.id,
id: action.id ?? v4(),
workspaceId,
},
};
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerContext<FlatCreateRolePermissionFlagAction>,
): Promise<void> {
const { flatAction, queryRunner } = context;
const { flatEntity } = flatAction;
await this.insertFlatEntitiesInRepository({
queryRunner,
flatEntities: [flatEntity],
});
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerContext<FlatCreateRolePermissionFlagAction>,
): Promise<void> {
return;
}
}
@@ -0,0 +1,50 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import { RolePermissionFlagEntity } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.entity';
import {
FlatDeleteRolePermissionFlagAction,
UniversalDeleteRolePermissionFlagAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/role-permission-flag/types/workspace-migration-role-permission-flag-action.type';
import {
WorkspaceMigrationActionRunnerArgs,
WorkspaceMigrationActionRunnerContext,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type';
@Injectable()
export class DeleteRolePermissionFlagActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'delete',
'rolePermissionFlag',
) {
constructor() {
super();
}
override async transpileUniversalActionToFlatAction(
context: WorkspaceMigrationActionRunnerArgs<UniversalDeleteRolePermissionFlagAction>,
): Promise<FlatDeleteRolePermissionFlagAction> {
return this.transpileUniversalDeleteActionToFlatDeleteAction(context);
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerContext<FlatDeleteRolePermissionFlagAction>,
): Promise<void> {
const { flatAction, queryRunner, workspaceId } = context;
const rolePermissionFlagRepository =
queryRunner.manager.getRepository<RolePermissionFlagEntity>(
RolePermissionFlagEntity,
);
await rolePermissionFlagRepository.delete({
id: flatAction.entityId,
workspaceId,
});
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerContext<FlatDeleteRolePermissionFlagAction>,
): Promise<void> {
return;
}
}
@@ -0,0 +1,71 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
import { RolePermissionFlagEntity } from 'src/engine/metadata-modules/role-permission-flag/role-permission-flag.entity';
import { resolveUniversalUpdateRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-update-relation-identifiers-to-ids.util';
import {
FlatUpdateRolePermissionFlagAction,
UniversalUpdateRolePermissionFlagAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/role-permission-flag/types/workspace-migration-role-permission-flag-action.type';
import {
WorkspaceMigrationActionRunnerArgs,
WorkspaceMigrationActionRunnerContext,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type';
@Injectable()
export class UpdateRolePermissionFlagActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'update',
'rolePermissionFlag',
) {
constructor() {
super();
}
override async transpileUniversalActionToFlatAction(
context: WorkspaceMigrationActionRunnerArgs<UniversalUpdateRolePermissionFlagAction>,
): Promise<FlatUpdateRolePermissionFlagAction> {
const { action, allFlatEntityMaps } = context;
const flatRolePermissionFlag = findFlatEntityByUniversalIdentifierOrThrow({
flatEntityMaps: allFlatEntityMaps.flatRolePermissionFlagMaps,
universalIdentifier: action.universalIdentifier,
});
const update = resolveUniversalUpdateRelationIdentifiersToIds({
metadataName: 'rolePermissionFlag',
universalUpdate: action.update,
allFlatEntityMaps,
});
return {
type: 'update',
metadataName: 'rolePermissionFlag',
entityId: flatRolePermissionFlag.id,
update,
};
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerContext<FlatUpdateRolePermissionFlagAction>,
): Promise<void> {
const { flatAction, queryRunner, workspaceId } = context;
const { entityId, update } = flatAction;
const rolePermissionFlagRepository =
queryRunner.manager.getRepository<RolePermissionFlagEntity>(
RolePermissionFlagEntity,
);
await rolePermissionFlagRepository.update(
{ id: entityId, workspaceId },
update,
);
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerContext<FlatUpdateRolePermissionFlagAction>,
): Promise<void> {
return;
}
}
@@ -44,7 +44,7 @@ export class CreateRoleActionHandlerService extends WorkspaceMigrationRunnerActi
rowLevelPermissionPredicateIds: [],
rowLevelPermissionPredicateGroupIds: [],
objectPermissionIds: [],
permissionFlagIds: [],
rolePermissionFlagIds: [],
fieldPermissionIds: [],
...emptyUniversalForeignKeyAggregators,
},
@@ -46,6 +46,9 @@ import { UpdatePageLayoutActionHandlerService } from 'src/engine/workspace-manag
import { CreatePermissionFlagActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/permission-flag/services/create-permission-flag-action-handler.service';
import { DeletePermissionFlagActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/permission-flag/services/delete-permission-flag-action-handler.service';
import { UpdatePermissionFlagActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/permission-flag/services/update-permission-flag-action-handler.service';
import { CreateRolePermissionFlagActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/role-permission-flag/services/create-role-permission-flag-action-handler.service';
import { DeleteRolePermissionFlagActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/role-permission-flag/services/delete-role-permission-flag-action-handler.service';
import { UpdateRolePermissionFlagActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/role-permission-flag/services/update-role-permission-flag-action-handler.service';
import { CreateRoleTargetActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/role-target/services/create-role-target-action-handler.service';
import { DeleteRoleTargetActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/role-target/services/delete-role-target-action-handler.service';
import { UpdateRoleTargetActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/role-target/services/update-role-target-action-handler.service';
@@ -151,6 +154,10 @@ import { UpdateConnectionProviderActionHandlerService } from 'src/engine/workspa
DeleteRoleTargetActionHandlerService,
UpdateRoleTargetActionHandlerService,
CreateRolePermissionFlagActionHandlerService,
UpdateRolePermissionFlagActionHandlerService,
DeleteRolePermissionFlagActionHandlerService,
CreatePermissionFlagActionHandlerService,
UpdatePermissionFlagActionHandlerService,
DeletePermissionFlagActionHandlerService,
@@ -19,6 +19,7 @@ export const METADATA_EVENTS_TO_EMIT = {
pageLayoutTab: true,
commandMenuItem: true,
navigationMenuItem: true,
rolePermissionFlag: true,
permissionFlag: true,
objectPermission: true,
fieldPermission: true,
@@ -90,7 +90,7 @@ export class WorkspaceMigrationRunnerService {
const shouldInvalidateRolesPermissionsCache =
flatMapsKeysSet.has('flatObjectPermissionMaps') ||
flatMapsKeysSet.has('flatFieldPermissionMaps') ||
flatMapsKeysSet.has('flatPermissionFlagMaps');
flatMapsKeysSet.has('flatRolePermissionFlagMaps');
if (
shouldIncrementMetadataGraphqlSchemaVersion ||
@@ -92,6 +92,7 @@ const deriveAllMetadataEventsFromCreateAction = (
case 'commandMenuItem':
case 'frontComponent':
case 'navigationMenuItem':
case 'rolePermissionFlag':
case 'permissionFlag':
case 'objectPermission':
case 'fieldPermission':
@@ -56,6 +56,7 @@ const deriveAllMetadataEventsFromDeleteAction = ({
case 'commandMenuItem':
case 'frontComponent':
case 'navigationMenuItem':
case 'rolePermissionFlag':
case 'permissionFlag':
case 'objectPermission':
case 'fieldPermission':
@@ -134,6 +134,7 @@ const deriveAllMetadataEventsFromUpdateAction = ({
case 'commandMenuItem':
case 'frontComponent':
case 'navigationMenuItem':
case 'rolePermissionFlag':
case 'permissionFlag':
case 'objectPermission':
case 'fieldPermission':
@@ -68,6 +68,7 @@ export const optimisticallyApplyCreateActionOnAllFlatEntityMaps = ({
case 'commandMenuItem':
case 'frontComponent':
case 'navigationMenuItem':
case 'rolePermissionFlag':
case 'permissionFlag':
case 'objectPermission':
case 'fieldPermission':
@@ -40,6 +40,7 @@ export const optimisticallyApplyDeleteActionOnAllFlatEntityMaps = ({
case 'commandMenuItem':
case 'frontComponent':
case 'navigationMenuItem':
case 'rolePermissionFlag':
case 'permissionFlag':
case 'objectPermission':
case 'fieldPermission':
@@ -61,6 +61,7 @@ export const optimisticallyApplyUpdateActionOnAllFlatEntityMaps = ({
case 'commandMenuItem':
case 'frontComponent':
case 'navigationMenuItem':
case 'rolePermissionFlag':
case 'permissionFlag':
case 'objectPermission':
case 'fieldPermission':