Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code e2a0122f1e GetCurrentUser query causes 3.6s response due to redundant role/permission DB queries
https://sonarly.com/issue/3776?type=bug

The `POST /metadata` endpoint handling `GetCurrentUser` takes 3.6 seconds because `getRolesByUserWorkspaces()` is called 3 times redundantly within the same request, and multiple core entities (User, UserWorkspace) are re-queried across auth middleware and resolver fields without request-scoped caching.

Fix: ## Fix: Eliminate redundant `getRolesByUserWorkspaces()` DB call in `workspaceMember()` resolver

The root cause is that `getRolesByUserWorkspaces()` is called 3 times per `GetCurrentUser` request, each executing an expensive `RoleTargetEntity` JOIN query (~533ms each, ~1,600ms total). Calls #1 and #2 fetch data for the **exact same** `userWorkspaceId` (the currently authenticated user's workspace).

### Changes

**`permissions.service.ts`** — Add optional `preloadedRoles` parameter to `getUserWorkspacePermissions()`:
```typescript file=packages/twenty-server/src/engine/metadata-modules/permissions/permissions.service.ts lines=38-54
public async getUserWorkspacePermissions({
  userWorkspaceId,
  workspaceId,
  preloadedRoles,
}: {
  userWorkspaceId: string;
  workspaceId: string;
  preloadedRoles?: Map<string, RoleEntity[]>;
}): Promise<UserWorkspacePermissions> {
  const [roleOfUserWorkspace] = preloadedRoles
    ? (preloadedRoles.get(userWorkspaceId) ?? [])
    : await this.userRoleService
        .getRolesByUserWorkspaces({ userWorkspaceIds: [userWorkspaceId], workspaceId })
        .then((roles) => roles?.get(userWorkspaceId) ?? []);
```

**`user.resolver.ts`** — Refactor private helper to fetch roles once and return them alongside permissions, then store the result on the user object for reuse by `workspaceMember()`:

```typescript file=packages/twenty-server/src/engine/core-modules/user/user.resolver.ts lines=98-134
private async getUserWorkspacePermissions({...}): Promise<{
  userWorkspacePermissions: UserWorkspacePermissions;
  rolesCache: Map<string, RoleEntity[]> | undefined;
}> {
  // ...pending workspace check...

  const rolesCache = await this.userRoleService.getRolesByUserWorkspaces({
    userWorkspaceIds: [currentUserWorkspace.id],
    workspaceId: workspace.id,
  });

  const userWorkspacePermissions =
    await this.permissionsService.getUserWorkspacePermissions({
      userWorkspaceId: currentUserWorkspace.id,
      workspaceId: workspace.id,
      preloadedRoles: rolesCache,  // skip internal DB fetch
    });

  return { userWorkspacePermissions, rolesCache };
}
```

`currentUser()` stores `rolesCache` on the returned object:
```typescript
return {
  ...user,
  currentUserWorkspace: { ...currentUserWorkspace, ...userWorkspacePermissionsDto, ... },
  currentWorkspace: workspace,
  __rolesCache: rolesCache,   // available to workspaceMember() ResolveField
} as UserEntity;
```

`workspaceMember()` uses cached roles when available, skipping the redundant DB call:
```typescript file=packages/twenty-server/src/engine/core-modules/user/user.resolver.ts lines=254-263
const cachedRoles = (
  user as UserEntity & { __rolesCache?: Map<string, RoleEntity[]> }
).__rolesCache;

const roleOfUserWorkspace =
  cachedRoles ??
  (await this.userRoleService.getRolesByUserWorkspaces({
    userWorkspaceIds: [userWorkspace.id],
    workspaceId,
  }));
```

### Result

- **Before**: 3 `RoleTargetEntity` DB queries per `GetCurrentUser` request (~1,600ms)
- **After**: 2 `RoleTargetEntity` DB queries (Call #1 + Call #3 remain; Call #2 eliminated when `__rolesCache` is populated by `currentUser()`)
- The fallback in `workspaceMember()` ensures correctness when called outside the `currentUser()` context (cache miss gracefully falls back to DB)
- No changes to the `workspaceMembers()` call path (Call #3 remains independent)
2026-03-04 17:56:24 +00:00
2 changed files with 49 additions and 19 deletions
@@ -61,6 +61,7 @@ import { PermissionsService } from 'src/engine/metadata-modules/permissions/perm
import { type UserWorkspacePermissions } from 'src/engine/metadata-modules/permissions/types/user-workspace-permissions';
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
import { fromUserWorkspacePermissionsToUserWorkspacePermissionsDto } from 'src/engine/metadata-modules/role/utils/fromUserWorkspacePermissionsToUserWorkspacePermissionsDto';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
@@ -100,20 +101,36 @@ export class UserResolver {
}: {
workspace: WorkspaceEntity;
currentUserWorkspace: UserWorkspaceEntity;
}): Promise<UserWorkspacePermissions> {
}): Promise<{
userWorkspacePermissions: UserWorkspacePermissions;
rolesCache: Map<string, RoleEntity[]> | undefined;
}> {
const workspaceIsPendingOrOngoingCreation = [
WorkspaceActivationStatus.PENDING_CREATION,
WorkspaceActivationStatus.ONGOING_CREATION,
].includes(workspace.activationStatus);
if (workspaceIsPendingOrOngoingCreation) {
return this.permissionsService.getDefaultUserWorkspacePermissions();
return {
userWorkspacePermissions:
this.permissionsService.getDefaultUserWorkspacePermissions(),
rolesCache: undefined,
};
}
return await this.permissionsService.getUserWorkspacePermissions({
userWorkspaceId: currentUserWorkspace.id,
const rolesCache = await this.userRoleService.getRolesByUserWorkspaces({
userWorkspaceIds: [currentUserWorkspace.id],
workspaceId: workspace.id,
});
const userWorkspacePermissions =
await this.permissionsService.getUserWorkspacePermissions({
userWorkspaceId: currentUserWorkspace.id,
workspaceId: workspace.id,
preloadedRoles: rolesCache,
});
return { userWorkspacePermissions, rolesCache };
}
@Query(() => UserEntity)
@@ -153,12 +170,15 @@ export class UserResolver {
throw new Error('Current user workspace not found');
}
const userWorkspacePermissions =
const { userWorkspacePermissions, rolesCache } =
await this.getUserWorkspacePermissions({
currentUserWorkspace,
workspace,
});
const userWorkspacePermissionsDto =
fromUserWorkspacePermissionsToUserWorkspacePermissionsDto(
await this.getUserWorkspacePermissions({
currentUserWorkspace,
workspace,
}),
userWorkspacePermissions,
);
const twoFactorAuthenticationMethodSummary =
@@ -170,11 +190,12 @@ export class UserResolver {
...user,
currentUserWorkspace: {
...currentUserWorkspace,
...userWorkspacePermissions,
...userWorkspacePermissionsDto,
twoFactorAuthenticationMethodSummary,
},
currentWorkspace: workspace,
};
__rolesCache: rolesCache,
} as UserEntity;
}
@ResolveField(() => GraphQLJSONObject, {
@@ -230,11 +251,16 @@ export class UserResolver {
workspaceId: workspace.id,
});
const cachedRoles = (
user as UserEntity & { __rolesCache?: Map<string, RoleEntity[]> }
).__rolesCache;
const roleOfUserWorkspace =
await this.userRoleService.getRolesByUserWorkspaces({
cachedRoles ??
(await this.userRoleService.getRolesByUserWorkspaces({
userWorkspaceIds: [userWorkspace.id],
workspaceId,
});
}));
const userWorkspaceRoles = roleOfUserWorkspace.get(userWorkspace.id);
@@ -38,16 +38,20 @@ export class PermissionsService {
public async getUserWorkspacePermissions({
userWorkspaceId,
workspaceId,
preloadedRoles,
}: {
userWorkspaceId: string;
workspaceId: string;
preloadedRoles?: Map<string, RoleEntity[]>;
}): Promise<UserWorkspacePermissions> {
const [roleOfUserWorkspace] = await this.userRoleService
.getRolesByUserWorkspaces({
userWorkspaceIds: [userWorkspaceId],
workspaceId,
})
.then((roles) => roles?.get(userWorkspaceId) ?? []);
const [roleOfUserWorkspace] = preloadedRoles
? (preloadedRoles.get(userWorkspaceId) ?? [])
: await this.userRoleService
.getRolesByUserWorkspaces({
userWorkspaceIds: [userWorkspaceId],
workspaceId,
})
.then((roles) => roles?.get(userWorkspaceId) ?? []);
if (!isDefined(roleOfUserWorkspace)) {
throw new PermissionsException(