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)