[permissions] Improve performances using a cache for userWorkspaces roles (#11587)

In this PR we are 

- introducing a cached map `{ userworkspaceId: roleId } `to reduce calls
to get a userWorkspace's role (we were having N+1 around that with
combinedFindMany queries and generally having a lot of avoidable
queries)
- using the roles permissions cache (`{ roleId: { objectNameSingular:
{ canRead: bool, canUpdate: bool, ...} } `) in Permissions V1's
userHasObjectPermission, in order to 1) improve performances to avoid
calls to get roles 2) start using our permissions cache
This commit is contained in:
Marie
2025-04-16 17:07:43 +02:00
committed by GitHub
parent ab277476a8
commit 4d78f5f97f
20 changed files with 692 additions and 350 deletions

View File

@ -5,7 +5,7 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat
import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity';
import { ObjectPermissionService } from 'src/engine/metadata-modules/object-permission/object-permission.service';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { WorkspaceRolesPermissionsCacheModule } from 'src/engine/metadata-modules/workspace-roles-permissions-cache/workspace-roles-permissions-cache.module';
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
@Module({
imports: [
@ -13,7 +13,7 @@ import { WorkspaceRolesPermissionsCacheModule } from 'src/engine/metadata-module
[ObjectPermissionEntity, RoleEntity, ObjectMetadataEntity],
'metadata',
),
WorkspaceRolesPermissionsCacheModule,
WorkspacePermissionsCacheModule,
],
providers: [ObjectPermissionService],
exports: [ObjectPermissionService],

View File

@ -12,7 +12,7 @@ import {
PermissionsExceptionMessage,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { WorkspaceRolesPermissionsCacheService } from 'src/engine/metadata-modules/workspace-roles-permissions-cache/workspace-roles-permissions-cache.service';
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
export class ObjectPermissionService {
constructor(
@ -22,7 +22,7 @@ export class ObjectPermissionService {
private readonly roleRepository: Repository<RoleEntity>,
@InjectRepository(ObjectMetadataEntity, 'metadata')
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
private readonly workspaceRolesPermissionsCacheService: WorkspaceRolesPermissionsCacheService,
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
) {}
public async upsertObjectPermission({
@ -54,9 +54,10 @@ export class ObjectPermissionService {
throw new Error('Failed to upsert object permission');
}
await this.workspaceRolesPermissionsCacheService.recomputeRolesPermissionsCache(
await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache(
{
workspaceId,
roleIds: [input.roleId],
},
);

View File

@ -7,6 +7,7 @@ import { PermissionsService } from 'src/engine/metadata-modules/permissions/perm
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity';
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
@Module({
imports: [
@ -14,6 +15,7 @@ import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.
FeatureFlagModule,
TypeOrmModule.forFeature([UserWorkspace], 'core'),
UserRoleModule,
WorkspacePermissionsCacheModule,
],
providers: [PermissionsService],
exports: [PermissionsService],

View File

@ -7,18 +7,24 @@ import {
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants';
import {
PermissionsException,
PermissionsExceptionCode,
PermissionsExceptionMessage,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
@Injectable()
export class PermissionsService {
constructor(private readonly userRoleService: UserRoleService) {}
constructor(
private readonly userRoleService: UserRoleService,
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
private readonly featureFlagService: FeatureFlagService,
) {}
public async getUserWorkspacePermissions({
userWorkspaceId,
@ -124,11 +130,21 @@ export class PermissionsService {
return true;
}
const settingPermissions = roleOfUserWorkspace.settingPermissions ?? [];
const isPermissionsV2Enabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsPermissionsV2Enabled,
workspaceId,
);
return settingPermissions.some(
(settingPermission) => settingPermission.setting === setting,
);
if (isPermissionsV2Enabled) {
const settingPermissions = roleOfUserWorkspace.settingPermissions ?? [];
return settingPermissions.some(
(settingPermission) => settingPermission.setting === setting,
);
} else {
return false;
}
}
public async userHasObjectRecordsPermission({
@ -142,6 +158,18 @@ export class PermissionsService {
requiredPermission: PermissionsOnAllObjectRecords;
isExecutedByApiKey: boolean;
}): Promise<boolean> {
const isPermissionsV2Enabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsPermissionsV2Enabled,
workspaceId,
);
if (isPermissionsV2Enabled) {
throw new Error(
'This should not be called once Permissions V2 is enabled',
);
}
if (isExecutedByApiKey) {
return true;
}
@ -153,31 +181,51 @@ export class PermissionsService {
);
}
const [roleOfUserWorkspace] = await this.userRoleService
.getRolesByUserWorkspaces({
userWorkspaceIds: [userWorkspaceId],
const roleIdOfUserWorkspace =
await this.userRoleService.getRoleIdForUserWorkspace({
userWorkspaceId,
workspaceId,
})
.then((roles) => roles?.get(userWorkspaceId) ?? []);
});
const roleColumn =
this.getRoleColumnForRequiredPermission(requiredPermission);
if (!isDefined(roleIdOfUserWorkspace)) {
throw new PermissionsException(
PermissionsExceptionMessage.NO_ROLE_FOUND_FOR_USER_WORKSPACE,
PermissionsExceptionCode.NO_ROLE_FOUND_FOR_USER_WORKSPACE,
);
}
return roleOfUserWorkspace?.[roleColumn] === true;
const { data: rolesPermissions } =
await this.workspacePermissionsCacheService.getRolesPermissionsFromCache({
workspaceId,
});
const rolePermissionsForUserWorkspaceRole =
rolesPermissions[roleIdOfUserWorkspace];
const objectPermissionKey =
this.getObjectPermissionKeyForRequiredPermission(requiredPermission);
// until permissions V2 is enabled all objects have the same permission values deriving from role, ex role.canReadAllObjectRecords
const objectPermissionValue =
rolePermissionsForUserWorkspaceRole[
Object.keys(rolePermissionsForUserWorkspaceRole)[0]
]?.[objectPermissionKey];
return objectPermissionValue === true;
}
private getRoleColumnForRequiredPermission(
private getObjectPermissionKeyForRequiredPermission(
requiredPermission: PermissionsOnAllObjectRecords,
): keyof RoleEntity {
) {
switch (requiredPermission) {
case PermissionsOnAllObjectRecords.READ_ALL_OBJECT_RECORDS:
return 'canReadAllObjectRecords';
return 'canRead';
case PermissionsOnAllObjectRecords.UPDATE_ALL_OBJECT_RECORDS:
return 'canUpdateAllObjectRecords';
return 'canUpdate';
case PermissionsOnAllObjectRecords.SOFT_DELETE_ALL_OBJECT_RECORDS:
return 'canSoftDeleteAllObjectRecords';
return 'canSoftDelete';
case PermissionsOnAllObjectRecords.DESTROY_ALL_OBJECT_RECORDS:
return 'canDestroyAllObjectRecords';
return 'canDestroy';
default:
throw new PermissionsException(
PermissionsExceptionMessage.UNKNOWN_REQUIRED_PERMISSION,

View File

@ -13,7 +13,7 @@ import { RoleService } from 'src/engine/metadata-modules/role/role.service';
import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity';
import { SettingPermissionModule } from 'src/engine/metadata-modules/setting-permission/setting-permission.module';
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
import { WorkspaceRolesPermissionsCacheModule } from 'src/engine/metadata-modules/workspace-roles-permissions-cache/workspace-roles-permissions-cache.module';
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
@Module({
imports: [
@ -25,7 +25,7 @@ import { WorkspaceRolesPermissionsCacheModule } from 'src/engine/metadata-module
FeatureFlagModule,
ObjectPermissionModule,
SettingPermissionModule,
WorkspaceRolesPermissionsCacheModule,
WorkspacePermissionsCacheModule,
],
providers: [RoleService, RoleResolver],
exports: [RoleService],

View File

@ -20,7 +20,7 @@ import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
import { isArgDefinedIfProvidedOrThrow } from 'src/engine/metadata-modules/utils/is-arg-defined-if-provided-or-throw.util';
import { WorkspaceRolesPermissionsCacheService } from 'src/engine/metadata-modules/workspace-roles-permissions-cache/workspace-roles-permissions-cache.service';
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
export class RoleService {
constructor(
@ -31,7 +31,7 @@ export class RoleService {
@InjectRepository(UserWorkspaceRoleEntity, 'metadata')
private readonly userWorkspaceRoleRepository: Repository<UserWorkspaceRoleEntity>,
private readonly userRoleService: UserRoleService,
private readonly workspaceRolesPermissionsCacheService: WorkspaceRolesPermissionsCacheService,
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
) {}
public async getWorkspaceRoles(workspaceId: string): Promise<RoleEntity[]> {
@ -69,7 +69,7 @@ export class RoleService {
}): Promise<RoleEntity> {
await this.validateRoleInput({ input, workspaceId });
const role = this.roleRepository.save({
const role = await this.roleRepository.save({
label: input.label,
description: input.description,
icon: input.icon,
@ -82,11 +82,10 @@ export class RoleService {
workspaceId,
});
await this.workspaceRolesPermissionsCacheService.recomputeRolesPermissionsCache(
{
workspaceId,
},
);
await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({
workspaceId,
roleIds: [role.id],
});
return role;
}
@ -128,11 +127,10 @@ export class RoleService {
...input.update,
});
await this.workspaceRolesPermissionsCacheService.recomputeRolesPermissionsCache(
{
workspaceId,
},
);
await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({
workspaceId,
roleIds: [input.id],
});
return { ...existingRole, ...updatedRole };
}
@ -196,11 +194,9 @@ export class RoleService {
workspaceId,
});
await this.workspaceRolesPermissionsCacheService.recomputeRolesPermissionsCache(
{
workspaceId,
},
);
await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({
workspaceId,
});
return roleId;
}
@ -317,10 +313,11 @@ export class RoleService {
workspaceId: string;
defaultRoleId: string;
}): Promise<void> {
const userWorkspaceIds = await this.getUserWorkspaceIdsForRole(
roleId,
workspaceId,
);
const userWorkspaceIds =
await this.userRoleService.getUserWorkspaceIdsAssignedToRole(
roleId,
workspaceId,
);
await Promise.all(
userWorkspaceIds.map((userWorkspaceId) =>
@ -333,24 +330,6 @@ export class RoleService {
);
}
private async getUserWorkspaceIdsForRole(
roleId: string,
workspaceId: string,
): Promise<string[]> {
return this.userWorkspaceRoleRepository
.find({
where: {
roleId: roleId,
workspaceId,
},
})
.then((userWorkspaceRoles) =>
userWorkspaceRoles.map(
(userWorkspaceRole) => userWorkspaceRole.userWorkspaceId,
),
);
}
private async getRole(
roleId: string,
workspaceId: string,

View File

@ -5,11 +5,13 @@ import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-works
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
@Module({
imports: [
TypeOrmModule.forFeature([RoleEntity, UserWorkspaceRoleEntity], 'metadata'),
TypeOrmModule.forFeature([UserWorkspace], 'core'),
WorkspacePermissionsCacheModule,
],
providers: [UserRoleService],
exports: [UserRoleService],

View File

@ -12,6 +12,7 @@ import {
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity';
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@ -24,6 +25,7 @@ export class UserRoleService {
@InjectRepository(UserWorkspace, 'core')
private readonly userWorkspaceRepository: Repository<UserWorkspace>,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
) {}
public async assignRoleToUserWorkspace({
@ -56,6 +58,12 @@ export class UserRoleService {
workspaceId,
id: Not(newUserWorkspaceRole.id),
});
await this.workspacePermissionsCacheService.recomputeUserWorkspaceRoleMapCache(
{
workspaceId,
},
);
}
public async getRoleIdForUserWorkspace({
@ -69,11 +77,14 @@ export class UserRoleService {
return;
}
const userWorkspaceRole = await this.userWorkspaceRoleRepository.findOne({
where: { userWorkspaceId, workspaceId },
});
const userWorkspaceRoleMap =
await this.workspacePermissionsCacheService.getUserWorkspaceRoleMapFromCache(
{
workspaceId,
},
);
return userWorkspaceRole?.roleId;
return userWorkspaceRoleMap.data[userWorkspaceId];
}
public async getRolesByUserWorkspaces({
@ -125,21 +136,13 @@ export class UserRoleService {
roleId: string,
workspaceId: string,
): Promise<WorkspaceMemberWorkspaceEntity[]> {
const userWorkspaceRoles = await this.userWorkspaceRoleRepository.find({
where: {
roleId,
workspaceId,
},
});
const userWorkspaceIdsWithRole =
await this.getUserWorkspaceIdsAssignedToRole(roleId, workspaceId);
const userIds = await this.userWorkspaceRepository
.find({
where: {
id: In(
userWorkspaceRoles.map(
(userWorkspaceRole) => userWorkspaceRole.userWorkspaceId,
),
),
id: In(userWorkspaceIdsWithRole),
},
})
.then((userWorkspaces) =>
@ -161,6 +164,22 @@ export class UserRoleService {
return workspaceMembers;
}
public async getUserWorkspaceIdsAssignedToRole(
roleId: string,
workspaceId: string,
): Promise<string[]> {
const userWorkspaceRoleMap =
await this.workspacePermissionsCacheService.getUserWorkspaceRoleMapFromCache(
{
workspaceId,
},
);
return Object.entries(userWorkspaceRoleMap.data)
.filter(([_, roleIdFromMap]) => roleIdFromMap === roleId)
.map(([userWorkspaceId]) => userWorkspaceId);
}
public async validateUserWorkspaceIsNotUniqueAdminOrThrow({
userWorkspaceId,
workspaceId,

View File

@ -10,6 +10,8 @@ import { TwentyORMExceptionCode } from 'src/engine/twenty-orm/exceptions/twenty-
import { getFromCacheWithRecompute } from 'src/engine/utils/get-data-from-cache-with-recompute.util';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
const FEATURE_FLAG_MAP = 'Feature flag map';
@Injectable()
export class WorkspaceFeatureFlagsMapCacheService {
logger = new Logger(WorkspaceFeatureFlagsMapCacheService.name);
@ -45,7 +47,7 @@ export class WorkspaceFeatureFlagsMapCacheService {
workspaceId,
),
recomputeCache: (params) => this.recomputeFeatureFlagsMapCache(params),
cachedEntityName: 'Feature flag map',
cachedEntityName: FEATURE_FLAG_MAP,
exceptionCode: TwentyORMExceptionCode.FEATURE_FLAG_MAP_VERSION_NOT_FOUND,
});
}

View File

@ -0,0 +1 @@
export type UserWorkspaceRoleMap = Record<string, string>;

View File

@ -0,0 +1,156 @@
import { Injectable, Logger } from '@nestjs/common';
import { ObjectRecordsPermissionsByRoleId } from 'twenty-shared/types';
import { v4 } from 'uuid';
import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator';
import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service';
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
import { UserWorkspaceRoleMap } from 'src/engine/metadata-modules/workspace-permissions-cache/types/user-workspace-role-map.type';
import { WorkspaceCacheKeys } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
const TTL_INFINITE = 0;
@Injectable()
export class WorkspacePermissionsCacheStorageService {
logger = new Logger(WorkspacePermissionsCacheStorageService.name);
constructor(
@InjectCacheStorage(CacheStorageNamespace.EngineWorkspace)
private readonly cacheStorageService: CacheStorageService,
) {}
async setRolesPermissionsVersion(workspaceId: string): Promise<string> {
const rolesPermissionsVersion = v4();
await this.cacheStorageService.set<string>(
`${WorkspaceCacheKeys.MetadataPermissionsRolesPermissionsVersion}:${workspaceId}`,
rolesPermissionsVersion,
TTL_INFINITE,
);
return rolesPermissionsVersion;
}
async setRolesPermissions(
workspaceId: string,
permissions: ObjectRecordsPermissionsByRoleId,
): Promise<{
newRolesPermissionsVersion: string;
}> {
const [, newRolesPermissionsVersion] = await Promise.all([
this.cacheStorageService.set<ObjectRecordsPermissionsByRoleId>(
`${WorkspaceCacheKeys.MetadataPermissionsRolesPermissions}:${workspaceId}`,
permissions,
TTL_INFINITE,
),
this.setRolesPermissionsVersion(workspaceId),
]);
return { newRolesPermissionsVersion };
}
getRolesPermissions(
workspaceId: string,
): Promise<ObjectRecordsPermissionsByRoleId | undefined> {
return this.cacheStorageService.get<ObjectRecordsPermissionsByRoleId>(
`${WorkspaceCacheKeys.MetadataPermissionsRolesPermissions}:${workspaceId}`,
);
}
getRolesPermissionsVersion(workspaceId: string): Promise<string | undefined> {
return this.cacheStorageService.get<string>(
`${WorkspaceCacheKeys.MetadataPermissionsRolesPermissionsVersion}:${workspaceId}`,
);
}
addRolesPermissionsOngoingCachingLock(workspaceId: string) {
return this.cacheStorageService.set<boolean>(
`${WorkspaceCacheKeys.MetadataPermissionsRolesPermissionsOngoingCachingLock}:${workspaceId}`,
true,
1_000 * 60, // 1 minute
);
}
removeRolesPermissionsOngoingCachingLock(workspaceId: string) {
return this.cacheStorageService.del(
`${WorkspaceCacheKeys.MetadataPermissionsRolesPermissionsOngoingCachingLock}:${workspaceId}`,
);
}
getRolesPermissionsOngoingCachingLock(
workspaceId: string,
): Promise<boolean | undefined> {
return this.cacheStorageService.get<boolean>(
`${WorkspaceCacheKeys.MetadataPermissionsRolesPermissionsOngoingCachingLock}:${workspaceId}`,
);
}
async setUserWorkspaceRoleMap(
workspaceId: string,
userWorkspaceRoleMap: UserWorkspaceRoleMap,
): Promise<{
newUserWorkspaceRoleMapVersion: string;
}> {
const [, newUserWorkspaceRoleMapVersion] = await Promise.all([
this.cacheStorageService.set<UserWorkspaceRoleMap>(
`${WorkspaceCacheKeys.MetadataPermissionsUserWorkspaceRoleMap}:${workspaceId}`,
userWorkspaceRoleMap,
TTL_INFINITE,
),
this.setUserWorkspaceRoleMapVersion(workspaceId),
]);
return { newUserWorkspaceRoleMapVersion };
}
async setUserWorkspaceRoleMapVersion(workspaceId: string) {
const userWorkspaceRoleMapVersion = v4();
await this.cacheStorageService.set<string>(
`${WorkspaceCacheKeys.MetadataPermissionsUserWorkspaceRoleMapVersion}:${workspaceId}`,
userWorkspaceRoleMapVersion,
TTL_INFINITE,
);
return userWorkspaceRoleMapVersion;
}
getUserWorkspaceRoleMap(
workspaceId: string,
): Promise<Record<string, string> | undefined> {
return this.cacheStorageService.get<Record<string, string>>(
`${WorkspaceCacheKeys.MetadataPermissionsUserWorkspaceRoleMap}:${workspaceId}`,
);
}
getUserWorkspaceRoleMapVersion(
workspaceId: string,
): Promise<string | undefined> {
return this.cacheStorageService.get<string>(
`${WorkspaceCacheKeys.MetadataPermissionsUserWorkspaceRoleMapVersion}:${workspaceId}`,
);
}
addUserWorkspaceRoleMapOngoingCachingLock(workspaceId: string) {
return this.cacheStorageService.set<boolean>(
`${WorkspaceCacheKeys.MetadataPermissionsUserWorkspaceRoleMapOngoingCachingLock}:${workspaceId}`,
true,
1_000 * 60, // 1 minute
);
}
removeUserWorkspaceRoleMapOngoingCachingLock(workspaceId: string) {
return this.cacheStorageService.del(
`${WorkspaceCacheKeys.MetadataPermissionsUserWorkspaceRoleMapOngoingCachingLock}:${workspaceId}`,
);
}
getUserWorkspaceRoleMapOngoingCachingLock(
workspaceId: string,
): Promise<boolean | undefined> {
return this.cacheStorageService.get<boolean>(
`${WorkspaceCacheKeys.MetadataPermissionsUserWorkspaceRoleMapOngoingCachingLock}:${workspaceId}`,
);
}
}

View File

@ -0,0 +1,33 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity';
import { WorkspacePermissionsCacheStorageService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache-storage.service';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
import { WorkspacePermissionsCacheService } from './workspace-permissions-cache.service';
@Module({
imports: [
TypeOrmModule.forFeature([Workspace], 'core'),
TypeOrmModule.forFeature(
[ObjectMetadataEntity, RoleEntity, UserWorkspaceRoleEntity],
'metadata',
),
WorkspaceCacheStorageModule,
FeatureFlagModule,
],
providers: [
WorkspacePermissionsCacheService,
WorkspacePermissionsCacheStorageService,
],
exports: [
WorkspacePermissionsCacheService,
WorkspacePermissionsCacheStorageService,
],
})
export class WorkspacePermissionsCacheModule {}

View File

@ -0,0 +1,318 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import {
ObjectRecordsPermissions,
ObjectRecordsPermissionsByRoleId,
} from 'twenty-shared/types';
import { In, Repository } from 'typeorm';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity';
import { UserWorkspaceRoleMap } from 'src/engine/metadata-modules/workspace-permissions-cache/types/user-workspace-role-map.type';
import { WorkspacePermissionsCacheStorageService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache-storage.service';
import { TwentyORMExceptionCode } from 'src/engine/twenty-orm/exceptions/twenty-orm.exception';
import { getFromCacheWithRecompute } from 'src/engine/utils/get-data-from-cache-with-recompute.util';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
type CacheResult<T, U> = {
version: T;
data: U;
};
const USER_WORKSPACE_ROLE_MAP = 'User workspace role map';
const ROLES_PERMISSIONS = 'Roles permissions';
@Injectable()
export class WorkspacePermissionsCacheService {
logger = new Logger(WorkspacePermissionsCacheService.name);
constructor(
@InjectRepository(ObjectMetadataEntity, 'metadata')
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@InjectRepository(RoleEntity, 'metadata')
private readonly roleRepository: Repository<RoleEntity>,
@InjectRepository(UserWorkspaceRoleEntity, 'metadata')
private readonly userWorkspaceRoleRepository: Repository<UserWorkspaceRoleEntity>,
private readonly workspacePermissionsCacheStorageService: WorkspacePermissionsCacheStorageService,
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
private readonly featureFlagService: FeatureFlagService,
) {}
async recomputeRolesPermissionsCache({
workspaceId,
ignoreLock = false,
roleIds,
}: {
workspaceId: string;
ignoreLock?: boolean;
roleIds?: string[];
}): Promise<void> {
const isPermissionsV2Enabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsPermissionsV2Enabled,
workspaceId,
);
const isAlreadyCaching =
await this.workspacePermissionsCacheStorageService.getRolesPermissionsOngoingCachingLock(
workspaceId,
);
if (!ignoreLock && isAlreadyCaching) {
return;
}
await this.workspacePermissionsCacheStorageService.addRolesPermissionsOngoingCachingLock(
workspaceId,
);
let currentRolesPermissions: ObjectRecordsPermissionsByRoleId | undefined =
undefined;
if (roleIds) {
currentRolesPermissions =
await this.workspacePermissionsCacheStorageService.getRolesPermissions(
workspaceId,
);
}
const recomputedRolesPermissions =
await this.getObjectRecordPermissionsForRoles({
workspaceId,
isPermissionsV2Enabled,
roleIds,
});
const freshObjectRecordsPermissionsByRoleId = roleIds
? { ...currentRolesPermissions, ...recomputedRolesPermissions }
: recomputedRolesPermissions;
await this.workspacePermissionsCacheStorageService.setRolesPermissions(
workspaceId,
freshObjectRecordsPermissionsByRoleId,
);
await this.workspacePermissionsCacheStorageService.removeRolesPermissionsOngoingCachingLock(
workspaceId,
);
}
async recomputeUserWorkspaceRoleMapCache({
workspaceId,
ignoreLock = false,
}: {
workspaceId: string;
ignoreLock?: boolean;
}): Promise<void> {
const isAlreadyCaching =
await this.workspacePermissionsCacheStorageService.getUserWorkspaceRoleMapOngoingCachingLock(
workspaceId,
);
if (!ignoreLock && isAlreadyCaching) {
return;
}
await this.workspacePermissionsCacheStorageService.addUserWorkspaceRoleMapOngoingCachingLock(
workspaceId,
);
const freshUserWorkspaceRoleMap =
await this.getUserWorkspaceRoleMapFromDatabase({
workspaceId,
});
await this.workspacePermissionsCacheStorageService.setUserWorkspaceRoleMap(
workspaceId,
freshUserWorkspaceRoleMap,
);
await this.workspacePermissionsCacheStorageService.removeUserWorkspaceRoleMapOngoingCachingLock(
workspaceId,
);
}
async getRolesPermissionsFromCache({
workspaceId,
}: {
workspaceId: string;
}): Promise<CacheResult<string, ObjectRecordsPermissionsByRoleId>> {
return getFromCacheWithRecompute<string, ObjectRecordsPermissionsByRoleId>({
workspaceId,
getCacheData: () =>
this.workspacePermissionsCacheStorageService.getRolesPermissions(
workspaceId,
),
getCacheVersion: () =>
this.workspacePermissionsCacheStorageService.getRolesPermissionsVersion(
workspaceId,
),
recomputeCache: (params) => this.recomputeRolesPermissionsCache(params),
cachedEntityName: ROLES_PERMISSIONS,
exceptionCode: TwentyORMExceptionCode.ROLES_PERMISSIONS_VERSION_NOT_FOUND,
});
}
async getUserWorkspaceRoleMapFromCache({
workspaceId,
}: {
workspaceId: string;
}): Promise<CacheResult<string, UserWorkspaceRoleMap>> {
return getFromCacheWithRecompute<string, UserWorkspaceRoleMap>({
workspaceId,
getCacheData: () =>
this.workspacePermissionsCacheStorageService.getUserWorkspaceRoleMap(
workspaceId,
),
getCacheVersion: () =>
this.workspacePermissionsCacheStorageService.getUserWorkspaceRoleMapVersion(
workspaceId,
),
recomputeCache: (params) =>
this.recomputeUserWorkspaceRoleMapCache(params),
cachedEntityName: USER_WORKSPACE_ROLE_MAP,
exceptionCode:
TwentyORMExceptionCode.USER_WORKSPACE_ROLE_MAP_VERSION_NOT_FOUND,
});
}
private async getObjectRecordPermissionsForRoles({
workspaceId,
isPermissionsV2Enabled,
roleIds,
}: {
workspaceId: string;
isPermissionsV2Enabled: boolean;
roleIds?: string[];
}): Promise<ObjectRecordsPermissionsByRoleId> {
let roles: RoleEntity[] = [];
roles = await this.roleRepository.find({
where: {
workspaceId,
...(roleIds ? { id: In(roleIds) } : {}),
},
relations: ['objectPermissions'],
});
const workspaceObjectMetadataNameIdMap =
await this.getWorkspaceObjectMetadataNameIdMap(workspaceId);
const permissionsByRoleId: ObjectRecordsPermissionsByRoleId = {};
for (const role of roles) {
const objectRecordsPermissions: ObjectRecordsPermissions = {};
for (const objectMetadataNameSingular of Object.keys(
workspaceObjectMetadataNameIdMap,
)) {
let canRead = role.canReadAllObjectRecords;
let canUpdate = role.canUpdateAllObjectRecords;
let canSoftDelete = role.canSoftDeleteAllObjectRecords;
let canDestroy = role.canDestroyAllObjectRecords;
if (isPermissionsV2Enabled) {
const objectRecordPermissionsOverride = role.objectPermissions.find(
(objectPermission) =>
objectPermission.objectMetadataId ===
workspaceObjectMetadataNameIdMap[objectMetadataNameSingular],
);
canRead =
objectRecordPermissionsOverride?.canReadObjectRecords ?? canRead;
canUpdate =
objectRecordPermissionsOverride?.canUpdateObjectRecords ??
canUpdate;
canSoftDelete =
objectRecordPermissionsOverride?.canSoftDeleteObjectRecords ??
canSoftDelete;
canDestroy =
objectRecordPermissionsOverride?.canDestroyObjectRecords ??
canDestroy;
}
objectRecordsPermissions[objectMetadataNameSingular] = {
canRead,
canUpdate,
canSoftDelete,
canDestroy,
};
}
permissionsByRoleId[role.id] = objectRecordsPermissions;
}
return permissionsByRoleId;
}
private async getWorkspaceObjectMetadataNameIdMap(
workspaceId: string,
): Promise<Record<string, string>> {
let workspaceObjectMetadataMap: Record<string, string> = {};
const metadataVersion =
await this.workspaceCacheStorageService.getMetadataVersion(workspaceId);
if (metadataVersion) {
const objectMetadataMaps =
await this.workspaceCacheStorageService.getObjectMetadataMaps(
workspaceId,
metadataVersion,
);
workspaceObjectMetadataMap = Object.values(
objectMetadataMaps?.byId ?? {},
).reduce(
(acc, objectMetadata) => {
acc[objectMetadata.nameSingular] = objectMetadata.id;
return acc;
},
{} as Record<string, string>,
);
}
if (
!metadataVersion ||
Object.keys(workspaceObjectMetadataMap).length === 0
) {
const workspaceObjectMetadata = await this.objectMetadataRepository.find({
where: {
workspaceId,
},
});
workspaceObjectMetadataMap = workspaceObjectMetadata.reduce(
(acc, objectMetadata) => {
acc[objectMetadata.nameSingular] = objectMetadata.id;
return acc;
},
{} as Record<string, string>,
);
}
return workspaceObjectMetadataMap;
}
private async getUserWorkspaceRoleMapFromDatabase({
workspaceId,
}: {
workspaceId: string;
}): Promise<UserWorkspaceRoleMap> {
const userWorkspaceRoleMap = await this.userWorkspaceRoleRepository.find({
where: {
workspaceId,
},
});
return userWorkspaceRoleMap.reduce((acc, userWorkspaceRole) => {
acc[userWorkspaceRole.userWorkspaceId] = userWorkspaceRole.roleId;
return acc;
}, {} as UserWorkspaceRoleMap);
}
}

View File

@ -1,20 +0,0 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
import { WorkspaceRolesPermissionsCacheService } from './workspace-roles-permissions-cache.service';
@Module({
imports: [
TypeOrmModule.forFeature([Workspace], 'core'),
TypeOrmModule.forFeature([ObjectMetadataEntity, RoleEntity], 'metadata'),
WorkspaceCacheStorageModule,
],
providers: [WorkspaceRolesPermissionsCacheService],
exports: [WorkspaceRolesPermissionsCacheService],
})
export class WorkspaceRolesPermissionsCacheModule {}

View File

@ -1,128 +0,0 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import {
ObjectRecordsPermissions,
ObjectRecordsPermissionsByRoleId,
} from 'twenty-shared/types';
import { Repository } from 'typeorm';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
@Injectable()
export class WorkspaceRolesPermissionsCacheService {
logger = new Logger(WorkspaceRolesPermissionsCacheService.name);
constructor(
@InjectRepository(ObjectMetadataEntity, 'metadata')
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@InjectRepository(RoleEntity, 'metadata')
private readonly roleRepository: Repository<RoleEntity>,
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
) {}
async recomputeRolesPermissionsCache({
workspaceId,
ignoreLock = false,
}: {
workspaceId: string;
ignoreLock?: boolean;
}): Promise<void> {
const isAlreadyCaching =
await this.workspaceCacheStorageService.getRolesPermissionsOngoingCachingLock(
workspaceId,
);
if (!ignoreLock && isAlreadyCaching) {
return;
}
await this.workspaceCacheStorageService.addRolesPermissionsOngoingCachingLock(
workspaceId,
);
const freshObjectRecordsPermissionsByRoleId =
await this.getObjectRecordPermissionsForRoles({
workspaceId,
});
await this.workspaceCacheStorageService.setRolesPermissions(
workspaceId,
freshObjectRecordsPermissionsByRoleId,
);
await this.workspaceCacheStorageService.removeRolesPermissionsOngoingCachingLock(
workspaceId,
);
}
private async getObjectRecordPermissionsForRoles({
workspaceId,
}: {
workspaceId: string;
}): Promise<ObjectRecordsPermissionsByRoleId> {
const roles = await this.roleRepository.find({
where: {
workspaceId,
},
});
const workspaceObjectMetadataNames =
await this.getWorkspaceObjectMetadataNames(workspaceId);
const permissionsByRoleId: ObjectRecordsPermissionsByRoleId = {};
for (const role of roles) {
const objectRecordsPermissions: ObjectRecordsPermissions = {};
for (const objectMetadataNameSingular of workspaceObjectMetadataNames) {
objectRecordsPermissions[objectMetadataNameSingular] = {
canRead: role.canReadAllObjectRecords,
canUpdate: role.canUpdateAllObjectRecords,
canSoftDelete: role.canSoftDeleteAllObjectRecords,
canDestroy: role.canDestroyAllObjectRecords,
};
}
permissionsByRoleId[role.id] = objectRecordsPermissions;
}
return permissionsByRoleId;
}
private async getWorkspaceObjectMetadataNames(
workspaceId: string,
): Promise<string[]> {
let workspaceObjectMetadataNames: string[] = [];
const metadataVersion =
await this.workspaceCacheStorageService.getMetadataVersion(workspaceId);
if (metadataVersion) {
const objectMetadataMaps =
await this.workspaceCacheStorageService.getObjectMetadataMaps(
workspaceId,
metadataVersion,
);
workspaceObjectMetadataNames = Object.values(
objectMetadataMaps?.byId ?? {},
).map((objectMetadata) => objectMetadata.nameSingular);
}
if (!metadataVersion || workspaceObjectMetadataNames.length === 0) {
const workspaceObjectMetadata = await this.objectMetadataRepository.find({
where: {
workspaceId,
},
});
workspaceObjectMetadataNames = workspaceObjectMetadata.map(
(objectMetadata) => objectMetadata.nameSingular,
);
}
return workspaceObjectMetadataNames;
}
}

View File

@ -18,16 +18,16 @@ export class WorkspaceDataSource extends DataSource {
readonly manager: WorkspaceEntityManager;
featureFlagMapVersion: string;
featureFlagMap: FeatureFlagMap;
rolesPermissionsVersion?: string;
permissionsPerRoleId?: ObjectRecordsPermissionsByRoleId;
rolesPermissionsVersion: string;
permissionsPerRoleId: ObjectRecordsPermissionsByRoleId;
constructor(
internalContext: WorkspaceInternalContext,
options: DataSourceOptions,
featureFlagMapVersion: string,
featureFlagMap: FeatureFlagMap,
rolesPermissionsVersion?: string,
permissionsPerRoleId?: ObjectRecordsPermissionsByRoleId,
rolesPermissionsVersion: string,
permissionsPerRoleId: ObjectRecordsPermissionsByRoleId,
) {
super(options);
this.internalContext = internalContext;

View File

@ -13,4 +13,5 @@ export enum TwentyORMExceptionCode {
WORKSPACE_SCHEMA_NOT_FOUND = 'WORKSPACE_SCHEMA_NOT_FOUND',
ROLES_PERMISSIONS_VERSION_NOT_FOUND = 'ROLES_PERMISSIONS_VERSION_NOT_FOUND',
FEATURE_FLAG_MAP_VERSION_NOT_FOUND = 'FEATURE_FLAG_MAP_VERSION_NOT_FOUND',
USER_WORKSPACE_ROLE_MAP_VERSION_NOT_FOUND = 'USER_WORKSPACE_ROLE_MAP_VERSION_NOT_FOUND',
}

View File

@ -7,12 +7,12 @@ import { EntitySchema } from 'typeorm';
import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
import { NodeEnvironment } from 'src/engine/core-modules/twenty-config/interfaces/node-environment.interface';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { WorkspaceFeatureFlagsMapCacheService } from 'src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.service';
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
import { WorkspaceRolesPermissionsCacheService } from 'src/engine/metadata-modules/workspace-roles-permissions-cache/workspace-roles-permissions-cache.service';
import { WorkspacePermissionsCacheStorageService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache-storage.service';
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource';
import {
TwentyORMException,
@ -40,7 +40,8 @@ export class WorkspaceDatasourceFactory {
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService,
private readonly entitySchemaFactory: EntitySchemaFactory,
private readonly workspaceRolesPermissionsCacheService: WorkspaceRolesPermissionsCacheService,
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
private readonly workspacePermissionsCacheStorageService: WorkspacePermissionsCacheStorageService,
private readonly workspaceFeatureFlagsMapCacheService: WorkspaceFeatureFlagsMapCacheService,
) {}
@ -60,15 +61,11 @@ export class WorkspaceDatasourceFactory {
{ workspaceId },
);
const isPermissionsV2Enabled =
cachedFeatureFlagMap[FeatureFlagKey.IsPermissionsV2Enabled];
const {
data: cachedRolesPermissions,
version: cachedRolesPermissionsVersion,
} = await this.getRolesPermissionsFromCache({
workspaceId,
isPermissionsV2Enabled,
});
if (
@ -196,13 +193,11 @@ export class WorkspaceDatasourceFactory {
throw new Error(`Failed to create WorkspaceDataSource for ${cacheKey}`);
}
if (isPermissionsV2Enabled) {
await this.updateWorkspaceDataSourceRolesPermissionsIfNeeded({
workspaceDataSource,
cachedRolesPermissionsVersion,
cachedRolesPermissions,
});
}
await this.updateWorkspaceDataSourceRolesPermissionsIfNeeded({
workspaceDataSource,
cachedRolesPermissionsVersion,
cachedRolesPermissions,
});
await this.updateWorkspaceDataSourceFeatureFlagsMapIfNeeded({
workspaceDataSource,
@ -215,33 +210,21 @@ export class WorkspaceDatasourceFactory {
private async getRolesPermissionsFromCache({
workspaceId,
isPermissionsV2Enabled,
}: {
workspaceId: string;
isPermissionsV2Enabled?: boolean;
}): Promise<
CacheResult<
string | undefined,
ObjectRecordsPermissionsByRoleId | undefined
>
> {
if (!isPermissionsV2Enabled) {
return { version: undefined, data: undefined };
}
return getFromCacheWithRecompute<
string | undefined,
ObjectRecordsPermissionsByRoleId | undefined
>({
}): Promise<CacheResult<string, ObjectRecordsPermissionsByRoleId>> {
return getFromCacheWithRecompute<string, ObjectRecordsPermissionsByRoleId>({
workspaceId,
getCacheData: () =>
this.workspaceCacheStorageService.getRolesPermissions(workspaceId),
this.workspacePermissionsCacheStorageService.getRolesPermissions(
workspaceId,
),
getCacheVersion: () =>
this.workspaceCacheStorageService.getRolesPermissionsVersionFromCache(
this.workspacePermissionsCacheStorageService.getRolesPermissionsVersion(
workspaceId,
),
recomputeCache: (params) =>
this.workspaceRolesPermissionsCacheService.recomputeRolesPermissionsCache(
this.workspacePermissionsCacheService.recomputeRolesPermissionsCache(
params,
),
cachedEntityName: 'Roles permissions',
@ -281,8 +264,8 @@ export class WorkspaceDatasourceFactory {
cachedRolesPermissions,
}: {
workspaceDataSource: WorkspaceDataSource;
cachedRolesPermissionsVersion: string | undefined;
cachedRolesPermissions: ObjectRecordsPermissionsByRoleId | undefined;
cachedRolesPermissionsVersion: string;
cachedRolesPermissions: ObjectRecordsPermissionsByRoleId;
}): Promise<void> {
this.updateWorkspaceDataSourceIfNeeded({
workspaceDataSource,

View File

@ -8,7 +8,7 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi
import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity';
import { WorkspaceFeatureFlagsMapCacheModule } from 'src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.module';
import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module';
import { WorkspaceRolesPermissionsCacheModule } from 'src/engine/metadata-modules/workspace-roles-permissions-cache/workspace-roles-permissions-cache.module';
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
import { entitySchemaFactories } from 'src/engine/twenty-orm/factories';
import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-schema.factory';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
@ -26,8 +26,8 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/
WorkspaceCacheStorageModule,
WorkspaceMetadataCacheModule,
PermissionsModule,
WorkspaceRolesPermissionsCacheModule,
WorkspaceFeatureFlagsMapCacheModule,
WorkspacePermissionsCacheModule,
FeatureFlagModule,
],
providers: [

View File

@ -2,9 +2,7 @@ import { Injectable } from '@nestjs/common';
import crypto from 'crypto';
import { ObjectRecordsPermissionsByRoleId } from 'twenty-shared/types';
import { EntitySchemaOptions } from 'typeorm';
import { v4 } from 'uuid';
import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
@ -23,12 +21,15 @@ export enum WorkspaceCacheKeys {
MetadataObjectMetadataMaps = 'metadata:object-metadata-maps',
MetadataObjectMetadataOngoingCachingLock = 'metadata:object-metadata-ongoing-caching-lock',
MetadataVersion = 'metadata:workspace-metadata-version',
MetadataRolesPermissions = 'metadata:roles-permissions',
MetadataRolesPermissionsVersion = 'metadata:roles-permissions-version',
MetadataRolesPermissionsOngoingCachingLock = 'metadata:roles-permissions-ongoing-caching-lock',
FeatureFlagMap = 'feature-flag-map',
FeatureFlagMapVersion = 'feature-flag-map-version',
FeatureFlagMap = 'feature-flag:feature-flag-map',
FeatureFlagMapVersion = 'feature-flag:feature-flag-map-version',
FeatureFlagMapOngoingCachingLock = 'feature-flag-map-ongoing-caching-lock',
MetadataPermissionsRolesPermissions = 'metadata:permissions:roles-permissions',
MetadataPermissionsRolesPermissionsVersion = 'metadata:permissions:roles-permissions-version',
MetadataPermissionsRolesPermissionsOngoingCachingLock = 'metadata:permissions:roles-permissions-ongoing-caching-lock',
MetadataPermissionsUserWorkspaceRoleMap = 'metadata:permissions:user-workspace-role-map',
MetadataPermissionsUserWorkspaceRoleMapVersion = 'metadata:permissions:user-workspace-role-map-version',
MetadataPermissionsUserWorkspaceRoleMapOngoingCachingLock = 'metadata:permissions:user-workspace-role-map-ongoing-caching-lock',
}
const TTL_INFINITE = 0;
@ -186,74 +187,6 @@ export class WorkspaceCacheStorageService {
);
}
getRolesPermissionsVersionFromCache(
workspaceId: string,
): Promise<string | undefined> {
return this.cacheStorageService.get<string>(
`${WorkspaceCacheKeys.MetadataRolesPermissionsVersion}:${workspaceId}`,
);
}
async setRolesPermissionsVersion(workspaceId: string): Promise<string> {
const rolesPermissionsVersion = v4();
await this.cacheStorageService.set<string>(
`${WorkspaceCacheKeys.MetadataRolesPermissionsVersion}:${workspaceId}`,
rolesPermissionsVersion,
TTL_INFINITE,
);
return rolesPermissionsVersion;
}
async setRolesPermissions(
workspaceId: string,
permissions: ObjectRecordsPermissionsByRoleId,
): Promise<{
newRolesPermissionsVersion: string;
}> {
const [, newRolesPermissionsVersion] = await Promise.all([
this.cacheStorageService.set<ObjectRecordsPermissionsByRoleId>(
`${WorkspaceCacheKeys.MetadataRolesPermissions}:${workspaceId}`,
permissions,
TTL_INFINITE,
),
this.setRolesPermissionsVersion(workspaceId),
]);
return { newRolesPermissionsVersion };
}
getRolesPermissions(
workspaceId: string,
): Promise<ObjectRecordsPermissionsByRoleId | undefined> {
return this.cacheStorageService.get<ObjectRecordsPermissionsByRoleId>(
`${WorkspaceCacheKeys.MetadataRolesPermissions}:${workspaceId}`,
);
}
addRolesPermissionsOngoingCachingLock(workspaceId: string) {
return this.cacheStorageService.set<boolean>(
`${WorkspaceCacheKeys.MetadataRolesPermissionsOngoingCachingLock}:${workspaceId}`,
true,
1_000 * 60, // 1 minute
);
}
removeRolesPermissionsOngoingCachingLock(workspaceId: string) {
return this.cacheStorageService.del(
`${WorkspaceCacheKeys.MetadataRolesPermissionsOngoingCachingLock}:${workspaceId}`,
);
}
getRolesPermissionsOngoingCachingLock(
workspaceId: string,
): Promise<boolean | undefined> {
return this.cacheStorageService.get<boolean>(
`${WorkspaceCacheKeys.MetadataRolesPermissionsOngoingCachingLock}:${workspaceId}`,
);
}
getFeatureFlagsMapVersionFromCache(
workspaceId: string,
): Promise<string | undefined> {
@ -341,15 +274,27 @@ export class WorkspaceCacheStorageService {
);
await this.cacheStorageService.del(
`${WorkspaceCacheKeys.MetadataRolesPermissions}:${workspaceId}`,
`${WorkspaceCacheKeys.MetadataPermissionsRolesPermissions}:${workspaceId}`,
);
await this.cacheStorageService.del(
`${WorkspaceCacheKeys.MetadataRolesPermissionsVersion}:${workspaceId}`,
`${WorkspaceCacheKeys.MetadataPermissionsRolesPermissionsVersion}:${workspaceId}`,
);
await this.cacheStorageService.del(
`${WorkspaceCacheKeys.MetadataRolesPermissionsOngoingCachingLock}:${workspaceId}`,
`${WorkspaceCacheKeys.MetadataPermissionsRolesPermissionsOngoingCachingLock}:${workspaceId}`,
);
await this.cacheStorageService.del(
`${WorkspaceCacheKeys.MetadataPermissionsUserWorkspaceRoleMap}:${workspaceId}`,
);
await this.cacheStorageService.del(
`${WorkspaceCacheKeys.MetadataPermissionsUserWorkspaceRoleMapVersion}:${workspaceId}`,
);
await this.cacheStorageService.del(
`${WorkspaceCacheKeys.MetadataPermissionsUserWorkspaceRoleMapOngoingCachingLock}:${workspaceId}`,
);
await this.cacheStorageService.del(