perf: use cache for role validation (#23383)

## Context

Role assignment validation queried PostgreSQL only to check whether a
role exists and whether `canBeAssignedToUsers` is enabled. Both values
already exist in `flatRoleMaps`.

This validation runs when inviting users and assigning a role to a user
workspace.

## What changed

- Replace the role repository lookup with `flatRoleMaps`
- Resolve the role through the existing keyed flat-map helper
- Preserve the existing role-not-found and role-not-assignable errors
- Replace unused TypeORM module wiring with the flat entity cache module

## Expected impact

On a warm workspace cache, role assignment validation uses an O(1) map
lookup and avoids a PostgreSQL round trip. Cold caches retain the normal
workspace cache recomputation behavior.

## Validation

- Typecheck reports no errors in the changed files
- Existing validation outcomes and exception codes are preserved

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23383?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Weiko
2026-07-28 09:53:42 +02:00
committed by GitHub
parent 8481c76bfb
commit cc5ff4869d
2 changed files with 16 additions and 18 deletions
@@ -1,16 +1,10 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { RoleValidationService } from 'src/engine/metadata-modules/role-validation/services/role-validation.service';
import { provideWorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/provide-workspace-scoped-repository';
@Module({
imports: [TypeOrmModule.forFeature([RoleEntity])],
providers: [
RoleValidationService,
provideWorkspaceScopedRepository(RoleEntity),
],
imports: [WorkspaceManyOrAllFlatEntityMapsCacheModule],
providers: [RoleValidationService],
exports: [RoleValidationService],
})
export class RoleValidationModule {}
@@ -5,25 +5,29 @@ import {
PermissionsExceptionCode,
PermissionsExceptionMessage,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { InjectWorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/inject-workspace-scoped-repository.decorator';
import { WorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/workspace-scoped-repository';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
@Injectable()
export class RoleValidationService {
constructor(
@InjectWorkspaceScopedRepository(RoleEntity)
private readonly roleRepository: WorkspaceScopedRepository<RoleEntity>,
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
) {}
async validateRoleAssignableToUsersOrThrow(
roleId: string,
workspaceId: string,
): Promise<void> {
const role = await this.roleRepository.findOne(workspaceId, {
where: {
id: roleId,
},
const { flatRoleMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatRoleMaps'],
},
);
const role = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: roleId,
flatEntityMaps: flatRoleMaps,
});
if (!role) {