Fix role lookup query and align calendar layout behavior (#22928)

## Summary
Fixes slow getRoles requests caused by loading several one-to-many role
relations in a single TypeORM query.

The previous query produced a Cartesian product across role targets,
permission flags, object permissions, and field permissions. In
production, a workspace with 9 roles expanded into more than 32,000
database rows before TypeORM hydration.

This change:
- Reads roles and their related permissions from the existing flat-map
caches.
- Hydrates relations using the same findManyWithRelationsFromCache
pattern as ViewService.
- Removes the expensive joined query from the getRoles path.
- Preserves the existing GraphQL response shape.

## Performance
### Before:
9 roles
32,257 SQL result rows
Approximately 10 seconds observed request latency
### After:
No Cartesian SQL query
Cache-backed lookups and in-memory relation hydration

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22928?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-15 20:56:09 +02:00
committed by GitHub
parent 79f3a5243a
commit 9c2d87a846
2 changed files with 66 additions and 13 deletions
@@ -57,7 +57,6 @@ import {
} from 'src/engine/metadata-modules/role/dtos/role.dto';
import { UpdateRoleInput } from 'src/engine/metadata-modules/role/dtos/update-role.input';
import { RoleService } from 'src/engine/metadata-modules/role/role.service';
import { fromRoleEntitiesToRoleDtos } from 'src/engine/metadata-modules/role/utils/fromRoleEntityToRoleDto.util';
import { UpsertRowLevelPermissionPredicatesInput } from 'src/engine/metadata-modules/row-level-permission-predicate/dtos/inputs/upsert-row-level-permission-predicates.input';
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';
@@ -103,9 +102,7 @@ export class RoleResolver {
async getRoles(
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<RoleDTO[]> {
const roleEntities = await this.roleService.getWorkspaceRoles(workspace.id);
return fromRoleEntitiesToRoleDtos(roleEntities);
return this.roleService.getWorkspaceRoles(workspace.id);
}
@Mutation(() => WorkspaceMemberDTO)
@@ -21,9 +21,13 @@ import {
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
import { findManyFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps.util';
import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type';
import { fromCreateRoleInputToFlatRoleToCreate } from 'src/engine/metadata-modules/flat-role/utils/from-create-role-input-to-flat-role-to-create.util';
import { fromDeleteRoleInputToFlatRoleOrThrow } from 'src/engine/metadata-modules/flat-role/utils/from-delete-role-input-to-flat-role-or-throw.util';
import { fromUpdateRoleInputToFlatRoleToUpdateOrThrow } from 'src/engine/metadata-modules/flat-role/utils/from-update-role-input-to-flat-role-to-update-or-throw.util';
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 { MEMBER_ROLE_LABEL } from 'src/engine/metadata-modules/permissions/constants/member-role-label.constants';
import {
PermissionsException,
@@ -35,6 +39,7 @@ import { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto';
import { type UpdateRoleInput } from 'src/engine/metadata-modules/role/dtos/update-role.input';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { fromFlatRoleToRoleDto } from 'src/engine/metadata-modules/role/utils/fromFlatRoleToRoleDto.util';
import { fromFlatRolePermissionFlagToRolePermissionFlagDto } from 'src/engine/metadata-modules/role-permission-flag/utils/from-flat-role-permission-flag-to-role-permission-flag-dto.util';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
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';
@@ -56,16 +61,67 @@ export class RoleService {
private readonly aiAgentRoleService: AiAgentRoleService,
) {}
public async getWorkspaceRoles(workspaceId: string): Promise<RoleEntity[]> {
return this.roleRepository.find(workspaceId, {
relations: {
roleTargets: true,
rolePermissionFlags: {
permissionFlag: true,
public async getWorkspaceRoles(workspaceId: string): Promise<RoleDTO[]> {
const { flatRoleMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatRoleMaps'],
},
objectPermissions: true,
fieldPermissions: true,
},
);
return this.findManyWithRelationsFromCache(
Object.values(flatRoleMaps.byUniversalIdentifier).filter(isDefined),
workspaceId,
);
}
private async findManyWithRelationsFromCache(
flatRoles: FlatRole[],
workspaceId: string,
): Promise<RoleDTO[]> {
const {
flatRolePermissionFlagMaps,
flatPermissionFlagMaps,
flatObjectPermissionMaps,
flatFieldPermissionMaps,
} =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: [
'flatRolePermissionFlagMaps',
'flatPermissionFlagMaps',
'flatObjectPermissionMaps',
'flatFieldPermissionMaps',
],
},
);
return flatRoles.map((flatRole) => {
const roleDto = fromFlatRoleToRoleDto(flatRole);
roleDto.permissionFlags = findManyFlatEntityByIdInFlatEntityMaps({
flatEntityIds: flatRole.rolePermissionFlagIds,
flatEntityMaps: flatRolePermissionFlagMaps,
}).map((flatRolePermissionFlag) =>
fromFlatRolePermissionFlagToRolePermissionFlagDto(
flatRolePermissionFlag,
flatPermissionFlagMaps,
),
);
roleDto.objectPermissions = findManyFlatEntityByIdInFlatEntityMaps({
flatEntityIds: flatRole.objectPermissionIds,
flatEntityMaps: flatObjectPermissionMaps,
}).map(fromFlatObjectPermissionToObjectPermissionDto);
roleDto.fieldPermissions = findManyFlatEntityByIdInFlatEntityMaps({
flatEntityIds: flatRole.fieldPermissionIds,
flatEntityMaps: flatFieldPermissionMaps,
}).map(fromFlatFieldPermissionToFieldPermissionDto);
return roleDto;
});
}